Compare commits

...

34 Commits

Author SHA1 Message Date
34272b872d WIP: workaround coredump hotkey linux
Fixes #4059
2024-12-10 15:47:30 -05:00
668e2afb99 Fix race condition in wasm test (#4740) 2024-12-10 20:38:51 +00:00
548c664db0 chore: implemented multiple instances instead of multiple appications? (#4733) 2024-12-10 18:51:24 +00:00
d3a3f4410c Fix to not have browser tooltip on top of CodeMirror tooltips (#4730)
* Fix to not have browser tooltip on top of LSP tooltips

* Switch title to aria-label
2024-12-10 12:15:55 -05:00
22eb343171 Add more Rust lints (#4698)
Add more lints
2024-12-10 12:14:03 -05:00
f2cfa4d5cf Adding point and click revolve workflow for sketch and axis selection (#4687)
* selection stuff

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* trigger CI

* fix bugs

* some edge cut stuff

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

* trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* fix sketch mode issues

* fix more tests, selection in sketch related

* more test fixing

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger ci

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger ci

* more sketch mode selection fixes

* fix unit tests

* rename function

* remove .only

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* lint

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* fix bad pathToNode issue

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* fix sketch on face

* migrate a more selections types

* migrate a more selections types

* fix code selection of fillets

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* fix bad path to node, looks like a race

* migrate a more selections types

* migrate a more selections types

* fix cmd bar selections

* fix cmd bar selections

* fix display issues

* feat: implementing axis selection for point and click revolve

* feat: enforcing selection of 2 options for axis rotation

* feat: added negative rotations for the revolve

* fix: fmt, tsc fixes

* migrate a more selections types

* Revert "migrate a more selections types"

This reverts commit 0d0e453bbb.

* migrate a more selections types

* clean up1

* clean up 2

* chore: improving the copy after discussing with Frank

* fix: merge main fixes

* chore: was able to add a seg to a line. Does not check if one exists already

* saving off some code

* chore: moving revolveSketch into own file for readability, improving variable names instead of node1

* chore: renaming more variables for readability

* chore: more renaming

* fix: allows creating a custom rotation on axis

* fix: added opposite edge logic and adj, need to error handle still

* fix: use other import

* feat: point and click on edges, crude implementation

* feat: implemented toast message and returned error message from validation

* fix: auto linter

* fix: addressing tsc errors

* fix: fighting typescript

* fix: cleaning up PR

* fix: trying to resolve more typescript issues

* fix: save off tsc fixes

* fix: adding comments

* fix: resolving tsc errors

* fix: tsc errors

* fix: auto linter fixes and tsc fixes

* fix:??

* fix: revolve ast works with declaration

* fix: retry logic to make sure the disable dry run actually runs

* fix: codespell typo

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-10 11:11:01 -06:00
3f1f40eeba Revert "More aggressive using of cache on engine settings changes (#4691)" (#4729)
This reverts commit 943cf21d34.
2024-12-10 12:03:41 -05:00
ff2d161606 KCL parser: fns with optional keyword args can set defaults (#4727)
Part of #4600
2024-12-10 10:20:51 -06:00
210c78029d KCL keyword args: calling user-defined functions (#4722)
Part of https://github.com/KittyCAD/modeling-app/issues/4600

You can now call a user-defined function via keyword args. E.g.

```
fn increment(@x) {
  return x + 1
}

fn add(@x, delta) {
  return x + delta
}

two = increment(1)
three = add(1, delta: 2)
```
2024-12-10 04:11:16 +00:00
e27840219b Fix so that tag declarators can be used as parameters (#4692)
* Add test with tag parameter

* Fix to not define TagIdentifiers pointing to nothing

* Add fixed test output

* Rename function to be clearer

* Remove optional param so that unparse works

* Fix to not allow redefining tags

* Fix to only ever define tags in memory when used with stdlib functions

* Fix to not define local variables when non-literal tag declarator is used

* Add removing mutability

* Change function signature since it isn't falliable
2024-12-10 01:47:34 +00:00
c943a3f192 Refactor TokenStream (and some minor changes to Token) (#4695)
* Refactor TokenStream (and some minor changes to Token)

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

* Tidy up lexer tests

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-10 14:26:53 +13:00
6aa588f09f Bug: KCL formatter removes 'fn' from closures: (#4718)
# Problem

Before this PR, our formatter reformats
```
squares_out = reduce(arr, 0, fn (i, squares)  {
  return 1
})
```
to 
```
squares_out = reduce(arr, 0, (i, squares) {
  return 1
})
```
i.e. it removes the `fn` keyword from the closure. This keyword is required, so, our formatter turned working code into invalid code.

# Cause

When this closure parameter is formatted, the ExprContext is ::Decl, so `Expr::recast` skips adding the `fn` keyword. The reason it's ::Decl is because the `squares_out = ` declaration sets it, and no subsequent call sets the context to something else.

# Solution

When recasting a call expression, set the context for every argument to `ExprContext::Other`.
2024-12-09 19:13:49 -06:00
59a6333aad Fix nightly release page link in bottom bar (#4714)
* Fix nightly release page link in bottom bar
Fixes #4713

* Working now

* Cleaner

* Add IS_NIGHTLY var to make these checks more consistent

* Review from Kevin

* Update url for settings content too
2024-12-10 00:43:11 +00:00
403f1507ae Update snapshots (#4724) 2024-12-10 00:32:00 +00:00
eac7b83504 Rework the walk module a bit (#4707)
* start a rework of the walk module

I'd like to have the code explicitly recurse in the visitor rather than
implicitly. This allows the calling code to build up an idea of the
depth of each node, or skip parts of the AST entirely.

i'm going to rebuild this enough to get the old API to work before
looking to merge this.

I've also discovered a number of new AST types that were just papered
over and/or excluded entirely -- I'm going to have to go through and
make sure that both Digest and the walker are still current, or if we
silently added types or, more likely, fields getting ignored.
2024-12-10 00:23:53 +00:00
667500d1b9 Bug: Setting mouse controls to OnShape or AutoCAD resets to default (#4681)
* Bugfix: Setting mouse controls to OnShape resets to KittyCAD
Fixes #4639

* Revert "Bugfix: Setting mouse controls to OnShape resets to KittyCAD"

This reverts commit c95878d617.

* Add cases for values without underscores

* Fix lint

* Pass yarn tsc and yarn fmt-check
2024-12-09 19:16:29 -05:00
b15aac9f48 Move length and named value constraint flows into command palette (#4675)
* Extend KCL argument input

* Migrate length constraint to be a command

* Add ability for `kcl` arguments to provide an initial variable name

* Move named variable flow into command palette

* Fix one e2e test

* Remove unwanted `ZERO` behavior when length constraint has no `variableName`

* Fix issue with `getSelectionCountByType` with sketches not yet in artifactGraph

* Update broken constraint tests

* Look at this (photo)Graph *in the voice of Nickelback*

* Fix segment overlays tests, which had out-of-date selectors

* Return early from `useConvertToVariable` if no selectionRanges

* Fixup for review comment from #4677 (#4696)

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

* Invalidate nightly bucket files after publish (#4627)

* Invalidate nightly bucket files after publish

* Fix conflict resolution

* Add some more warnings (#4697)

* Add installation instructions for all platforms (#4592)

* Add installation instructions for all platforms
Fixes #4511

* Typo

* Typo2

* Improve linux instructions, thanks @TomPridham

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>

---------

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>

* Bump node to v22.12.0 (LTS) (#4706)

* Point-and-click Shell (#4666)

* WIP: experimenting with Loft UI
Relates to #4470

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Add selection guard

* Working loft for two sketches in the right hardcoded order

* First pass at handling more than 2 sketches

* WIP selections

* WIP selections

* More checks

* Appends the loft line after the 'last' sketch in the code

* Clean up

* Enable multiple selections after the button click

* First point-click loft test (not working locally, loft gets inserted at the wrong place)

* Lint

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

* Clean up and working pw test

* Add test for doesSceneHaveSweepableSketch with count = 2

* Clean up loftSketches function

* Add pw test for preselected sketches

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

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

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Move to fromPromise-based Actor

* Move error logic out of loftSketches, fix pw tests

* Remove comments

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

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

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Fix typo

* Revert snapshots

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

* WIP: initial shell code addition

* Rollback pw values to pre cam change

* WIP: more additions

* WIP: closer

* WIP: first time working shell mod

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

* Add extrude lookup for more generic shell

* Handle walls

* Add pw tests for cap shell

* Add shell wall test

* Fix lint

* Add selection guard and clean up

* Lint fix

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

* WIP mutliple faces

* WIP circular dep

* Lint

* Look at this (photo)Graph *in the voice of Nickelback*

* Trigger CI

* Working multi-face shell across types

* Cap and wall pw test

* Apply suggestions from Frank's review

Co-authored-by: Frank Noirot <frank@zoo.dev>

* Fix test annotations

* Add unit tests for doesSceneHaveExtrudedSketch

* Manual resolution of snapshot conflicts

* Fix assertParse

* Updated pathToNode construct

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>

* More aggressive using of cache on engine settings changes (#4691)

* move around the files for cache to better localtions

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

* updates

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

* cleanup

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

* updates

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

* updates

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

* updates

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

* updates

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

* udpates

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>

* cleanup

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

* updates

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

* ensure we can change the grid setting via the command bar

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

* pass thru all setttings

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* fix playwright test

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

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* emoty

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix use of `as`

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-12-09 21:43:58 +00:00
54153aa646 fix: replace whitespace with a - so that ids are valid and scroll to works (#4710)
Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
2024-12-09 16:10:33 -05:00
943cf21d34 More aggressive using of cache on engine settings changes (#4691)
* move around the files for cache to better localtions

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

* updates

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

* cleanup

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

* updates

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

* updates

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

* updates

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

* updates

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

* udpates

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>

* cleanup

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

* updates

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

* ensure we can change the grid setting via the command bar

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

* pass thru all setttings

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* fix playwright test

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

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* emoty

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-09 21:06:53 +00:00
5a6728c45a Point-and-click Shell (#4666)
* WIP: experimenting with Loft UI
Relates to #4470

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Add selection guard

* Working loft for two sketches in the right hardcoded order

* First pass at handling more than 2 sketches

* WIP selections

* WIP selections

* More checks

* Appends the loft line after the 'last' sketch in the code

* Clean up

* Enable multiple selections after the button click

* First point-click loft test (not working locally, loft gets inserted at the wrong place)

* Lint

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

* Clean up and working pw test

* Add test for doesSceneHaveSweepableSketch with count = 2

* Clean up loftSketches function

* Add pw test for preselected sketches

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

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

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Move to fromPromise-based Actor

* Move error logic out of loftSketches, fix pw tests

* Remove comments

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

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

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Fix typo

* Revert snapshots

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

* WIP: initial shell code addition

* Rollback pw values to pre cam change

* WIP: more additions

* WIP: closer

* WIP: first time working shell mod

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

* Add extrude lookup for more generic shell

* Handle walls

* Add pw tests for cap shell

* Add shell wall test

* Fix lint

* Add selection guard and clean up

* Lint fix

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

* WIP mutliple faces

* WIP circular dep

* Lint

* Look at this (photo)Graph *in the voice of Nickelback*

* Trigger CI

* Working multi-face shell across types

* Cap and wall pw test

* Apply suggestions from Frank's review

Co-authored-by: Frank Noirot <frank@zoo.dev>

* Fix test annotations

* Add unit tests for doesSceneHaveExtrudedSketch

* Manual resolution of snapshot conflicts

* Fix assertParse

* Updated pathToNode construct

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-12-09 20:20:48 +00:00
ff2103d493 Bump node to v22.12.0 (LTS) (#4706) 2024-12-09 13:37:01 -05:00
2dfa8f2176 Add installation instructions for all platforms (#4592)
* Add installation instructions for all platforms
Fixes #4511

* Typo

* Typo2

* Improve linux instructions, thanks @TomPridham

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>

---------

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
2024-12-09 12:30:24 -05:00
29ed330326 Add some more warnings (#4697) 2024-12-10 06:27:04 +13:00
ca2cc825a6 Invalidate nightly bucket files after publish (#4627)
* Invalidate nightly bucket files after publish

* Fix conflict resolution
2024-12-09 11:45:32 -05:00
83fe1b7ce0 Fixup for review comment from #4677 (#4696)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-09 10:42:03 -05:00
157b76cc78 KCL refactor: combine two fields into one struct (#4689) 2024-12-06 19:11:31 -06:00
cf957d880e KCL refactor: Fix argument names (#4690)
Does not change behaviour. It just clarifies whether JS is passing a string containing KCL source code, or containing a JSON-stringified KCL AST.
2024-12-06 17:20:06 -06:00
dfc3d19677 remove clearScene from TS side (#4684)
* updates

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

* fix lint

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

* add failing tests

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

* more tests

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

* the scene is cleared

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

* create new clear scene and bust cache function from rust side

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

* pull thru

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

* set that we switched files

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

* updates

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

* updates

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

* updates

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

* fix two dirties

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

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-06 22:56:53 +00:00
dd370a9365 AST: Allow unlabeled kw args (#4686)
When declaring a function, its first parameter is allowed to be prefixed with `@`. This means that when users call this function, they don't have to label this argument.

Only the first parameter is allowed this prefix, no others.

Part of https://github.com/KittyCAD/modeling-app/issues/4600
2024-12-06 15:44:39 -06:00
2274d6459c Bump @types/react-dom from 18.3.0 to 18.3.1 (#4411)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.3.0 to 18.3.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2024-12-06 15:21:28 -06:00
32ce857119 Update bracket KCL variable syntax in onboarding (#4685) 2024-12-06 16:04:17 -05:00
88b51da417 Run external contributor branch through CI (#4679)
* fix: make variable declaration errors Cut instead of Backtrace

* fix: clippy, move comma to empty case and add test

* fix: add missing TokenType case

* fix: incorrect fn args after merge

* fix: clippy lint

* fix: update error message being looked for in e2e test

---------

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-06 14:12:20 -05:00
30d365aeb3 Module/import upgrades (#4677)
* Parse more import syntax

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

* Remove unnecessary Vec from VariableDeclaration

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

* Parse export import

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

* Factor out an execution module

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

* imports: constants, globs, export import

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

* test fixups

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-06 13:16:04 -05:00
7af62399ac Change vite-plugin-eslint to maintained package (#4645)
* Change vite-plugin-eslint to maintained package

* Add .eslintcache to ignores
2024-12-06 12:28:58 -05:00
332 changed files with 111594 additions and 106451 deletions

View File

@ -365,7 +365,7 @@ jobs:
- name: Set more complete nightly release notes - name: Set more complete nightly release notes
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
run: | run: |
# Note: prefered going this way instead of a full clone in the checkout step, # Note: preferred going this way instead of a full clone in the checkout step,
# see https://github.com/actions/checkout/issues/1471 # see https://github.com/actions/checkout/issues/1471
git fetch --prune --unshallow --tags git fetch --prune --unshallow --tags
export TAG="nightly-${VERSION}" export TAG="nightly-${VERSION}"
@ -394,6 +394,10 @@ jobs:
parent: false parent: false
destination: 'dl.kittycad.io/releases/modeling-app/nightly' destination: 'dl.kittycad.io/releases/modeling-app/nightly'
- name: Invalidate bucket cache on latest*.yml and last_download.json files
if: ${{ env.IS_NIGHTLY == 'true' }}
run: yarn files:invalidate-bucket:nightly
- name: Tag nightly commit - name: Tag nightly commit
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
uses: actions/github-script@v7 uses: actions/github-script@v7

View File

@ -126,11 +126,7 @@ jobs:
destination: 'dl.kittycad.io/releases/modeling-app' destination: 'dl.kittycad.io/releases/modeling-app'
- name: Invalidate bucket cache on latest*.yml and last_download.json files - name: Invalidate bucket cache on latest*.yml and last_download.json files
run: | run: yarn files:invalidate-bucket
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/last_download.json" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-linux-arm64.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async
- name: Upload release files to Github - name: Upload release files to Github
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}

1
.gitignore vendored
View File

@ -61,6 +61,7 @@ Mac_App_Distribution.provisionprofile
*.tsbuildinfo *.tsbuildinfo
src/wasm-lib/pkg src/wasm-lib/pkg
.eslintcache
venv venv
.vite/ .vite/

2
.nvmrc
View File

@ -1 +1 @@
v21.7.3 v22.12.0

43
INSTALL.md Normal file
View File

@ -0,0 +1,43 @@
# Setting Up Zoo Modeling App
Compared to other CAD software, getting Zoo Modeling App up and running is quick and straightforward across platforms. It's about 100MB to download and is quick to install.
## Windows
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Windows and for your processor type.
2. Once downloaded, run the installer `Zoo Modeling App-{version}-{arch}-win.exe` which should take a few seconds.
3. The installation happens at `C:\Program Files\Zoo Modeling App`. A shortcut in the start menu is also created so you can run the app easily by clicking on it.
## macOS
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for macOS and for your processor type.
2. Once downloaded, open the disk image `Zoo Modeling App-{version}-{arch}-mac.dmg` and drag the applications to your `Applications` directory.
3. You can then open your `Applications` directory and double-click on `Zoo Modeling App` to open.
## Linux
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Linux and for your processor type.
2. Install the dependencies needed to run the [AppImage format](https://appimage.org/).
- On Ubuntu, install the FUSE library with these commands in a terminal.
```bash
sudo apt update
sudo apt install libfuse2
```
- Optionally, follow [these steps](https://github.com/probonopd/go-appimage/blob/master/src/appimaged/README.md#initial-setup) to install `appimaged`. It is a daemon that makes interacting with AppImage files more seamless.
- Once installed, copy the downloaded `Zoo Modeling App-{version}-{arch}-linux.AppImage` to the directory of your choice, for instance `~/Applications`.
- `appimaged` should automatically find it and make it executable. If not, run:
```bash
chmod a+x ~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
```
3. You can double-click on the AppImage to run it, or in a terminal with this command:
```bash
~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
```

File diff suppressed because one or more lines are too long

View File

@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
```js ```js
r = 10 // radius r = 10 // radius
// Call `map`, using an anonymous function instead of a named one. // Call `map`, using an anonymous function instead of a named one.
circles = map([1..3], (id) { circles = map([1..3], fn(id) {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
}) })

View File

@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
// an anonymous `add` function as its parameter, instead of declaring a // an anonymous `add` function as its parameter, instead of declaring a
// named function outside. // named function outside.
arr = [1, 2, 3] arr = [1, 2, 3]
sum = reduce(arr, 0, (i, result_so_far) { sum = reduce(arr, 0, fn(i, result_so_far) {
return i + result_so_far return i + result_so_far
}) })
@ -84,7 +84,7 @@ fn decagon(radius) {
// Use a `reduce` to draw the remaining decagon sides. // Use a `reduce` to draw the remaining decagon sides.
// For each number in the array 1..10, run the given function, // For each number in the array 1..10, run the given function,
// which takes a partially-sketched decagon and adds one more edge to it. // which takes a partially-sketched decagon and adds one more edge to it.
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) { fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
// Draw one edge of the decagon. // Draw one edge of the decagon.
x = cos(stepAngle * i) * radius x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius y = sin(stepAngle * i) * radius

View File

@ -78970,7 +78970,7 @@
"model = import(\"tests/inputs/cube.gltf\")", "model = import(\"tests/inputs/cube.gltf\")",
"model = import(\"tests/inputs/cube.sldprt\")", "model = import(\"tests/inputs/cube.sldprt\")",
"model = import(\"tests/inputs/cube.step\")", "model = import(\"tests/inputs/cube.step\")",
"import height, buildSketch from 'common.kcl'\n\nplane = 'XZ'\nmargin = 2\ns1 = buildSketch(plane, [0, 0])\ns2 = buildSketch(plane, [0, height() + margin])" "import height, buildSketch from \"common.kcl\"\n\nplane = 'XZ'\nmargin = 2\ns1 = buildSketch(plane, [0, 0])\ns2 = buildSketch(plane, [0, height() + margin])"
] ]
}, },
{ {
@ -100436,7 +100436,7 @@
"deprecated": false, "deprecated": false,
"examples": [ "examples": [
"r = 10 // radius\nfn drawCircle(id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n}\n\n// Call `drawCircle`, passing in each element of the array.\n// The outputs from each `drawCircle` form a new array,\n// which is the return value from `map`.\ncircles = map([1..3], drawCircle)", "r = 10 // radius\nfn drawCircle(id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n}\n\n// Call `drawCircle`, passing in each element of the array.\n// The outputs from each `drawCircle` form a new array,\n// which is the return value from `map`.\ncircles = map([1..3], drawCircle)",
"r = 10 // radius\n// Call `map`, using an anonymous function instead of a named one.\ncircles = map([1..3], (id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n})" "r = 10 // radius\n// Call `map`, using an anonymous function instead of a named one.\ncircles = map([1..3], fn(id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n})"
] ]
}, },
{ {
@ -146129,8 +146129,8 @@
"deprecated": false, "deprecated": false,
"examples": [ "examples": [
"// This function adds two numbers.\nfn add(a, b) {\n return a + b\n}\n\n// This function adds an array of numbers.\n// It uses the `reduce` function, to call the `add` function on every\n// element of the `arr` parameter. The starting value is 0.\nfn sum(arr) {\n return reduce(arr, 0, add)\n}\n\n/* The above is basically like this pseudo-code:\nfn sum(arr):\n let sumSoFar = 0\n for i in arr:\n sumSoFar = add(sumSoFar, i)\n return sumSoFar */\n\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum([1, 2, 3]), 6, 0.00001, \"1 + 2 + 3 summed is 6\")", "// This function adds two numbers.\nfn add(a, b) {\n return a + b\n}\n\n// This function adds an array of numbers.\n// It uses the `reduce` function, to call the `add` function on every\n// element of the `arr` parameter. The starting value is 0.\nfn sum(arr) {\n return reduce(arr, 0, add)\n}\n\n/* The above is basically like this pseudo-code:\nfn sum(arr):\n let sumSoFar = 0\n for i in arr:\n sumSoFar = add(sumSoFar, i)\n return sumSoFar */\n\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum([1, 2, 3]), 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
"// This example works just like the previous example above, but it uses\n// an anonymous `add` function as its parameter, instead of declaring a\n// named function outside.\narr = [1, 2, 3]\nsum = reduce(arr, 0, (i, result_so_far) {\n return i + result_so_far\n})\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum, 6, 0.00001, \"1 + 2 + 3 summed is 6\")", "// This example works just like the previous example above, but it uses\n// an anonymous `add` function as its parameter, instead of declaring a\n// named function outside.\narr = [1, 2, 3]\nsum = reduce(arr, 0, fn(i, result_so_far) {\n return i + result_so_far\n})\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum, 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
"// Declare a function that sketches a decagon.\nfn decagon(radius) {\n // Each side of the decagon is turned this many degrees from the previous angle.\n stepAngle = 1 / 10 * tau()\n\n // Start the decagon sketch at this point.\n startOfDecagonSketch = startSketchAt([cos(0) * radius, sin(0) * radius])\n\n // Use a `reduce` to draw the remaining decagon sides.\n // For each number in the array 1..10, run the given function,\n // which takes a partially-sketched decagon and adds one more edge to it.\n fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {\n // Draw one edge of the decagon.\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n return lineTo([x, y], partialDecagon)\n })\n\n return fullDecagon\n}\n\n/* The `decagon` above is basically like this pseudo-code:\nfn decagon(radius):\n let stepAngle = (1/10) * tau()\n let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])\n\n // Here's the reduce part.\n let partialDecagon = startOfDecagonSketch\n for i in [1..10]:\n let x = cos(stepAngle * i) * radius\n let y = sin(stepAngle * i) * radius\n partialDecagon = lineTo([x, y], partialDecagon)\n fullDecagon = partialDecagon // it's now full\n return fullDecagon */\n\n\n// Use the `decagon` function declared above, to sketch a decagon with radius 5.\ndecagon(5.0)\n |> close(%)" "// Declare a function that sketches a decagon.\nfn decagon(radius) {\n // Each side of the decagon is turned this many degrees from the previous angle.\n stepAngle = 1 / 10 * tau()\n\n // Start the decagon sketch at this point.\n startOfDecagonSketch = startSketchAt([cos(0) * radius, sin(0) * radius])\n\n // Use a `reduce` to draw the remaining decagon sides.\n // For each number in the array 1..10, run the given function,\n // which takes a partially-sketched decagon and adds one more edge to it.\n fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {\n // Draw one edge of the decagon.\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n return lineTo([x, y], partialDecagon)\n })\n\n return fullDecagon\n}\n\n/* The `decagon` above is basically like this pseudo-code:\nfn decagon(radius):\n let stepAngle = (1/10) * tau()\n let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])\n\n // Here's the reduce part.\n let partialDecagon = startOfDecagonSketch\n for i in [1..10]:\n let x = cos(stepAngle * i) * radius\n let y = sin(stepAngle * i) * radius\n partialDecagon = lineTo([x, y], partialDecagon)\n fullDecagon = partialDecagon // it's now full\n return fullDecagon */\n\n\n// Use the `decagon` function declared above, to sketch a decagon with radius 5.\ndecagon(5.0)\n |> close(%)"
] ]
}, },
{ {

View File

@ -458,8 +458,8 @@ test.describe('Editor tests', () => {
/* add the following code to the editor ($ error is not a valid line) /* add the following code to the editor ($ error is not a valid line)
$ error $ error
const topAng = 30 topAng = 30
const bottomAng = 25 bottomAng = 25
*/ */
await u.codeLocator.click() await u.codeLocator.click()
await page.keyboard.type('$ error') await page.keyboard.type('$ error')
@ -474,12 +474,14 @@ test.describe('Editor tests', () => {
await page.keyboard.type('bottomAng = 25') await page.keyboard.type('bottomAng = 25')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// error in guter // error in gutter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token: $').first()).toBeVisible() await expect(
page.getByText('Tag names must not be empty').first()
).toBeVisible()
// select the line that's causing the error and delete it // select the line that's causing the error and delete it
await page.getByText('$ error').click() await page.getByText('$ error').click()

View File

@ -7,6 +7,7 @@ export class ToolbarFixture {
extrudeButton!: Locator extrudeButton!: Locator
loftButton!: Locator loftButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
@ -28,6 +29,7 @@ export class ToolbarFixture {
this.page = page this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft') this.loftButton = page.getByTestId('loft')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')

View File

@ -768,3 +768,168 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
}) })
}) })
}) })
const shellPointAndClickCapCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
]
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
app,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
extrude001 = extrude(30, sketch001)
`
await app.initialise(initialCode)
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration =
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
})
if (!shouldPreselect) {
await test.step(`Go through the command bar flow without preselected faces`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnCap()
await app.page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
} else {
await test.step(`Preselect the cap`, async () => {
await clickOnCap()
await app.page.waitForTimeout(500)
})
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
await toolbar.shellButton.click()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
}
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [shellDeclaration],
highlightedCode: '',
})
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
})
})
})
test('Shell point-and-click wall', async ({
app,
page,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-20, 20], %)
|> xLine(40, %)
|> yLine(-60, %)
|> xLine(-40, %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(40, sketch001)
`
await app.initialise(initialCode)
// One dumb hardcoded screen pixel value
const testPoint = { x: 580, y: 180 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
const mutatedCode = 'xLine(-40, %, $seg01)'
const shellDeclaration =
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
const formattedOutLastLine = '}, extrude001)'
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
})
await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnCap()
await page.keyboard.down('Shift')
await clickOnWall()
await app.page.waitForTimeout(500)
await page.keyboard.up('Shift')
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap, 1 face',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(mutatedCode)
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [formattedOutLastLine],
highlightedCode: '',
})
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
})
})

View File

@ -136,6 +136,335 @@ test(
} }
) )
test(
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
const errorDir = join(dir, 'broken-code')
await fsp.mkdir(errorDir, { recursive: true })
await fsp.copyFile(
executorInputPath('broken-code-test.kcl'),
join(errorDir, 'main.kcl')
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('broken-code')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening broken code project should clear the scene and show the error', async () => {
// Go back home.
await expect(page.getByText('broken-code')).toBeVisible()
await page.getByText('broken-code').click()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
const emptyDir = join(dir, 'empty')
await fsp.mkdir(emptyDir, { recursive: true })
await fsp.writeFile(join(emptyDir, 'main.kcl'), '')
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening empty code project should clear the scene', async () => {
// Go back home.
await expect(page.getByText('empty')).toBeVisible()
await page.getByText('empty').click()
// Ensure the code is empty.
await expect(u.codeLocator).toContainText('')
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
// planes colors means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open empty file, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.writeFile(join(bracketDir, 'empty.kcl'), '')
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('creating a empty file should clear the scene', async () => {
// open the file pane.
await page.getByTestId('files-pane-button').click()
// OPen the other file.
const file = page.getByRole('button', { name: 'empty.kcl' })
await expect(file).toBeVisible()
await file.click()
// planes colors means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
timeout: 10_000,
})
.toBeLessThan(15)
// Ensure the code is empty.
await expect(u.codeLocator).toContainText('')
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open another file in the same project with errors, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('broken-code-test.kcl'),
join(bracketDir, 'broken-code-test.kcl')
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('opening broken code file should clear the scene and show the error', async () => {
// open the file pane.
await page.getByTestId('files-pane-button').click()
// OPen the other file.
const file = page.getByRole('button', { name: 'broken-code-test.kcl' })
await expect(file).toBeVisible()
await file.click()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test( test(
'when code with error first loads you get errors in console', 'when code with error first loads you get errors in console',
{ tag: '@electron' }, { tag: '@electron' },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -26,7 +26,17 @@ test.describe('Testing constraints', () => {
}) })
const u = await getUtils(page) const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio // constants and locators
const lengthValue = {
old: '20',
new: '25',
}
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -36,26 +46,26 @@ test.describe('Testing constraints', () => {
await u.closeDebugPanel() await u.closeDebugPanel()
// Click the line of code for line. // Click the line of code for line.
await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick() // TODO remove this and reinstate `await topHorzSegmentClick()`
await page.getByText(`line([0, ${lengthValue.old}], %)`).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// enter sketch again // enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500) // wait for animation await page.waitForTimeout(500) // wait for animation
const startXPx = 500
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.down('Shift')
await page.mouse.click(834, 244)
await page.keyboard.up('Shift')
await page await page
.getByRole('button', { name: 'dimension Length', exact: true }) .getByRole('button', { name: 'dimension Length', exact: true })
.click() .click()
await page.getByText('Add constraining value').click() await expect(cmdBarKclInput).toHaveText('20')
await cmdBarKclInput.fill(lengthValue.new)
await expect(
page.getByText(`Can't calculate`),
`Something went wrong with the KCL expression evaluation`
).not.toBeVisible()
await cmdBarSubmitButton.click()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`length001 = 20sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)` `length001 = ${lengthValue.new}sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
) )
// Make sure we didn't pop out of sketch mode. // Make sure we didn't pop out of sketch mode.
@ -66,7 +76,6 @@ test.describe('Testing constraints', () => {
await page.waitForTimeout(500) // wait for animation await page.waitForTimeout(500) // wait for animation
// Exit sketch // Exit sketch
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect( await expect(
page.getByRole('button', { name: 'Exit Sketch' }) page.getByRole('button', { name: 'Exit Sketch' })
@ -524,7 +533,7 @@ part002 = startSketchOn('XZ')
}) })
} }
}) })
test.describe('Test Angle/Length constraint single selection', () => { test.describe('Test Angle constraint single selection', () => {
const cases = [ const cases = [
{ {
testName: 'Angle - Add variable', testName: 'Angle - Add variable',
@ -538,18 +547,6 @@ part002 = startSketchOn('XZ')
constraint: 'angle', constraint: 'angle',
value: '83, 78.33', value: '83, 78.33',
}, },
{
testName: 'Length - Add variable',
addVariable: true,
constraint: 'length',
value: '83, length001',
},
{
testName: 'Length - No variable',
addVariable: false,
constraint: 'length',
value: '83, 78.33',
},
] as const ] as const
for (const { testName, addVariable, value, constraint } of cases) { for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ page }) => { test(`${testName}`, async ({ page }) => {
@ -608,6 +605,90 @@ part002 = startSketchOn('XZ')
}) })
} }
}) })
test.describe('Test Length constraint single selection', () => {
const cases = [
{
testName: 'Length - Add variable',
addVariable: true,
constraint: 'length',
value: '83, length001',
},
{
testName: 'Length - No variable',
addVariable: false,
constraint: 'length',
value: '83, 78.33',
},
] as const
for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ page }) => {
// constants and locators
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarKclVariableNameInput =
page.getByPlaceholder('Variable name')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line3 = await u.getSegmentBodyCoords(
`[data-overlay-index="${2}"]`
)
await page.mouse.click(line3.x, line3.y)
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page.getByTestId('dropdown-constraint-' + constraint).click()
if (!addVariable) {
await test.step(`Clear the variable input`, async () => {
await cmdBarKclVariableNameInput.clear()
await cmdBarKclVariableNameInput.press('Backspace')
})
}
await expect(cmdBarKclInput).toHaveText('78.33')
await cmdBarSubmitButton.click()
const changedCode = `|> angledLine([${value}], %)`
await expect(page.locator('.cm-content')).toContainText(changedCode)
// checking active assures the cursor is where it should be
await expect(page.locator('.cm-activeLine')).toHaveText(changedCode)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
}
})
test.describe('Many segments - no modal constraints', () => { test.describe('Many segments - no modal constraints', () => {
const cases = [ const cases = [
{ {
@ -868,6 +949,15 @@ part002 = startSketchOn('XZ')
|> line([3.13, -2.4], %)` |> line([3.13, -2.4], %)`
) )
}) })
// constants and locators
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -928,8 +1018,8 @@ part002 = startSketchOn('XZ')
// await page.getByRole('button', { name: 'length', exact: true }).click() // await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByTestId('dropdown-constraint-length').click() await page.getByTestId('dropdown-constraint-length').click()
await page.getByLabel('length Value').fill('10') await cmdBarKclInput.fill('10')
await page.getByRole('button', { name: 'Add constraining value' }).click() await cmdBarSubmitButton.click()
activeLinesContent = await page.locator('.cm-activeLine').all() activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`) await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)

View File

@ -91,7 +91,14 @@ test.describe('Testing segment overlays', () => {
await page.getByTestId('constraint-symbol-popover').count() await page.getByTestId('constraint-symbol-popover').count()
).toBeGreaterThan(0) ).toBeGreaterThan(0)
await unconstrainedLocator.click() await unconstrainedLocator.click()
await page.getByText('Add variable').click() await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await expect(page.locator('.cm-content')).toContainText(expectFinal) await expect(page.locator('.cm-content')).toContainText(expectFinal)
} }
@ -151,7 +158,14 @@ test.describe('Testing segment overlays', () => {
await page.getByTestId('constraint-symbol-popover').count() await page.getByTestId('constraint-symbol-popover').count()
).toBeGreaterThan(0) ).toBeGreaterThan(0)
await unconstrainedLocator.click() await unconstrainedLocator.click()
await page.getByText('Add variable').click() await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText(
expectAfterUnconstrained expectAfterUnconstrained
) )

View File

@ -81,6 +81,7 @@
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000", "simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver:bg": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:bg": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver:stop": "kill-port 3000",
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh", "fetch:wasm": "./get-latest-wasm-bundle.sh",
@ -95,6 +96,8 @@
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh", "files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh", "files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
"postinstall": "yarn fetch:samples && yarn xstate:typegen && ./node_modules/.bin/electron-rebuild", "postinstall": "yarn fetch:samples && yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"", "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev", "make:dev": "make dev",
@ -158,6 +161,7 @@
"@electron/rebuild": "^3.6.0", "@electron/rebuild": "^3.6.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1", "@lezer/generator": "^1.7.1",
"@nabla/vite-plugin-eslint": "^2.0.5",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.46.1",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2", "@testing-library/react": "^15.0.2",
@ -170,7 +174,7 @@
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4", "@types/react": "^18.3.4",
"@types/react-dom": "^18.2.25", "@types/react-dom": "^18.3.1",
"@types/react-modal": "^3.16.3", "@types/react-modal": "^3.16.3",
"@types/three": "^0.163.0", "@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
@ -207,7 +211,6 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^5.4.6", "vite": "^5.4.6",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",

View File

@ -0,0 +1,11 @@
#!/bin/bash
base_dir="/releases/modeling-app"
if [[ $1 = "--nightly" ]]; then
base_dir="/releases/modeling-app/nightly"
fi
echo "Invalidating json and yml files at $base_dir in the download bucket"
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/last_download.json" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/latest-linux-arm64.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/latest-mac.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/latest.yml" --async

View File

@ -200,7 +200,10 @@ function CoreDump() {
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []
) )
useHotkeyWrapper(['mod + shift + .'], () => { // TODO: revisit once progress is made on upstream issue
// https://github.com/JohannesKlauss/react-hotkeys-hook/issues/1064
// const hotkey = process.platform !== 'linux' ? 'mod + shift + .' : 'mod + shift + >'
useHotkeyWrapper(['mod + shift + .', 'mod + shift + >'], () => {
toast toast
.promise( .promise(
coreDump(coreDumpManager, true), coreDump(coreDumpManager, true),

View File

@ -505,7 +505,8 @@ const ConstraintSymbol = ({
constrainInfo: ConstrainInfo constrainInfo: ConstrainInfo
verticalPosition: 'top' | 'bottom' verticalPosition: 'top' | 'bottom'
}) => { }) => {
const { context, send } = useModelingContext() const { commandBarSend } = useCommandsContext()
const { context } = useModelingContext()
const varNameMap: { const varNameMap: {
[key in ConstrainInfo['type']]: { [key in ConstrainInfo['type']]: {
varName: string varName: string
@ -624,11 +625,18 @@ const ConstraintSymbol = ({
// disabled={implicitDesc} TODO why does this change styles that are hard to override? // disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={toSync(async () => { onClick={toSync(async () => {
if (!isConstrained) { if (!isConstrained) {
send({ commandBarSend({
type: 'Convert to variable', type: 'Find and select command',
data: { data: {
name: 'Constrain with named value',
groupId: 'modeling',
argDefaultValues: {
currentValue: {
pathToNode, pathToNode,
variableName: varName, variableName: varName,
valueText: value,
},
},
}, },
}) })
} else if (isConstrained) { } else if (isConstrained) {

View File

@ -701,8 +701,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = const variableDeclarationName = _node1.node?.declaration.id?.name || ''
_node1.node?.declarations?.[0]?.id?.name || ''
const sg = sketchFromKclValue( const sg = sketchFromKclValue(
kclManager.programMemory.get(variableDeclarationName), kclManager.programMemory.get(variableDeclarationName),
@ -902,10 +901,9 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = const variableDeclarationName = _node1.node?.declaration.id?.name || ''
_node1.node?.declarations?.[0]?.id?.name || '' const startSketchOn = _node1.node?.declaration
const startSketchOn = _node1.node?.declarations const startSketchOnInit = startSketchOn?.init
const startSketchOnInit = startSketchOn?.[0]?.init
const tags: [string, string, string] = [ const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'), findUniqueName(_ast, 'rectangleSegmentA'),
@ -913,7 +911,7 @@ export class SceneEntities {
findUniqueName(_ast, 'rectangleSegmentC'), findUniqueName(_ast, 'rectangleSegmentC'),
] ]
startSketchOn[0].init = createPipeExpression([ startSketchOn.init = createPipeExpression([
startSketchOnInit, startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags), ...getRectangleCallExpressions(rectangleOrigin, tags),
]) ])
@ -943,7 +941,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return Promise.reject(_node) if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0] const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1] const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
@ -992,7 +990,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
if (sketchInit.type !== 'PipeExpression') { if (sketchInit.type !== 'PipeExpression') {
return return
@ -1058,10 +1056,9 @@ export class SceneEntities {
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
// startSketchOn already exists // startSketchOn already exists
const variableDeclarationName = const variableDeclarationName = _node1.node?.declaration.id?.name || ''
_node1.node?.declarations?.[0]?.id?.name || '' const startSketchOn = _node1.node?.declaration
const startSketchOn = _node1.node?.declarations const startSketchOnInit = startSketchOn?.init
const startSketchOnInit = startSketchOn?.[0]?.init
const tags: [string, string, string] = [ const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'), findUniqueName(_ast, 'rectangleSegmentA'),
@ -1069,7 +1066,7 @@ export class SceneEntities {
findUniqueName(_ast, 'rectangleSegmentC'), findUniqueName(_ast, 'rectangleSegmentC'),
] ]
startSketchOn[0].init = createPipeExpression([ startSketchOn.init = createPipeExpression([
startSketchOnInit, startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags), ...getRectangleCallExpressions(rectangleOrigin, tags),
]) ])
@ -1099,7 +1096,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return Promise.reject(_node) if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0] const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1] const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
@ -1155,7 +1152,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
if (sketchInit.type === 'PipeExpression') { if (sketchInit.type === 'PipeExpression') {
updateCenterRectangleSketch( updateCenterRectangleSketch(
@ -1224,12 +1221,11 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = const variableDeclarationName = _node1.node?.declaration.id?.name || ''
_node1.node?.declarations?.[0]?.id?.name || '' const startSketchOn = _node1.node?.declaration
const startSketchOn = _node1.node?.declarations const startSketchOnInit = startSketchOn?.init
const startSketchOnInit = startSketchOn?.[0]?.init
startSketchOn[0].init = createPipeExpression([ startSketchOn.init = createPipeExpression([
startSketchOnInit, startSketchOnInit,
createCallExpressionStdLib('circle', [ createCallExpressionStdLib('circle', [
createObjectExpression({ createObjectExpression({
@ -1271,7 +1267,7 @@ export class SceneEntities {
) )
let modded = structuredClone(truncatedAst) let modded = structuredClone(truncatedAst)
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node.declaration.init
const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0] const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0]
const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1] const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1]
@ -1339,7 +1335,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
let modded = structuredClone(_ast) let modded = structuredClone(_ast)
if (sketchInit.type === 'PipeExpression') { if (sketchInit.type === 'PipeExpression') {
@ -2060,7 +2056,7 @@ function prepareTruncatedMemoryAndAst(
'VariableDeclaration' 'VariableDeclaration'
) )
if (err(_node)) return _node if (err(_node)) return _node
const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || '' const variableDeclarationName = _node.node?.declaration.id?.name || ''
const sg = sketchFromKclValue( const sg = sketchFromKclValue(
programMemory.get(variableDeclarationName), programMemory.get(variableDeclarationName),
variableDeclarationName variableDeclarationName
@ -2085,7 +2081,7 @@ function prepareTruncatedMemoryAndAst(
]) ])
} }
;( ;(
(_ast.body[bodyIndex] as VariableDeclaration).declarations[0] (_ast.body[bodyIndex] as VariableDeclaration).declaration
.init as PipeExpression .init as PipeExpression
).body.push(newSegment) ).body.push(newSegment)
// update source ranges to section we just added. // update source ranges to section we just added.
@ -2096,19 +2092,19 @@ function prepareTruncatedMemoryAndAst(
const updatedSrcRangeAst = pResult.program const updatedSrcRangeAst = pResult.program
const lastPipeItem = ( const lastPipeItem = (
(updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration) (updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration).declaration
.declarations[0].init as PipeExpression .init as PipeExpression
).body.slice(-1)[0] ).body.slice(-1)[0]
;( ;(
(_ast.body[bodyIndex] as VariableDeclaration).declarations[0] (_ast.body[bodyIndex] as VariableDeclaration).declaration
.init as PipeExpression .init as PipeExpression
).body.slice(-1)[0].start = lastPipeItem.start ).body.slice(-1)[0].start = lastPipeItem.start
_ast.end = lastPipeItem.end _ast.end = lastPipeItem.end
const varDec = _ast.body[bodyIndex] as Node<VariableDeclaration> const varDec = _ast.body[bodyIndex] as Node<VariableDeclaration>
varDec.end = lastPipeItem.end varDec.end = lastPipeItem.end
const declarator = varDec.declarations[0] const declarator = varDec.declaration
declarator.end = lastPipeItem.end declarator.end = lastPipeItem.end
const init = declarator.init as Node<PipeExpression> const init = declarator.init as Node<PipeExpression>
init.end = lastPipeItem.end init.end = lastPipeItem.end
@ -2145,7 +2141,7 @@ function prepareTruncatedMemoryAndAst(
if (node.type !== 'VariableDeclaration') { if (node.type !== 'VariableDeclaration') {
continue continue
} }
const name = node.declarations[0].id.name const name = node.declaration.id.name
const memoryItem = programMemory.get(name) const memoryItem = programMemory.get(name)
if (!memoryItem) { if (!memoryItem) {
continue continue

View File

@ -169,11 +169,11 @@ export function useCalc({
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__' a.declaration.id?.name === '__result__'
) )
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declaration.init
const result = execState.memory?.get('__result__')?.value const result = execState.memory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)

View File

@ -8,11 +8,16 @@ import { getSystemTheme } from 'lib/theme'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression' import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { varMentions } from 'lib/varCompletionExtension' import { varMentions } from 'lib/varCompletionExtension'
import { useEffect, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import styles from './CommandBarKclInput.module.css' import styles from './CommandBarKclInput.module.css'
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst' import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor' import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
import { useSelector } from '@xstate/react'
const machineContextSelector = (snapshot?: {
context: Record<string, unknown>
}) => snapshot?.context
function CommandBarKclInput({ function CommandBarKclInput({
arg, arg,
@ -31,12 +36,44 @@ function CommandBarKclInput({
arg.name arg.name
] as KclCommandValue | undefined ] as KclCommandValue | undefined
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const defaultValue = (arg.defaultValue as string) || '' const argMachineContext = useSelector(
arg.machineActor,
machineContextSelector
)
const defaultValue = useMemo(
() =>
arg.defaultValue
? arg.defaultValue instanceof Function
? arg.defaultValue(commandBarState.context, argMachineContext)
: arg.defaultValue
: '',
[arg.defaultValue, commandBarState.context, argMachineContext]
)
const initialVariableName = useMemo(() => {
// Use the configured variable name if it exists
if (arg.variableName !== undefined) {
return arg.variableName instanceof Function
? arg.variableName(commandBarState.context, argMachineContext)
: arg.variableName
}
// or derive it from the previously set value or the argument name
return previouslySetValue && 'variableName' in previouslySetValue
? previouslySetValue.variableName
: arg.name
}, [
arg.variableName,
commandBarState.context,
argMachineContext,
arg.name,
previouslySetValue,
])
const [value, setValue] = useState( const [value, setValue] = useState(
previouslySetValue?.valueText || defaultValue || '' previouslySetValue?.valueText || defaultValue || ''
) )
const [createNewVariable, setCreateNewVariable] = useState( const [createNewVariable, setCreateNewVariable] = useState(
previouslySetValue && 'variableName' in previouslySetValue (previouslySetValue && 'variableName' in previouslySetValue) ||
arg.createVariableByDefault ||
false
) )
const [canSubmit, setCanSubmit] = useState(true) const [canSubmit, setCanSubmit] = useState(true)
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
@ -52,10 +89,7 @@ function CommandBarKclInput({
isNewVariableNameUnique, isNewVariableNameUnique,
} = useCalculateKclExpression({ } = useCalculateKclExpression({
value, value,
initialVariableName: initialVariableName,
previouslySetValue && 'variableName' in previouslySetValue
? previouslySetValue.variableName
: arg.name,
}) })
const varMentionData: Completion[] = prevVariables.map((v) => ({ const varMentionData: Completion[] = prevVariables.map((v) => ({
label: v.key, label: v.key,

View File

@ -266,6 +266,7 @@ const FileTreeItem = ({
// Let the lsp servers know we closed a file. // Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null) onFileClose(currentFile?.path || null, project?.path || null)
onFileOpen(fileOrDir.path, project?.path || null) onFileOpen(fileOrDir.path, project?.path || null)
kclManager.switchedFiles = true
// Open kcl files // Open kcl files
navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`) navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`)

View File

@ -1,4 +1,4 @@
import { APP_VERSION } from 'routes/Settings' import { APP_VERSION, RELEASE_URL } from 'routes/Settings'
import { CustomIcon } from 'components/CustomIcon' import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
@ -72,10 +72,8 @@ export function LowerRightControls({
<menu className="flex items-center justify-end gap-3 pointer-events-auto"> <menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />} {!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a <a
onClick={openExternalBrowserIfDesktop( onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}` href={RELEASE_URL}
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={'!no-underline font-mono text-xs ' + linkOverrideClassName} className={'!no-underline font-mono text-xs ' + linkOverrideClassName}

View File

@ -41,7 +41,10 @@ import {
angleBetweenInfo, angleBetweenInfo,
applyConstraintAngleBetween, applyConstraintAngleBetween,
} from './Toolbar/SetAngleBetween' } from './Toolbar/SetAngleBetween'
import { applyConstraintAngleLength } from './Toolbar/setAngleLength' import {
applyConstraintAngleLength,
applyConstraintLength,
} from './Toolbar/setAngleLength'
import { import {
canSweepSelection, canSweepSelection,
handleSelectionBatch, handleSelectionBatch,
@ -51,6 +54,8 @@ import {
Selections, Selections,
updateSelections, updateSelections,
canLoftSelection, canLoftSelection,
canRevolveSelection,
canShellSelection,
} from 'lib/selections' } from 'lib/selections'
import { applyConstraintIntersect } from './Toolbar/Intersect' import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
@ -62,13 +67,15 @@ import {
getSketchOrientationDetails, getSketchOrientationDetails,
} from 'clientSideScene/sceneEntities' } from 'clientSideScene/sceneEntities'
import { import {
moveValueIntoNewVariablePath, insertNamedConstant,
replaceValueAtNodePath,
sketchOnExtrudedFace, sketchOnExtrudedFace,
sketchOnOffsetPlane, sketchOnOffsetPlane,
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { Program, parse, recast, resultIsOk } from 'lang/wasm' import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
import { import {
doesSceneHaveExtrudedSketch,
doesSceneHaveSweepableSketch, doesSceneHaveSweepableSketch,
getNodePathFromSourceRange, getNodePathFromSourceRange,
isSingleCursorInPipe, isSingleCursorInPipe,
@ -79,7 +86,6 @@ import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@codemirror/state' import { EditorSelection, Transaction } from '@codemirror/state'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, reportRejection, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager' import { modelingMachineEvent } from 'editor/manager'
@ -570,6 +576,26 @@ export const ModelingMachineProvider = ({
if (err(canSweep)) return false if (err(canSweep)) return false
return canSweep return canSweep
}, },
'has valid revolve selection': ({ context: { selectionRanges } }) => {
// A user can begin extruding if they either have 1+ faces selected or nothing selected
// TODO: I believe this guard only allows for extruding a single face at a time
const hasNoSelection =
selectionRanges.graphSelections.length === 0 ||
isRangeBetweenCharacters(selectionRanges) ||
isSelectionLastLine(selectionRanges, codeManager.code)
if (hasNoSelection) {
// they have no selection, we should enable the button
// so they can select the face through the cmdbar
// BUT only if there's extrudable geometry
return doesSceneHaveSweepableSketch(kclManager.ast)
}
if (!isSketchPipe(selectionRanges)) return false
const canSweep = canRevolveSelection(selectionRanges)
if (err(canSweep)) return false
return canSweep
},
'has valid loft selection': ({ context: { selectionRanges } }) => { 'has valid loft selection': ({ context: { selectionRanges } }) => {
const hasNoSelection = const hasNoSelection =
selectionRanges.graphSelections.length === 0 || selectionRanges.graphSelections.length === 0 ||
@ -585,6 +611,24 @@ export const ModelingMachineProvider = ({
if (err(canLoft)) return false if (err(canLoft)) return false
return canLoft return canLoft
}, },
'has valid shell selection': ({
context: { selectionRanges },
event,
}) => {
const hasNoSelection =
selectionRanges.graphSelections.length === 0 ||
isRangeBetweenCharacters(selectionRanges) ||
isSelectionLastLine(selectionRanges, codeManager.code)
if (hasNoSelection) {
return doesSceneHaveExtrudedSketch(kclManager.ast)
}
const canShell = canShellSelection(selectionRanges)
console.log('canShellSelection', canShellSelection(selectionRanges))
if (err(canShell)) return false
return canShell
},
'has valid selection for deletion': ({ 'has valid selection for deletion': ({
context: { selectionRanges }, context: { selectionRanges },
}) => { }) => {
@ -869,12 +913,18 @@ export const ModelingMachineProvider = ({
} }
} }
), ),
'Get length info': fromPromise( astConstrainLength: fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({
const { modifiedAst, pathToNodeMap } = input: { selectionRanges, sketchDetails, lengthValue },
await applyConstraintAngleLength({ }) => {
if (!lengthValue)
return Promise.reject(new Error('No length value'))
const constraintResult = await applyConstraintLength({
selectionRanges, selectionRanges,
length: lengthValue,
}) })
if (err(constraintResult)) return Promise.reject(constraintResult)
const { modifiedAst, pathToNodeMap } = constraintResult
const pResult = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
@ -1043,38 +1093,88 @@ export const ModelingMachineProvider = ({
} }
} }
), ),
'Get convert to variable info': fromPromise( 'Apply named value constraint': fromPromise(
async ({ input: { selectionRanges, sketchDetails, data } }) => { async ({ input: { selectionRanges, sketchDetails, data } }) => {
if (!sketchDetails) if (!sketchDetails) {
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const { variableName } = await getVarNameModal({ }
valueName: data?.variableName || 'var', if (!data) {
}) return Promise.reject(new Error('No data from command flow'))
}
let pResult = parse(recast(kclManager.ast)) let pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
let parsed = pResult.program let parsed = pResult.program
const { modifiedAst: _modifiedAst, pathToReplacedNode } = let result: {
moveValueIntoNewVariablePath( modifiedAst: Node<Program>
parsed, pathToReplaced: PathToNode | null
kclManager.programMemory, } = {
data?.pathToNode || [], modifiedAst: parsed,
variableName pathToReplaced: null,
}
// If the user provided a constant name,
// we need to insert the named constant
// and then replace the node with the constant's name.
if ('variableName' in data.namedValue) {
const astAfterReplacement = replaceValueAtNodePath({
ast: parsed,
pathToNode: data.currentValue.pathToNode,
newExpressionString: data.namedValue.variableName,
})
if (trap(astAfterReplacement)) {
return Promise.reject(astAfterReplacement)
}
const parseResultAfterInsertion = parse(
recast(
insertNamedConstant({
node: astAfterReplacement.modifiedAst,
newExpression: data.namedValue,
})
) )
pResult = parse(recast(_modifiedAst)) )
if (
trap(parseResultAfterInsertion) ||
!resultIsOk(parseResultAfterInsertion)
)
return Promise.reject(parseResultAfterInsertion)
result = {
modifiedAst: parseResultAfterInsertion.program,
pathToReplaced: astAfterReplacement.pathToReplaced,
}
} else if ('valueText' in data.namedValue) {
// If they didn't provide a constant name,
// just replace the node with the value.
const astAfterReplacement = replaceValueAtNodePath({
ast: parsed,
pathToNode: data.currentValue.pathToNode,
newExpressionString: data.namedValue.valueText,
})
if (trap(astAfterReplacement)) {
return Promise.reject(astAfterReplacement)
}
// The `replacer` function returns a pathToNode that assumes
// an identifier is also being inserted into the AST, creating an off-by-one error.
// This corrects that error, but TODO we should fix this upstream
// to avoid this kind of error in the future.
astAfterReplacement.pathToReplaced[1][0] =
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
result = astAfterReplacement
}
pResult = parse(recast(result.modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
parsed = pResult.program parsed = pResult.program
if (trap(parsed)) return Promise.reject(parsed) if (trap(parsed)) return Promise.reject(parsed)
parsed = parsed as Node<Program> parsed = parsed as Node<Program>
if (!pathToReplacedNode) if (!result.pathToReplaced)
return Promise.reject(new Error('No path to replaced node')) return Promise.reject(new Error('No path to replaced node'))
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
pathToReplacedNode || [], result.pathToReplaced || [],
parsed, parsed,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1087,7 +1187,7 @@ export const ModelingMachineProvider = ({
) )
const selection = updateSelections( const selection = updateSelections(
{ 0: pathToReplacedNode }, { 0: result.pathToReplaced },
selectionRanges, selectionRanges,
updatedAst.newAst updatedAst.newAst
) )
@ -1095,7 +1195,7 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode: pathToReplacedNode, updatedPathToNode: result.pathToReplaced,
} }
} }
), ),

View File

@ -76,7 +76,7 @@ export const ModelingPane = ({
return ( return (
<section <section
{...props} {...props}
title={title && typeof title === 'string' ? title : ''} aria-label={title && typeof title === 'string' ? title : ''}
data-testid={detailsTestId} data-testid={detailsTestId}
id={id} id={id}
className={ className={

View File

@ -40,7 +40,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50"> <Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
<Menu.Item> <Menu.Item>
<button <button
onClick={() => kclManager.format()} onClick={() => {
kclManager.format().catch(reportRejection)
}}
className={styles.button} className={styles.button}
> >
<span>Format code</span> <span>Format code</span>

View File

@ -10,7 +10,7 @@ import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider' import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
@ -68,8 +68,7 @@ function AppLogoLink({
data-testid="app-logo" data-testid="app-logo"
onClick={() => { onClick={() => {
onProjectClose(file || null, project?.path || null, false) onProjectClose(file || null, project?.path || null, false)
// Clear the scene. kclManager.switchedFiles = true
engineCommandManager.clearScene()
}} }}
to={PATHS.HOME} to={PATHS.HOME}
className={wrapperClassName + ' hover:before:brightness-110'} className={wrapperClassName + ' hover:before:brightness-110'}
@ -190,8 +189,7 @@ function ProjectMenuPopover({
className: !isDesktop() ? 'hidden' : '', className: !isDesktop() ? 'hidden' : '',
onClick: () => { onClick: () => {
onProjectClose(file || null, project?.path || null, true) onProjectClose(file || null, project?.path || null, true)
// Clear the scene. kclManager.switchedFiles = true
engineCommandManager.clearScene()
}, },
}, },
].filter( ].filter(

View File

@ -10,7 +10,7 @@ interface AllKeybindingsFieldsProps {}
export const AllKeybindingsFields = forwardRef( export const AllKeybindingsFields = forwardRef(
( (
props: AllKeybindingsFieldsProps, _props: AllKeybindingsFieldsProps,
scrollRef: ForwardedRef<HTMLDivElement> scrollRef: ForwardedRef<HTMLDivElement>
) => { ) => {
// This is how we will get the interaction map from the context // This is how we will get the interaction map from the context
@ -25,7 +25,7 @@ export const AllKeybindingsFields = forwardRef(
.map(([category, categoryItems]) => ( .map(([category, categoryItems]) => (
<div className="flex flex-col gap-4 px-2 pr-4"> <div className="flex flex-col gap-4 px-2 pr-4">
<h2 <h2
id={`category-${category}`} id={`category-${category.replaceAll(/\s/g, '-')}`}
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold" className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
> >
{category} {category}

View File

@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput' import { SettingsFieldInput } from './SettingsFieldInput'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { APP_VERSION, PACKAGE_NAME } from 'routes/Settings' import { APP_VERSION, IS_NIGHTLY, RELEASE_URL } from 'routes/Settings'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { import {
createAndOpenNewTutorialProject, createAndOpenNewTutorialProject,
@ -246,10 +246,8 @@ export const AllSettingsFields = forwardRef(
to inject the version from package.json */} to inject the version from package.json */}
App version {APP_VERSION}.{' '} App version {APP_VERSION}.{' '}
<a <a
onClick={openExternalBrowserIfDesktop( onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}` href={RELEASE_URL}
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -271,7 +269,7 @@ export const AllSettingsFields = forwardRef(
, and start a discussion if you don't see it! Your feedback will , and start a discussion if you don't see it! Your feedback will
help us prioritize what to build next. help us prioritize what to build next.
</p> </p>
{PACKAGE_NAME.indexOf('-nightly') === -1 && ( {!IS_NIGHTLY && (
<p className="max-w-2xl mt-6"> <p className="max-w-2xl mt-6">
Want to experience the latest and (hopefully) greatest from our Want to experience the latest and (hopefully) greatest from our
main development branch?{' '} main development branch?{' '}

View File

@ -19,7 +19,7 @@ export function KeybindingsSectionsList({
key={category} key={category}
onClick={() => onClick={() =>
scrollRef.current scrollRef.current
?.querySelector(`#category-${category}`) ?.querySelector(`#category-${category.replaceAll(/\s/g, '-')}`)
?.scrollIntoView({ ?.scrollIntoView({
block: 'center', block: 'center',
behavior: 'smooth', behavior: 'smooth',

View File

@ -22,6 +22,7 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { normaliseAngle } from '../../lib/utils' import { normaliseAngle } from '../../lib/utils'
import { kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { KclCommandValue } from 'lib/commandTypes'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
@ -63,6 +64,57 @@ export function angleLengthInfo({
return { enabled, transforms } return { enabled, transforms }
} }
export async function applyConstraintLength({
length,
selectionRanges,
}: {
length: KclCommandValue
selectionRanges: Selections
}) {
const ast = kclManager.ast
const angleLength = angleLengthInfo({ selectionRanges })
if (err(angleLength)) return angleLength
const { transforms } = angleLength
let distanceExpression: Expr = length.valueAst
/**
* To be "constrained", the value must be a binary expression, a named value, or a function call.
* If it has a variable name, we need to insert a variable declaration at the correct index.
*/
if (
'variableName' in length &&
length.variableName &&
length.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(length.insertIndex, 0, length.variableDeclarationAst)
ast.body = newBody
distanceExpression = createIdentifier(length.variableName)
}
if (!isExprBinaryPart(distanceExpression)) {
return new Error('Invalid valueNode, is not a BinaryPart')
}
const retval = transformAstSketchLines({
ast,
selectionRanges,
transformInfos: transforms,
programMemory: kclManager.programMemory,
referenceSegName: '',
forceValueUsedInTransform: distanceExpression,
})
if (err(retval)) return Promise.reject(retval)
const { modifiedAst: _modifiedAst, pathToNodeMap } = retval
return {
modifiedAst: _modifiedAst,
pathToNodeMap,
}
}
export async function applyConstraintAngleLength({ export async function applyConstraintAngleLength({
selectionRanges, selectionRanges,
angleOrLength = 'setLength', angleOrLength = 'setLength',

View File

@ -24,6 +24,8 @@ export function useConvertToVariable(range?: SourceRange) {
}, [enable]) }, [enable])
useEffect(() => { useEffect(() => {
// Return early if there are no selection ranges for whatever reason
if (!context.selectionRanges) return
const parsed = ast const parsed = ast
const meta = isNodeSafeToReplace( const meta = isNodeSafeToReplace(

View File

@ -12,6 +12,7 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { import {
CallExpression, CallExpression,
clearSceneAndBustCache,
emptyExecState, emptyExecState,
ExecState, ExecState,
initPromise, initPromise,
@ -60,6 +61,7 @@ export class KclManager {
private _executeIsStale: ExecuteArgs | null = null private _executeIsStale: ExecuteArgs | null = null
private _wasmInitFailed = true private _wasmInitFailed = true
private _hasErrors = false private _hasErrors = false
private _switchedFiles = false
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
@ -79,6 +81,10 @@ export class KclManager {
this._astCallBack(ast) this._astCallBack(ast)
} }
set switchedFiles(switchedFiles: boolean) {
this._switchedFiles = switchedFiles
}
get programMemory() { get programMemory() {
return this._programMemory return this._programMemory
} }
@ -166,8 +172,12 @@ export class KclManager {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ensureWasmInit().then(() => { this.ensureWasmInit().then(async () => {
this.ast = this.safeParse(codeManager.code) || this.ast await this.safeParse(codeManager.code).then((ast) => {
if (ast) {
this.ast = ast
}
})
}) })
} }
@ -211,7 +221,25 @@ export class KclManager {
} }
} }
safeParse(code: string): Node<Program> | null { // (jess) I'm not in love with this, but it ensures we clear the scene and
// bust the cache on
// errors from parsing when opening new files.
// Why not just clear the cache on all parse errors, you ask? well its actually
// really nice to keep the cache on parse errors within the same file, and
// only bust on engine errors esp if they take a long time to execute and
// you hit the wrong key!
private async checkIfSwitchedFilesShouldClear() {
// If we were switching files and we hit an error on parse we need to bust
// the cache and clear the scene.
if (this._hasErrors && this._switchedFiles) {
await clearSceneAndBustCache(this.engineCommandManager)
} else if (this._switchedFiles) {
// Reset the switched files boolean.
this._switchedFiles = false
}
}
async safeParse(code: string): Promise<Node<Program> | null> {
const result = parse(code) const result = parse(code)
this.diagnostics = [] this.diagnostics = []
this._hasErrors = false this._hasErrors = false
@ -220,6 +248,8 @@ export class KclManager {
const kclerror: KCLError = result as KCLError const kclerror: KCLError = result as KCLError
this.diagnostics = kclErrorsToDiagnostics([kclerror]) this.diagnostics = kclErrorsToDiagnostics([kclerror])
this._hasErrors = true this._hasErrors = true
await this.checkIfSwitchedFilesShouldClear()
return null return null
} }
@ -228,6 +258,7 @@ export class KclManager {
if (result.errors.length > 0) { if (result.errors.length > 0) {
this._hasErrors = true this._hasErrors = true
await this.checkIfSwitchedFilesShouldClear()
return null return null
} }
@ -353,7 +384,7 @@ export class KclManager {
console.error(newCode) console.error(newCode)
return return
} }
const newAst = this.safeParse(newCode) const newAst = await this.safeParse(newCode)
if (!newAst) { if (!newAst) {
this.clearAst() this.clearAst()
return return
@ -408,7 +439,7 @@ export class KclManager {
}) })
} }
async executeCode(zoomToFit?: boolean): Promise<void> { async executeCode(zoomToFit?: boolean): Promise<void> {
const ast = this.safeParse(codeManager.code) const ast = await this.safeParse(codeManager.code)
if (!ast) { if (!ast) {
this.clearAst() this.clearAst()
return return
@ -416,9 +447,9 @@ export class KclManager {
this.ast = { ...ast } this.ast = { ...ast }
return this.executeAst({ zoomToFit }) return this.executeAst({ zoomToFit })
} }
format() { async format() {
const originalCode = codeManager.code const originalCode = codeManager.code
const ast = this.safeParse(originalCode) const ast = await this.safeParse(originalCode)
if (!ast) { if (!ast) {
this.clearAst() this.clearAst()
return return
@ -458,7 +489,7 @@ export class KclManager {
const newCode = recast(ast) const newCode = recast(ast)
if (err(newCode)) return Promise.reject(newCode) if (err(newCode)) return Promise.reject(newCode)
const astWithUpdatedSource = this.safeParse(newCode) const astWithUpdatedSource = await this.safeParse(newCode)
if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast')) if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast'))
let returnVal: Selections | undefined = undefined let returnVal: Selections | undefined = undefined

View File

@ -60,8 +60,7 @@ const b1 = cube([0,0], 10)`
expect(nodePath).toEqual([ expect(nodePath).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['params', 'FunctionExpression'], ['params', 'FunctionExpression'],
[0, 'index'], [0, 'index'],
@ -96,14 +95,12 @@ const b1 = cube([0,0], 10)`
expect(nodePath).toEqual([ expect(nodePath).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'FunctionExpression'], ['body', 'FunctionExpression'],
['body', 'FunctionExpression'], ['body', 'FunctionExpression'],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'PipeExpression'], ['body', 'PipeExpression'],
[2, 'index'], [2, 'index'],

View File

@ -82,11 +82,11 @@ describe('Testing createVariableDeclaration', () => {
it('should create a variable declaration', () => { it('should create a variable declaration', () => {
const result = createVariableDeclaration('myVar', createLiteral(5)) const result = createVariableDeclaration('myVar', createLiteral(5))
expect(result.type).toBe('VariableDeclaration') expect(result.type).toBe('VariableDeclaration')
expect(result.declarations[0].type).toBe('VariableDeclarator') expect(result.declaration.type).toBe('VariableDeclarator')
expect(result.declarations[0].id.type).toBe('Identifier') expect(result.declaration.id.type).toBe('Identifier')
expect(result.declarations[0].id.name).toBe('myVar') expect(result.declaration.id.name).toBe('myVar')
expect(result.declarations[0].init.type).toBe('Literal') expect(result.declaration.init.type).toBe('Literal')
expect((result.declarations[0].init as any).value).toBe(5) expect((result.declaration.init as any).value).toBe(5)
}) })
}) })
describe('Testing createPipeExpression', () => { describe('Testing createPipeExpression', () => {

View File

@ -45,6 +45,7 @@ import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine' import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { KclExpressionWithVariable } from 'lib/commandTypes'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Node<Program>, node: Node<Program>,
@ -66,8 +67,7 @@ export function startSketchOnDefault(
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[sketchIndex, 'index'], [sketchIndex, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
] ]
@ -94,7 +94,7 @@ export function addStartProfileAt(
return new Error('variableDeclaration.init.type !== PipeExpression') return new Error('variableDeclaration.init.type !== PipeExpression')
} }
const _node = { ...node } const _node = { ...node }
const init = variableDeclaration.declarations[0].init const init = variableDeclaration.declaration.init
const startProfileAt = createCallExpressionStdLib('startProfileAt', [ const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createArrayExpression([ createArrayExpression([
createLiteral(roundOff(at[0])), createLiteral(roundOff(at[0])),
@ -105,7 +105,7 @@ export function addStartProfileAt(
if (init.type === 'PipeExpression') { if (init.type === 'PipeExpression') {
init.body.splice(1, 0, startProfileAt) init.body.splice(1, 0, startProfileAt)
} else { } else {
variableDeclaration.declarations[0].init = createPipeExpression([ variableDeclaration.declaration.init = createPipeExpression([
init, init,
startProfileAt, startProfileAt,
]) ])
@ -149,8 +149,7 @@ export function addSketchTo(
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[sketchIndex, 'index'], [sketchIndex, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
] ]
if (axis !== 'xy') { if (axis !== 'xy') {
@ -333,8 +332,7 @@ export function extrudeSketch(
const pathToExtrudeArg: PathToNode = [ const pathToExtrudeArg: PathToNode = [
['body', ''], ['body', ''],
[sketchIndexInBody + 1, 'index'], [sketchIndexInBody + 1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -364,8 +362,7 @@ export function loftSketches(
const pathToNode: PathToNode = [ const pathToNode: PathToNode = [
['body', ''], ['body', ''],
[modifiedAst.body.length - 1, 'index'], [modifiedAst.body.length - 1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -460,8 +457,7 @@ export function revolveSketch(
const pathToRevolveArg: PathToNode = [ const pathToRevolveArg: PathToNode = [
['body', ''], ['body', ''],
[sketchIndexInBody + 1, 'index'], [sketchIndexInBody + 1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -547,8 +543,7 @@ export function sketchOnExtrudedFace(
const newpathToNode: PathToNode = [ const newpathToNode: PathToNode = [
['body', ''], ['body', ''],
[expressionIndex + 1, 'index'], [expressionIndex + 1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
] ]
@ -585,8 +580,7 @@ export function addOffsetPlane({
const pathToNode: PathToNode = [ const pathToNode: PathToNode = [
['body', ''], ['body', ''],
[modifiedAst.body.length - 1, 'index'], [modifiedAst.body.length - 1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -597,6 +591,25 @@ export function addOffsetPlane({
} }
} }
/**
* Return a modified clone of an AST with a named constant inserted into the body
*/
export function insertNamedConstant({
node,
newExpression,
}: {
node: Node<Program>
newExpression: KclExpressionWithVariable
}): Node<Program> {
const ast = structuredClone(node)
ast.body.splice(
newExpression.insertIndex,
0,
newExpression.variableDeclarationAst
)
return ast
}
/** /**
* Modify the AST to create a new sketch using the variable declaration * Modify the AST to create a new sketch using the variable declaration
* of an offset plane. The new sketch just has to come after the offset * of an offset plane. The new sketch just has to come after the offset
@ -823,8 +836,7 @@ export function createVariableDeclaration(
end: 0, end: 0,
moduleId: 0, moduleId: 0,
declarations: [ declaration: {
{
type: 'VariableDeclarator', type: 'VariableDeclarator',
start: 0, start: 0,
end: 0, end: 0,
@ -833,7 +845,6 @@ export function createVariableDeclaration(
id: createIdentifier(varName), id: createIdentifier(varName),
init, init,
}, },
],
visibility, visibility,
kind, kind,
} }
@ -942,6 +953,31 @@ export function giveSketchFnCallTag(
} }
} }
/**
* Replace a
*/
export function replaceValueAtNodePath({
ast,
pathToNode,
newExpressionString,
}: {
ast: Node<Program>
pathToNode: PathToNode
newExpressionString: string
}) {
const replaceCheckResult = isNodeSafeToReplacePath(ast, pathToNode)
if (err(replaceCheckResult)) {
return replaceCheckResult
}
const { isSafe, value, replacer } = replaceCheckResult
if (!isSafe || value.type === 'Identifier') {
return new Error('Not safe to replace')
}
return replacer(ast, newExpressionString)
}
export function moveValueIntoNewVariablePath( export function moveValueIntoNewVariablePath(
ast: Node<Program>, ast: Node<Program>,
programMemory: ProgramMemory, programMemory: ProgramMemory,
@ -1120,7 +1156,7 @@ export async function deleteFromSelection(
traverse(astClone, { traverse(astClone, {
enter: (node, path) => { enter: (node, path) => {
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
const dec = node.declarations[0] const dec = node.declaration
if ( if (
dec.init.type === 'CallExpression' && dec.init.type === 'CallExpression' &&
(dec.init.callee.name === 'extrude' || (dec.init.callee.name === 'extrude' ||
@ -1155,7 +1191,7 @@ export async function deleteFromSelection(
enter: (node, path) => { enter: (node, path) => {
;(async () => { ;(async () => {
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
currentVariableName = node.declarations[0].id.name currentVariableName = node.declaration.id.name
} }
if ( if (
// match startSketchOn(${extrudeNameToDelete}) // match startSketchOn(${extrudeNameToDelete})

View File

@ -22,7 +22,7 @@ import {
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst' import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env' import { VITE_KC_DEV_TOKEN } from 'env'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
@ -118,13 +118,8 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length, code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
true, true,
] ]
const selection: Selections = { const selection: Selection = {
graphSelections: [
{
codeRef: codeRefFromRange(segmentRange, ast), codeRef: codeRefFromRange(segmentRange, ast),
},
],
otherSelections: [],
} }
// executeAst and artifactGraph // executeAst and artifactGraph

View File

@ -29,7 +29,7 @@ import {
sketchLineHelperMap, sketchLineHelperMap,
} from '../std/sketch' } from '../std/sketch'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { import {
Artifact, Artifact,
@ -99,14 +99,9 @@ export function modifyAstWithEdgeTreatmentAndTag(
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
for (const selection of selections.graphSelections) { for (const selection of selections.graphSelections) {
const singleSelection = {
graphSelections: [selection],
otherSelections: [],
}
const result = getPathToExtrudeForSegmentSelection( const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude, clonedAstForGetExtrude,
singleSelection, selection,
artifactGraph artifactGraph
) )
if (err(result)) return result if (err(result)) return result
@ -259,12 +254,12 @@ function insertParametersIntoAst(
export function getPathToExtrudeForSegmentSelection( export function getPathToExtrudeForSegmentSelection(
ast: Program, ast: Program,
selection: Selections, selection: Selection,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error { ): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange( const pathToSegmentNode = getNodePathFromSourceRange(
ast, ast,
selection.graphSelections[0]?.codeRef?.range selection.codeRef?.range
) )
const varDecNode = getNodeFromPath<VariableDeclaration>( const varDecNode = getNodeFromPath<VariableDeclaration>(
@ -273,7 +268,7 @@ export function getPathToExtrudeForSegmentSelection(
'VariableDeclaration' 'VariableDeclaration'
) )
if (err(varDecNode)) return varDecNode if (err(varDecNode)) return varDecNode
const sketchVar = varDecNode.node.declarations[0].id.name const sketchVar = varDecNode.node.declaration.id.name
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
kclManager.programMemory.get(sketchVar), kclManager.programMemory.get(sketchVar),
@ -308,7 +303,7 @@ async function updateAstAndFocus(
} }
} }
function mutateAstWithTagForSketchSegment( export function mutateAstWithTagForSketchSegment(
astClone: Node<Program>, astClone: Node<Program>,
pathToSegmentNode: PathToNode pathToSegmentNode: PathToNode
): { modifiedAst: Program; tag: string } | Error { ): { modifiedAst: Program; tag: string } | Error {
@ -340,7 +335,7 @@ function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag } return { modifiedAst: astClone, tag }
} }
function getEdgeTagCall( export function getEdgeTagCall(
tag: string, tag: string,
artifact: Artifact artifact: Artifact
): Node<Identifier | CallExpression> { ): Node<Identifier | CallExpression> {
@ -367,7 +362,7 @@ function locateExtrudeDeclarator(
if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall
const { node: extrudeVarDecl } = nodeOfExtrudeCall const { node: extrudeVarDecl } = nodeOfExtrudeCall
const extrudeDeclarator = extrudeVarDecl.declarations[0] const extrudeDeclarator = extrudeVarDecl.declaration
if (!extrudeDeclarator) { if (!extrudeDeclarator) {
return new Error('Extrude Declarator not found.') return new Error('Extrude Declarator not found.')
} }

View File

@ -0,0 +1,154 @@
import { err } from 'lib/trap'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import {
Program,
PathToNode,
Expr,
CallExpression,
PipeExpression,
VariableDeclarator,
} from 'lang/wasm'
import { Selections } from 'lib/selections'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import {
createLiteral,
createCallExpressionStdLib,
createObjectExpression,
createIdentifier,
createPipeExpression,
findUniqueName,
createVariableDeclaration,
} from 'lang/modifyAst'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import {
mutateAstWithTagForSketchSegment,
getEdgeTagCall,
} from 'lang/modifyAst/addEdgeTreatment'
export function revolveSketch(
ast: Node<Program>,
pathToSketchNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4),
axis: Selections
):
| {
modifiedAst: Node<Program>
pathToSketchNode: PathToNode
pathToRevolveArg: PathToNode
}
| Error {
const clonedAst = structuredClone(ast)
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
if (err(sketchNode)) return sketchNode
// testing code
const pathToAxisSelection = getNodePathFromSourceRange(
clonedAst,
axis.graphSelections[0]?.codeRef.range
)
const lineNode = getNodeFromPath<CallExpression>(
clonedAst,
pathToAxisSelection,
'CallExpression'
)
if (err(lineNode)) return lineNode
// TODO Kevin: What if |> close(%)?
// TODO Kevin: What if opposite edge
// TODO Kevin: What if the edge isn't planar to the sketch?
// TODO Kevin: add a tag.
const tagResult = mutateAstWithTagForSketchSegment(
clonedAst,
pathToAxisSelection
)
// Have the tag whether it is already created or a new one is generated
if (err(tagResult)) return tagResult
const { tag } = tagResult
/* Original Code */
const { node: sketchExpression } = sketchNode
// determine if sketchExpression is in a pipeExpression or not
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
clonedAst,
pathToSketchNode,
'PipeExpression'
)
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
const { node: sketchPipeExpression } = sketchPipeExpressionNode
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
clonedAst,
pathToSketchNode,
'VariableDeclarator'
)
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
const {
node: sketchVariableDeclarator,
shallowPath: sketchPathToDecleration,
} = sketchVariableDeclaratorNode
const axisSelection = axis?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Axis selection is missing.')
const revolveCall = createCallExpressionStdLib('revolve', [
createObjectExpression({
angle: angle,
axis: getEdgeTagCall(tag, axisSelection),
}),
createIdentifier(sketchVariableDeclarator.id.name),
])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...sketchPipeExpression.body, revolveCall]
: [sketchExpression as any, revolveCall]
)
sketchVariableDeclarator.init = pipeChain
const pathToRevolveArg: PathToNode = [
...sketchPathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode,
pathToRevolveArg,
}
}
// We're not creating a pipe expression,
// but rather a separate constant for the extrusion
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
const sketchIndexInPathToNode =
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
if (typeof sketchIndexInBody !== 'number')
return new Error('expected sketchIndexInBody to be a number')
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
const pathToRevolveArg: PathToNode = [
['body', ''],
[sketchIndexInBody + 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
pathToRevolveArg,
}
}

View File

@ -0,0 +1,123 @@
import { ArtifactGraph } from 'lang/std/artifactGraph'
import { Selections } from 'lib/selections'
import { Expr } from 'wasm-lib/kcl/bindings/Expr'
import { Program } from 'wasm-lib/kcl/bindings/Program'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { PathToNode, VariableDeclarator } from 'lang/wasm'
import {
getPathToExtrudeForSegmentSelection,
mutateAstWithTagForSketchSegment,
} from './addEdgeTreatment'
import { getNodeFromPath } from 'lang/queryAst'
import { err } from 'lib/trap'
import {
createLiteral,
createIdentifier,
findUniqueName,
createCallExpressionStdLib,
createObjectExpression,
createArrayExpression,
createVariableDeclaration,
} from 'lang/modifyAst'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
export function addShell({
node,
selection,
artifactGraph,
thickness,
}: {
node: Node<Program>
selection: Selections
artifactGraph: ArtifactGraph
thickness: Expr
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(node)
// Look up the corresponding extrude
const clonedAstForGetExtrude = structuredClone(modifiedAst)
const expressions: Expr[] = []
let pathToExtrudeNode: PathToNode | undefined = undefined
for (const graphSelection of selection.graphSelections) {
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
graphSelection,
artifactGraph
)
if (err(extrudeLookupResult)) {
return new Error("Couldn't find extrude")
}
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
// Get the sketch ref from the selection
// TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between.
// We must find a technique for these situations that is robust to intermediate declarations
const sketchNode = getNodeFromPath<VariableDeclarator>(
modifiedAst,
graphSelection.codeRef.pathToNode,
'VariableDeclarator'
)
if (err(sketchNode)) {
return sketchNode
}
const selectedArtifact = graphSelection.artifact
if (!selectedArtifact) {
return new Error('Bad artifact')
}
// Check on the selection, and handle the wall vs cap casees
let expr: Expr
if (selectedArtifact.type === 'cap') {
expr = createLiteral(selectedArtifact.subType)
} else if (selectedArtifact.type === 'wall') {
const tagResult = mutateAstWithTagForSketchSegment(
modifiedAst,
extrudeLookupResult.pathToSegmentNode
)
if (err(tagResult)) return tagResult
const { tag } = tagResult
expr = createIdentifier(tag)
} else {
continue
}
expressions.push(expr)
}
if (!pathToExtrudeNode) return new Error('No extrude found')
const extrudeNode = getNodeFromPath<VariableDeclarator>(
modifiedAst,
pathToExtrudeNode,
'VariableDeclarator'
)
if (err(extrudeNode)) {
return extrudeNode
}
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SHELL)
const shell = createCallExpressionStdLib('shell', [
createObjectExpression({
faces: createArrayExpression(expressions),
thickness,
}),
createIdentifier(extrudeNode.node.id.name),
])
const declaration = createVariableDeclaration(name, shell)
// TODO: check if we should append at the end like here or right after the extrude
modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst,
pathToNode,
}
}

View File

@ -17,6 +17,7 @@ import {
doesSceneHaveSweepableSketch, doesSceneHaveSweepableSketch,
traverse, traverse,
getNodeFromPath, getNodeFromPath,
doesSceneHaveExtrudedSketch,
} from './queryAst' } from './queryAst'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { import {
@ -230,8 +231,7 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'PipeExpression'], ['body', 'PipeExpression'],
[2, 'index'], [2, 'index'],
@ -250,8 +250,7 @@ describe('testing getNodePathFromSourceRange', () => {
const expected = [ const expected = [
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'PipeExpression'], ['body', 'PipeExpression'],
[3, 'index'], [3, 'index'],
@ -293,8 +292,7 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[1, 'index'], [1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['cond', 'IfExpression'], ['cond', 'IfExpression'],
['left', 'BinaryExpression'], ['left', 'BinaryExpression'],
@ -324,8 +322,7 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[1, 'index'], [1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['then_val', 'IfExpression'], ['then_val', 'IfExpression'],
['body', 'IfExpression'], ['body', 'IfExpression'],
@ -353,7 +350,8 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['items', 'ImportStatement'], ['selector', 'ImportStatement'],
['items', 'ImportSelector'],
[1, 'index'], [1, 'index'],
['name', 'ImportItem'], ['name', 'ImportItem'],
]) ])
@ -657,6 +655,38 @@ extrude001 = extrude(10, sketch001)
}) })
}) })
describe('Testing doesSceneHaveExtrudedSketch', () => {
it('finds extruded sketch as variable', async () => {
const exampleCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %)
extrude001 = extrude(1, sketch001)
`
const ast = assertParse(exampleCode)
if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeTruthy()
})
it('finds extruded sketch in pipe', async () => {
const exampleCode = `extrude001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %)
|> extrude(1, %)
`
const ast = assertParse(exampleCode)
if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeTruthy()
})
it('finds no extrusion with sketch only', async () => {
const exampleCode = `extrude001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %)
`
const ast = assertParse(exampleCode)
if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeFalsy()
})
})
describe('Testing traverse and pathToNode', () => { describe('Testing traverse and pathToNode', () => {
it.each([ it.each([
['basic', '2.73'], ['basic', '2.73'],

View File

@ -259,13 +259,10 @@ function moreNodePathFromSourceRange(
return moreNodePathFromSourceRange(expression, sourceRange, path) return moreNodePathFromSourceRange(expression, sourceRange, path)
} }
if (_node.type === 'VariableDeclaration' && isInRange) { if (_node.type === 'VariableDeclaration' && isInRange) {
const declarations = _node.declarations const declaration = _node.declaration
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
const declaration = declarations[decIndex]
if (declaration.start <= start && declaration.end >= end) { if (declaration.start <= start && declaration.end >= end) {
path.push(['declarations', 'VariableDeclaration']) path.push(['declaration', 'VariableDeclaration'])
path.push([decIndex, 'index'])
const init = declaration.init const init = declaration.init
if (init.start <= start && init.end >= end) { if (init.start <= start && init.end >= end) {
path.push(['init', '']) path.push(['init', ''])
@ -273,22 +270,17 @@ function moreNodePathFromSourceRange(
} }
} }
} }
}
if (_node.type === 'VariableDeclaration' && isInRange) { if (_node.type === 'VariableDeclaration' && isInRange) {
const declarations = _node.declarations const declaration = _node.declaration
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
const declaration = declarations[decIndex]
if (declaration.start <= start && declaration.end >= end) { if (declaration.start <= start && declaration.end >= end) {
const init = declaration.init const init = declaration.init
if (init.start <= start && init.end >= end) { if (init.start <= start && init.end >= end) {
path.push(['declarations', 'VariableDeclaration']) path.push(['declaration', 'VariableDeclaration'])
path.push([decIndex, 'index'])
path.push(['init', '']) path.push(['init', ''])
return moreNodePathFromSourceRange(init, sourceRange, path) return moreNodePathFromSourceRange(init, sourceRange, path)
} }
} }
}
return path return path
} }
if (_node.type === 'UnaryExpression' && isInRange) { if (_node.type === 'UnaryExpression' && isInRange) {
@ -380,17 +372,23 @@ function moreNodePathFromSourceRange(
} }
if (_node.type === 'ImportStatement' && isInRange) { if (_node.type === 'ImportStatement' && isInRange) {
const { items } = _node if (_node.selector && _node.selector.type === 'List') {
path.push(['selector', 'ImportStatement'])
const { items } = _node.selector
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i] const item = items[i]
if (item.start <= start && item.end >= end) { if (item.start <= start && item.end >= end) {
path.push(['items', 'ImportStatement']) path.push(['items', 'ImportSelector'])
path.push([i, 'index']) path.push([i, 'index'])
if (item.name.start <= start && item.name.end >= end) { if (item.name.start <= start && item.name.end >= end) {
path.push(['name', 'ImportItem']) path.push(['name', 'ImportItem'])
return path return path
} }
if (item.alias && item.alias.start <= start && item.alias.end >= end) { if (
item.alias &&
item.alias.start <= start &&
item.alias.end >= end
) {
path.push(['alias', 'ImportItem']) path.push(['alias', 'ImportItem'])
return path return path
} }
@ -399,6 +397,7 @@ function moreNodePathFromSourceRange(
} }
return path return path
} }
}
console.error('not implemented: ' + node.type) console.error('not implemented: ' + node.type)
@ -451,13 +450,10 @@ export function traverse(
traverse(node, option, pathToNode) traverse(node, option, pathToNode)
if (_node.type === 'VariableDeclaration') { if (_node.type === 'VariableDeclaration') {
_node.declarations.forEach((declaration, index) => _traverse(_node.declaration, [
_traverse(declaration, [
...pathToNode, ...pathToNode,
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[index, 'index'],
]) ])
)
} else if (_node.type === 'VariableDeclarator') { } else if (_node.type === 'VariableDeclarator') {
_traverse(_node.init, [...pathToNode, ['init', '']]) _traverse(_node.init, [...pathToNode, ['init', '']])
} else if (_node.type === 'PipeExpression') { } else if (_node.type === 'PipeExpression') {
@ -567,7 +563,7 @@ export function findAllPreviousVariablesPath(
const variables: PrevVariable<any>[] = [] const variables: PrevVariable<any>[] = []
bodyItems?.forEach?.((item) => { bodyItems?.forEach?.((item) => {
if (item.type !== 'VariableDeclaration' || item.end > startRange) return if (item.type !== 'VariableDeclaration' || item.end > startRange) return
const varName = item.declarations[0].id.name const varName = item.declaration.id.name
const varValue = programMemory?.get(varName) const varValue = programMemory?.get(varName)
if (!varValue || typeof varValue?.value !== type) return if (!varValue || typeof varValue?.value !== type) return
variables.push({ variables.push({
@ -761,7 +757,7 @@ export function isLinesParallelAndConstrained(
const _varDec = getNodeFromPath(ast, primaryPath, 'VariableDeclaration') const _varDec = getNodeFromPath(ast, primaryPath, 'VariableDeclaration')
if (err(_varDec)) return _varDec if (err(_varDec)) return _varDec
const varDec = _varDec.node const varDec = _varDec.node
const varName = (varDec as VariableDeclaration)?.declarations[0]?.id?.name const varName = (varDec as VariableDeclaration)?.declaration.id?.name
const sg = sketchFromKclValue(programMemory?.get(varName), varName) const sg = sketchFromKclValue(programMemory?.get(varName), varName)
if (err(sg)) return sg if (err(sg)) return sg
const _primarySegment = getSketchSegmentFromSourceRange( const _primarySegment = getSketchSegmentFromSourceRange(
@ -881,7 +877,7 @@ export function hasExtrudeSketch({
} }
const varDec = varDecMeta.node const varDec = varDecMeta.node
if (varDec.type !== 'VariableDeclaration') return false if (varDec.type !== 'VariableDeclaration') return false
const varName = varDec.declarations[0].id.name const varName = varDec.declaration.id.name
const varValue = programMemory?.get(varName) const varValue = programMemory?.get(varName)
return ( return (
varValue?.type === 'Solid' || varValue?.type === 'Solid' ||
@ -1068,6 +1064,35 @@ export function doesSceneHaveSweepableSketch(ast: Node<Program>, count = 1) {
return Object.keys(theMap).length >= count return Object.keys(theMap).length >= count
} }
export function doesSceneHaveExtrudedSketch(ast: Node<Program>) {
const theMap: any = {}
traverse(ast as any, {
enter(node) {
if (
node.type === 'VariableDeclarator' &&
node.init?.type === 'PipeExpression'
) {
for (const pipe of node.init.body) {
if (
pipe.type === 'CallExpression' &&
pipe.callee.name === 'extrude'
) {
theMap[node.id.name] = true
break
}
}
} else if (
node.type === 'CallExpression' &&
node.callee.name === 'extrude' &&
node.arguments[1]?.type === 'Identifier'
) {
theMap[node.moduleId] = true
}
},
})
return Object.keys(theMap).length > 0
}
export function getObjExprProperty( export function getObjExprProperty(
node: ObjectExpression, node: ObjectExpression,
propName: string propName: string

View File

@ -871,3 +871,15 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
pathToNode: getNodePathFromSourceRange(ast, range), pathToNode: getNodePathFromSourceRange(ast, range),
} }
} }
export function isSolid2D(artifact: Artifact): artifact is solid2D {
return (artifact as solid2D).pathId !== undefined
}
export function isSegment(artifact: Artifact): artifact is SegmentArtifact {
return (artifact as SegmentArtifact).pathId !== undefined
}
export function isSweep(artifact: Artifact): artifact is SweepArtifact {
return (artifact as SweepArtifact).pathId !== undefined
}

View File

@ -1879,17 +1879,6 @@ export class EngineCommandManager extends EventTarget {
} }
return JSON.stringify(this.defaultPlanes) return JSON.stringify(this.defaultPlanes)
} }
clearScene(): void {
const deleteCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'scene_clear_all',
},
}
this.clearDefaultPlanes()
this.engineConnection?.send(deleteCmd)
}
addCommandLog(message: CommandLog) { addCommandLog(message: CommandLog) {
if (this.commandLogs.length > 500) { if (this.commandLogs.length > 500) {
this.commandLogs.shift() this.commandLogs.shift()

View File

@ -164,8 +164,7 @@ mySketch001 = startSketchOn('XY')
pathToNode: [ pathToNode: [
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
], ],
}) })
@ -189,8 +188,7 @@ mySketch001 = startSketchOn('XY')
pathToNode: [ pathToNode: [
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
], ],
}) })

View File

@ -1701,7 +1701,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
if (err(nodeMeta2)) return nodeMeta2 if (err(nodeMeta2)) return nodeMeta2
const { node: varDec } = nodeMeta2 const { node: varDec } = nodeMeta2
const varName = varDec.declarations[0].id.name const varName = varDec.declaration.id.name
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
previousProgramMemory.get(varName), previousProgramMemory.get(varName),
varName varName

View File

@ -111,13 +111,11 @@ export function isSketchVariablesLinked(
let nextVarDec: VariableDeclarator | undefined let nextVarDec: VariableDeclarator | undefined
for (const node of ast.body) { for (const node of ast.body) {
if (node.type !== 'VariableDeclaration') continue if (node.type !== 'VariableDeclaration') continue
const found = node.declarations.find( if (node.declaration.id.name === secondArg.name) {
({ id }) => id?.name === secondArg.name nextVarDec = node.declaration
)
if (!found) continue
nextVarDec = found
break break
} }
}
if (!nextVarDec) return false if (!nextVarDec) return false
return isSketchVariablesLinked(nextVarDec, primaryVarDec, ast) return isSketchVariablesLinked(nextVarDec, primaryVarDec, ast)
} }

View File

@ -1,9 +1,13 @@
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { parse, ParseResult } from './wasm' import { initPromise, parse, ParseResult } from './wasm'
import { enginelessExecutor } from 'lib/testHelpers' import { enginelessExecutor } from 'lib/testHelpers'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { Program } from '../wasm-lib/kcl/bindings/Program' import { Program } from '../wasm-lib/kcl/bindings/Program'
beforeEach(async () => {
await initPromise
})
it('can execute parsed AST', async () => { it('can execute parsed AST', async () => {
const code = `x = 1 const code = `x = 1
// A comment.` // A comment.`

View File

@ -16,6 +16,7 @@ import init, {
parse_project_settings, parse_project_settings,
default_project_settings, default_project_settings,
base64_decode, base64_decode,
clear_scene_and_bust_cache,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -698,6 +699,21 @@ export function defaultAppSettings(): DeepPartial<Configuration> | Error {
return default_app_settings() return default_app_settings()
} }
export async function clearSceneAndBustCache(
engineCommandManager: EngineCommandManager
): Promise<null | Error> {
try {
await clear_scene_and_bust_cache(engineCommandManager)
} catch (e: any) {
console.error('clear_scene_and_bust_cache: error', e)
return Promise.reject(
new Error(`Error on clear_scene_and_bust_cache: ${e}`)
)
}
return null
}
export function parseAppSettings( export function parseAppSettings(
toml: string toml: string
): DeepPartial<Configuration> | Error { ): DeepPartial<Configuration> | Error {

View File

@ -32,8 +32,14 @@ export function mouseControlsToCameraSystem(
mouseControl: MouseControlType | undefined mouseControl: MouseControlType | undefined
): CameraSystem | undefined { ): CameraSystem | undefined {
switch (mouseControl) { switch (mouseControl) {
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'kittycad':
case 'kitty_cad': case 'kitty_cad':
return 'KittyCAD' return 'KittyCAD'
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'onshape':
case 'on_shape': case 'on_shape':
return 'OnShape' return 'OnShape'
case 'trackpad_friendly': case 'trackpad_friendly':
@ -44,6 +50,9 @@ export function mouseControlsToCameraSystem(
return 'NX' return 'NX'
case 'creo': case 'creo':
return 'Creo' return 'Creo'
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'autocad':
case 'auto_cad': case 'auto_cad':
return 'AutoCAD' return 'AutoCAD'
default: default:

View File

@ -1,9 +1,15 @@
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
import { transformAstSketchLines } from 'lang/std/sketchcombos'
import { PathToNode } from 'lang/wasm'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants' import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api' import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { modelingMachine, SketchTool } from 'machines/modelingMachine' import { modelingMachine, SketchTool } from 'machines/modelingMachine'
import { revolveAxisValidator } from './validators'
type OutputFormat = Models['OutputFormat_type'] type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type'] type OutputTypeKey = OutputFormat['type']
@ -34,9 +40,14 @@ export type ModelingCommandSchema = {
Loft: { Loft: {
selection: Selections selection: Selections
} }
Shell: {
selection: Selections
thickness: KclCommandValue
}
Revolve: { Revolve: {
selection: Selections selection: Selections
angle: KclCommandValue angle: KclCommandValue
axis: Selections
} }
Fillet: { Fillet: {
// todo // todo
@ -50,6 +61,18 @@ export type ModelingCommandSchema = {
'change tool': { 'change tool': {
tool: SketchTool tool: SketchTool
} }
'Constrain length': {
selection: Selections
length: KclCommandValue
}
'Constrain with named value': {
currentValue: {
valueText: string
pathToNode: PathToNode
variableName: string
}
namedValue: KclCommandValue
}
'Text-to-CAD': { 'Text-to-CAD': {
prompt: string prompt: string
} }
@ -277,6 +300,25 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
Shell: {
description: 'Hollow out a 3D solid.',
icon: 'shell',
needsReview: true,
args: {
selection: {
inputType: 'selection',
selectionTypes: ['cap', 'wall'],
multiple: true,
required: true,
skip: false,
},
thickness: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection // TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
Revolve: { Revolve: {
description: 'Create a 3D body by rotating a sketch region about an axis.', description: 'Create a 3D body by rotating a sketch region about an axis.',
@ -290,6 +332,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
required: true, required: true,
skip: true, skip: true,
}, },
axis: {
required: true,
inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
multiple: false,
validation: revolveAxisValidator,
},
angle: { angle: {
inputType: 'kcl', inputType: 'kcl',
defaultValue: KCL_DEFAULT_DEGREE, defaultValue: KCL_DEFAULT_DEGREE,
@ -337,6 +386,88 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
'Constrain length': {
description: 'Constrain the length of one or more segments.',
icon: 'dimension',
args: {
selection: {
inputType: 'selection',
selectionTypes: ['segment'],
multiple: false,
required: true,
skip: true,
},
length: {
inputType: 'kcl',
required: true,
createVariableByDefault: true,
defaultValue(_, machineContext) {
const selectionRanges = machineContext?.selectionRanges
if (!selectionRanges) return KCL_DEFAULT_LENGTH
const angleLength = angleLengthInfo({
selectionRanges,
angleOrLength: 'setLength',
})
if (err(angleLength)) return KCL_DEFAULT_LENGTH
const { transforms } = angleLength
// QUESTION: is it okay to reference kclManager here? will its state be up to date?
const sketched = transformAstSketchLines({
ast: structuredClone(kclManager.ast),
selectionRanges,
transformInfos: transforms,
programMemory: kclManager.programMemory,
referenceSegName: '',
})
if (err(sketched)) return KCL_DEFAULT_LENGTH
const { valueUsedInTransform } = sketched
return valueUsedInTransform?.toString() || KCL_DEFAULT_LENGTH
},
},
},
},
'Constrain with named value': {
description: 'Constrain a value by making it a named constant.',
icon: 'make-variable',
args: {
currentValue: {
description:
'Path to the node in the AST to constrain. This is never shown to the user.',
inputType: 'text',
required: false,
skip: true,
},
namedValue: {
inputType: 'kcl',
required: true,
createVariableByDefault: true,
variableName(commandBarContext, machineContext) {
const { currentValue } = commandBarContext.argumentsToSubmit
if (
!currentValue ||
!(currentValue instanceof Object) ||
!('variableName' in currentValue) ||
typeof currentValue.variableName !== 'string'
) {
return 'value'
}
return currentValue.variableName
},
defaultValue: (commandBarContext) => {
const { currentValue } = commandBarContext.argumentsToSubmit
if (
!currentValue ||
!(currentValue instanceof Object) ||
!('valueText' in currentValue) ||
typeof currentValue.valueText !== 'string'
) {
return KCL_DEFAULT_LENGTH
}
return currentValue.valueText
},
},
},
},
'Text-to-CAD': { 'Text-to-CAD': {
description: 'Use the Zoo Text-to-CAD API to generate part starters.', description: 'Use the Zoo Text-to-CAD API to generate part starters.',
icon: 'chat', icon: 'chat',

View File

@ -0,0 +1,107 @@
import { Models } from '@kittycad/lib'
import { engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections'
import { isSolid2D, isSegment, isSweep } from 'lang/std/artifactGraph'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) {
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'disable_dry_run' },
})
// Exit out since the command was successful
return
} catch (e) {
console.error(e)
console.error('disable_dry_run failed. This is bad!')
}
}
}
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
export const dryRunWrapper = async (callback: () => Promise<any>) => {
// Gotcha: What about race conditions?
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'enable_dry_run' },
})
const result = await callback()
return result
} catch (e) {
console.error(e)
} finally {
await disableDryRunWithRetry(5)
}
}
function isSelections(selections: unknown): selections is Selections {
return (
(selections as Selections).graphSelections !== undefined &&
(selections as Selections).otherSelections !== undefined
)
}
export const revolveAxisValidator = async ({
data,
context,
}: {
data: { [key: string]: Selections }
context: CommandBarContext
}): Promise<boolean | string> => {
if (!isSelections(context.argumentsToSubmit.selection)) {
return 'Unable to revolve, selections are missing'
}
const artifact =
context.argumentsToSubmit.selection.graphSelections[0].artifact
if (!artifact) {
return 'Unable to revolve, sketch not found'
}
if (!(isSolid2D(artifact) || isSegment(artifact) || isSweep(artifact))) {
return 'Unable to revolve, sketch has no path'
}
const sketchSelection = artifact.pathId
let edgeSelection = data.axis.graphSelections[0].artifact?.id
if (!sketchSelection) {
return 'Unable to revolve, sketch is missing'
}
if (!edgeSelection) {
return 'Unable to revolve, edge is missing'
}
const angleInDegrees: Models['Angle_type'] = {
unit: 'degrees',
value: 360,
}
const revolveAboutEdgeCommand = async () => {
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'revolve_about_edge',
angle: angleInDegrees,
edge_id: edgeSelection,
target: sketchSelection,
tolerance: 0.0001,
},
})
}
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
if (attemptRevolve?.success) {
return true
} else {
// return error message for the toast
return 'Unable to revolve with selected axis'
}
}

View File

@ -7,7 +7,7 @@ import { ReactNode } from 'react'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { Artifact } from 'lang/std/artifactGraph' import { Artifact } from 'lang/std/artifactGraph'
import { CommandBarContext } from 'machines/commandBarMachine'
type Icon = CustomIconName type Icon = CustomIconName
const PLATFORMS = ['both', 'web', 'desktop'] as const const PLATFORMS = ['both', 'web', 'desktop'] as const
const INPUT_TYPES = [ const INPUT_TYPES = [
@ -147,8 +147,30 @@ export type CommandArgumentConfig<
inputType: 'selection' inputType: 'selection'
selectionTypes: Artifact['type'][] selectionTypes: Artifact['type'][]
multiple: boolean multiple: boolean
validation?: ({
data,
context,
}: {
data: any
context: CommandBarContext
}) => Promise<boolean | string>
}
| {
inputType: 'kcl'
createVariableByDefault?: boolean
variableName?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: C
) => string)
defaultValue?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: C
) => string)
} }
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
| { | {
inputType: 'string' inputType: 'string'
defaultValue?: defaultValue?:
@ -221,8 +243,30 @@ export type CommandArgument<
inputType: 'selection' inputType: 'selection'
selectionTypes: Artifact['type'][] selectionTypes: Artifact['type'][]
multiple: boolean multiple: boolean
validation?: ({
data,
context,
}: {
data: any
context: CommandBarContext
}) => Promise<boolean | string>
}
| {
inputType: 'kcl'
createVariableByDefault?: boolean
variableName?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: ContextFrom<T>
) => string)
defaultValue?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: ContextFrom<T>
) => string)
} }
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value
| { | {
inputType: 'string' inputType: 'string'
defaultValue?: defaultValue?:

View File

@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch', SKETCH: 'sketch',
EXTRUDE: 'extrude', EXTRUDE: 'extrude',
LOFT: 'loft', LOFT: 'loft',
SHELL: 'shell',
SEGMENT: 'seg', SEGMENT: 'seg',
REVOLVE: 'revolve', REVOLVE: 'revolve',
PLANE: 'plane', PLANE: 'plane',
@ -110,3 +111,10 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
/** Toast id for the app auto-updater toast */ /** Toast id for the app auto-updater toast */
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast' export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
/** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */
export const KCL_AXIS_X = 'X'
export const KCL_AXIS_Y = 'Y'
export const KCL_AXIS_NEG_X = '-X'
export const KCL_AXIS_NEG_Y = '-Y'
export const KCL_DEFAULT_AXIS = 'X'

View File

@ -155,6 +155,8 @@ export function buildCommandArgument<
context: ContextFrom<T>, context: ContextFrom<T>,
machineActor: Actor<T> machineActor: Actor<T>
): CommandArgument<O, T> & { inputType: typeof arg.inputType } { ): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
// GOTCHA: modelingCommandConfig is not a 1:1 mapping to this baseCommandArgument
// You need to manually add key/value pairs here.
const baseCommandArgument = { const baseCommandArgument = {
description: arg.description, description: arg.description,
required: arg.required, required: arg.required,
@ -181,10 +183,13 @@ export function buildCommandArgument<
...baseCommandArgument, ...baseCommandArgument,
multiple: arg.multiple, multiple: arg.multiple,
selectionTypes: arg.selectionTypes, selectionTypes: arg.selectionTypes,
validation: arg.validation,
} satisfies CommandArgument<O, T> & { inputType: 'selection' } } satisfies CommandArgument<O, T> & { inputType: 'selection' }
} else if (arg.inputType === 'kcl') { } else if (arg.inputType === 'kcl') {
return { return {
inputType: arg.inputType, inputType: arg.inputType,
createVariableByDefault: arg.createVariableByDefault,
variableName: arg.variableName,
defaultValue: arg.defaultValue, defaultValue: arg.defaultValue,
...baseCommandArgument, ...baseCommandArgument,
} satisfies CommandArgument<O, T> & { inputType: 'kcl' } } satisfies CommandArgument<O, T> & { inputType: 'kcl' }

View File

@ -13,7 +13,6 @@ import {
listProjects, listProjects,
readAppSettingsFile, readAppSettingsFile,
} from './desktop' } from './desktop'
import { engineCommandManager } from './singletons'
export const isHidden = (fileOrDir: FileEntry) => export const isHidden = (fileOrDir: FileEntry) =>
!!fileOrDir.name?.startsWith('.') !!fileOrDir.name?.startsWith('.')
@ -116,9 +115,6 @@ export async function createAndOpenNewTutorialProject({
) => void ) => void
navigate: (path: string) => void navigate: (path: string) => void
}) { }) {
// Clear the scene.
engineCommandManager.clearScene()
// Create a new project with the onboarding project name // Create a new project with the onboarding project name
const configuration = await readAppSettingsFile() const configuration = await readAppSettingsFile()
const projects = await listProjects(configuration) const projects = await listProjects(configuration)

View File

@ -3,27 +3,27 @@ export const bracket = `// Shelf Bracket
// Define constants // Define constants
const sigmaAllow = 35000 // psi (6061-T6 aluminum) sigmaAllow = 35000 // psi (6061-T6 aluminum)
const width = 6 // inch width = 6 // inch
const p = 300 // Force on shelf - lbs p = 300 // Force on shelf - lbs
const factorOfSafety = 1.2 // FOS of 1.2 factorOfSafety = 1.2 // FOS of 1.2
const shelfMountL = 5 // inches shelfMountL = 5 // inches
const wallMountL = 2 // inches wallMountL = 2 // inches
const shelfDepth = 12 // Shelf is 12 inches in depth from the wall shelfDepth = 12 // Shelf is 12 inches in depth from the wall
const moment = shelfDepth * p // assume the force is applied at the end of the shelf to be conservative (lb-in) moment = shelfDepth * p // assume the force is applied at the end of the shelf to be conservative (lb-in)
const filletRadius = .375 // inches filletRadius = .375 // inches
const extFilletRadius = .25 // inches extFilletRadius = .25 // inches
const mountingHoleDiameter = 0.5 // inches mountingHoleDiameter = 0.5 // inches
// Calculate required thickness of bracket // Calculate required thickness of bracket
const thickness = sqrt(moment * factorOfSafety * 6 / (sigmaAllow * width)) // this is the calculation of two brackets holding up the shelf (inches) thickness = sqrt(moment * factorOfSafety * 6 / (sigmaAllow * width)) // this is the calculation of two brackets holding up the shelf (inches)
// Sketch the bracket body and fillet the inner and outer edges of the bend // Sketch the bracket body and fillet the inner and outer edges of the bend
const bracketLeg1Sketch = startSketchOn('XY') bracketLeg1Sketch = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([shelfMountL - filletRadius, 0], %, $fillet1) |> line([shelfMountL - filletRadius, 0], %, $fillet1)
|> line([0, width], %, $fillet2) |> line([0, width], %, $fillet2)
@ -47,7 +47,7 @@ const bracketLeg1Sketch = startSketchOn('XY')
}, %), %) }, %), %)
// Extrude the leg 2 bracket sketch // Extrude the leg 2 bracket sketch
const bracketLeg1Extrude = extrude(thickness, bracketLeg1Sketch) bracketLeg1Extrude = extrude(thickness, bracketLeg1Sketch)
|> fillet({ |> fillet({
radius = extFilletRadius, radius = extFilletRadius,
tags = [ tags = [
@ -57,7 +57,7 @@ const bracketLeg1Extrude = extrude(thickness, bracketLeg1Sketch)
}, %) }, %)
// Sketch the fillet arc // Sketch the fillet arc
const filletSketch = startSketchOn('XZ') filletSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([0, thickness], %) |> line([0, thickness], %)
|> arc({ |> arc({
@ -73,10 +73,10 @@ const filletSketch = startSketchOn('XZ')
}, %) }, %)
// Sketch the bend // Sketch the bend
const filletExtrude = extrude(-width, filletSketch) filletExtrude = extrude(-width, filletSketch)
// Create a custom plane for the leg that sits on the wall // Create a custom plane for the leg that sits on the wall
const customPlane = { customPlane = {
plane = { plane = {
origin = { x = -filletRadius, y = 0, z = 0 }, origin = { x = -filletRadius, y = 0, z = 0 },
xAxis = { x = 0, y = 1, z = 0 }, xAxis = { x = 0, y = 1, z = 0 },
@ -86,7 +86,7 @@ const customPlane = {
} }
// Create a sketch for the second leg // Create a sketch for the second leg
const bracketLeg2Sketch = startSketchOn(customPlane) bracketLeg2Sketch = startSketchOn(customPlane)
|> startProfileAt([0, -filletRadius], %) |> startProfileAt([0, -filletRadius], %)
|> line([width, 0], %) |> line([width, 0], %)
|> line([0, -wallMountL], %, $fillet3) |> line([0, -wallMountL], %, $fillet3)
@ -102,7 +102,7 @@ const bracketLeg2Sketch = startSketchOn(customPlane)
}, %), %) }, %), %)
// Extrude the second leg // Extrude the second leg
const bracketLeg2Extrude = extrude(-thickness, bracketLeg2Sketch) bracketLeg2Extrude = extrude(-thickness, bracketLeg2Sketch)
|> fillet({ |> fillet({
radius = extFilletRadius, radius = extFilletRadius,
tags = [ tags = [
@ -135,8 +135,8 @@ function findLineInExampleCode({
} }
export const bracketWidthConstantLine = findLineInExampleCode({ export const bracketWidthConstantLine = findLineInExampleCode({
searchText: 'const width', searchText: 'width =',
}) })
export const bracketThicknessCalculationLine = findLineInExampleCode({ export const bracketThicknessCalculationLine = findLineInExampleCode({
searchText: 'const thickness', searchText: 'thickness =',
}) })

View File

@ -5,7 +5,7 @@ import { isDesktop } from './isDesktop'
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants' import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { parseProjectSettings } from 'lang/wasm' import { parseProjectSettings } from 'lang/wasm'
import { err } from './trap' import { err, reportRejection } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils' import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
interface OnSubmitProps { interface OnSubmitProps {
@ -28,7 +28,7 @@ export function kclCommands(
groupId: 'code', groupId: 'code',
icon: 'code', icon: 'code',
onSubmit: () => { onSubmit: () => {
kclManager.format() kclManager.format().catch(reportRejection)
}, },
}, },
{ {

View File

@ -569,6 +569,17 @@ export function canSweepSelection(selection: Selections) {
) )
} }
export function canRevolveSelection(selection: Selections) {
const commonNodes = selection.graphSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
)
return (
!!isSketchPipe(selection) &&
(commonNodes.every((n) => nodeHasClose(n)) ||
commonNodes.every((n) => nodeHasCircle(n)))
)
}
export function canLoftSelection(selection: Selections) { export function canLoftSelection(selection: Selections) {
const commonNodes = selection.graphSelections.map((_, i) => const commonNodes = selection.graphSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i) buildCommonNodeFromSelection(selection, i)
@ -585,6 +596,17 @@ export function canLoftSelection(selection: Selections) {
) )
} }
export function canShellSelection(selection: Selections) {
const commonNodes = selection.graphSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
)
return commonNodes.every(
(n) =>
n.selection.artifact?.type === 'cap' ||
n.selection.artifact?.type === 'wall'
)
}
// This accounts for non-geometry selections under "other" // This accounts for non-geometry selections under "other"
export type ResolvedSelectionType = Artifact['type'] | 'other' export type ResolvedSelectionType = Artifact['type'] | 'other'
export type SelectionCountsByType = Map<ResolvedSelectionType, number> export type SelectionCountsByType = Map<ResolvedSelectionType, number>
@ -619,12 +641,29 @@ export function getSelectionCountByType(
} }
}) })
selection.graphSelections.forEach((selection) => { selection.graphSelections.forEach((graphSelection) => {
if (!selection.artifact) { if (!graphSelection.artifact) {
/**
* TODO: remove this heuristic-based selection type detection.
* Currently, if you've created a sketch and have not left sketch mode,
* the selection will be a segment selection with no artifact.
* This is because the mock execution does not update the artifact graph.
* Once we move the artifactGraph creation to WASM, we can remove this,
* as the artifactGraph will always be up-to-date.
*/
if (isSingleCursorInPipe(selection, kclManager.ast)) {
incrementOrInitializeSelectionType('segment')
return
} else {
console.warn(
'Selection is outside of a sketch but has no artifact. Sketch segment selections are the only kind that can have a valid selection with no artifact.',
JSON.stringify(graphSelection)
)
incrementOrInitializeSelectionType('other') incrementOrInitializeSelectionType('other')
return return
} }
incrementOrInitializeSelectionType(selection.artifact.type) }
incrementOrInitializeSelectionType(graphSelection.artifact.type)
}) })
return selectionsByType return selectionsByType

View File

@ -12,7 +12,7 @@ export type InteractionMapItem = {
* Controls both the available names for interaction map categories * Controls both the available names for interaction map categories
* and the order in which they are displayed. * and the order in which they are displayed.
*/ */
export const interactionMapCategories = [ const interactionMapCategories = [
'Sketching', 'Sketching',
'Modeling', 'Modeling',
'Command Palette', 'Command Palette',

View File

@ -190,9 +190,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}, },
{ {
id: 'shell', id: 'shell',
onClick: () => console.error('Shell not yet implemented'), onClick: ({ commandBarSend }) => {
commandBarSend({
type: 'Find and select command',
data: { name: 'Shell', groupId: 'modeling' },
})
},
disabled: (state) => !state.can({ type: 'Shell' }),
icon: 'shell', icon: 'shell',
status: 'kcl-only', status: 'available',
title: 'Shell', title: 'Shell',
description: 'Hollow out a 3D solid.', description: 'Hollow out a 3D solid.',
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }], links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }],
@ -534,13 +540,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[ [
{ {
id: 'constraint-length', id: 'constraint-length',
disabled: (state) => disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
!( onClick: ({ commandBarSend }) =>
state.matches({ Sketch: 'SketchIdle' }) && commandBarSend({
state.can({ type: 'Constrain length' }) type: 'Find and select command',
), data: {
onClick: ({ modelingSend }) => name: 'Constrain length',
modelingSend({ type: 'Constrain length' }), groupId: 'modeling',
},
}),
icon: 'dimension', icon: 'dimension',
status: 'available', status: 'available',
title: 'Length', title: 'Length',

View File

@ -109,11 +109,11 @@ export function useCalculateKclExpression({
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__' a.declaration.id?.name === '__result__'
) )
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declaration.init
const result = execState.memory?.get('__result__')?.value const result = execState.memory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)

View File

@ -8,6 +8,7 @@ import {
import { Selections__old } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils' import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import toast from 'react-hot-toast'
export type CommandBarContext = { export type CommandBarContext = {
commands: Command[] commands: Command[]
@ -247,14 +248,69 @@ export const commandBarMachine = setup({
'All arguments are skippable': () => false, 'All arguments are skippable': () => false,
}, },
actors: { actors: {
'Validate argument': fromPromise(({ input }) => { 'Validate argument': fromPromise(
({
input,
}: {
input: {
context: CommandBarContext | undefined
event: CommandBarMachineEvent | undefined
}
}) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// TODO: figure out if we should validate argument data here or in the form itself, if (!input || input?.event?.type !== 'Submit argument') {
// and if we should support people configuring a argument's validation function toast.error(`Unable to validate, wrong event type.`)
return reject(`Unable to validate, wrong event type`)
}
resolve(input) const context = input?.context
if (!context) {
toast.error(`Unable to validate, wrong argument.`)
return reject(`Unable to validate, wrong argument`)
}
const data = input.event.data
const argName = context.currentArgument?.name
const args = context?.selectedCommand?.args
const argConfig = args && argName ? args[argName] : undefined
// Only do a validation check if the argument, selectedCommand, and the validation function are defined
if (
context.currentArgument &&
context.selectedCommand &&
argConfig?.inputType === 'selection' &&
argConfig?.validation
) {
argConfig
.validation({ context, data })
.then((result) => {
if (typeof result === 'boolean' && result === true) {
return resolve(data)
} else {
// validation failed
if (typeof result === 'string') {
// The result of the validation is the error message
toast.error(result)
return reject(
`unable to validate ${argName}, Message: ${result}`
)
} else {
// Default message if there is not a custom one sent
toast.error(`Unable to validate ${argName}`)
return reject(`unable to validate ${argName}}`)
}
}
}) })
}), .catch(() => {
return reject(`unable to validate ${argName}}`)
})
} else {
// Missing several requirements for validate argument, just bypass
return resolve(data)
}
})
}
),
'Validate all arguments': fromPromise( 'Validate all arguments': fromPromise(
({ input }: { input: CommandBarContext }) => { ({ input }: { input: CommandBarContext }) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -449,9 +505,10 @@ export const commandBarMachine = setup({
invoke: { invoke: {
src: 'Validate argument', src: 'Validate argument',
id: 'validateSingleArgument', id: 'validateSingleArgument',
input: ({ event }) => { input: ({ event, context }) => {
if (event.type !== 'Submit argument') return {} if (event.type !== 'Submit argument')
return event.data return { event: undefined, context: undefined }
return { event, context }
}, },
onDone: { onDone: {
target: '#Command Bar.Checking Arguments', target: '#Command Bar.Checking Arguments',

File diff suppressed because one or more lines are too long

View File

@ -23,6 +23,15 @@ import argvFromYargs from './commandLineArgs'
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
// Supporting multiple instances instead of multiple applications
let cmdQPressed = false
const instances: BrowserWindow[] = []
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
process.exit(0)
}
// Check the command line arguments for a project path // Check the command line arguments for a project path
const args = parseCLIArgs() const args = parseCLIArgs()
@ -117,16 +126,34 @@ const createWindow = (filePath?: string): BrowserWindow => {
newWindow.show() newWindow.show()
instances.push(newWindow)
return newWindow return newWindow
} }
// before-quit with multiple instances
if (process.platform === 'darwin') {
// Quit from the dock context menu should quit the application directly
app.on('before-quit', () => {
cmdQPressed = true
})
}
// Quit when all windows are closed, even on macOS. There, it's common // Quit when all windows are closed, even on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q, but it is a really weird behavior with our app. // explicitly with Cmd + Q, but it is a really weird behavior with our app.
// app.on('window-all-closed', () => {
// app.quit()
// })
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (cmdQPressed || process.platform !== 'darwin') {
app.quit() app.quit()
}
}) })
// Various actions can trigger this event, such as launching the application for the first time,
// attempting to re-launch the application when it's already running, or clicking on the application's dock or taskbar icon.
app.on('activate', () => createWindow())
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
@ -135,6 +162,10 @@ app.on('ready', (event, data) => {
mainWindow = createWindow() mainWindow = createWindow()
}) })
// This event will be emitted inside the primary instance of your application when a second instance
// has been executed and calls app.requestSingleInstanceLock().
app.on('second-instance', (event, argv, workingDirectory) => createWindow())
// For now there is no good reason to separate these out to another file(s) // For now there is no good reason to separate these out to another file(s)
// There is just not enough code to warrant it and further abstracts everything // There is just not enough code to warrant it and further abstracts everything
// which is already quite abstracted // which is already quite abstracted

View File

@ -30,6 +30,12 @@ export const PACKAGE_NAME = isDesktop()
? window.electron.packageJson.name ? window.electron.packageJson.name
: 'zoo-modeling-app' : 'zoo-modeling-app'
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
export const RELEASE_URL = `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : ''
}v${APP_VERSION}`
export const Settings = () => { export const Settings = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()

View File

@ -79,7 +79,10 @@ kittycad = { version = "0.3.28", default-features = false, features = ["js", "re
kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] } kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] }
[workspace.lints.clippy] [workspace.lints.clippy]
assertions_on_result_states = "warn"
dbg_macro = "warn"
iter_over_hash_type = "warn" iter_over_hash_type = "warn"
lossy_float_literal = "warn"
[[test]] [[test]]
name = "executor" name = "executor"

View File

@ -395,10 +395,10 @@ fn do_stdlib_inner(
#const_struct #const_struct
fn #boxed_fn_name_ident( fn #boxed_fn_name_ident(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>> + Send + '_>, Box<dyn std::future::Future<Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>> + Send + '_>,
> { > {
Box::pin(#fn_name_ident(exec_state, args)) Box::pin(#fn_name_ident(exec_state, args))
} }
@ -770,12 +770,12 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn #test_name_mock() { async fn #test_name_mock() {
let program = crate::Program::parse_no_errs(#code_block).unwrap(); let program = crate::Program::parse_no_errs(#code_block).unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())), engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()).await.unwrap(); ctx.run(program.into(), &mut crate::ExecState::default()).await.unwrap();
@ -785,7 +785,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
async fn #test_name() { async fn #test_name() {
let code = #code_block; let code = #code_block;
// Note, `crate` must be kcl_lib // Note, `crate` must be kcl_lib
let result = crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm).await.unwrap(); let result = crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm, None).await.unwrap();
twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #output_test_name_str), &result, 0.99); twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #output_test_name_str), &result, 0.99);
} }
} }

View File

@ -3,7 +3,7 @@ mod test_examples_someFn {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_someFn0() { async fn test_mock_example_someFn0() {
let program = crate::Program::parse_no_errs("someFn()").unwrap(); let program = crate::Program::parse_no_errs("someFn()").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -12,7 +12,7 @@ mod test_examples_someFn {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -22,8 +22,11 @@ mod test_examples_someFn {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_someFn0() { async fn kcl_test_example_someFn0() {
let code = "someFn()"; let code = "someFn()";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -44,12 +47,12 @@ pub(crate) struct SomeFn {}
#[doc = "Std lib function: someFn\nDocs"] #[doc = "Std lib function: someFn\nDocs"]
pub(crate) const SomeFn: SomeFn = SomeFn {}; pub(crate) const SomeFn: SomeFn = SomeFn {};
fn boxed_someFn( fn boxed_someFn(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -3,7 +3,7 @@ mod test_examples_someFn {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_someFn0() { async fn test_mock_example_someFn0() {
let program = crate::Program::parse_no_errs("someFn()").unwrap(); let program = crate::Program::parse_no_errs("someFn()").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -12,7 +12,7 @@ mod test_examples_someFn {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -22,8 +22,11 @@ mod test_examples_someFn {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_someFn0() { async fn kcl_test_example_someFn0() {
let code = "someFn()"; let code = "someFn()";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -44,12 +47,12 @@ pub(crate) struct SomeFn {}
#[doc = "Std lib function: someFn\nDocs"] #[doc = "Std lib function: someFn\nDocs"]
pub(crate) const SomeFn: SomeFn = SomeFn {}; pub(crate) const SomeFn: SomeFn = SomeFn {};
fn boxed_someFn( fn boxed_someFn(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -4,7 +4,7 @@ mod test_examples_show {
async fn test_mock_example_show0() { async fn test_mock_example_show0() {
let program = let program =
crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nshow").unwrap(); crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -13,7 +13,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -23,8 +23,11 @@ mod test_examples_show {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_show0() { async fn kcl_test_example_show0() {
let code = "This is another code block.\nyes sirrr.\nshow"; let code = "This is another code block.\nyes sirrr.\nshow";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -38,7 +41,7 @@ mod test_examples_show {
async fn test_mock_example_show1() { async fn test_mock_example_show1() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -47,7 +50,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -57,8 +60,11 @@ mod test_examples_show {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_show1() { async fn kcl_test_example_show1() {
let code = "This is code.\nIt does other shit.\nshow"; let code = "This is code.\nIt does other shit.\nshow";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -79,12 +85,12 @@ pub(crate) struct Show {}
#[doc = "Std lib function: show\nThis is some function.\nIt does shit."] #[doc = "Std lib function: show\nThis is some function.\nIt does shit."]
pub(crate) const Show: Show = Show {}; pub(crate) const Show: Show = Show {};
fn boxed_show( fn boxed_show(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -4,7 +4,7 @@ mod test_examples_show {
async fn test_mock_example_show0() { async fn test_mock_example_show0() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -13,7 +13,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -23,8 +23,11 @@ mod test_examples_show {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_show0() { async fn kcl_test_example_show0() {
let code = "This is code.\nIt does other shit.\nshow"; let code = "This is code.\nIt does other shit.\nshow";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -45,12 +48,12 @@ pub(crate) struct Show {}
#[doc = "Std lib function: show\nThis is some function.\nIt does shit."] #[doc = "Std lib function: show\nThis is some function.\nIt does shit."]
pub(crate) const Show: Show = Show {}; pub(crate) const Show: Show = Show {};
fn boxed_show( fn boxed_show(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -5,7 +5,7 @@ mod test_examples_my_func {
let program = let program =
crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nmyFunc") crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nmyFunc")
.unwrap(); .unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -14,7 +14,7 @@ mod test_examples_my_func {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -24,8 +24,11 @@ mod test_examples_my_func {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_my_func0() { async fn kcl_test_example_my_func0() {
let code = "This is another code block.\nyes sirrr.\nmyFunc"; let code = "This is another code block.\nyes sirrr.\nmyFunc";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -39,7 +42,7 @@ mod test_examples_my_func {
async fn test_mock_example_my_func1() { async fn test_mock_example_my_func1() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nmyFunc").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nmyFunc").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -48,7 +51,7 @@ mod test_examples_my_func {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -58,8 +61,11 @@ mod test_examples_my_func {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_my_func1() { async fn kcl_test_example_my_func1() {
let code = "This is code.\nIt does other shit.\nmyFunc"; let code = "This is code.\nIt does other shit.\nmyFunc";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -80,12 +86,12 @@ pub(crate) struct MyFunc {}
#[doc = "Std lib function: myFunc\nThis is some function.\nIt does shit."] #[doc = "Std lib function: myFunc\nThis is some function.\nIt does shit."]
pub(crate) const MyFunc: MyFunc = MyFunc {}; pub(crate) const MyFunc: MyFunc = MyFunc {};
fn boxed_my_func( fn boxed_my_func(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -5,7 +5,7 @@ mod test_examples_line_to {
let program = let program =
crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nlineTo") crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nlineTo")
.unwrap(); .unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -14,7 +14,7 @@ mod test_examples_line_to {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -24,8 +24,11 @@ mod test_examples_line_to {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_line_to0() { async fn kcl_test_example_line_to0() {
let code = "This is another code block.\nyes sirrr.\nlineTo"; let code = "This is another code block.\nyes sirrr.\nlineTo";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -39,7 +42,7 @@ mod test_examples_line_to {
async fn test_mock_example_line_to1() { async fn test_mock_example_line_to1() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nlineTo").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nlineTo").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -48,7 +51,7 @@ mod test_examples_line_to {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -58,8 +61,11 @@ mod test_examples_line_to {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_line_to1() { async fn kcl_test_example_line_to1() {
let code = "This is code.\nIt does other shit.\nlineTo"; let code = "This is code.\nIt does other shit.\nlineTo";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -80,12 +86,12 @@ pub(crate) struct LineTo {}
#[doc = "Std lib function: lineTo\nThis is some function.\nIt does shit."] #[doc = "Std lib function: lineTo\nThis is some function.\nIt does shit."]
pub(crate) const LineTo: LineTo = LineTo {}; pub(crate) const LineTo: LineTo = LineTo {};
fn boxed_line_to( fn boxed_line_to(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -4,7 +4,7 @@ mod test_examples_min {
async fn test_mock_example_min0() { async fn test_mock_example_min0() {
let program = let program =
crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nmin").unwrap(); crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nmin").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -13,7 +13,7 @@ mod test_examples_min {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -23,8 +23,11 @@ mod test_examples_min {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_min0() { async fn kcl_test_example_min0() {
let code = "This is another code block.\nyes sirrr.\nmin"; let code = "This is another code block.\nyes sirrr.\nmin";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -38,7 +41,7 @@ mod test_examples_min {
async fn test_mock_example_min1() { async fn test_mock_example_min1() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nmin").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nmin").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -47,7 +50,7 @@ mod test_examples_min {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -57,8 +60,11 @@ mod test_examples_min {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_min1() { async fn kcl_test_example_min1() {
let code = "This is code.\nIt does other shit.\nmin"; let code = "This is code.\nIt does other shit.\nmin";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -79,12 +85,12 @@ pub(crate) struct Min {}
#[doc = "Std lib function: min\nThis is some function.\nIt does shit."] #[doc = "Std lib function: min\nThis is some function.\nIt does shit."]
pub(crate) const Min: Min = Min {}; pub(crate) const Min: Min = Min {};
fn boxed_min( fn boxed_min(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -4,7 +4,7 @@ mod test_examples_show {
async fn test_mock_example_show0() { async fn test_mock_example_show0() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -13,7 +13,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -23,8 +23,11 @@ mod test_examples_show {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_show0() { async fn kcl_test_example_show0() {
let code = "This is code.\nIt does other shit.\nshow"; let code = "This is code.\nIt does other shit.\nshow";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -45,12 +48,12 @@ pub(crate) struct Show {}
#[doc = "Std lib function: show\nThis is some function.\nIt does shit."] #[doc = "Std lib function: show\nThis is some function.\nIt does shit."]
pub(crate) const Show: Show = Show {}; pub(crate) const Show: Show = Show {};
fn boxed_show( fn boxed_show(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -4,7 +4,7 @@ mod test_examples_import {
async fn test_mock_example_import0() { async fn test_mock_example_import0() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nimport").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nimport").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -13,7 +13,7 @@ mod test_examples_import {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -23,8 +23,11 @@ mod test_examples_import {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_import0() { async fn kcl_test_example_import0() {
let code = "This is code.\nIt does other shit.\nimport"; let code = "This is code.\nIt does other shit.\nimport";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -45,12 +48,12 @@ pub(crate) struct Import {}
#[doc = "Std lib function: import\nThis is some function.\nIt does shit."] #[doc = "Std lib function: import\nThis is some function.\nIt does shit."]
pub(crate) const Import: Import = Import {}; pub(crate) const Import: Import = Import {};
fn boxed_import( fn boxed_import(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -4,7 +4,7 @@ mod test_examples_import {
async fn test_mock_example_import0() { async fn test_mock_example_import0() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nimport").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nimport").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -13,7 +13,7 @@ mod test_examples_import {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -23,8 +23,11 @@ mod test_examples_import {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_import0() { async fn kcl_test_example_import0() {
let code = "This is code.\nIt does other shit.\nimport"; let code = "This is code.\nIt does other shit.\nimport";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -45,12 +48,12 @@ pub(crate) struct Import {}
#[doc = "Std lib function: import\nThis is some function.\nIt does shit."] #[doc = "Std lib function: import\nThis is some function.\nIt does shit."]
pub(crate) const Import: Import = Import {}; pub(crate) const Import: Import = Import {};
fn boxed_import( fn boxed_import(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -4,7 +4,7 @@ mod test_examples_import {
async fn test_mock_example_import0() { async fn test_mock_example_import0() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nimport").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nimport").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -13,7 +13,7 @@ mod test_examples_import {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -23,8 +23,11 @@ mod test_examples_import {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_import0() { async fn kcl_test_example_import0() {
let code = "This is code.\nIt does other shit.\nimport"; let code = "This is code.\nIt does other shit.\nimport";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -45,12 +48,12 @@ pub(crate) struct Import {}
#[doc = "Std lib function: import\nThis is some function.\nIt does shit."] #[doc = "Std lib function: import\nThis is some function.\nIt does shit."]
pub(crate) const Import: Import = Import {}; pub(crate) const Import: Import = Import {};
fn boxed_import( fn boxed_import(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -4,7 +4,7 @@ mod test_examples_show {
async fn test_mock_example_show0() { async fn test_mock_example_show0() {
let program = let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap(); crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -13,7 +13,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -23,8 +23,11 @@ mod test_examples_show {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_show0() { async fn kcl_test_example_show0() {
let code = "This is code.\nIt does other shit.\nshow"; let code = "This is code.\nIt does other shit.\nshow";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -45,12 +48,12 @@ pub(crate) struct Show {}
#[doc = "Std lib function: show\nThis is some function.\nIt does shit."] #[doc = "Std lib function: show\nThis is some function.\nIt does shit."]
pub(crate) const Show: Show = Show {}; pub(crate) const Show: Show = Show {};
fn boxed_show( fn boxed_show(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -3,7 +3,7 @@ mod test_examples_some_function {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_some_function0() { async fn test_mock_example_some_function0() {
let program = crate::Program::parse_no_errs("someFunction()").unwrap(); let program = crate::Program::parse_no_errs("someFunction()").unwrap();
let ctx = crate::executor::ExecutorContext { let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
@ -12,7 +12,7 @@ mod test_examples_some_function {
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()), stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::execution::ContextType::Mock,
}; };
ctx.run(program.into(), &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
@ -22,8 +22,11 @@ mod test_examples_some_function {
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn kcl_test_example_some_function0() { async fn kcl_test_example_some_function0() {
let code = "someFunction()"; let code = "someFunction()";
let result = let result = crate::test_server::execute_and_snapshot(
crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm) code,
crate::settings::types::UnitLength::Mm,
None,
)
.await .await
.unwrap(); .unwrap();
twenty_twenty::assert_image( twenty_twenty::assert_image(
@ -44,12 +47,12 @@ pub(crate) struct SomeFunction {}
#[doc = "Std lib function: someFunction\nDocs"] #[doc = "Std lib function: someFunction\nDocs"]
pub(crate) const SomeFunction: SomeFunction = SomeFunction {}; pub(crate) const SomeFunction: SomeFunction = SomeFunction {};
fn boxed_some_function( fn boxed_some_function(
exec_state: &mut crate::executor::ExecState, exec_state: &mut crate::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>,
> + Send > + Send
+ '_, + '_,
>, >,

View File

@ -12,9 +12,8 @@ redo-kcl-stdlib-docs-no-imgs:
# Generate the stdlib image artifacts # Generate the stdlib image artifacts
# Then run the stdlib docs generation # Then run the stdlib docs generation
redo-kcl-stdlib-docs: redo-kcl-stdlib-docs:
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib kcl_test_example TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- kcl_test_example
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib EXPECTORATE=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- docs::gen_std_tests::test_generate_stdlib
# Copy a test KCL file from executor tests into a new simulation test. # Copy a test KCL file from executor tests into a new simulation test.
copy-exec-test-into-sim-test test_name: copy-exec-test-into-sim-test test_name:

View File

@ -15,5 +15,5 @@ async fn kcl_to_core_test() {
) )
.await; .await;
assert!(result.is_ok()); result.unwrap();
} }

View File

@ -18,7 +18,7 @@ pub fn bench_execute(c: &mut Criterion) {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
// Spawn a future onto the runtime // Spawn a future onto the runtime
b.iter(|| { b.iter(|| {
rt.block_on(test_server::execute_and_snapshot(s, Mm)).unwrap(); rt.block_on(test_server::execute_and_snapshot(s, Mm, None)).unwrap();
}); });
}); });
group.finish(); group.finish();
@ -38,7 +38,7 @@ pub fn bench_lego(c: &mut Criterion) {
let code = LEGO_PROGRAM.replace("{{N}}", &size.to_string()); let code = LEGO_PROGRAM.replace("{{N}}", &size.to_string());
// Spawn a future onto the runtime // Spawn a future onto the runtime
b.iter(|| { b.iter(|| {
rt.block_on(test_server::execute_and_snapshot(&code, Mm)).unwrap(); rt.block_on(test_server::execute_and_snapshot(&code, Mm, None)).unwrap();
}); });
}); });
} }

View File

@ -3,7 +3,7 @@ use iai::black_box;
async fn execute_server_rack_heavy() { async fn execute_server_rack_heavy() {
let code = SERVER_RACK_HEAVY_PROGRAM; let code = SERVER_RACK_HEAVY_PROGRAM;
black_box( black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm) kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
.await .await
.unwrap(), .unwrap(),
); );
@ -12,7 +12,7 @@ async fn execute_server_rack_heavy() {
async fn execute_server_rack_lite() { async fn execute_server_rack_lite() {
let code = SERVER_RACK_LITE_PROGRAM; let code = SERVER_RACK_LITE_PROGRAM;
black_box( black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm) kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
.await .await
.unwrap(), .unwrap(),
); );

View File

@ -1,3 +0,0 @@
pub mod cache;
pub mod modify;
pub mod types;

View File

@ -22,7 +22,7 @@ use super::ExecutionKind;
use crate::{ use crate::{
engine::EngineManager, engine::EngineManager,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{DefaultPlanes, IdGenerator}, execution::{DefaultPlanes, IdGenerator},
SourceRange, SourceRange,
}; };

View File

@ -20,7 +20,7 @@ use kittycad_modeling_cmds::{self as kcmc};
use super::ExecutionKind; use super::ExecutionKind;
use crate::{ use crate::{
errors::KclError, errors::KclError,
executor::{DefaultPlanes, IdGenerator}, execution::{DefaultPlanes, IdGenerator},
SourceRange, SourceRange,
}; };

View File

@ -11,7 +11,7 @@ use wasm_bindgen::prelude::*;
use crate::{ use crate::{
engine::ExecutionKind, engine::ExecutionKind,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{DefaultPlanes, IdGenerator}, execution::{DefaultPlanes, IdGenerator},
SourceRange, SourceRange,
}; };

View File

@ -32,7 +32,7 @@ use uuid::Uuid;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{DefaultPlanes, IdGenerator, Point3d}, execution::{DefaultPlanes, IdGenerator, Point3d},
SourceRange, SourceRange,
}; };

View File

@ -4,14 +4,19 @@ use async_recursion::async_recursion;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{BodyType, ExecState, ExecutorContext, KclValue, Metadata, StatementKind, TagEngineInfo, TagIdentifier}, execution::{
BodyType, ExecState, ExecutorContext, KclValue, Metadata, StatementKind, TagEngineInfo, TagIdentifier,
},
parsing::ast::types::{ parsing::ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, CallExpression, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, CallExpression,
CallExpressionKw, Expr, IfExpression, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, CallExpressionKw, Expr, IfExpression, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node,
ObjectExpression, TagDeclarator, UnaryExpression, UnaryOperator, ObjectExpression, PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator,
}, },
source_range::SourceRange, source_range::SourceRange,
std::{args::Arg, FunctionKind}, std::{
args::{Arg, KwArgs},
FunctionKind,
},
}; };
const FLOAT_TO_INT_MAX_DELTA: f64 = 0.01; const FLOAT_TO_INT_MAX_DELTA: f64 = 0.01;
@ -361,9 +366,11 @@ impl Node<CallExpressionKw> {
#[async_recursion] #[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let fn_name = &self.callee.name; let fn_name = &self.callee.name;
let callsite: SourceRange = self.into();
// Build a hashmap from argument labels to the final evaluated values. // Build a hashmap from argument labels to the final evaluated values.
let mut fn_args = HashMap::with_capacity(self.arguments.len()); let mut fn_args = HashMap::with_capacity(self.arguments.len());
let mut tag_declarator_args = Vec::new();
for arg_expr in &self.arguments { for arg_expr in &self.arguments {
let source_range = SourceRange::from(arg_expr.arg.clone()); let source_range = SourceRange::from(arg_expr.arg.clone());
let metadata = Metadata { source_range }; let metadata = Metadata { source_range };
@ -371,8 +378,12 @@ impl Node<CallExpressionKw> {
.execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression) .execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression)
.await?; .await?;
fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range)); fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range));
if let Expr::TagDeclarator(td) = &arg_expr.arg {
tag_declarator_args.push((td.inner.clone(), source_range));
}
} }
let fn_args = fn_args; // remove mutability let fn_args = fn_args; // remove mutability
let tag_declarator_args = tag_declarator_args; // remove mutability
// Evaluate the unlabeled first param, if any exists. // Evaluate the unlabeled first param, if any exists.
let unlabeled = if let Some(ref arg_expr) = self.unlabeled { let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
@ -386,16 +397,55 @@ impl Node<CallExpressionKw> {
None None
}; };
let args = crate::std::Args::new_kw(fn_args, unlabeled, self.into(), ctx.clone()); let args = crate::std::Args::new_kw(
KwArgs {
unlabeled,
labeled: fn_args,
},
self.into(),
ctx.clone(),
);
match ctx.stdlib.get_either(fn_name) { match ctx.stdlib.get_either(fn_name) {
FunctionKind::Core(func) => { FunctionKind::Core(func) => {
// Attempt to call the function. // Attempt to call the function.
let mut result = func.std_lib_fn()(exec_state, args).await?; let mut result = func.std_lib_fn()(exec_state, args).await?;
update_memory_for_tags_of_geometry(&mut result, exec_state)?; update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
Ok(result) Ok(result)
} }
FunctionKind::UserDefined => { FunctionKind::UserDefined => {
todo!("Part of modeling-app#4600: Support keyword arguments for user-defined functions") let source_range = SourceRange::from(self);
// Clone the function so that we can use a mutable reference to
// exec_state.
let func = exec_state.memory.get(fn_name, source_range)?.clone();
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
let return_value = {
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let result = func
.call_fn_kw(args, exec_state, ctx.clone(), callsite)
.await
.map_err(|e| {
// Add the call expression to the source ranges.
// TODO currently ignored by the frontend
e.add_source_ranges(vec![source_range])
});
exec_state.dynamic_state = previous_dynamic_state;
result?
};
let result = return_value.ok_or_else(move || {
let mut source_ranges: Vec<SourceRange> = vec![source_range];
// We want to send the source range of the original function.
if let KclValue::Function { meta, .. } = func {
source_ranges = meta.iter().map(|m| m.source_range).collect();
};
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of user-defined function {} is undefined", fn_name),
source_ranges,
})
})?;
Ok(result)
} }
} }
} }
@ -407,6 +457,7 @@ impl Node<CallExpression> {
let fn_name = &self.callee.name; let fn_name = &self.callee.name;
let mut fn_args: Vec<Arg> = Vec::with_capacity(self.arguments.len()); let mut fn_args: Vec<Arg> = Vec::with_capacity(self.arguments.len());
let mut tag_declarator_args = Vec::new();
for arg_expr in &self.arguments { for arg_expr in &self.arguments {
let metadata = Metadata { let metadata = Metadata {
@ -416,15 +467,19 @@ impl Node<CallExpression> {
.execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression) .execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression)
.await?; .await?;
let arg = Arg::new(value, SourceRange::from(arg_expr)); let arg = Arg::new(value, SourceRange::from(arg_expr));
if let Expr::TagDeclarator(td) = arg_expr {
tag_declarator_args.push((td.inner.clone(), arg.source_range));
}
fn_args.push(arg); fn_args.push(arg);
} }
let tag_declarator_args = tag_declarator_args; // remove mutability
match ctx.stdlib.get_either(fn_name) { match ctx.stdlib.get_either(fn_name) {
FunctionKind::Core(func) => { FunctionKind::Core(func) => {
// Attempt to call the function. // Attempt to call the function.
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone()); let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
let mut result = func.std_lib_fn()(exec_state, args).await?; let mut result = func.std_lib_fn()(exec_state, args).await?;
update_memory_for_tags_of_geometry(&mut result, exec_state)?; update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
Ok(result) Ok(result)
} }
FunctionKind::UserDefined => { FunctionKind::UserDefined => {
@ -463,7 +518,24 @@ impl Node<CallExpression> {
} }
} }
fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> { /// `tag_declarator_args` should only contain tag declarator literals, which
/// will be defined as local variables. Non-literals that evaluate to tag
/// declarators should not be defined.
fn update_memory_for_tags_of_geometry(
result: &mut KclValue,
tag_declarator_args: &[(TagDeclarator, SourceRange)],
exec_state: &mut ExecState,
) -> Result<(), KclError> {
// Define all the tags in the memory.
for (tag_declarator, arg_sr) in tag_declarator_args {
let tag = TagIdentifier {
value: tag_declarator.name.clone(),
info: None,
meta: vec![Metadata { source_range: *arg_sr }],
};
exec_state.memory.add_tag(&tag.value, tag.clone(), *arg_sr)?;
}
// If the return result is a sketch or solid, we want to update the // If the return result is a sketch or solid, we want to update the
// memory for the tags of the group. // memory for the tags of the group.
// TODO: This could probably be done in a better way, but as of now this was my only idea // TODO: This could probably be done in a better way, but as of now this was my only idea
@ -471,7 +543,7 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
match result { match result {
KclValue::Sketch { value: ref mut sketch } => { KclValue::Sketch { value: ref mut sketch } => {
for (_, tag) in sketch.tags.iter() { for (_, tag) in sketch.tags.iter() {
exec_state.memory.update_tag(&tag.value, tag.clone())?; exec_state.memory.update_tag_if_defined(&tag.value, tag.clone());
} }
} }
KclValue::Solid(ref mut solid) => { KclValue::Solid(ref mut solid) => {
@ -509,7 +581,7 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
info.sketch = solid.id; info.sketch = solid.id;
t.info = Some(info); t.info = Some(info);
exec_state.memory.update_tag(&tag.name, t.clone())?; exec_state.memory.update_tag_if_defined(&tag.name, t.clone());
// update the sketch tags. // update the sketch tags.
solid.sketch.tags.insert(tag.name.clone(), t); solid.sketch.tags.insert(tag.name.clone(), t);
@ -530,22 +602,6 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
Ok(()) Ok(())
} }
impl Node<TagDeclarator> {
pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
value: self.name.clone(),
info: None,
meta: vec![Metadata {
source_range: self.into(),
}],
}));
exec_state.memory.add(&self.name, memory_item.clone(), self.into())?;
Ok(self.into())
}
}
impl Node<ArrayExpression> { impl Node<ArrayExpression> {
#[async_recursion] #[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
@ -807,3 +863,10 @@ impl Property {
} }
} }
} }
impl Node<PipeExpression> {
#[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
execute_pipe_body(exec_state, &self.body, self.into(), ctx).await
}
}

View File

@ -2,7 +2,7 @@ use schemars::JsonSchema;
use crate::{ use crate::{
errors::KclError, errors::KclError,
executor::{ execution::{
call_user_defined_function, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ProgramMemory, call_user_defined_function, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ProgramMemory,
}, },
parsing::ast::types::FunctionExpression, parsing::ast::types::FunctionExpression,

View File

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
errors::KclErrorDetails, errors::KclErrorDetails,
exec::{ProgramMemory, Sketch}, exec::{ProgramMemory, Sketch},
executor::{Face, ImportedGeometry, MemoryFunction, Metadata, Plane, SketchSet, Solid, SolidSet, TagIdentifier}, execution::{Face, ImportedGeometry, MemoryFunction, Metadata, Plane, SketchSet, Solid, SolidSet, TagIdentifier},
parsing::ast::types::{FunctionExpression, KclNone, LiteralValue, TagDeclarator, TagNode}, parsing::ast::types::{FunctionExpression, KclNone, LiteralValue, TagDeclarator, TagNode},
std::{args::Arg, FnAsArg}, std::{args::Arg, FnAsArg},
ExecState, ExecutorContext, KclError, SourceRange, ExecState, ExecutorContext, KclError, SourceRange,
@ -72,6 +72,10 @@ pub enum KclValue {
ImportedGeometry(ImportedGeometry), ImportedGeometry(ImportedGeometry),
#[ts(skip)] #[ts(skip)]
Function { Function {
/// Adam Chalmers speculation:
/// Reference to a KCL stdlib function (written in Rust).
/// Some if the KCL value is an alias of a stdlib function,
/// None if it's a KCL function written/declared in KCL.
#[serde(skip)] #[serde(skip)]
func: Option<MemoryFunction>, func: Option<MemoryFunction>,
#[schemars(skip)] #[schemars(skip)]
@ -262,9 +266,6 @@ impl KclValue {
} }
} }
pub(crate) fn is_function(&self) -> bool {
matches!(self, KclValue::Function { .. })
}
/// Put the number into a KCL value. /// Put the number into a KCL value.
pub const fn from_number(f: f64, meta: Vec<Metadata>) -> Self { pub const fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
Self::Number { value: f, meta } Self::Number { value: f, meta }
@ -496,7 +497,7 @@ impl KclValue {
) )
.await .await
} else { } else {
crate::executor::call_user_defined_function( crate::execution::call_user_defined_function(
args, args,
closure_memory.as_ref(), closure_memory.as_ref(),
expression.as_ref(), expression.as_ref(),
@ -506,4 +507,39 @@ impl KclValue {
.await .await
} }
} }
/// If this is a function, call it by applying keyword arguments.
/// If it's not a function, returns an error.
pub async fn call_fn_kw(
&self,
args: crate::std::Args,
exec_state: &mut ExecState,
ctx: ExecutorContext,
callsite: SourceRange,
) -> Result<Option<KclValue>, KclError> {
let KclValue::Function {
func,
expression,
memory: closure_memory,
meta: _,
} = &self
else {
return Err(KclError::Semantic(KclErrorDetails {
message: "cannot call this because it isn't a function".to_string(),
source_ranges: vec![callsite],
}));
};
if let Some(_func) = func {
todo!("Implement calling KCL stdlib fns that are aliased. Part of https://github.com/KittyCAD/modeling-app/issues/4600");
} else {
crate::execution::call_user_defined_function_kw(
args.kw_args,
closure_memory.as_ref(),
expression.as_ref(),
exec_state,
&ctx,
)
.await
}
}
} }

View File

@ -1,6 +1,6 @@
//! The executor for the AST. //! The executor for the AST.
use std::{collections::HashSet, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use anyhow::Result; use anyhow::Result;
use async_recursion::async_recursion; use async_recursion::async_recursion;
@ -20,14 +20,18 @@ use serde::{Deserialize, Serialize};
type Point2D = kcmc::shared::Point2d<f64>; type Point2D = kcmc::shared::Point2d<f64>;
type Point3D = kcmc::shared::Point3d<f64>; type Point3D = kcmc::shared::Point3d<f64>;
pub use crate::kcl_value::KclValue; pub use function_param::FunctionParam;
pub use kcl_value::{KclObjectFields, KclValue};
use crate::{ use crate::{
engine::{EngineManager, ExecutionKind}, engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
fs::{FileManager, FileSystem}, fs::{FileManager, FileSystem},
parsing::ast::{ parsing::ast::{
cache::{get_changed_program, CacheInformation}, cache::{get_changed_program, CacheInformation},
types::{BodyItem, Expr, FunctionExpression, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode}, types::{
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
},
}, },
settings::types::UnitLength, settings::types::UnitLength,
source_range::{ModuleId, SourceRange}, source_range::{ModuleId, SourceRange},
@ -35,6 +39,10 @@ use crate::{
ExecError, Program, ExecError, Program,
}; };
mod exec_ast;
mod function_param;
mod kcl_value;
/// State for executing a program. /// State for executing a program.
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
@ -50,7 +58,7 @@ pub struct ExecState {
/// expression. If we're not currently in a pipeline, this will be None. /// expression. If we're not currently in a pipeline, this will be None.
pub pipe_value: Option<KclValue>, pub pipe_value: Option<KclValue>,
/// Identifiers that have been exported from the current module. /// Identifiers that have been exported from the current module.
pub module_exports: HashSet<String>, pub module_exports: Vec<String>,
/// The stack of import statements for detecting circular module imports. /// The stack of import statements for detecting circular module imports.
/// If this is empty, we're not currently executing an import statement. /// If this is empty, we're not currently executing an import statement.
pub import_stack: Vec<std::path::PathBuf>, pub import_stack: Vec<std::path::PathBuf>,
@ -61,7 +69,7 @@ pub struct ExecState {
} }
impl ExecState { impl ExecState {
pub fn add_module(&mut self, path: std::path::PathBuf) -> ModuleId { fn add_module(&mut self, path: std::path::PathBuf) -> ModuleId {
// Need to avoid borrowing self in the closure. // Need to avoid borrowing self in the closure.
let new_module_id = ModuleId::from_usize(self.path_to_source_id.len()); let new_module_id = ModuleId::from_usize(self.path_to_source_id.len());
let mut is_new = false; let mut is_new = false;
@ -117,10 +125,16 @@ impl ProgramMemory {
Ok(()) Ok(())
} }
pub fn update_tag(&mut self, tag: &str, value: TagIdentifier) -> Result<(), KclError> { pub fn add_tag(&mut self, tag: &str, value: TagIdentifier, source_range: SourceRange) -> Result<(), KclError> {
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value))); self.add(tag, KclValue::TagIdentifier(Box::new(value)), source_range)
}
Ok(()) pub fn update_tag_if_defined(&mut self, tag: &str, value: TagIdentifier) {
if !self.environments[self.current_env.index()].contains_key(tag) {
// Do nothing if the tag isn't defined.
return;
}
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value)));
} }
/// Get a value from the program memory. /// Get a value from the program memory.
@ -837,7 +851,7 @@ impl GetTangentialInfoFromPathsResult {
impl Sketch { impl Sketch {
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) { pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) {
let mut tag_identifier: TagIdentifier = tag.into(); let mut tag_identifier = TagIdentifier::from(tag);
let base = current_path.get_base(); let base = current_path.get_base();
tag_identifier.info = Some(TagEngineInfo { tag_identifier.info = Some(TagEngineInfo {
id: base.geo_meta.id, id: base.geo_meta.id,
@ -1498,6 +1512,9 @@ pub struct ExecutorSettings {
/// Should engine store this for replay? /// Should engine store this for replay?
/// If so, under what name? /// If so, under what name?
pub replay: Option<String>, pub replay: Option<String>,
/// The directory of the current project. This is used for resolving import
/// paths. If None is given, the current working directory is used.
pub project_directory: Option<PathBuf>,
} }
impl Default for ExecutorSettings { impl Default for ExecutorSettings {
@ -1508,6 +1525,7 @@ impl Default for ExecutorSettings {
enable_ssao: false, enable_ssao: false,
show_grid: false, show_grid: false,
replay: None, replay: None,
project_directory: None,
} }
} }
} }
@ -1520,6 +1538,7 @@ impl From<crate::settings::types::Configuration> for ExecutorSettings {
enable_ssao: config.settings.modeling.enable_ssao.into(), enable_ssao: config.settings.modeling.enable_ssao.into(),
show_grid: config.settings.modeling.show_scale_grid, show_grid: config.settings.modeling.show_scale_grid,
replay: None, replay: None,
project_directory: None,
} }
} }
} }
@ -1532,6 +1551,7 @@ impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSet
enable_ssao: config.settings.modeling.enable_ssao.into(), enable_ssao: config.settings.modeling.enable_ssao.into(),
show_grid: config.settings.modeling.show_scale_grid, show_grid: config.settings.modeling.show_scale_grid,
replay: None, replay: None,
project_directory: None,
} }
} }
} }
@ -1544,6 +1564,7 @@ impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
enable_ssao: modeling.enable_ssao.into(), enable_ssao: modeling.enable_ssao.into(),
show_grid: modeling.show_scale_grid, show_grid: modeling.show_scale_grid,
replay: None, replay: None,
project_directory: None,
} }
} }
} }
@ -1774,6 +1795,7 @@ impl ExecutorContext {
enable_ssao: false, enable_ssao: false,
show_grid: false, show_grid: false,
replay: None, replay: None,
project_directory: None,
}, },
None, None,
engine_addr, engine_addr,
@ -1785,7 +1807,7 @@ impl ExecutorContext {
pub async fn reset_scene( pub async fn reset_scene(
&self, &self,
exec_state: &mut ExecState, exec_state: &mut ExecState,
source_range: crate::executor::SourceRange, source_range: crate::execution::SourceRange,
) -> Result<(), KclError> { ) -> Result<(), KclError> {
self.engine self.engine
.clear_scene(&mut exec_state.id_generator, source_range) .clear_scene(&mut exec_state.id_generator, source_range)
@ -1850,7 +1872,7 @@ impl ExecutorContext {
) )
.await?; .await?;
self.inner_execute(&cache_result.program, exec_state, crate::executor::BodyType::Root) self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root)
.await?; .await?;
let session_data = self.engine.get_session_data(); let session_data = self.engine.get_session_data();
Ok(session_data) Ok(session_data)
@ -1870,67 +1892,15 @@ impl ExecutorContext {
match statement { match statement {
BodyItem::ImportStatement(import_stmt) => { BodyItem::ImportStatement(import_stmt) => {
let source_range = SourceRange::from(import_stmt); let source_range = SourceRange::from(import_stmt);
let path = import_stmt.path.clone(); let (module_memory, module_exports) =
// Empty path is used by the top-level module. self.open_module(&import_stmt.path, exec_state, source_range).await?;
if path.is_empty() {
return Err(KclError::Semantic(KclErrorDetails {
message: "import path cannot be empty".to_owned(),
source_ranges: vec![source_range],
}));
}
let resolved_path = std::path::PathBuf::from(&path);
if exec_state.import_stack.contains(&resolved_path) {
return Err(KclError::ImportCycle(KclErrorDetails {
message: format!(
"circular import of modules is not allowed: {} -> {}",
exec_state
.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
resolved_path.to_string_lossy()
),
source_ranges: vec![import_stmt.into()],
}));
}
let module_id = exec_state.add_module(resolved_path.clone());
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
// TODO handle parsing errors properly
let program = crate::parsing::parse_str(&source, module_id).parse_errs_as_err()?;
let (module_memory, module_exports) = {
exec_state.import_stack.push(resolved_path.clone());
let original_execution = self.engine.replace_execution_kind(ExecutionKind::Isolated);
let original_memory = std::mem::take(&mut exec_state.memory);
let original_exports = std::mem::take(&mut exec_state.module_exports);
let result = self
.inner_execute(&program, exec_state, crate::executor::BodyType::Root)
.await;
let module_exports = std::mem::replace(&mut exec_state.module_exports, original_exports);
let module_memory = std::mem::replace(&mut exec_state.memory, original_memory);
self.engine.replace_execution_kind(original_execution);
exec_state.import_stack.pop();
result.map_err(|err| { match &import_stmt.selector {
if let KclError::ImportCycle(_) = err { ImportSelector::List { items } => {
// It was an import cycle. Keep the original message. for import_item in items {
err.override_source_ranges(vec![source_range])
} else {
KclError::Semantic(KclErrorDetails {
message: format!(
"Error loading imported file. Open it to view more details. {path}: {}",
err.message()
),
source_ranges: vec![source_range],
})
}
})?;
(module_memory, module_exports)
};
for import_item in &import_stmt.items {
// Extract the item from the module. // Extract the item from the module.
let item = module_memory let item =
module_memory
.get(&import_item.name.name, import_item.into()) .get(&import_item.name.name, import_item.into())
.map_err(|_err| { .map_err(|_err| {
KclError::UndefinedValue(KclErrorDetails { KclError::UndefinedValue(KclErrorDetails {
@ -1955,6 +1925,33 @@ impl ExecutorContext {
item.clone(), item.clone(),
SourceRange::from(&import_item.name), SourceRange::from(&import_item.name),
)?; )?;
if let ItemVisibility::Export = import_stmt.visibility {
exec_state.module_exports.push(import_item.identifier().to_owned());
}
}
}
ImportSelector::Glob(_) => {
for name in module_exports.iter() {
let item = module_memory.get(name, source_range).map_err(|_err| {
KclError::Internal(KclErrorDetails {
message: format!("{} is not defined in module (but was exported?)", name),
source_ranges: vec![source_range],
})
})?;
exec_state.memory.add(name, item.clone(), source_range)?;
if let ItemVisibility::Export = import_stmt.visibility {
exec_state.module_exports.push(name.clone());
}
}
}
ImportSelector::None(_) => {
return Err(KclError::Semantic(KclErrorDetails {
message: "Importing whole module is not yet implemented, sorry.".to_owned(),
source_ranges: vec![source_range],
}));
}
} }
last_expr = None; last_expr = None;
} }
@ -1971,34 +1968,23 @@ impl ExecutorContext {
); );
} }
BodyItem::VariableDeclaration(variable_declaration) => { BodyItem::VariableDeclaration(variable_declaration) => {
for declaration in &variable_declaration.declarations { let var_name = variable_declaration.declaration.id.name.to_string();
let var_name = declaration.id.name.to_string(); let source_range = SourceRange::from(&variable_declaration.declaration.init);
let source_range = SourceRange::from(&declaration.init);
let metadata = Metadata { source_range }; let metadata = Metadata { source_range };
let memory_item = self let memory_item = self
.execute_expr( .execute_expr(
&declaration.init, &variable_declaration.declaration.init,
exec_state, exec_state,
&metadata, &metadata,
StatementKind::Declaration { name: &var_name }, StatementKind::Declaration { name: &var_name },
) )
.await?; .await?;
let is_function = memory_item.is_function();
exec_state.memory.add(&var_name, memory_item, source_range)?; exec_state.memory.add(&var_name, memory_item, source_range)?;
// Track exports. // Track exports.
match variable_declaration.visibility { if let ItemVisibility::Export = variable_declaration.visibility {
ItemVisibility::Export => { exec_state.module_exports.push(var_name);
if !is_function {
return Err(KclError::Semantic(KclErrorDetails {
message: "Only functions can be exported".to_owned(),
source_ranges: vec![source_range],
}));
}
exec_state.module_exports.insert(var_name);
}
ItemVisibility::Default => {}
}
} }
last_expr = None; last_expr = None;
} }
@ -2033,6 +2019,68 @@ impl ExecutorContext {
Ok(last_expr) Ok(last_expr)
} }
async fn open_module(
&self,
path: &str,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<(ProgramMemory, Vec<String>), KclError> {
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
project_dir.join(path)
} else {
std::path::PathBuf::from(&path)
};
if exec_state.import_stack.contains(&resolved_path) {
return Err(KclError::ImportCycle(KclErrorDetails {
message: format!(
"circular import of modules is not allowed: {} -> {}",
exec_state
.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
resolved_path.to_string_lossy()
),
source_ranges: vec![source_range],
}));
}
let module_id = exec_state.add_module(resolved_path.clone());
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
// TODO handle parsing errors properly
let program = crate::parsing::parse_str(&source, module_id).parse_errs_as_err()?;
exec_state.import_stack.push(resolved_path.clone());
let original_execution = self.engine.replace_execution_kind(ExecutionKind::Isolated);
let original_memory = std::mem::take(&mut exec_state.memory);
let original_exports = std::mem::take(&mut exec_state.module_exports);
let result = self
.inner_execute(&program, exec_state, crate::execution::BodyType::Root)
.await;
let module_exports = std::mem::replace(&mut exec_state.module_exports, original_exports);
let module_memory = std::mem::replace(&mut exec_state.memory, original_memory);
self.engine.replace_execution_kind(original_execution);
exec_state.import_stack.pop();
result.map_err(|err| {
if let KclError::ImportCycle(_) = err {
// It was an import cycle. Keep the original message.
err.override_source_ranges(vec![source_range])
} else {
KclError::Semantic(KclErrorDetails {
message: format!(
"Error loading imported file. Open it to view more details. {path}: {}",
err.message()
),
source_ranges: vec![source_range],
})
}
})?;
Ok((module_memory, module_exports))
}
pub async fn execute_expr<'a>( pub async fn execute_expr<'a>(
&self, &self,
init: &Expr, init: &Expr,
@ -2043,7 +2091,7 @@ impl ExecutorContext {
let item = match init { let item = match init {
Expr::None(none) => KclValue::from(none), Expr::None(none) => KclValue::from(none),
Expr::Literal(literal) => KclValue::from(literal), Expr::Literal(literal) => KclValue::from(literal),
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?, Expr::TagDeclarator(tag) => KclValue::from(tag),
Expr::Identifier(identifier) => { Expr::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?; let value = exec_state.memory.get(&identifier.name, identifier.into())?;
value.clone() value.clone()
@ -2120,7 +2168,7 @@ impl ExecutorContext {
self.engine self.engine
.send_modeling_cmd( .send_modeling_cmd(
uuid::Uuid::new_v4(), uuid::Uuid::new_v4(),
crate::executor::SourceRange::default(), crate::execution::SourceRange::default(),
ModelingCmd::from(mcmd::ZoomToFit { ModelingCmd::from(mcmd::ZoomToFit {
object_ids: Default::default(), object_ids: Default::default(),
animated: false, animated: false,
@ -2134,7 +2182,7 @@ impl ExecutorContext {
.engine .engine
.send_modeling_cmd( .send_modeling_cmd(
uuid::Uuid::new_v4(), uuid::Uuid::new_v4(),
crate::executor::SourceRange::default(), crate::execution::SourceRange::default(),
ModelingCmd::from(mcmd::TakeSnapshot { ModelingCmd::from(mcmd::TakeSnapshot {
format: ImageFormat::Png, format: ImageFormat::Png,
}), }),
@ -2205,6 +2253,59 @@ fn assign_args_to_params(
Ok(fn_memory) Ok(fn_memory)
} }
fn assign_args_to_params_kw(
function_expression: NodeRef<'_, FunctionExpression>,
mut args: crate::std::args::KwArgs,
mut fn_memory: ProgramMemory,
) -> Result<ProgramMemory, KclError> {
// Add the arguments to the memory. A new call frame should have already
// been created.
let source_ranges = vec![function_expression.into()];
for param in function_expression.params.iter() {
if param.labeled {
let arg = args.labeled.get(&param.identifier.name);
let arg_val = match arg {
Some(arg) => arg.value.clone(),
None => match param.default_value {
Some(ref default_val) => KclValue::from(default_val.clone()),
None => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges,
message: format!(
"This function requires a parameter {}, but you haven't passed it one.",
param.identifier.name
),
}));
}
},
};
fn_memory.add(&param.identifier.name, arg_val, (&param.identifier).into())?;
} else {
let Some(unlabeled) = args.unlabeled.take() else {
let param_name = &param.identifier.name;
return Err(if args.labeled.contains_key(param_name) {
KclError::Semantic(KclErrorDetails {
source_ranges,
message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
})
} else {
KclError::Semantic(KclErrorDetails {
source_ranges,
message: "This function expects an unlabeled first parameter, but you haven't passed it one."
.to_owned(),
})
});
};
fn_memory.add(
&param.identifier.name,
unlabeled.value.clone(),
(&param.identifier).into(),
)?;
}
}
Ok(fn_memory)
}
pub(crate) async fn call_user_defined_function( pub(crate) async fn call_user_defined_function(
args: Vec<Arg>, args: Vec<Arg>,
memory: &ProgramMemory, memory: &ProgramMemory,
@ -2235,6 +2336,36 @@ pub(crate) async fn call_user_defined_function(
result.map(|_| fn_memory.return_) result.map(|_| fn_memory.return_)
} }
pub(crate) async fn call_user_defined_function_kw(
args: crate::std::args::KwArgs,
memory: &ProgramMemory,
function_expression: NodeRef<'_, FunctionExpression>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<Option<KclValue>, KclError> {
// Create a new environment to execute the function body in so that local
// variables shadow variables in the parent scope. The new environment's
// parent should be the environment of the closure.
let mut body_memory = memory.clone();
let body_env = body_memory.new_env_for_call(memory.current_env);
body_memory.current_env = body_env;
let fn_memory = assign_args_to_params_kw(function_expression, args, body_memory)?;
// Execute the function body using the memory we just created.
let (result, fn_memory) = {
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
let result = ctx
.inner_execute(&function_expression.body, exec_state, BodyType::Block)
.await;
// Restore the previous memory.
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
(result, fn_memory)
};
result.map(|_| fn_memory.return_)
}
pub enum StatementKind<'a> { pub enum StatementKind<'a> {
Declaration { name: &'a str }, Declaration { name: &'a str },
Expression, Expression,
@ -2846,8 +2977,10 @@ let notTagDeclarator = !myTagDeclarator";
); );
let code9 = " let code9 = "
let myTagDeclarator = $myTag sk = startSketchOn('XY')
let notTagIdentifier = !myTag"; |> startProfileAt([0, 0], %)
|> line([5, 0], %, $myTag)
notTagIdentifier = !myTag";
let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap(); let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap();
// These are currently printed out as JSON objects, so we don't want to // These are currently printed out as JSON objects, so we don't want to
// check the full error. // check the full error.

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