Compare commits

...

31 Commits

Author SHA1 Message Date
00e97257ae Set disableDifferentialDownload to true for the auto updater (#4742)
Fixes #4120
2024-12-11 09:04:02 -06:00
aeb656d176 Remove flags we had for code sign on Cut Release PRs (#4728) 2024-12-11 09:03:12 -06:00
ac49ebd6e0 Revert "chore: implemented multiple instances instead of multiple appications?" (#4750)
Revert "chore: implemented multiple instances instead of multiple appications…"

This reverts commit 548c664db0.
2024-12-11 08:58:42 -06:00
b40f03ad25 Fix wasm init deprecation warning (#4747)
See `__wbg_init()` in `src/wasm-lib/pkg/wasm_lib.js`.
2024-12-11 04:54:20 -05:00
a8ad86e645 Add some comments to new API in wasm.ts (#4712)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-11 21:37:03 +13:00
87f50cd5e9 Implement as aliases for sub-expressions (#4723)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-11 21:26:42 +13:00
0400e6228e Add a "current" marker to UnitsMenu (#4744) 2024-12-11 06:00:19 +00:00
26f150fd6c Change diagnostic action button to primary color (#4737) 2024-12-11 00:25:43 -05:00
3049f405f5 Reapply "More aggressive using of cache on engine settings changes" (#4736)
* Reapply "More aggressive using of cache on engine settings changes (#4691)" (#4729)

This reverts commit 3f1f40eeba.

* Add a utility to get all the current values from the settings object

* Use an XState selector to get the latest settings snapshot for WASM

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-12-11 02:50:22 +00:00
53d40301dc start of Appearance function (#4743)
* initial commit

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 docs

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

* updates

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

* updates

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

* add more samples

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

* updates

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

* updatres

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

* regenerate docs

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

* updates

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

* patterns and appearance samples

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>

* fmt

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-11 01:51:51 +00:00
671c01e36f More consistent GitHub links (#4741)
Fixes #4726
Tested locally with regular and nightly (`yarn files:flip-to-nightly`) configs.
2024-12-10 15:52:57 -06:00
e80151979b Pin electron versions and remove unused packages (#4708)
* Remove forge dependencies for packaging
Fixes #4628

* More clean up and pin current electron versions
2024-12-10 16:37:08 -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
146 changed files with 11664 additions and 3697 deletions

View File

@ -165,7 +165,6 @@ jobs:
- name: Build the app (release) - name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
env: env:
PUBLISH_FOR_PULL_REQUEST: true
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@ -173,7 +172,6 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always run: yarn electron-builder --config --publish always
@ -229,7 +227,6 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always run: yarn electron-builder --config --publish always

View File

@ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes.
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple - **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
chamfer cases work currently. chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.

210
docs/kcl/appearance.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ layout: manual
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects) * [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
* [`angledLineToX`](kcl/angledLineToX) * [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY) * [`angledLineToY`](kcl/angledLineToY)
* [`appearance`](kcl/appearance)
* [`arc`](kcl/arc) * [`arc`](kcl/arc)
* [`arcTo`](kcl/arcTo) * [`arcTo`](kcl/arcTo)
* [`asin`](kcl/asin) * [`asin`](kcl/asin)

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
---
title: "AppearanceData"
excerpt: "Data for appearance."
layout: manual
---
Data for appearance.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |

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

@ -950,7 +950,75 @@ test(
test.describe('Grid visibility', { tag: '@snapshot' }, () => { test.describe('Grid visibility', { tag: '@snapshot' }, () => {
// FIXME: Skip on macos its being weird. // FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos') // test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off to on via command bar', async ({ page }) => {
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// wait for execution done
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(1)
await u.closeDebugPanel()
await u.closeKclCodePanel()
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
// Open the command bar.
await page
.getByRole('button', { name: 'Commands', exact: false })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
const commandName = 'show scale grid'
const commandOption = page.getByRole('option', {
name: commandName,
exact: false,
})
const cmdSearchBar = page.getByPlaceholder('Search commands')
// This selector changes after we set the setting
await cmdSearchBar.fill(commandName)
await expect(commandOption).toBeVisible()
await commandOption.click()
const toggleInput = page.getByPlaceholder('Off')
await expect(toggleInput).toBeVisible()
await expect(toggleInput).toBeFocused()
// Select On
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
'data-headlessui-state',
'active selected'
)
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set show scale grid to "true" as a user default`)
).toBeVisible()
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
})
})
test('Grid turned off', async ({ page }) => { test('Grid turned off', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 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

@ -1,20 +1,9 @@
import type { ForgeConfig } from '@electron-forge/shared-types' import type { ForgeConfig } from '@electron-forge/shared-types'
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
import { MakerZIP } from '@electron-forge/maker-zip'
import { MakerDeb } from '@electron-forge/maker-deb'
import { MakerRpm } from '@electron-forge/maker-rpm'
import { VitePlugin } from '@electron-forge/plugin-vite' import { VitePlugin } from '@electron-forge/plugin-vite'
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
import { FusesPlugin } from '@electron-forge/plugin-fuses' import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { FuseV1Options, FuseVersion } from '@electron/fuses' import { FuseV1Options, FuseVersion } from '@electron/fuses'
import path from 'path' import path from 'path'
interface ExtendedMakerWixConfig extends MakerWixConfig {
// see https://github.com/electron/forge/issues/3673
// this is an undocumented property of electron-wix-msi
associateExtensions?: string
}
const rootDir = process.cwd() const rootDir = process.cwd()
const config: ForgeConfig = { const config: ForgeConfig = {
@ -39,26 +28,7 @@ const config: ForgeConfig = {
extendInfo: 'Info.plist', // Information for file associations. extendInfo: 'Info.plist', // Information for file associations.
}, },
rebuildConfig: {}, rebuildConfig: {},
makers: [ makers: [],
new MakerSquirrel({
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
}),
new MakerWix({
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
associateExtensions: 'kcl',
} as ExtendedMakerWixConfig),
new MakerZIP({}, ['darwin']),
new MakerRpm({
options: {
icon: path.resolve(rootDir, 'assets', 'icon.png'),
},
}),
new MakerDeb({
options: {
icon: path.resolve(rootDir, 'assets', 'icon.png'),
},
}),
],
plugins: [ plugins: [
new VitePlugin({ new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc. // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.

View File

@ -39,7 +39,6 @@
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "6.3.0", "electron-updater": "6.3.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
@ -69,7 +68,7 @@
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite --port=3000 --host=0.0.0.0",
"start:prod": "vite preview --port=3000", "start:prod": "vite preview --port=3000",
"serve": "vite serve --port=3000", "serve": "vite serve --port=3000",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build", "build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
@ -104,8 +103,6 @@
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"tron:start": "electron-forge start", "tron:start": "electron-forge start",
"tron:package": "electron-forge package", "tron:package": "electron-forge package",
"tron:make": "electron-forge make",
"tron:publish": "electron-forge publish",
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron", "tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts", "tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
"tronb:package": "electron-builder --config electron-builder.yml", "tronb:package": "electron-builder --config electron-builder.yml",
@ -148,17 +145,10 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.4",
"@electron-forge/cli": "^7.4.0", "@electron-forge/cli": "7.4.0",
"@electron-forge/maker-deb": "^7.4.0", "@electron-forge/plugin-fuses": "7.4.0",
"@electron-forge/maker-rpm": "^7.4.0", "@electron-forge/plugin-vite": "7.4.0",
"@electron-forge/maker-squirrel": "^7.4.0", "@electron/fuses": "1.8.0",
"@electron-forge/maker-wix": "^7.5.0",
"@electron-forge/maker-zip": "^7.5.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
"@electron-forge/plugin-fuses": "^7.4.0",
"@electron-forge/plugin-vite": "^7.4.0",
"@electron/fuses": "^1.8.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", "@nabla/vite-plugin-eslint": "^2.0.5",
@ -188,9 +178,9 @@
"@xstate/cli": "^0.5.17", "@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"d3-force": "^3.0.0", "d3-force": "^3.0.0",
"electron": "^32.1.2", "electron": "32.1.2",
"electron-builder": "^24.13.3", "electron-builder": "24.13.3",
"electron-notarize": "^1.2.2", "electron-notarize": "1.2.2",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",

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: {
pathToNode, name: 'Constrain with named value',
variableName: varName, groupId: 'modeling',
argDefaultValues: {
currentValue: {
pathToNode,
variableName: varName,
valueText: value,
},
},
}, },
}) })
} else if (isConstrained) { } else if (isConstrained) {

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

@ -1,4 +1,4 @@
import { APP_VERSION } from 'routes/Settings' import { APP_VERSION, getReleaseUrl } 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(getReleaseUrl())}
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}` href={getReleaseUrl()}
)}
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

@ -69,14 +69,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false) const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false) const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { const { auth } = useSettingsAuthContext()
auth,
settings: {
context: {
modeling: { defaultUnit },
},
},
} = useSettingsAuthContext()
const token = auth?.context.token const token = auth?.context.token
const navigate = useNavigate() const navigate = useNavigate()
@ -92,7 +85,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: KclWorkerOptions = { const initEvent: KclWorkerOptions = {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
baseUnit: defaultUnit.current,
apiBaseUrl: VITE_KC_API_BASE_URL, apiBaseUrl: VITE_KC_API_BASE_URL,
} }
lspWorker.postMessage({ lspWorker.postMessage({

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({ }) => {
selectionRanges, if (!lengthValue)
}) return Promise.reject(new Error('No length value'))
const constraintResult = await applyConstraintLength({
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

@ -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, getReleaseUrl } 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(getReleaseUrl())}
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}` href={getReleaseUrl()}
)}
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

@ -1,5 +1,5 @@
import { trap } from 'lib/trap' import { trap } from 'lib/trap'
import { useMachine } from '@xstate/react' import { useMachine, useSelector } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom' import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths' import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
@ -23,7 +23,6 @@ import {
engineCommandManager, engineCommandManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { settings } from 'lib/settings/initialSettings' import { settings } from 'lib/settings/initialSettings'
import { import {
@ -55,11 +54,15 @@ type SettingsAuthContextType = {
settings: MachineContext<typeof settingsMachine> settings: MachineContext<typeof settingsMachine>
} }
// a little hacky for sure, open to changing it /**
// this implies that we should only even have one instance of this provider mounted at any one time * This variable is used to store the last snapshot of the settings context
// but I think that's a safe assumption * for use outside of React, such as in `wasm.ts`. It is updated every time
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined * the settings machine changes with `useSelector`.
export const getSettingsState = () => settingsStateRef * TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts`
*/
export let lastSettingsContextSnapshot:
| ContextFrom<typeof settingsMachine>
| undefined
export const SettingsAuthContext = createContext({} as SettingsAuthContextType) export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
@ -129,27 +132,11 @@ export const SettingsAuthProviderBase = ({
.setTheme(context.app.theme.current) .setTheme(context.app.theme.current)
.catch(reportRejection) .catch(reportRejection)
}, },
setEngineScaleGridVisibility: ({ context }) => {
engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current
)
},
setClientTheme: ({ context }) => { setClientTheme: ({ context }) => {
const opposingTheme = getOppositeTheme(context.app.theme.current) const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme sceneInfra.theme = opposingTheme
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme) sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
}, },
setEngineEdges: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'edge_lines_visible' as any, // TODO update kittycad.ts to get this new command type
hidden: !context.modeling.highlightEdges.current,
},
})
},
toastSuccess: ({ event }) => { toastSuccess: ({ event }) => {
if (!('data' in event)) return if (!('data' in event)) return
const eventParts = event.type.replace(/^set./, '').split('.') as [ const eventParts = event.type.replace(/^set./, '').split('.') as [
@ -175,17 +162,27 @@ export const SettingsAuthProviderBase = ({
}, },
'Execute AST': ({ context, event }) => { 'Execute AST': ({ context, event }) => {
try { try {
const relevantSetting = (s: typeof settings) => {
return (
s.modeling?.defaultUnit?.current !==
context.modeling.defaultUnit.current ||
s.modeling.showScaleGrid.current !==
context.modeling.showScaleGrid.current ||
s.modeling?.highlightEdges.current !==
context.modeling.highlightEdges.current
)
}
const allSettingsIncludesUnitChange = const allSettingsIncludesUnitChange =
event.type === 'Set all settings' && event.type === 'Set all settings' &&
event.settings?.modeling?.defaultUnit?.current !== relevantSetting(event.settings)
context.modeling.defaultUnit.current
const resetSettingsIncludesUnitChange = const resetSettingsIncludesUnitChange =
event.type === 'Reset settings' && event.type === 'Reset settings' && relevantSetting(settings)
context.modeling.defaultUnit.current !==
settings?.modeling?.defaultUnit?.default
if ( if (
event.type === 'set.modeling.defaultUnit' || event.type === 'set.modeling.defaultUnit' ||
event.type === 'set.modeling.showScaleGrid' ||
event.type === 'set.modeling.highlightEdges' ||
allSettingsIncludesUnitChange || allSettingsIncludesUnitChange ||
resetSettingsIncludesUnitChange resetSettingsIncludesUnitChange
) { ) {
@ -214,7 +211,10 @@ export const SettingsAuthProviderBase = ({
}), }),
{ input: loadedSettings } { input: loadedSettings }
) )
settingsStateRef = settingsState.context // Any time the actor changes, update the settings state for external use
useSelector(settingsActor, (s) => {
lastSettingsContextSnapshot = s.context
})
useEffect(() => { useEffect(() => {
if (!isDesktop()) return if (!isDesktop()) return

View File

@ -2,6 +2,7 @@ import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import { getReleaseUrl } from 'routes/Settings'
export function ToastUpdate({ export function ToastUpdate({
version, version,
@ -32,10 +33,8 @@ export function ToastUpdate({
A new update has downloaded and will be available next time you A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '} start the app. You can view the release notes{' '}
<a <a
onClick={openExternalBrowserIfDesktop( onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}` href={getReleaseUrl(version)}
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >

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

@ -41,7 +41,10 @@ export function UnitsMenu() {
close() close()
}} }}
> >
{baseUnitLabels[unit]} <span className="flex-1">{baseUnitLabels[unit]}</span>
{unit === settings.context.modeling.defaultUnit.current && (
<span className="text-chalkboard-60">current</span>
)}
</button> </button>
</li> </li>
))} ))}

View File

@ -1,7 +1,5 @@
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client' import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
export enum LspWorker { export enum LspWorker {
Kcl = 'kcl', Kcl = 'kcl',
Copilot = 'copilot', Copilot = 'copilot',
@ -9,7 +7,6 @@ export enum LspWorker {
export interface KclWorkerOptions { export interface KclWorkerOptions {
wasmUrl: string wasmUrl: string
token: string token: string
baseUnit: UnitLength
apiBaseUrl: string apiBaseUrl: string
} }

View File

@ -17,7 +17,6 @@ import {
KclWorkerOptions, KclWorkerOptions,
CopilotWorkerOptions, CopilotWorkerOptions,
} from 'editor/plugins/lsp/types' } from 'editor/plugins/lsp/types'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
const intoServer: IntoServer = new IntoServer() const intoServer: IntoServer = new IntoServer()
@ -46,14 +45,12 @@ export async function copilotLspRun(
export async function kclLspRun( export async function kclLspRun(
config: ServerConfig, config: ServerConfig,
engineCommandManager: EngineCommandManager | null,
token: string, token: string,
baseUnit: string,
baseUrl: string baseUrl: string
) { ) {
try { try {
console.log('start kcl lsp') console.log('start kcl lsp')
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl) await kcl_lsp_run(config, null, undefined, token, baseUrl)
} catch (e: any) { } catch (e: any) {
console.log('kcl lsp failed', e) console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way. // We can't restart here because a moved value, we should do this another way.
@ -82,13 +79,7 @@ onmessage = function (event: MessageEvent) {
switch (worker) { switch (worker) {
case LspWorker.Kcl: case LspWorker.Kcl:
const kclData = eventData as KclWorkerOptions const kclData = eventData as KclWorkerOptions
await kclLspRun( await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
config,
null,
kclData.token,
kclData.baseUnit,
kclData.apiBaseUrl
)
break break
case LspWorker.Copilot: case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions let copilotData = eventData as CopilotWorkerOptions

View File

@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm' import { makeDefaultPlanes } from 'lang/wasm'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { useAppState, useAppStream } from 'AppState' import { useAppState, useAppStream } from 'AppState'
@ -56,9 +56,6 @@ export function useSetupEngineManager(
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager) return makeDefaultPlanes(kclManager.engineCommandManager)
}, },
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
}) })
hasSetNonZeroDimensions.current = true hasSetNonZeroDimensions.current = true
} }

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

@ -317,3 +317,8 @@ code {
#code-mirror-override .cm-editor { #code-mirror-override .cm-editor {
height: 100% !important; height: 100% !important;
} }
/* Can't use #code-mirror-override here as we're outside of this div */
.body-bg .cm-diagnosticAction {
@apply bg-primary;
}

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>,
@ -590,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
@ -933,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,

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'
@ -40,7 +40,6 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager), makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {},
callbackOnEngineLiteConnect: () => { callbackOnEngineLiteConnect: () => {
resolve(true) resolve(true)
}, },
@ -118,13 +117,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>(
@ -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> {

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 {
@ -654,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

@ -1064,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

@ -139,7 +139,6 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager), makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => { callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [ const cacheEntries = Object.entries(codeToWriteCacheFor) as [

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

@ -1399,7 +1399,6 @@ export class EngineCommandManager extends EventTarget {
} }
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {} private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {} private onEngineConnectionClosed = () => {}
@ -1432,7 +1431,6 @@ export class EngineCommandManager extends EventTarget {
height, height,
token, token,
makeDefaultPlanes, makeDefaultPlanes,
modifyGrid,
settings = { settings = {
pool: null, pool: null,
theme: Themes.Dark, theme: Themes.Dark,
@ -1452,14 +1450,12 @@ export class EngineCommandManager extends EventTarget {
height: number height: number
token?: string token?: string
makeDefaultPlanes: () => Promise<DefaultPlanes> makeDefaultPlanes: () => Promise<DefaultPlanes>
modifyGrid: (hidden: boolean) => Promise<void>
settings?: SettingsViaQueryString settings?: SettingsViaQueryString
}) { }) {
if (settings) { if (settings) {
this.settings = settings this.settings = settings
} }
this.makeDefaultPlanes = makeDefaultPlanes this.makeDefaultPlanes = makeDefaultPlanes
this.modifyGrid = modifyGrid
if (width === 0 || height === 0) { if (width === 0 || height === 0) {
return return
} }
@ -1539,21 +1535,15 @@ export class EngineCommandManager extends EventTarget {
type: 'default_camera_get_settings', type: 'default_camera_get_settings',
}, },
}) })
// We want modify the grid first because we don't want it to flash. await this.initPlanes()
// Ideally these would already be default hidden in engine (TODO do setIsStreamReady(true)
// that) https://github.com/KittyCAD/engine/issues/2282
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
setIsStreamReady(true)
// Other parts of the application should use this to react on scene ready. // Other parts of the application should use this to react on scene ready.
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.SceneReady, { new CustomEvent(EngineCommandManagerEvents.SceneReady, {
detail: this.engineConnection, detail: this.engineConnection,
}) })
) )
})
} }
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
@ -2212,15 +2202,6 @@ export class EngineCommandManager extends EventTarget {
}).catch(reportRejection) }).catch(reportRejection)
} }
/**
* Set the visibility of the scale grid in the engine scene.
* @param visible - whether to show or hide the scale grid
*/
setScaleGridVisibility(visible: boolean) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!visible)
}
// Some "objects" have the same source range, such as sketch_mode_start and start_path. // Some "objects" have the same source range, such as sketch_mode_start and start_path.
// So when passing a range, we need to also specify the command type // So when passing a range, we need to also specify the command type
mapRangeToObjectId( mapRangeToObjectId(

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

@ -1,14 +1,13 @@
import init, { import init, {
parse_wasm, parse_wasm,
recast_wasm, recast_wasm,
execute_wasm, execute,
kcl_lint, kcl_lint,
modify_ast_for_sketch_wasm, modify_ast_for_sketch_wasm,
is_points_ccw, is_points_ccw,
get_tangential_arc_to_info, get_tangential_arc_to_info,
program_memory_init, program_memory_init,
make_default_planes, make_default_planes,
modify_grid,
coredump, coredump,
toml_stringify, toml_stringify,
default_app_settings, default_app_settings,
@ -43,7 +42,9 @@ import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError' import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange' import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr' export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression' export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
@ -92,12 +93,26 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue' export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface' export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
/**
* The first two items are the start and end points (byte offsets from the start of the file).
* The third item is whether the source range belongs to the 'main' file, i.e., the file currently
* being rendered/displayed in the editor (TODO we need to handle modules better in the frontend).
*/
export type SourceRange = [number, number, boolean] export type SourceRange = [number, number, boolean]
/**
* Convert a SourceRange as used inside the KCL interpreter into the above one for use in the
* frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange
* so as not to expose details of the interpreter's current representation of module ids throughout
* the frontend).
*/
export function sourceRangeFromRust(s: RustSourceRange): SourceRange { export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
return [s[0], s[1], s[2] === 0] return [s[0], s[1], s[2] === 0]
} }
/**
* Create a default SourceRange for testing or as a placeholder.
*/
export function defaultSourceRange(): SourceRange { export function defaultSourceRange(): SourceRange {
return [0, 0, true] return [0, 0, true]
} }
@ -122,7 +137,7 @@ const initialise = async () => {
const fullUrl = wasmUrl() const fullUrl = wasmUrl()
const input = await fetch(fullUrl) const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer() const buffer = await input.arrayBuffer()
return await init(buffer) return await init({ module_or_path: buffer })
} catch (e) { } catch (e) {
console.log('Error initialising WASM', e) console.log('Error initialising WASM', e)
return Promise.reject(e) return Promise.reject(e)
@ -163,6 +178,10 @@ export class ParseResult {
} }
} }
/**
* Parsing was successful. There is guaranteed to be an AST and no fatal errors. There may or may
* not be warnings or non-fatal errors.
*/
class SuccessParseResult extends ParseResult { class SuccessParseResult extends ParseResult {
program: Node<Program> program: Node<Program>
@ -493,18 +512,19 @@ export const _executor = async (
return Promise.reject(programMemoryOverride) return Promise.reject(programMemoryOverride)
try { try {
let baseUnit = 'mm' let jsAppSettings = default_app_settings()
if (!TEST) { if (!TEST) {
const getSettingsState = import('components/SettingsAuthProvider').then( const lastSettingsSnapshot = await import(
(module) => module.getSettingsState 'components/SettingsAuthProvider'
) ).then((module) => module.lastSettingsContextSnapshot)
baseUnit = if (lastSettingsSnapshot) {
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm' jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
}
} }
const execState: RawExecState = await execute_wasm( const execState: RawExecState = await execute(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemoryOverride?.toRaw() || null), JSON.stringify(programMemoryOverride?.toRaw() || null),
baseUnit, JSON.stringify({ settings: jsAppSettings }),
engineCommandManager, engineCommandManager,
fileSystemManager fileSystemManager
) )
@ -552,20 +572,6 @@ export const makeDefaultPlanes = async (
} }
} }
export const modifyGrid = async (
engineCommandManager: EngineCommandManager,
hidden: boolean
): Promise<void> => {
try {
await modify_grid(engineCommandManager, hidden)
return
} catch (e) {
// TODO: do something real with the error.
console.log('modify grid error', e)
return Promise.reject(e)
}
}
export const modifyAstForSketch = async ( export const modifyAstForSketch = async (
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
ast: Node<Program>, ast: Node<Program>,

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

@ -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) {
incrementOrInitializeSelectionType('other') /**
return * 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')
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

@ -2,6 +2,7 @@ import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration' import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { import {
configurationToSettingsPayload, configurationToSettingsPayload,
getAllCurrentSettings,
projectConfigurationToSettingsPayload, projectConfigurationToSettingsPayload,
setSettingsAtLevel, setSettingsAtLevel,
} from './settingsUtils' } from './settingsUtils'
@ -65,3 +66,48 @@ describe(`testing settings initialization`, () => {
expect(settings.app.themeColor.current).toBe('200') expect(settings.app.themeColor.current).toBe('200')
}) })
}) })
describe(`testing getAllCurrentSettings`, () => {
it(`returns the correct settings`, () => {
// Set up the settings
let settings = createSettings()
const appConfiguration: DeepPartial<Configuration> = {
settings: {
app: {
appearance: {
theme: 'dark',
color: 190,
},
},
},
}
const projectConfiguration: DeepPartial<Configuration> = {
settings: {
app: {
appearance: {
theme: 'light',
color: 200,
},
},
modeling: {
base_unit: 'ft',
},
},
}
const appSettingsPayload = configurationToSettingsPayload(appConfiguration)
const projectSettingsPayload =
projectConfigurationToSettingsPayload(projectConfiguration)
setSettingsAtLevel(settings, 'user', appSettingsPayload)
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
// Now the test: get all the settings' current resolved values
const allCurrentSettings = getAllCurrentSettings(settings)
// This one gets the 'user'-level theme because it's ignored at the project level
// (see the test "doesn't read theme from project settings")
expect(allCurrentSettings.app.theme).toBe('dark')
expect(allCurrentSettings.app.themeColor).toBe('200')
expect(allCurrentSettings.modeling.defaultUnit).toBe('ft')
})
})

View File

@ -286,6 +286,27 @@ export function getChangedSettingsAtLevel(
return changedSettings return changedSettings
} }
export function getAllCurrentSettings(
allSettings: typeof settings
): SaveSettingsPayload {
const currentSettings = {} as SaveSettingsPayload
Object.entries(allSettings).forEach(([category, settingsCategory]) => {
const categoryKey = category as keyof typeof settings
Object.entries(settingsCategory).forEach(
([setting, settingValue]: [string, Setting]) => {
const settingKey =
setting as keyof (typeof settings)[typeof categoryKey]
currentSettings[categoryKey] = {
...currentSettings[categoryKey],
[settingKey]: settingValue.current,
}
}
)
})
return currentSettings
}
export function setSettingsAtLevel( export function setSettingsAtLevel(
allSettings: typeof settings, allSettings: typeof settings,
level: SettingsLevel, level: SettingsLevel,

View File

@ -112,9 +112,6 @@ export async function executor(
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
return new Promise((resolve) => resolve(defaultPlanes)) return new Promise((resolve) => resolve(defaultPlanes))
}, },
modifyGrid: (hidden: boolean) => {
return new Promise((resolve) => resolve())
},
}) })
return new Promise((resolve) => { return new Promise((resolve) => {

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

@ -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(
return new Promise((resolve, reject) => { ({
// TODO: figure out if we should validate argument data here or in the form itself, input,
// and if we should support people configuring a argument's validation function }: {
input: {
context: CommandBarContext | undefined
event: CommandBarMachineEvent | undefined
}
}) => {
return new Promise((resolve, reject) => {
if (!input || input?.event?.type !== 'Submit argument') {
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

@ -42,8 +42,6 @@ export const settingsMachine = setup({
setClientTheme: () => {}, setClientTheme: () => {},
'Execute AST': () => {}, 'Execute AST': () => {},
toastSuccess: () => {}, toastSuccess: () => {},
setEngineEdges: () => {},
setEngineScaleGridVisibility: () => {},
setClientSideSceneUnits: () => {}, setClientSideSceneUnits: () => {},
persistSettings: () => {}, persistSettings: () => {},
resetSettings: assign(({ context, event }) => { resetSettings: assign(({ context, event }) => {
@ -172,7 +170,7 @@ export const settingsMachine = setup({
'set.modeling.highlightEdges': { 'set.modeling.highlightEdges': {
target: 'persisting settings', target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'], actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
}, },
'Reset settings': { 'Reset settings': {
@ -201,11 +199,7 @@ export const settingsMachine = setup({
'set.modeling.showScaleGrid': { 'set.modeling.showScaleGrid': {
target: 'persisting settings', target: 'persisting settings',
actions: [ actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
'setSettingAtLevel',
'toastSuccess',
'setEngineScaleGridVisibility',
],
}, },
}, },
}, },

View File

@ -44,11 +44,6 @@ process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
process.env.VITE_KC_SKIP_AUTH ??= 'false' process.env.VITE_KC_SKIP_AUTH ??= 'false'
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000' process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
app.quit()
}
const ZOO_STUDIO_PROTOCOL = 'zoo-studio' const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
/// Register our application to handle all "electron-fiddle://" protocols. /// Register our application to handle all "electron-fiddle://" protocols.
@ -256,6 +251,9 @@ export function getAutoUpdater(): AppUpdater {
app.on('ready', () => { app.on('ready', () => {
const autoUpdater = getAutoUpdater() const autoUpdater = getAutoUpdater()
// TODO: we're getting `Error: Response ends without calling any handlers` with our setup,
// so at the moment this isn't worth enabling
autoUpdater.disableDifferentialDownload = true
setTimeout(() => { setTimeout(() => {
autoUpdater.checkForUpdates().catch(reportRejection) autoUpdater.checkForUpdates().catch(reportRejection)
}, 1000) }, 1000)

View File

@ -30,6 +30,14 @@ 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 function getReleaseUrl(version: string = APP_VERSION) {
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : ''
}v${version}`
}
export const Settings = () => { export const Settings = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()

View File

@ -1721,7 +1721,9 @@ dependencies = [
"parse-display 0.9.1", "parse-display 0.9.1",
"pretty_assertions", "pretty_assertions",
"pyo3", "pyo3",
"regex",
"reqwest", "reqwest",
"rgba_simple",
"ropey", "ropey",
"schemars", "schemars",
"serde", "serde",
@ -2971,6 +2973,12 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
] ]
[[package]]
name = "rgba_simple"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6cd655523701785087f69900df39892fb7b9b0721aa67682f571c38c32ac58a"
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.8"

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

@ -12,8 +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

@ -40,10 +40,12 @@ miette = "7.2.0"
mime_guess = "2.0.5" mime_guess = "2.0.5"
parse-display = "0.9.1" parse-display = "0.9.1"
pyo3 = { version = "0.22.6", optional = true } pyo3 = { version = "0.22.6", optional = true }
regex = "1.11.1"
reqwest = { version = "0.12", default-features = false, features = [ reqwest = { version = "0.12", default-features = false, features = [
"stream", "stream",
"rustls-tls", "rustls-tls",
] } ] }
rgba_simple = "0.10.0"
ropey = "1.6.1" ropey = "1.6.1"
schemars = { version = "0.8.17", features = [ schemars = { version = "0.8.17", features = [
"impl_json_schema", "impl_json_schema",

View File

@ -489,6 +489,12 @@ fn get_autocomplete_snippet_from_schema(
continue; continue;
} }
if prop_name == "color" {
fn_docs.push_str(&format!("\t{}: ${{{}:\"#ff0000\"}},\n", prop_name, i));
i += 1;
continue;
}
if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? { if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet)); fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
i = new_index + 1; i = new_index + 1;
@ -946,6 +952,21 @@ mod tests {
); );
} }
#[test]
fn get_autocomplete_snippet_appearance() {
let appearance_fn: Box<dyn StdLibFn> = Box::new(crate::std::appearance::Appearance);
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"appearance({
color: ${0:"#
.to_owned()
+ "\"#"
+ r#"ff0000"},
}, ${1:%})${}"#
);
}
// We want to test the snippets we compile at lsp start. // We want to test the snippets we compile at lsp start.
#[test] #[test]
fn get_all_stdlib_autocomplete_snippets() { fn get_all_stdlib_autocomplete_snippets() {

View File

@ -120,6 +120,61 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
Ok(()) Ok(())
} }
/// Set the visibility of edges.
async fn set_edge_visibility(
&self,
visible: bool,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
)
.await?;
Ok(())
}
async fn set_units(
&self,
units: crate::UnitLength,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
// Before we even start executing the program, set the units.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }),
)
.await?;
Ok(())
}
/// Re-run the command to apply the settings.
async fn reapply_settings(
&self,
settings: &crate::ExecutorSettings,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
// Set the edge visibility.
self.set_edge_visibility(settings.highlight_edges, source_range).await?;
// Change the units.
self.set_units(settings.units, source_range).await?;
// Send the command to show the grid.
self.modify_grid(!settings.show_grid, source_range).await?;
// We do not have commands for changing ssao on the fly.
// Flush the batch queue, so the settings are applied right away.
self.flush_batch(false, source_range).await?;
Ok(())
}
// Add a modeling command to the batch but don't fire it right away. // Add a modeling command to the batch but don't fire it right away.
async fn batch_modeling_cmd( async fn batch_modeling_cmd(
&self, &self,
@ -504,11 +559,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
})) }))
} }
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> { async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
// Hide/show the grid. // Hide/show the grid.
self.batch_modeling_cmd( self.batch_modeling_cmd(
uuid::Uuid::new_v4(), uuid::Uuid::new_v4(),
Default::default(), source_range,
&ModelingCmd::from(mcmd::ObjectVisible { &ModelingCmd::from(mcmd::ObjectVisible {
hidden, hidden,
object_id: *GRID_OBJECT_ID, object_id: *GRID_OBJECT_ID,
@ -519,7 +574,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Hide/show the grid scale text. // Hide/show the grid scale text.
self.batch_modeling_cmd( self.batch_modeling_cmd(
uuid::Uuid::new_v4(), uuid::Uuid::new_v4(),
Default::default(), source_range,
&ModelingCmd::from(mcmd::ObjectVisible { &ModelingCmd::from(mcmd::ObjectVisible {
hidden, hidden,
object_id: *GRID_SCALE_TEXT_OBJECT_ID, object_id: *GRID_SCALE_TEXT_OBJECT_ID,
@ -527,8 +582,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
) )
.await?; .await?;
self.flush_batch(false, Default::default()).await?;
Ok(()) Ok(())
} }

View File

@ -0,0 +1,50 @@
//! Functions for helping with caching an ast and finding the parts the changed.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
execution::ExecState,
parsing::ast::types::{Node, Program},
};
/// Information for the caching an AST and smartly re-executing it if we can.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheInformation {
/// The old information.
pub old: Option<OldAstState>,
/// The new ast to executed.
pub new_ast: Node<Program>,
}
/// The old ast and program memory.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct OldAstState {
/// The ast.
pub ast: Node<Program>,
/// The exec state.
pub exec_state: ExecState,
/// The last settings used for execution.
pub settings: crate::execution::ExecutorSettings,
}
impl From<crate::Program> for CacheInformation {
fn from(program: crate::Program) -> Self {
CacheInformation {
old: None,
new_ast: program.ast,
}
}
}
/// The result of a cache check.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheResult {
/// Should we clear the scene and start over?
pub clear_scene: bool,
/// The program that needs to be executed.
pub program: Node<Program>,
}

View File

@ -326,29 +326,12 @@ async fn inner_execute_pipe_body(
ctx: &ExecutorContext, ctx: &ExecutorContext,
) -> Result<KclValue, KclError> { ) -> Result<KclValue, KclError> {
for expression in body { for expression in body {
match expression { if let Expr::TagDeclarator(_) = expression {
Expr::TagDeclarator(_) => { return Err(KclError::Semantic(KclErrorDetails {
return Err(KclError::Semantic(KclErrorDetails { message: format!("This cannot be in a PipeExpression: {:?}", expression),
message: format!("This cannot be in a PipeExpression: {:?}", expression), source_ranges: vec![expression.into()],
source_ranges: vec![expression.into()], }));
})); }
}
Expr::Literal(_)
| Expr::Identifier(_)
| Expr::BinaryExpression(_)
| Expr::FunctionExpression(_)
| Expr::CallExpression(_)
| Expr::CallExpressionKw(_)
| Expr::PipeExpression(_)
| Expr::PipeSubstitution(_)
| Expr::ArrayExpression(_)
| Expr::ArrayRangeExpression(_)
| Expr::ObjectExpression(_)
| Expr::MemberExpression(_)
| Expr::UnaryExpression(_)
| Expr::IfExpression(_)
| Expr::None(_) => {}
};
let metadata = Metadata { let metadata = Metadata {
source_range: SourceRange::from(expression), source_range: SourceRange::from(expression),
}; };
@ -366,9 +349,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 };
@ -376,8 +361,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 {
@ -403,11 +392,43 @@ impl Node<CallExpressionKw> {
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)
} }
} }
} }
@ -419,6 +440,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 {
@ -428,15 +450,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 => {
@ -475,7 +501,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
@ -483,7 +526,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) => {
@ -521,7 +564,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);
@ -542,22 +585,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> {

View File

@ -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)]
@ -503,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

@ -23,15 +23,18 @@ type Point3D = kcmc::shared::Point3d<f64>;
pub use function_param::FunctionParam; pub use function_param::FunctionParam;
pub use kcl_value::{KclObjectFields, KclValue}; pub use kcl_value::{KclObjectFields, KclValue};
pub(crate) mod cache;
mod exec_ast;
mod function_param;
mod kcl_value;
use crate::{ use crate::{
engine::{EngineManager, ExecutionKind}, engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::cache::{CacheInformation, CacheResult},
fs::{FileManager, FileSystem}, fs::{FileManager, FileSystem},
parsing::ast::{ parsing::ast::types::{
cache::{get_changed_program, CacheInformation}, BodyItem, Expr, FunctionExpression, ImportSelector, 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},
@ -39,10 +42,6 @@ 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)]
@ -125,10 +124,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.
@ -845,7 +850,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,
@ -1654,17 +1659,6 @@ impl ExecutorContext {
let engine: Arc<Box<dyn EngineManager>> = let engine: Arc<Box<dyn EngineManager>> =
Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?)); Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
// Set the edge visibility.
engine
.batch_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
&ModelingCmd::from(mcmd::EdgeLinesVisible {
hidden: !settings.highlight_edges,
}),
)
.await?;
Ok(Self { Ok(Self {
engine, engine,
fs: Arc::new(FileManager::new()), fs: Arc::new(FileManager::new()),
@ -1691,7 +1685,7 @@ impl ExecutorContext {
pub async fn new( pub async fn new(
engine_manager: crate::engine::conn_wasm::EngineCommandManager, engine_manager: crate::engine::conn_wasm::EngineCommandManager,
fs_manager: crate::fs::wasm::FileSystemManager, fs_manager: crate::fs::wasm::FileSystemManager,
units: UnitLength, settings: ExecutorSettings,
) -> Result<Self, String> { ) -> Result<Self, String> {
Ok(ExecutorContext { Ok(ExecutorContext {
engine: Arc::new(Box::new( engine: Arc::new(Box::new(
@ -1701,16 +1695,16 @@ impl ExecutorContext {
)), )),
fs: Arc::new(FileManager::new(fs_manager)), fs: Arc::new(FileManager::new(fs_manager)),
stdlib: Arc::new(StdLib::new()), stdlib: Arc::new(StdLib::new()),
settings: ExecutorSettings { settings,
units,
..Default::default()
},
context_type: ContextType::Live, context_type: ContextType::Live,
}) })
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub async fn new_mock(fs_manager: crate::fs::wasm::FileSystemManager, units: UnitLength) -> Result<Self, String> { pub async fn new_mock(
fs_manager: crate::fs::wasm::FileSystemManager,
settings: ExecutorSettings,
) -> Result<Self, String> {
Ok(ExecutorContext { Ok(ExecutorContext {
engine: Arc::new(Box::new( engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -1719,10 +1713,7 @@ impl ExecutorContext {
)), )),
fs: Arc::new(FileManager::new(fs_manager)), fs: Arc::new(FileManager::new(fs_manager)),
stdlib: Arc::new(StdLib::new()), stdlib: Arc::new(StdLib::new()),
settings: ExecutorSettings { settings,
units,
..Default::default()
},
context_type: ContextType::Mock, context_type: ContextType::Mock,
}) })
} }
@ -1811,6 +1802,71 @@ impl ExecutorContext {
// AND if we aren't in wasm it doesn't really matter. // AND if we aren't in wasm it doesn't really matter.
Ok(()) Ok(())
} }
// Given an old ast, old program memory and new ast, find the parts of the code that need to be
// re-executed.
// This function should never error, because in the case of any internal error, we should just pop
// the cache.
pub async fn get_changed_program(&self, info: CacheInformation) -> Option<CacheResult> {
let Some(old) = info.old else {
// We have no old info, we need to re-execute the whole thing.
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
};
// If the settings are different we might need to bust the cache.
// We specifically do this before checking if they are the exact same.
if old.settings != self.settings {
// If the units are different we need to re-execute the whole thing.
if old.settings.units != self.settings.units {
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
}
// If anything else is different we do not need to re-execute, but rather just
// run the settings again.
if self
.engine
.reapply_settings(&self.settings, Default::default())
.await
.is_err()
{
// Bust the cache, we errored.
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
}
}
// If the ASTs are the EXACT same we return None.
// We don't even need to waste time computing the digests.
if old.ast == info.new_ast {
return None;
}
let mut old_ast = old.ast.inner;
old_ast.compute_digest();
let mut new_ast = info.new_ast.inner.clone();
new_ast.compute_digest();
// Check if the digest is the same.
if old_ast.digest == new_ast.digest {
return None;
}
// Check if the changes were only to Non-code areas, like comments or whitespace.
// For any unhandled cases just re-execute the whole thing.
Some(CacheResult {
clear_scene: true,
program: info.new_ast,
})
}
/// Perform the execution of a program. /// Perform the execution of a program.
/// You can optionally pass in some initialization memory. /// You can optionally pass in some initialization memory.
@ -1831,7 +1887,7 @@ impl ExecutorContext {
let _stats = crate::log::LogPerfStats::new("Interpretation"); let _stats = crate::log::LogPerfStats::new("Interpretation");
// Get the program that actually changed from the old and new information. // Get the program that actually changed from the old and new information.
let cache_result = get_changed_program(cache_info.clone(), &self.settings); let cache_result = self.get_changed_program(cache_info.clone()).await;
// Check if we don't need to re-execute. // Check if we don't need to re-execute.
let Some(cache_result) = cache_result else { let Some(cache_result) = cache_result else {
@ -1848,23 +1904,9 @@ impl ExecutorContext {
// TODO: Use the top-level file's path. // TODO: Use the top-level file's path.
exec_state.add_module(std::path::PathBuf::from("")); exec_state.add_module(std::path::PathBuf::from(""));
// Before we even start executing the program, set the units.
self.engine // Re-apply the settings, in case the cache was busted.
.batch_modeling_cmd( self.engine.reapply_settings(&self.settings, Default::default()).await?;
exec_state.id_generator.next_uuid(),
SourceRange::default(),
&ModelingCmd::from(mcmd::SetSceneUnits {
unit: match self.settings.units {
UnitLength::Cm => kcmc::units::UnitLength::Centimeters,
UnitLength::Ft => kcmc::units::UnitLength::Feet,
UnitLength::In => kcmc::units::UnitLength::Inches,
UnitLength::M => kcmc::units::UnitLength::Meters,
UnitLength::Mm => kcmc::units::UnitLength::Millimeters,
UnitLength::Yd => kcmc::units::UnitLength::Yards,
},
}),
)
.await?;
self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root) self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root)
.await?; .await?;
@ -2075,7 +2117,8 @@ impl ExecutorContext {
Ok((module_memory, module_exports)) Ok((module_memory, module_exports))
} }
pub async fn execute_expr<'a>( #[async_recursion]
pub async fn execute_expr<'a: 'async_recursion>(
&self, &self,
init: &Expr, init: &Expr,
exec_state: &mut ExecState, exec_state: &mut ExecState,
@ -2085,7 +2128,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()
@ -2132,6 +2175,14 @@ impl ExecutorContext {
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?, Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?, Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?, Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
Expr::LabelledExpression(expr) => {
let result = self
.execute_expr(&expr.expr, exec_state, metadata, statement_kind)
.await?;
exec_state.memory.add(&expr.label.name, result.clone(), init.into())?;
// TODO this lets us use the label as a variable name, but not as a tag in most cases
result
}
}; };
Ok(item) Ok(item)
} }
@ -2141,23 +2192,8 @@ impl ExecutorContext {
self.settings.units = units; self.settings.units = units;
} }
/// Execute the program, then get a PNG screenshot. /// Get a snapshot of the current scene.
pub async fn execute_and_prepare_snapshot( pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
&self,
program: &Program,
exec_state: &mut ExecState,
) -> std::result::Result<TakeSnapshot, ExecError> {
self.execute_and_prepare(program, exec_state).await
}
/// Execute the program, return the interpreter and outputs.
pub async fn execute_and_prepare(
&self,
program: &Program,
exec_state: &mut ExecState,
) -> std::result::Result<TakeSnapshot, ExecError> {
self.run(program.clone().into(), exec_state).await?;
// Zoom to fit. // Zoom to fit.
self.engine self.engine
.send_modeling_cmd( .send_modeling_cmd(
@ -2193,6 +2229,17 @@ impl ExecutorContext {
}; };
Ok(contents) Ok(contents)
} }
/// Execute the program, then get a PNG screenshot.
pub async fn execute_and_prepare_snapshot(
&self,
program: &Program,
exec_state: &mut ExecState,
) -> std::result::Result<TakeSnapshot, ExecError> {
self.run(program.clone().into(), exec_state).await?;
self.prepare_snapshot().await
}
} }
/// For each argument given, /// For each argument given,
@ -2247,6 +2294,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,
@ -2277,6 +2377,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,
@ -2289,9 +2419,12 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*; use super::*;
use crate::parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter}; use crate::{
parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter},
OldAstState,
};
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> { pub async fn parse_execute(code: &str) -> Result<(Program, ExecutorContext, ExecState)> {
let program = Program::parse_no_errs(code)?; let program = Program::parse_no_errs(code)?;
let ctx = ExecutorContext { let ctx = ExecutorContext {
@ -2302,9 +2435,9 @@ mod tests {
context_type: ContextType::Mock, context_type: ContextType::Mock,
}; };
let mut exec_state = ExecState::default(); let mut exec_state = ExecState::default();
ctx.run(program.into(), &mut exec_state).await?; ctx.run(program.clone().into(), &mut exec_state).await?;
Ok(exec_state.memory) Ok((program, ctx, exec_state))
} }
/// Convenience function to get a JSON value from memory and unwrap. /// Convenience function to get a JSON value from memory and unwrap.
@ -2715,36 +2848,39 @@ let shape = layer() |> patternTransform(10, transform, %)
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() { async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#; let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let memory = parse_execute(ast).await.unwrap(); let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(5.0, mem_get_json(&memory, "myVar").as_f64().unwrap()); assert_eq!(5.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute() { async fn test_math_execute() {
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#; let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
let memory = parse_execute(ast).await.unwrap(); let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&memory, "myVar").as_f64().unwrap()); assert_eq!(7.4, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_start_negative() { async fn test_math_execute_start_negative() {
let ast = r#"const myVar = -5 + 6"#; let ast = r#"const myVar = -5 + 6"#;
let memory = parse_execute(ast).await.unwrap(); let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(1.0, mem_get_json(&memory, "myVar").as_f64().unwrap()); assert_eq!(1.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_pi() { async fn test_math_execute_with_pi() {
let ast = r#"const myVar = pi() * 2"#; let ast = r#"const myVar = pi() * 2"#;
let memory = parse_execute(ast).await.unwrap(); let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(std::f64::consts::TAU, mem_get_json(&memory, "myVar").as_f64().unwrap()); assert_eq!(
std::f64::consts::TAU,
mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap()
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_define_decimal_without_leading_zero() { async fn test_math_define_decimal_without_leading_zero() {
let ast = r#"let thing = .4 + 7"#; let ast = r#"let thing = .4 + 7"#;
let memory = parse_execute(ast).await.unwrap(); let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&memory, "thing").as_f64().unwrap()); assert_eq!(7.4, mem_get_json(&exec_state.memory, "thing").as_f64().unwrap());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -2783,11 +2919,11 @@ fn check = (x) => {
} }
check(false) check(false)
"#; "#;
let mem = parse_execute(ast).await.unwrap(); let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(false, mem_get_json(&mem, "notTrue").as_bool().unwrap()); assert_eq!(false, mem_get_json(&exec_state.memory, "notTrue").as_bool().unwrap());
assert_eq!(true, mem_get_json(&mem, "notFalse").as_bool().unwrap()); assert_eq!(true, mem_get_json(&exec_state.memory, "notFalse").as_bool().unwrap());
assert_eq!(true, mem_get_json(&mem, "c").as_bool().unwrap()); assert_eq!(true, mem_get_json(&exec_state.memory, "c").as_bool().unwrap());
assert_eq!(false, mem_get_json(&mem, "d").as_bool().unwrap()); assert_eq!(false, mem_get_json(&exec_state.memory, "d").as_bool().unwrap());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -2888,8 +3024,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.
@ -3167,4 +3305,310 @@ let w = f() + f()
let json = serde_json::to_string(&mem).unwrap(); let json = serde_json::to_string(&mem).unwrap();
assert_eq!(json, r#"{"type":"Solids","value":[]}"#); assert_eq!(json, r#"{"type":"Solids","value":[]}"#);
} }
// Easy case where we have no old ast and memory.
// We need to re-execute everything.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_no_old_information() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, ctx, _) = parse_execute(new).await.unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: None,
new_ast: program.ast.clone(),
})
.await;
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program.ast);
assert!(result.clear_scene);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, ctx, exec_state) = parse_execute(new).await.unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
})
.await;
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_whitespace() {
let old = r#" // Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program_old, ctx, exec_state) = parse_execute(old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program_old.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
})
.await;
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
let old = r#" // Removed the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
})
.await;
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comments() {
let old = r#" // Removed the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %) // my thing
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
})
.await;
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program_new.ast);
assert!(result.clear_scene);
}
// Changing the units with the exact same file should bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_units() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
// Change the settings to cm.
ctx.settings.units = crate::UnitLength::Cm;
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
})
.await;
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program.ast);
assert!(result.clear_scene);
}
// Changing the grid settings with the exact same file should NOT bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_grid_setting() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
// Change the settings.
ctx.settings.show_grid = !ctx.settings.show_grid;
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
})
.await;
assert_eq!(result, None);
}
// Changing the edge visibility settings with the exact same file should NOT bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
// Change the settings.
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
})
.await;
assert_eq!(result, None);
}
} }

View File

@ -82,16 +82,15 @@ mod wasm;
pub use coredump::CoreDump; pub use coredump::CoreDump;
pub use engine::{EngineManager, ExecutionKind}; pub use engine::{EngineManager, ExecutionKind};
pub use errors::{CompilationError, ConnectionError, ExecError, KclError}; pub use errors::{CompilationError, ConnectionError, ExecError, KclError};
pub use execution::{ExecState, ExecutorContext, ExecutorSettings}; pub use execution::{
cache::{CacheInformation, OldAstState},
ExecState, ExecutorContext, ExecutorSettings,
};
pub use lsp::{ pub use lsp::{
copilot::Backend as CopilotLspBackend, copilot::Backend as CopilotLspBackend,
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand}, kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
}; };
pub use parsing::ast::{ pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
cache::{CacheInformation, OldAstState},
modify::modify_ast_for_sketch,
types::FormatOptions,
};
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength}; pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
pub use source_range::{ModuleId, SourceRange}; pub use source_range::{ModuleId, SourceRange};
@ -138,7 +137,7 @@ pub use lsp::test_util::kcl_lsp_server;
impl Program { impl Program {
pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> { pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
let module_id = ModuleId::default(); let module_id = ModuleId::default();
let tokens = parsing::token::lexer(input, module_id)?; let tokens = parsing::token::lex(input, module_id)?;
let (ast, errs) = parsing::parse_tokens(tokens).0?; let (ast, errs) = parsing::parse_tokens(tokens).0?;
Ok((ast.map(|ast| Program { ast }), errs)) Ok((ast.map(|ast| Program { ast }), errs))
@ -146,7 +145,7 @@ impl Program {
pub fn parse_no_errs(input: &str) -> Result<Program, KclError> { pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
let module_id = ModuleId::default(); let module_id = ModuleId::default();
let tokens = parsing::token::lexer(input, module_id)?; let tokens = parsing::token::lex(input, module_id)?;
let ast = parsing::parse_tokens(tokens).parse_errs_as_err()?; let ast = parsing::parse_tokens(tokens).parse_errs_as_err()?;
Ok(Program { ast }) Ok(Program { ast })

View File

@ -45,38 +45,32 @@ use crate::{
errors::Suggestion, errors::Suggestion,
lsp::{backend::Backend as _, util::IntoDiagnostic}, lsp::{backend::Backend as _, util::IntoDiagnostic},
parsing::{ parsing::{
ast::{ ast::types::{Expr, Node, VariableKind},
cache::{CacheInformation, OldAstState}, token::TokenStream,
types::{Expr, Node, VariableKind},
},
token::TokenType,
PIPE_OPERATOR, PIPE_OPERATOR,
}, },
ModuleId, Program, SourceRange, CacheInformation, ModuleId, OldAstState, Program, SourceRange,
}; };
const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [
SemanticTokenType::NUMBER,
SemanticTokenType::VARIABLE,
SemanticTokenType::KEYWORD,
SemanticTokenType::TYPE,
SemanticTokenType::STRING,
SemanticTokenType::OPERATOR,
SemanticTokenType::COMMENT,
SemanticTokenType::FUNCTION,
SemanticTokenType::PARAMETER,
SemanticTokenType::PROPERTY,
];
lazy_static::lazy_static! { const SEMANTIC_TOKEN_MODIFIERS: [SemanticTokenModifier; 5] = [
pub static ref SEMANTIC_TOKEN_TYPES: Vec<SemanticTokenType> = { SemanticTokenModifier::DECLARATION,
// This is safe to unwrap because we know all the token types are valid. SemanticTokenModifier::DEFINITION,
// And the test would fail if they were not. SemanticTokenModifier::DEFAULT_LIBRARY,
let mut gen = TokenType::all_semantic_token_types().unwrap(); SemanticTokenModifier::READONLY,
gen.extend(vec![ SemanticTokenModifier::STATIC,
SemanticTokenType::PARAMETER, ];
SemanticTokenType::PROPERTY,
]);
gen
};
pub static ref SEMANTIC_TOKEN_MODIFIERS: Vec<SemanticTokenModifier> = {
vec![
SemanticTokenModifier::DECLARATION,
SemanticTokenModifier::DEFINITION,
SemanticTokenModifier::DEFAULT_LIBRARY,
SemanticTokenModifier::READONLY,
SemanticTokenModifier::STATIC,
]
};
}
/// A subcommand for running the server. /// A subcommand for running the server.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -105,7 +99,7 @@ pub struct Backend {
/// The stdlib signatures for the language. /// The stdlib signatures for the language.
pub stdlib_signatures: HashMap<String, SignatureHelp>, pub stdlib_signatures: HashMap<String, SignatureHelp>,
/// Token maps. /// Token maps.
pub token_map: DashMap<String, Vec<crate::parsing::token::Token>>, pub(super) token_map: DashMap<String, TokenStream>,
/// AST maps. /// AST maps.
pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>, pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>,
/// Last successful execution. /// Last successful execution.
@ -284,7 +278,7 @@ impl crate::lsp::backend::Backend for Backend {
// Lets update the tokens. // Lets update the tokens.
let module_id = ModuleId::default(); let module_id = ModuleId::default();
let tokens = match crate::parsing::token::lexer(&params.text, module_id) { let tokens = match crate::parsing::token::lex(&params.text, module_id) {
Ok(tokens) => tokens, Ok(tokens) => tokens,
Err(err) => { Err(err) => {
self.add_to_diagnostics(&params, &[err], true).await; self.add_to_diagnostics(&params, &[err], true).await;
@ -410,11 +404,11 @@ impl Backend {
self.executor_ctx.read().await self.executor_ctx.read().await
} }
async fn update_semantic_tokens(&self, tokens: &[crate::parsing::token::Token], params: &TextDocumentItem) { async fn update_semantic_tokens(&self, tokens: &TokenStream, params: &TextDocumentItem) {
// Update the semantic tokens map. // Update the semantic tokens map.
let mut semantic_tokens = vec![]; let mut semantic_tokens = vec![];
let mut last_position = Position::new(0, 0); let mut last_position = Position::new(0, 0);
for token in tokens { for token in tokens.as_slice() {
let Ok(token_type) = SemanticTokenType::try_from(token.token_type) else { let Ok(token_type) = SemanticTokenType::try_from(token.token_type) else {
// We continue here because not all tokens can be converted this way, we will get // We continue here because not all tokens can be converted this way, we will get
// the rest from the ast. // the rest from the ast.
@ -444,8 +438,11 @@ impl Backend {
let token_modifiers_bitset = if let Some(ast) = self.ast_map.get(params.uri.as_str()) { let token_modifiers_bitset = if let Some(ast) = self.ast_map.get(params.uri.as_str()) {
let token_index = Arc::new(Mutex::new(token_type_index)); let token_index = Arc::new(Mutex::new(token_type_index));
let modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0)); let modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
crate::walk::walk(&ast, &|node: crate::walk::Node| { crate::walk::walk(&ast, |node: crate::walk::Node| {
let node_range: SourceRange = (&node).into(); let Ok(node_range): Result<SourceRange, _> = (&node).try_into() else {
return Ok(true);
};
if !node_range.contains(source_range.start()) { if !node_range.contains(source_range.start()) {
return Ok(true); return Ok(true);
} }
@ -563,7 +560,7 @@ impl Backend {
let semantic_token = SemanticToken { let semantic_token = SemanticToken {
delta_line: position.line - last_position.line + 1, delta_line: position.line - last_position.line + 1,
delta_start: 0, delta_start: 0,
length: token.value.len() as u32, length: (token.end - token.start) as u32,
token_type: token_type_index, token_type: token_type_index,
token_modifiers_bitset, token_modifiers_bitset,
}; };
@ -582,7 +579,7 @@ impl Backend {
} else { } else {
position.character - last_position.character position.character - last_position.character
}, },
length: token.value.len() as u32, length: (token.end - token.start) as u32,
token_type: token_type_index, token_type: token_type_index,
token_modifiers_bitset, token_modifiers_bitset,
}; };
@ -963,8 +960,8 @@ impl LanguageServer for Backend {
semantic_tokens_options: SemanticTokensOptions { semantic_tokens_options: SemanticTokensOptions {
work_done_progress_options: WorkDoneProgressOptions::default(), work_done_progress_options: WorkDoneProgressOptions::default(),
legend: SemanticTokensLegend { legend: SemanticTokensLegend {
token_types: SEMANTIC_TOKEN_TYPES.clone(), token_types: SEMANTIC_TOKEN_TYPES.to_vec(),
token_modifiers: SEMANTIC_TOKEN_MODIFIERS.clone(), token_modifiers: SEMANTIC_TOKEN_MODIFIERS.to_vec(),
}, },
range: Some(false), range: Some(false),
full: Some(SemanticTokensFullOptions::Bool(true)), full: Some(SemanticTokensFullOptions::Bool(true)),

View File

@ -1082,7 +1082,7 @@ fn myFn = (param1) => {
// Get the token map. // Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone(); let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
assert!(token_map != vec![]); assert!(!token_map.is_empty());
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -2206,7 +2206,7 @@ part001 = cube([0,0], 20)
// Get the tokens. // Get the tokens.
let tokens = server.token_map.get("file:///test.kcl").unwrap().clone(); let tokens = server.token_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(tokens.len(), 120); assert_eq!(tokens.as_slice().len(), 120);
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3379,11 +3379,11 @@ part001 = startSketchOn('XY')
// Get the symbols map. // Get the symbols map.
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone(); let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
assert!(symbols_map != vec![]); assert!(!symbols_map.is_empty());
// Get the semantic tokens map. // Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(semantic_tokens_map != vec![]); assert!(!semantic_tokens_map.is_empty());
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone(); let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
@ -3422,7 +3422,7 @@ NEW_LINT = 1"#
// Get the semantic tokens map. // Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(semantic_tokens_map != vec![]); assert!(!semantic_tokens_map.is_empty());
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl"); let memory = server.memory_map.get("file:///test.kcl");
@ -3466,7 +3466,7 @@ part001 = startSketchOn('XY')
// Get the token map. // Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone(); let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
assert!(token_map != vec![]); assert!(!token_map.is_empty());
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3474,11 +3474,11 @@ part001 = startSketchOn('XY')
// Get the symbols map. // Get the symbols map.
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone(); let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
assert!(symbols_map != vec![]); assert!(!symbols_map.is_empty());
// Get the semantic tokens map. // Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(semantic_tokens_map != vec![]); assert!(!semantic_tokens_map.is_empty());
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone(); let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
@ -3509,7 +3509,7 @@ part001 = startSketchOn('XY')
// Get the token map. // Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone(); let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
assert!(token_map != vec![]); assert!(!token_map.is_empty());
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3517,11 +3517,11 @@ part001 = startSketchOn('XY')
// Get the symbols map. // Get the symbols map.
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone(); let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
assert!(symbols_map != vec![]); assert!(!symbols_map.is_empty());
// Get the semantic tokens map. // Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(semantic_tokens_map != vec![]); assert!(!semantic_tokens_map.is_empty());
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl"); let memory = server.memory_map.get("file:///test.kcl");

View File

@ -1,373 +0,0 @@
//! Functions for helping with caching an ast and finding the parts the changed.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
execution::ExecState,
parsing::ast::types::{Node, Program},
};
/// Information for the caching an AST and smartly re-executing it if we can.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheInformation {
/// The old information.
pub old: Option<OldAstState>,
/// The new ast to executed.
pub new_ast: Node<Program>,
}
/// The old ast and program memory.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct OldAstState {
/// The ast.
pub ast: Node<Program>,
/// The exec state.
pub exec_state: ExecState,
/// The last settings used for execution.
pub settings: crate::execution::ExecutorSettings,
}
impl From<crate::Program> for CacheInformation {
fn from(program: crate::Program) -> Self {
CacheInformation {
old: None,
new_ast: program.ast,
}
}
}
/// The result of a cache check.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheResult {
/// Should we clear the scene and start over?
pub clear_scene: bool,
/// The program that needs to be executed.
pub program: Node<Program>,
}
// Given an old ast, old program memory and new ast, find the parts of the code that need to be
// re-executed.
// This function should never error, because in the case of any internal error, we should just pop
// the cache.
pub fn get_changed_program(
info: CacheInformation,
new_settings: &crate::execution::ExecutorSettings,
) -> Option<CacheResult> {
let Some(old) = info.old else {
// We have no old info, we need to re-execute the whole thing.
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
};
// If the settings are different we need to bust the cache.
// We specifically do this before checking if they are the exact same.
if old.settings != *new_settings {
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
}
// If the ASTs are the EXACT same we return None.
// We don't even need to waste time computing the digests.
if old.ast == info.new_ast {
return None;
}
let mut old_ast = old.ast.inner;
old_ast.compute_digest();
let mut new_ast = info.new_ast.inner.clone();
new_ast.compute_digest();
// Check if the digest is the same.
if old_ast.digest == new_ast.digest {
return None;
}
// Check if the changes were only to Non-code areas, like comments or whitespace.
// For any unhandled cases just re-execute the whole thing.
Some(CacheResult {
clear_scene: true,
program: info.new_ast,
})
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use anyhow::Result;
use pretty_assertions::assert_eq;
use super::*;
async fn execute(program: &crate::Program) -> Result<ExecState> {
let ctx = crate::execution::ExecutorContext {
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
let mut exec_state = crate::execution::ExecState::default();
ctx.run(program.clone().into(), &mut exec_state).await?;
Ok(exec_state)
}
// Easy case where we have no old ast and memory.
// We need to re-execute everything.
#[test]
fn test_get_changed_program_no_old_information() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program = crate::Program::parse_no_errs(new).unwrap().ast;
let result = get_changed_program(
CacheInformation {
old: None,
new_ast: program.clone(),
},
&Default::default(),
);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program);
assert!(result.clear_scene);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program = crate::Program::parse_no_errs(new).unwrap();
let executed = execute(&program).await.unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
},
&Default::default(),
);
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_whitespace() {
let old = r#" // Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program_old = crate::Program::parse_no_errs(old).unwrap();
let executed = execute(&program_old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program_old.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
},
&Default::default(),
);
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
let old = r#" // Removed the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program_old = crate::Program::parse_no_errs(old).unwrap();
let executed = execute(&program_old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program_old.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
},
&Default::default(),
);
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comments() {
let old = r#" // Removed the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %) // my thing
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program_old = crate::Program::parse_no_errs(old).unwrap();
let executed = execute(&program_old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program_old.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
},
&Default::default(),
);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program_new.ast);
assert!(result.clear_scene);
}
// Changing the units with the exact same file should bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_units() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program = crate::Program::parse_no_errs(new).unwrap();
let executed = execute(&program).await.unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
},
&crate::ExecutorSettings {
units: crate::UnitLength::Cm,
..Default::default()
},
);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program.ast);
assert!(result.clear_scene);
}
}

View File

@ -1,6 +1,6 @@
use sha2::{Digest as DigestTrait, Sha256}; use sha2::{Digest as DigestTrait, Sha256};
use super::types::{DefaultParamVal, ItemVisibility, VariableKind}; use super::types::{DefaultParamVal, ItemVisibility, LabelledExpression, VariableKind};
use crate::parsing::ast::types::{ use crate::parsing::ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw,
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression, CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression,
@ -115,6 +115,7 @@ impl Expr {
Expr::MemberExpression(me) => me.compute_digest(), Expr::MemberExpression(me) => me.compute_digest(),
Expr::UnaryExpression(ue) => ue.compute_digest(), Expr::UnaryExpression(ue) => ue.compute_digest(),
Expr::IfExpression(e) => e.compute_digest(), Expr::IfExpression(e) => e.compute_digest(),
Expr::LabelledExpression(e) => e.compute_digest(),
Expr::None(_) => { Expr::None(_) => {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(b"Value::None"); hasher.update(b"Value::None");
@ -396,6 +397,13 @@ impl UnaryExpression {
}); });
} }
impl LabelledExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.expr.compute_digest());
hasher.update(slf.label.compute_digest());
});
}
impl PipeExpression { impl PipeExpression {
compute_digest!(|slf, hasher| { compute_digest!(|slf, hasher| {
hasher.update(slf.body.len().to_ne_bytes()); hasher.update(slf.body.len().to_ne_bytes());

View File

@ -1,4 +1,3 @@
pub(crate) mod cache;
pub(crate) mod digest; pub(crate) mod digest;
pub mod modify; pub mod modify;
pub mod types; pub mod types;
@ -37,6 +36,7 @@ impl Expr {
Expr::MemberExpression(member_expression) => member_expression.module_id, Expr::MemberExpression(member_expression) => member_expression.module_id,
Expr::UnaryExpression(unary_expression) => unary_expression.module_id, Expr::UnaryExpression(unary_expression) => unary_expression.module_id,
Expr::IfExpression(expr) => expr.module_id, Expr::IfExpression(expr) => expr.module_id,
Expr::LabelledExpression(expr) => expr.expr.module_id(),
Expr::None(none) => none.module_id, Expr::None(none) => none.module_id,
} }
} }

View File

@ -184,7 +184,7 @@ impl Node<Program> {
/// Walk the ast and get all the variables and tags as completion items. /// Walk the ast and get all the variables and tags as completion items.
pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> { pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> {
let completions = Arc::new(Mutex::new(vec![])); let completions = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, &|node: crate::walk::Node<'a>| { crate::walk::walk(self, |node: crate::walk::Node<'a>| {
let mut findings = completions.lock().map_err(|_| anyhow::anyhow!("mutex"))?; let mut findings = completions.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
match node { match node {
crate::walk::Node::TagDeclarator(tag) => { crate::walk::Node::TagDeclarator(tag) => {
@ -195,7 +195,7 @@ impl Node<Program> {
} }
_ => {} _ => {}
} }
Ok(true) Ok::<bool, anyhow::Error>(true)
})?; })?;
let x = completions.lock().unwrap(); let x = completions.lock().unwrap();
Ok(x.clone()) Ok(x.clone())
@ -204,7 +204,7 @@ impl Node<Program> {
/// Returns all the lsp symbols in the program. /// Returns all the lsp symbols in the program.
pub fn get_lsp_symbols<'a>(&'a self, code: &str) -> Result<Vec<DocumentSymbol>> { pub fn get_lsp_symbols<'a>(&'a self, code: &str) -> Result<Vec<DocumentSymbol>> {
let symbols = Arc::new(Mutex::new(vec![])); let symbols = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, &|node: crate::walk::Node<'a>| { crate::walk::walk(self, |node: crate::walk::Node<'a>| {
let mut findings = symbols.lock().map_err(|_| anyhow::anyhow!("mutex"))?; let mut findings = symbols.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
match node { match node {
crate::walk::Node::TagDeclarator(tag) => { crate::walk::Node::TagDeclarator(tag) => {
@ -215,7 +215,7 @@ impl Node<Program> {
} }
_ => {} _ => {}
} }
Ok(true) Ok::<bool, anyhow::Error>(true)
})?; })?;
let x = symbols.lock().unwrap(); let x = symbols.lock().unwrap();
Ok(x.clone()) Ok(x.clone())
@ -227,10 +227,10 @@ impl Node<Program> {
RuleT: crate::lint::Rule<'a>, RuleT: crate::lint::Rule<'a>,
{ {
let v = Arc::new(Mutex::new(vec![])); let v = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, &|node: crate::walk::Node<'a>| { crate::walk::walk(self, |node: crate::walk::Node<'a>| {
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?; let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
findings.append(&mut rule.check(node)?); findings.append(&mut rule.check(node)?);
Ok(true) Ok::<bool, anyhow::Error>(true)
})?; })?;
let x = v.lock().unwrap(); let x = v.lock().unwrap();
Ok(x.clone()) Ok(x.clone())
@ -598,6 +598,7 @@ pub enum Expr {
MemberExpression(BoxNode<MemberExpression>), MemberExpression(BoxNode<MemberExpression>),
UnaryExpression(BoxNode<UnaryExpression>), UnaryExpression(BoxNode<UnaryExpression>),
IfExpression(BoxNode<IfExpression>), IfExpression(BoxNode<IfExpression>),
LabelledExpression(BoxNode<LabelledExpression>),
None(Node<KclNone>), None(Node<KclNone>),
} }
@ -640,6 +641,7 @@ impl Expr {
Expr::UnaryExpression(_unary_exp) => None, Expr::UnaryExpression(_unary_exp) => None,
Expr::PipeSubstitution(_pipe_substitution) => None, Expr::PipeSubstitution(_pipe_substitution) => None,
Expr::IfExpression(_) => None, Expr::IfExpression(_) => None,
Expr::LabelledExpression(expr) => expr.expr.get_non_code_meta(),
Expr::None(_none) => None, Expr::None(_none) => None,
} }
} }
@ -666,6 +668,7 @@ impl Expr {
Expr::UnaryExpression(ref mut unary_exp) => unary_exp.replace_value(source_range, new_value), Expr::UnaryExpression(ref mut unary_exp) => unary_exp.replace_value(source_range, new_value),
Expr::IfExpression(_) => {} Expr::IfExpression(_) => {}
Expr::PipeSubstitution(_) => {} Expr::PipeSubstitution(_) => {}
Expr::LabelledExpression(expr) => expr.expr.replace_value(source_range, new_value),
Expr::None(_) => {} Expr::None(_) => {}
} }
} }
@ -687,6 +690,7 @@ impl Expr {
Expr::MemberExpression(member_expression) => member_expression.start, Expr::MemberExpression(member_expression) => member_expression.start,
Expr::UnaryExpression(unary_expression) => unary_expression.start, Expr::UnaryExpression(unary_expression) => unary_expression.start,
Expr::IfExpression(expr) => expr.start, Expr::IfExpression(expr) => expr.start,
Expr::LabelledExpression(expr) => expr.start,
Expr::None(none) => none.start, Expr::None(none) => none.start,
} }
} }
@ -708,6 +712,7 @@ impl Expr {
Expr::MemberExpression(member_expression) => member_expression.end, Expr::MemberExpression(member_expression) => member_expression.end,
Expr::UnaryExpression(unary_expression) => unary_expression.end, Expr::UnaryExpression(unary_expression) => unary_expression.end,
Expr::IfExpression(expr) => expr.end, Expr::IfExpression(expr) => expr.end,
Expr::LabelledExpression(expr) => expr.end,
Expr::None(none) => none.end, Expr::None(none) => none.end,
} }
} }
@ -734,6 +739,8 @@ impl Expr {
Expr::Literal(_) => None, Expr::Literal(_) => None,
Expr::Identifier(_) => None, Expr::Identifier(_) => None,
Expr::TagDeclarator(_) => None, Expr::TagDeclarator(_) => None,
// TODO LSP hover info for tag
Expr::LabelledExpression(expr) => expr.expr.get_hover_value_for_position(pos, code),
// TODO: LSP hover information for symbols. https://github.com/KittyCAD/modeling-app/issues/1127 // TODO: LSP hover information for symbols. https://github.com/KittyCAD/modeling-app/issues/1127
Expr::PipeSubstitution(_) => None, Expr::PipeSubstitution(_) => None,
} }
@ -763,6 +770,7 @@ impl Expr {
} }
Expr::UnaryExpression(ref mut unary_expression) => unary_expression.rename_identifiers(old_name, new_name), Expr::UnaryExpression(ref mut unary_expression) => unary_expression.rename_identifiers(old_name, new_name),
Expr::IfExpression(ref mut expr) => expr.rename_identifiers(old_name, new_name), Expr::IfExpression(ref mut expr) => expr.rename_identifiers(old_name, new_name),
Expr::LabelledExpression(expr) => expr.expr.rename_identifiers(old_name, new_name),
Expr::None(_) => {} Expr::None(_) => {}
} }
} }
@ -788,9 +796,19 @@ impl Expr {
Expr::MemberExpression(member_expression) => member_expression.get_constraint_level(), Expr::MemberExpression(member_expression) => member_expression.get_constraint_level(),
Expr::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(), Expr::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
Expr::IfExpression(expr) => expr.get_constraint_level(), Expr::IfExpression(expr) => expr.get_constraint_level(),
Expr::LabelledExpression(expr) => expr.expr.get_constraint_level(),
Expr::None(none) => none.get_constraint_level(), Expr::None(none) => none.get_constraint_level(),
} }
} }
pub fn has_substitution_arg(&self) -> bool {
match self {
Expr::CallExpression(call_expression) => call_expression.has_substitution_arg(),
Expr::CallExpressionKw(call_expression) => call_expression.has_substitution_arg(),
Expr::LabelledExpression(expr) => expr.expr.has_substitution_arg(),
_ => false,
}
}
} }
impl From<Expr> for SourceRange { impl From<Expr> for SourceRange {
@ -805,6 +823,36 @@ impl From<&Expr> for SourceRange {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct LabelledExpression {
pub expr: Expr,
pub label: Node<Identifier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
impl LabelledExpression {
pub(crate) fn new(expr: Expr, label: Node<Identifier>) -> Node<LabelledExpression> {
let start = expr.start();
let end = label.end;
let module_id = expr.module_id();
Node::new(
LabelledExpression {
expr,
label,
digest: None,
},
start,
end,
module_id,
)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -2810,7 +2858,8 @@ pub struct Parameter {
pub identifier: Node<Identifier>, pub identifier: Node<Identifier>,
/// The type of the parameter. /// The type of the parameter.
/// This is optional if the user defines a type. /// This is optional if the user defines a type.
#[serde(skip)] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(skip)]
pub type_: Option<FnArgType>, pub type_: Option<FnArgType>,
/// Is the parameter optional? /// Is the parameter optional?
/// If so, what is its default value? /// If so, what is its default value?

View File

@ -2,7 +2,7 @@ use crate::{
errors::{CompilationError, KclError, KclErrorDetails}, errors::{CompilationError, KclError, KclErrorDetails},
parsing::{ parsing::{
ast::types::{Node, Program}, ast::types::{Node, Program},
token::{Token, TokenType}, token::TokenStream,
}, },
source_range::{ModuleId, SourceRange}, source_range::{ModuleId, SourceRange},
}; };
@ -34,15 +34,13 @@ pub fn top_level_parse(code: &str) -> ParseResult {
/// Parse the given KCL code into an AST. /// Parse the given KCL code into an AST.
pub fn parse_str(code: &str, module_id: ModuleId) -> ParseResult { pub fn parse_str(code: &str, module_id: ModuleId) -> ParseResult {
let tokens = pr_try!(crate::parsing::token::lexer(code, module_id)); let tokens = pr_try!(crate::parsing::token::lex(code, module_id));
parse_tokens(tokens) parse_tokens(tokens)
} }
/// Parse the supplied tokens into an AST. /// Parse the supplied tokens into an AST.
pub fn parse_tokens(tokens: Vec<Token>) -> ParseResult { pub fn parse_tokens(mut tokens: TokenStream) -> ParseResult {
let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens let unknown_tokens = tokens.remove_unknown();
.into_iter()
.partition(|token| token.token_type != TokenType::Unknown);
if !unknown_tokens.is_empty() { if !unknown_tokens.is_empty() {
let source_ranges = unknown_tokens.iter().map(SourceRange::from).collect(); let source_ranges = unknown_tokens.iter().map(SourceRange::from).collect();
@ -69,7 +67,7 @@ pub fn parse_tokens(tokens: Vec<Token>) -> ParseResult {
return Node::<Program>::default().into(); return Node::<Program>::default().into();
} }
parser::run_parser(&mut tokens.as_slice()) parser::run_parser(tokens.as_slice())
} }
/// Result of parsing. /// Result of parsing.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,76 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 35,
"id": {
"end": 6,
"name": "foo",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 33,
"raw": "1",
"start": 32,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"end": 33,
"start": 25,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 35,
"start": 23
},
"end": 35,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 8,
"name": "x",
"start": 7,
"type": "Identifier"
},
"type_": {
"type": "Primitive",
"type": "Number"
},
"default_value": {
"type": "Literal",
"type": "Literal",
"value": 2.0,
"raw": "2"
}
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 35,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 35,
"start": 0
}

View File

@ -0,0 +1,72 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 27,
"id": {
"end": 6,
"name": "foo",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 25,
"raw": "1",
"start": 24,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"end": 25,
"start": 17,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 27,
"start": 15
},
"end": 27,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 8,
"name": "x",
"start": 7,
"type": "Identifier"
},
"default_value": {
"type": "Literal",
"type": "Literal",
"value": 2.0,
"raw": "2"
}
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 27,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 27,
"start": 0
}

View File

@ -1,11 +1,17 @@
use std::str::FromStr; // Clippy does not agree with rustc here for some reason.
#![allow(clippy::needless_lifetimes)]
use std::{fmt, iter::Enumerate, num::NonZeroUsize};
use anyhow::Result; use anyhow::Result;
use parse_display::{Display, FromStr}; use parse_display::Display;
use schemars::JsonSchema; use tokeniser::Input;
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::SemanticTokenType; use tower_lsp::lsp_types::SemanticTokenType;
use winnow::{error::ParseError, stream::ContainsToken}; use winnow::{
self,
error::ParseError,
stream::{ContainsToken, Stream},
};
use crate::{ use crate::{
errors::KclError, errors::KclError,
@ -15,14 +21,201 @@ use crate::{
mod tokeniser; mod tokeniser;
// Re-export
pub use tokeniser::Input;
#[cfg(test)] #[cfg(test)]
pub(crate) use tokeniser::RESERVED_WORDS; pub(crate) use tokeniser::RESERVED_WORDS;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct TokenStream {
tokens: Vec<Token>,
}
impl TokenStream {
fn new(tokens: Vec<Token>) -> Self {
Self { tokens }
}
pub(super) fn remove_unknown(&mut self) -> Vec<Token> {
let tokens = std::mem::take(&mut self.tokens);
let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens
.into_iter()
.partition(|token| token.token_type != TokenType::Unknown);
self.tokens = tokens;
unknown_tokens
}
pub fn iter(&self) -> impl Iterator<Item = &Token> {
self.tokens.iter()
}
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
pub fn as_slice(&self) -> TokenSlice {
TokenSlice::from(self)
}
}
impl<'a> From<&'a TokenStream> for TokenSlice<'a> {
fn from(stream: &'a TokenStream) -> Self {
TokenSlice {
start: 0,
end: stream.tokens.len(),
stream,
}
}
}
impl IntoIterator for TokenStream {
type Item = Token;
type IntoIter = std::vec::IntoIter<Token>;
fn into_iter(self) -> Self::IntoIter {
self.tokens.into_iter()
}
}
#[derive(Debug, Clone)]
pub(crate) struct TokenSlice<'a> {
stream: &'a TokenStream,
start: usize,
end: usize,
}
impl<'a> std::ops::Deref for TokenSlice<'a> {
type Target = [Token];
fn deref(&self) -> &Self::Target {
&self.stream.tokens[self.start..self.end]
}
}
impl<'a> TokenSlice<'a> {
pub fn token(&self, i: usize) -> &Token {
&self.stream.tokens[i + self.start]
}
pub fn iter(&self) -> impl Iterator<Item = &Token> {
(**self).iter()
}
pub fn without_ends(&self) -> Self {
Self {
start: self.start + 1,
end: self.end - 1,
stream: self.stream,
}
}
}
impl<'a> IntoIterator for TokenSlice<'a> {
type Item = &'a Token;
type IntoIter = std::slice::Iter<'a, Token>;
fn into_iter(self) -> Self::IntoIter {
self.stream.tokens[self.start..self.end].iter()
}
}
impl<'a> Stream for TokenSlice<'a> {
type Token = Token;
type Slice = Self;
type IterOffsets = Enumerate<std::vec::IntoIter<Token>>;
type Checkpoint = Checkpoint;
fn iter_offsets(&self) -> Self::IterOffsets {
#[allow(clippy::unnecessary_to_owned)]
self.to_vec().into_iter().enumerate()
}
fn eof_offset(&self) -> usize {
self.len()
}
fn next_token(&mut self) -> Option<Self::Token> {
let token = self.first()?.clone();
self.start += 1;
Some(token)
}
fn offset_for<P>(&self, predicate: P) -> Option<usize>
where
P: Fn(Self::Token) -> bool,
{
self.iter().position(|b| predicate(b.clone()))
}
fn offset_at(&self, tokens: usize) -> Result<usize, winnow::error::Needed> {
if let Some(needed) = tokens.checked_sub(self.len()).and_then(NonZeroUsize::new) {
Err(winnow::error::Needed::Size(needed))
} else {
Ok(tokens)
}
}
fn next_slice(&mut self, offset: usize) -> Self::Slice {
assert!(self.start + offset <= self.end);
let next = TokenSlice {
stream: self.stream,
start: self.start,
end: self.start + offset,
};
self.start += offset;
next
}
fn checkpoint(&self) -> Self::Checkpoint {
Checkpoint(self.start, self.end)
}
fn reset(&mut self, checkpoint: &Self::Checkpoint) {
self.start = checkpoint.0;
self.end = checkpoint.1;
}
fn raw(&self) -> &dyn fmt::Debug {
self
}
}
impl<'a> winnow::stream::Offset for TokenSlice<'a> {
fn offset_from(&self, start: &Self) -> usize {
self.start - start.start
}
}
impl<'a> winnow::stream::Offset<Checkpoint> for TokenSlice<'a> {
fn offset_from(&self, start: &Checkpoint) -> usize {
self.start - start.0
}
}
impl winnow::stream::Offset for Checkpoint {
fn offset_from(&self, start: &Self) -> usize {
self.0 - start.0
}
}
impl<'a> winnow::stream::StreamIsPartial for TokenSlice<'a> {
type PartialState = ();
fn complete(&mut self) -> Self::PartialState {}
fn restore_partial(&mut self, _: Self::PartialState) {}
fn is_partial_supported() -> bool {
false
}
}
#[derive(Clone, Debug)]
pub struct Checkpoint(usize, usize);
/// The types of tokens. /// The types of tokens.
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, JsonSchema, FromStr, Display)] #[derive(Debug, PartialEq, Eq, Copy, Clone, Display)]
#[serde(rename_all = "camelCase")]
#[display(style = "camelCase")] #[display(style = "camelCase")]
pub enum TokenType { pub enum TokenType {
/// A number. /// A number.
@ -73,6 +266,8 @@ pub enum TokenType {
impl TryFrom<TokenType> for SemanticTokenType { impl TryFrom<TokenType> for SemanticTokenType {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(token_type: TokenType) -> Result<Self> { fn try_from(token_type: TokenType) -> Result<Self> {
// If you return a new kind of `SemanticTokenType`, make sure to update `SEMANTIC_TOKEN_TYPES`
// in the LSP implementation.
Ok(match token_type { Ok(match token_type {
TokenType::Number => Self::NUMBER, TokenType::Number => Self::NUMBER,
TokenType::Word => Self::VARIABLE, TokenType::Word => Self::VARIABLE,
@ -102,52 +297,6 @@ impl TryFrom<TokenType> for SemanticTokenType {
} }
impl TokenType { impl TokenType {
// This is for the lsp server.
// Don't call this function directly in the code use a lazy_static instead
// like we do in the lsp server.
pub fn all_semantic_token_types() -> Result<Vec<SemanticTokenType>> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
let schema = TokenType::json_schema(&mut generator);
let schemars::schema::Schema::Object(o) = &schema else {
anyhow::bail!("expected object schema: {:#?}", schema);
};
let Some(subschemas) = &o.subschemas else {
anyhow::bail!("expected subschemas: {:#?}", schema);
};
let Some(one_ofs) = &subschemas.one_of else {
anyhow::bail!("expected one_of: {:#?}", schema);
};
let mut semantic_tokens = vec![];
for one_of in one_ofs {
let schemars::schema::Schema::Object(o) = one_of else {
anyhow::bail!("expected object one_of: {:#?}", one_of);
};
let Some(enum_values) = o.enum_values.as_ref() else {
anyhow::bail!("expected enum values: {:#?}", o);
};
if enum_values.len() > 1 {
anyhow::bail!("expected only one enum value: {:#?}", o);
}
if enum_values.is_empty() {
anyhow::bail!("expected at least one enum value: {:#?}", o);
}
let label = TokenType::from_str(&enum_values[0].to_string().replace('"', ""))?;
if let Ok(semantic_token_type) = SemanticTokenType::try_from(label) {
semantic_tokens.push(semantic_token_type);
}
}
Ok(semantic_tokens)
}
pub fn is_whitespace(&self) -> bool { pub fn is_whitespace(&self) -> bool {
matches!(self, Self::Whitespace) matches!(self, Self::Whitespace)
} }
@ -157,17 +306,15 @@ impl TokenType {
} }
} }
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct Token { pub struct Token {
#[serde(rename = "type")]
pub token_type: TokenType, pub token_type: TokenType,
/// Offset in the source code where this token begins. /// Offset in the source code where this token begins.
pub start: usize, pub start: usize,
/// Offset in the source code where this token ends. /// Offset in the source code where this token ends.
pub end: usize, pub end: usize,
#[serde(default, skip_serializing_if = "ModuleId::is_top_level")] pub(super) module_id: ModuleId,
pub module_id: ModuleId, pub(super) value: String,
pub value: String,
} }
impl ContainsToken<Token> for (TokenType, &str) { impl ContainsToken<Token> for (TokenType, &str) {
@ -249,7 +396,7 @@ impl From<&Token> for SourceRange {
} }
} }
pub fn lexer(s: &str, module_id: ModuleId) -> Result<Vec<Token>, KclError> { pub fn lex(s: &str, module_id: ModuleId) -> Result<TokenStream, KclError> {
tokeniser::lex(s, module_id).map_err(From::from) tokeniser::lex(s, module_id).map_err(From::from)
} }
@ -281,15 +428,3 @@ impl From<ParseError<Input<'_>, winnow::error::ContextError>> for KclError {
}) })
} }
} }
#[cfg(test)]
mod tests {
use super::*;
// We have this as a test so we can ensure it never panics with an unwrap in the server.
#[test]
fn test_token_type_to_semantic_token_type() {
let semantic_types = TokenType::all_semantic_token_types().unwrap();
assert!(!semantic_types.is_empty());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ fn read(filename: &'static str, test_name: &str) -> String {
fn parse(test_name: &str) { fn parse(test_name: &str) {
let input = read("input.kcl", test_name); let input = read("input.kcl", test_name);
let tokens = crate::parsing::token::lexer(&input, ModuleId::default()).unwrap(); let tokens = crate::parsing::token::lex(&input, ModuleId::default()).unwrap();
// Parse the tokens into an AST. // Parse the tokens into an AST.
let parse_res = Result::<_, KclError>::Ok(crate::parsing::parse_tokens(tokens).unwrap()); let parse_res = Result::<_, KclError>::Ok(crate::parsing::parse_tokens(tokens).unwrap());
@ -1502,3 +1502,108 @@ mod kw_fn {
super::execute(TEST_NAME, true).await super::execute(TEST_NAME, true).await
} }
} }
mod tag_can_be_proxied_through_parameter {
const TEST_NAME: &str = "tag_can_be_proxied_through_parameter";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod tag_proxied_through_function_does_not_define_var {
const TEST_NAME: &str = "tag_proxied_through_function_does_not_define_var";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod kw_fn_too_few_args {
const TEST_NAME: &str = "kw_fn_too_few_args";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod kw_fn_unlabeled_but_has_label {
const TEST_NAME: &str = "kw_fn_unlabeled_but_has_label";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod kw_fn_with_defaults {
const TEST_NAME: &str = "kw_fn_with_defaults";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}

View File

@ -0,0 +1,263 @@
//! Standard library appearance.
use anyhow::Result;
use derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc, shared::Color};
use regex::Regex;
use rgba_simple::Hex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use validator::Validate;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Solid, SolidSet},
std::Args,
};
lazy_static::lazy_static! {
static ref HEX_REGEX: Regex = Regex::new(r"^#[0-9a-fA-F]{6}$").unwrap();
}
/// Data for appearance.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct AppearanceData {
/// Color of the new material, a hex string like "#ff0000".
#[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
pub color: String,
/// Metalness of the new material, a percentage like 95.7.
#[validate(range(min = 0.0, max = 100.0))]
pub metalness: Option<f64>,
/// Roughness of the new material, a percentage like 95.7.
#[validate(range(min = 0.0, max = 100.0))]
pub roughness: Option<f64>,
// TODO(jess): we can also ambient occlusion here I just don't know what it is.
}
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, solid_set): (AppearanceData, SolidSet) = args.get_data_and_solid_set()?;
// Validate the data.
data.validate().map_err(|err| {
KclError::Semantic(KclErrorDetails {
message: format!("Invalid appearance data: {}", err),
source_ranges: vec![args.source_range],
})
})?;
// Make sure the color if set is valid.
if !HEX_REGEX.is_match(&data.color) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Invalid hex color (`{}`), try something like `#fff000`", data.color),
source_ranges: vec![args.source_range],
}));
}
let result = inner_appearance(data, solid_set, args).await?;
Ok(result.into())
}
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
///
/// This will work on any solid, including extruded solids, revolved solids, and shelled solids.
/// ```no_run
/// /// Add color to an extruded solid.
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> lineTo([10, 0], %)
/// |> lineTo([0, 10], %)
/// |> lineTo([-10, 0], %)
/// |> close(%)
///
/// example = extrude(5, exampleSketch)
/// |> appearance({color= '#ff0000', metalness= 50, roughness= 50}, %)
/// ```
///
/// ```no_run
/// /// Add color to a revolved solid.
/// sketch001 = startSketchOn('XY')
/// |> circle({ center = [15, 0], radius = 5 }, %)
/// |> revolve({ angle = 360, axis = 'y' }, %)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// ```
///
/// ```no_run
/// /// Add color to different solids.
/// fn cube(center) {
/// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %)
/// |> lineTo([center[0] + 10, center[1] - 10], %)
/// |> lineTo([center[0] + 10, center[1] + 10], %)
/// |> lineTo([center[0] - 10, center[1] + 10], %)
/// |> close(%)
/// |> extrude(10, %)
/// }
///
/// example0 = cube([0, 0])
/// example1 = cube([20, 0])
/// example2 = cube([40, 0])
///
/// appearance({color= '#ff0000', metalness= 50, roughness= 50}, [example0, example1])
/// appearance({color= '#00ff00', metalness= 50, roughness= 50}, example2)
/// ```
///
/// ```no_run
/// /// You can set the appearance before or after you shell it will yield the same result.
/// /// This example shows setting the appearance _after_ the shell.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line([24, 0], %)
/// |> line([0, -24], %)
/// |> line([-24, 0], %)
/// |> close(%)
/// |> extrude(6, %)
///
/// shell({
/// faces = ['end'],
/// thickness = 0.25,
/// }, firstSketch)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// ```
///
/// ```no_run
/// /// You can set the appearance before or after you shell it will yield the same result.
/// /// This example shows setting the appearance _before_ the shell.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line([24, 0], %)
/// |> line([0, -24], %)
/// |> line([-24, 0], %)
/// |> close(%)
/// |> extrude(6, %)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
///
/// shell({
/// faces = ['end'],
/// thickness = 0.25,
/// }, firstSketch)
/// ```
///
/// ```no_run
/// /// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
/// /// This example shows _before_ the pattern.
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line([0, 2], %)
/// |> line([3, 1], %)
/// |> line([0, -4], %)
/// |> close(%)
///
/// example = extrude(1, exampleSketch)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// |> patternLinear3d({
/// axis = [1, 0, 1],
/// instances = 7,
/// distance = 6
/// }, %)
/// ```
///
/// ```no_run
/// /// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
/// /// This example shows _after_ the pattern.
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line([0, 2], %)
/// |> line([3, 1], %)
/// |> line([0, -4], %)
/// |> close(%)
///
/// example = extrude(1, exampleSketch)
/// |> patternLinear3d({
/// axis = [1, 0, 1],
/// instances = 7,
/// distance = 6
/// }, %)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// ```
///
/// ```no_run
/// /// Color the result of a 2D pattern that was extruded.
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([.5, 25], %)
/// |> line([0, 5], %)
/// |> line([-1, 0], %)
/// |> line([0, -5], %)
/// |> close(%)
/// |> patternCircular2d({
/// center = [0, 0],
/// instances = 13,
/// arcDegrees = 360,
/// rotateDuplicates = true
/// }, %)
///
/// example = extrude(1, exampleSketch)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// ```
#[stdlib {
name = "appearance",
}]
async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args) -> Result<SolidSet, KclError> {
let solids: Vec<Box<Solid>> = solid_set.into();
for solid in &solids {
// Set the material properties.
let rgb = rgba_simple::RGB::<f32>::from_hex(&data.color).map_err(|err| {
KclError::Semantic(KclErrorDetails {
message: format!("Invalid hex color (`{}`): {}", data.color, err),
source_ranges: vec![args.source_range],
})
})?;
let color = Color {
r: rgb.red,
g: rgb.green,
b: rgb.blue,
a: 100.0,
};
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
object_id: solid.id,
color,
metalness: data.metalness.unwrap_or_default() as f32 / 100.0,
roughness: data.roughness.unwrap_or_default() as f32 / 100.0,
ambient_occlusion: 0.0,
}),
)
.await?;
// Idk if we want to actually modify the memory for the colors, but I'm not right now since
// I can't think of a use case for it.
}
Ok(SolidSet::from(solids))
}

View File

@ -50,6 +50,13 @@ pub struct KwArgs {
pub labeled: HashMap<String, Arg>, pub labeled: HashMap<String, Arg>,
} }
impl KwArgs {
/// How many arguments are there?
pub fn len(&self) -> usize {
self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 }
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Args { pub struct Args {
/// Positional args. /// Positional args.
@ -1089,6 +1096,20 @@ impl<'a> FromKclValue<'a> for super::fillet::FilletData {
} }
} }
impl<'a> FromKclValue<'a> for super::appearance::AppearanceData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, color);
let_field_of!(obj, metalness?);
let_field_of!(obj, roughness?);
Some(Self {
color,
metalness,
roughness,
})
}
}
impl<'a> FromKclValue<'a> for super::helix::HelixData { impl<'a> FromKclValue<'a> for super::helix::HelixData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?; let obj = arg.as_object()?;

View File

@ -1,5 +1,6 @@
//! Functions implemented for language execution. //! Functions implemented for language execution.
pub mod appearance;
pub mod args; pub mod args;
pub mod array; pub mod array;
pub mod assert; pub mod assert;
@ -50,6 +51,7 @@ lazy_static! {
Box::new(LegLen), Box::new(LegLen),
Box::new(LegAngX), Box::new(LegAngX),
Box::new(LegAngY), Box::new(LegAngY),
Box::new(crate::std::appearance::Appearance),
Box::new(crate::std::convert::Int), Box::new(crate::std::convert::Int),
Box::new(crate::std::extrude::Extrude), Box::new(crate::std::extrude::Extrude),
Box::new(crate::std::segment::SegEnd), Box::new(crate::std::segment::SegEnd),

View File

@ -55,7 +55,11 @@ async fn do_execute_and_snapshot(
program: Program, program: Program,
) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecError> { ) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecError> {
let mut exec_state = Default::default(); let mut exec_state = Default::default();
let snapshot_png_bytes = ctx.execute_and_prepare(&program, &mut exec_state).await?.contents.0; let snapshot_png_bytes = ctx
.execute_and_prepare_snapshot(&program, &mut exec_state)
.await?
.contents
.0;
// Decode the snapshot, return it. // Decode the snapshot, return it.
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes)) let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))

View File

@ -3,10 +3,10 @@ use std::fmt::Write;
use crate::parsing::{ use crate::parsing::{
ast::types::{ ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
CallExpressionKw, Expr, FnArgType, FormatOptions, FunctionExpression, IfExpression, ImportSelector, CallExpressionKw, DefaultParamVal, Expr, FnArgType, FormatOptions, FunctionExpression, IfExpression,
ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression, ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue,
MemberObject, Node, NonCodeValue, ObjectExpression, Parameter, PipeExpression, Program, TagDeclarator, MemberExpression, MemberObject, Node, NonCodeValue, ObjectExpression, Parameter, PipeExpression, Program,
UnaryExpression, VariableDeclaration, VariableKind, TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind,
}, },
PIPE_OPERATOR, PIPE_OPERATOR,
}; };
@ -166,7 +166,14 @@ pub(crate) enum ExprContext {
} }
impl Expr { impl Expr {
pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String { pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, mut ctxt: ExprContext) -> String {
let is_decl = matches!(ctxt, ExprContext::Decl);
if is_decl {
// Just because this expression is being bound to a variable, doesn't mean that every child
// expression is being bound. So, reset the expression context if necessary.
// This will still preserve the "::Pipe" context though.
ctxt = ExprContext::Other;
}
match &self { match &self {
Expr::BinaryExpression(bin_exp) => bin_exp.recast(options), Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt), Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
@ -175,11 +182,7 @@ impl Expr {
Expr::MemberExpression(mem_exp) => mem_exp.recast(), Expr::MemberExpression(mem_exp) => mem_exp.recast(),
Expr::Literal(literal) => literal.recast(), Expr::Literal(literal) => literal.recast(),
Expr::FunctionExpression(func_exp) => { Expr::FunctionExpression(func_exp) => {
let mut result = if ctxt == ExprContext::Decl { let mut result = if is_decl { String::new() } else { "fn".to_owned() };
String::new()
} else {
"fn".to_owned()
};
result += &func_exp.recast(options, indentation_level); result += &func_exp.recast(options, indentation_level);
result result
} }
@ -191,6 +194,12 @@ impl Expr {
Expr::UnaryExpression(unary_exp) => unary_exp.recast(options), Expr::UnaryExpression(unary_exp) => unary_exp.recast(options),
Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt), Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt),
Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(), Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(),
Expr::LabelledExpression(e) => {
let mut result = e.expr.recast(options, indentation_level, ctxt);
result += " as ";
result += &e.label.name;
result
}
Expr::None(_) => { Expr::None(_) => {
unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115") unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
} }
@ -404,7 +413,8 @@ fn expr_is_trivial(expr: &Expr) -> bool {
| Expr::ObjectExpression(_) | Expr::ObjectExpression(_)
| Expr::MemberExpression(_) | Expr::MemberExpression(_)
| Expr::UnaryExpression(_) | Expr::UnaryExpression(_)
| Expr::IfExpression(_) => false, | Expr::IfExpression(_)
| Expr::LabelledExpression(_) => false,
} }
} }
@ -659,15 +669,18 @@ impl FunctionExpression {
impl Parameter { impl Parameter {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String { pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let mut result = format!( let at_sign = if self.labeled { "" } else { "@" };
"{}{}", let identifier = &self.identifier.name;
if self.labeled { "" } else { "@" }, let question_mark = if self.default_value.is_some() { "?" } else { "" };
self.identifier.name.clone() let mut result = format!("{at_sign}{identifier}{question_mark}");
);
if let Some(ty) = &self.type_ { if let Some(ty) = &self.type_ {
result += ": "; result += ": ";
result += &ty.recast(options, indentation_level); result += &ty.recast(options, indentation_level);
} }
if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
let lit = literal.recast();
result.push_str(&format!(" = {lit}"));
};
result result
} }
@ -1513,6 +1526,28 @@ tabs_l = startSketchOn({
); );
} }
#[test]
fn test_as() {
let some_program_string = r#"fn cube(pos, scale) {
x = dfsfs + dfsfsd as y
sg = startSketchOn('XY')
|> startProfileAt(pos, %) as foo
|> line([0, scale], %)
|> line([scale, 0], %) as bar
|> line([0 as baz, -scale] as qux, %)
|> close(%)
|> extrude(scale, %)
}
cube(0, 0) as cub
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string,);
}
#[test] #[test]
fn test_recast_with_bad_indentation() { fn test_recast_with_bad_indentation() {
let some_program_string = r#"part001 = startSketchOn('XY') let some_program_string = r#"part001 = startSketchOn('XY')
@ -2134,8 +2169,10 @@ fn f() {
.into_iter() .into_iter()
.enumerate() .enumerate()
{ {
let tokens = crate::parsing::token::lexer(raw, ModuleId::default()).unwrap(); let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
let literal = crate::parsing::parser::unsigned_number_literal.parse(&tokens).unwrap(); let literal = crate::parsing::parser::unsigned_number_literal
.parse(tokens.as_slice())
.unwrap();
assert_eq!( assert_eq!(
literal.recast(), literal.recast(),
expected, expected,
@ -2170,6 +2207,28 @@ sketch002 = startSketchOn({
assert_eq!(actual, expected); assert_eq!(actual, expected);
} }
#[test]
fn unparse_fn_unnamed() {
let input = r#"squares_out = reduce(arr, 0, fn(i, squares) {
return 1
})
"#;
let ast = crate::parsing::top_level_parse(input).unwrap();
let actual = ast.recast(&FormatOptions::new(), 0);
assert_eq!(actual, input);
}
#[test]
fn unparse_fn_named() {
let input = r#"fn f(x) {
return 1
}
"#;
let ast = crate::parsing::top_level_parse(input).unwrap();
let actual = ast.recast(&FormatOptions::new(), 0);
assert_eq!(actual, input);
}
#[test] #[test]
fn recast_objects_with_comments() { fn recast_objects_with_comments() {
use winnow::Parser; use winnow::Parser;
@ -2191,9 +2250,9 @@ sketch002 = startSketchOn({
.into_iter() .into_iter()
.enumerate() .enumerate()
{ {
let tokens = crate::parsing::token::lexer(input, ModuleId::default()).unwrap(); let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
crate::parsing::parser::print_tokens(&tokens); crate::parsing::parser::print_tokens(tokens.as_slice());
let expr = crate::parsing::parser::object.parse(&tokens).unwrap(); let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
assert_eq!( assert_eq!(
expr.recast(&FormatOptions::new(), 0, ExprContext::Other), expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
expected, expected,
@ -2289,8 +2348,10 @@ sketch002 = startSketchOn({
.into_iter() .into_iter()
.enumerate() .enumerate()
{ {
let tokens = crate::parsing::token::lexer(input, ModuleId::default()).unwrap(); let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
let expr = crate::parsing::parser::array_elem_by_elem.parse(&tokens).unwrap(); let expr = crate::parsing::parser::array_elem_by_elem
.parse(tokens.as_slice())
.unwrap();
assert_eq!( assert_eq!(
expr.recast(&FormatOptions::new(), 0, ExprContext::Other), expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
expected, expected,

View File

@ -5,7 +5,7 @@ use crate::{
/// The "Node" type wraps all the AST elements we're able to find in a KCL /// The "Node" type wraps all the AST elements we're able to find in a KCL
/// file. Tokens we walk through will be one of these. /// file. Tokens we walk through will be one of these.
#[derive(Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum Node<'a> { pub enum Node<'a> {
Program(NodeRef<'a, types::Program>), Program(NodeRef<'a, types::Program>),
@ -31,6 +31,8 @@ pub enum Node<'a> {
MemberExpression(NodeRef<'a, types::MemberExpression>), MemberExpression(NodeRef<'a, types::MemberExpression>),
UnaryExpression(NodeRef<'a, types::UnaryExpression>), UnaryExpression(NodeRef<'a, types::UnaryExpression>),
IfExpression(NodeRef<'a, types::IfExpression>), IfExpression(NodeRef<'a, types::IfExpression>),
ElseIf(&'a types::ElseIf),
LabelledExpression(NodeRef<'a, types::LabelledExpression>),
Parameter(&'a types::Parameter), Parameter(&'a types::Parameter),
@ -38,11 +40,22 @@ pub enum Node<'a> {
MemberObject(&'a types::MemberObject), MemberObject(&'a types::MemberObject),
LiteralIdentifier(&'a types::LiteralIdentifier), LiteralIdentifier(&'a types::LiteralIdentifier),
KclNone(&'a types::KclNone),
} }
impl From<&Node<'_>> for SourceRange { /// Returned during source_range conversion.
fn from(node: &Node) -> Self { #[derive(Debug)]
match node { pub enum AstNodeError {
/// Returned if we try and [SourceRange] a [types::KclNone].
NoSourceForAKclNone,
}
impl TryFrom<&Node<'_>> for SourceRange {
type Error = AstNodeError;
fn try_from(node: &Node) -> Result<Self, Self::Error> {
Ok(match node {
Node::Program(n) => SourceRange::from(*n), Node::Program(n) => SourceRange::from(*n),
Node::ImportStatement(n) => SourceRange::from(*n), Node::ImportStatement(n) => SourceRange::from(*n),
Node::ExpressionStatement(n) => SourceRange::from(*n), Node::ExpressionStatement(n) => SourceRange::from(*n),
@ -67,7 +80,65 @@ impl From<&Node<'_>> for SourceRange {
Node::ObjectProperty(n) => SourceRange::from(*n), Node::ObjectProperty(n) => SourceRange::from(*n),
Node::MemberObject(m) => SourceRange::new(m.start(), m.end(), m.module_id()), Node::MemberObject(m) => SourceRange::new(m.start(), m.end(), m.module_id()),
Node::IfExpression(n) => SourceRange::from(*n), Node::IfExpression(n) => SourceRange::from(*n),
Node::LabelledExpression(n) => SourceRange::from(*n),
Node::LiteralIdentifier(l) => SourceRange::new(l.start(), l.end(), l.module_id()), Node::LiteralIdentifier(l) => SourceRange::new(l.start(), l.end(), l.module_id()),
// This is broken too
Node::ElseIf(n) => SourceRange::new(n.cond.start(), n.cond.end(), n.cond.module_id()),
// The KclNone type here isn't an actual node, so it has no
// start/end information.
Node::KclNone(_) => return Err(Self::Error::NoSourceForAKclNone),
})
}
}
impl<'tree> From<&'tree types::BodyItem> for Node<'tree> {
fn from(node: &'tree types::BodyItem) -> Self {
match node {
types::BodyItem::ImportStatement(v) => v.as_ref().into(),
types::BodyItem::ExpressionStatement(v) => v.into(),
types::BodyItem::VariableDeclaration(v) => v.as_ref().into(),
types::BodyItem::ReturnStatement(v) => v.into(),
}
}
}
impl<'tree> From<&'tree types::Expr> for Node<'tree> {
fn from(node: &'tree types::Expr) -> Self {
match node {
types::Expr::Literal(lit) => lit.as_ref().into(),
types::Expr::TagDeclarator(tag) => tag.as_ref().into(),
types::Expr::Identifier(id) => id.as_ref().into(),
types::Expr::BinaryExpression(be) => be.as_ref().into(),
types::Expr::FunctionExpression(fe) => fe.as_ref().into(),
types::Expr::CallExpression(ce) => ce.as_ref().into(),
types::Expr::CallExpressionKw(ce) => ce.as_ref().into(),
types::Expr::PipeExpression(pe) => pe.as_ref().into(),
types::Expr::PipeSubstitution(ps) => ps.as_ref().into(),
types::Expr::ArrayExpression(ae) => ae.as_ref().into(),
types::Expr::ArrayRangeExpression(are) => are.as_ref().into(),
types::Expr::ObjectExpression(oe) => oe.as_ref().into(),
types::Expr::MemberExpression(me) => me.as_ref().into(),
types::Expr::UnaryExpression(ue) => ue.as_ref().into(),
types::Expr::IfExpression(e) => e.as_ref().into(),
types::Expr::LabelledExpression(e) => e.as_ref().into(),
types::Expr::None(n) => n.into(),
}
}
}
impl<'tree> From<&'tree types::BinaryPart> for Node<'tree> {
fn from(node: &'tree types::BinaryPart) -> Self {
match node {
types::BinaryPart::Literal(lit) => lit.as_ref().into(),
types::BinaryPart::Identifier(id) => id.as_ref().into(),
types::BinaryPart::BinaryExpression(be) => be.as_ref().into(),
types::BinaryPart::CallExpression(ce) => ce.as_ref().into(),
types::BinaryPart::CallExpressionKw(ce) => ce.as_ref().into(),
types::BinaryPart::UnaryExpression(ue) => ue.as_ref().into(),
types::BinaryPart::MemberExpression(me) => me.as_ref().into(),
types::BinaryPart::IfExpression(e) => e.as_ref().into(),
} }
} }
} }
@ -116,4 +187,7 @@ impl_from!(Node, ObjectProperty);
impl_from_ref!(Node, Parameter); impl_from_ref!(Node, Parameter);
impl_from_ref!(Node, MemberObject); impl_from_ref!(Node, MemberObject);
impl_from!(Node, IfExpression); impl_from!(Node, IfExpression);
impl_from!(Node, ElseIf);
impl_from!(Node, LabelledExpression);
impl_from_ref!(Node, LiteralIdentifier); impl_from_ref!(Node, LiteralIdentifier);
impl_from!(Node, KclNone);

View File

@ -0,0 +1,201 @@
use anyhow::Result;
use crate::walk::Node;
/// Walk-specific trait adding the ability to traverse the KCL AST.
///
/// This trait is implemented on [Node] to handle the fairly tricky bit of
/// recursing into the AST in a single place, as well as helpers for traversing
/// the tree. for callers to use.
pub trait Visitable<'tree> {
/// Return a `Vec<Node>` for all *direct* children of this AST node. This
/// should only contain direct descendants.
fn children(&self) -> Vec<Node<'tree>>;
/// Return `self` as a [Node]. Generally speaking, the [Visitable] trait
/// is only going to be implemented on [Node], so this is purely used by
/// helpers that are generic over a [Visitable] and want to deref back
/// into a [Node].
fn node(&self) -> Node<'tree>;
/// Call the provided [Visitor] in order to Visit `self`. This will
/// only be called on `self` -- the [Visitor] is responsible for
/// recursing into any children, if desired.
fn visit<VisitorT>(&self, visitor: VisitorT) -> Result<bool, VisitorT::Error>
where
VisitorT: Visitor<'tree>,
{
visitor.visit_node(self.node())
}
}
/// Trait used to enable visiting members of KCL AST.
///
/// Implementing this trait enables the implementer to be invoked over
/// members of KCL AST by using the [Visitable::visit] function on
/// a [Node].
pub trait Visitor<'tree> {
/// Error type returned by the [Self::visit] function.
type Error;
/// Visit a KCL AST [Node].
///
/// In general, implementers likely wish to check to see if a Node is what
/// they're looking for, and either descend into that [Node]'s children (by
/// calling [Visitable::children] on [Node] to get children nodes,
/// calling [Visitable::visit] on each node of interest), or perform
/// some action.
fn visit_node(&self, node: Node<'tree>) -> Result<bool, Self::Error>;
}
impl<'a, FnT, ErrorT> Visitor<'a> for FnT
where
FnT: Fn(Node<'a>) -> Result<bool, ErrorT>,
{
type Error = ErrorT;
fn visit_node(&self, n: Node<'a>) -> Result<bool, ErrorT> {
self(n)
}
}
impl<'tree> Visitable<'tree> for Node<'tree> {
fn node(&self) -> Node<'tree> {
*self
}
fn children(&self) -> Vec<Node<'tree>> {
match self {
Node::Program(n) => n.body.iter().map(|node| node.into()).collect(),
Node::ExpressionStatement(n) => {
vec![(&n.expression).into()]
}
Node::BinaryExpression(n) => {
vec![(&n.left).into(), (&n.right).into()]
}
Node::FunctionExpression(n) => {
let mut children = n.params.iter().map(|v| v.into()).collect::<Vec<Node>>();
children.push((&n.body).into());
children
}
Node::CallExpression(n) => {
let mut children = n.arguments.iter().map(|v| v.into()).collect::<Vec<Node>>();
children.insert(0, (&n.callee).into());
children
}
Node::CallExpressionKw(n) => {
let mut children = n.unlabeled.iter().map(|v| v.into()).collect::<Vec<Node>>();
// TODO: this is wrong but it's what the old walk code was doing.
// We likely need a real LabeledArg AST node, but I don't
// want to tango with it since it's a lot deeper than
// adding it to the enum.
children.extend(n.arguments.iter().map(|v| (&v.arg).into()).collect::<Vec<Node>>());
children
}
Node::PipeExpression(n) => n.body.iter().map(|v| v.into()).collect(),
Node::ArrayExpression(n) => n.elements.iter().map(|v| v.into()).collect(),
Node::ArrayRangeExpression(n) => {
vec![(&n.start_element).into(), (&n.end_element).into()]
}
Node::ObjectExpression(n) => n.properties.iter().map(|v| v.into()).collect(),
Node::MemberExpression(n) => {
vec![(&n.object).into(), (&n.property).into()]
}
Node::IfExpression(n) => {
let mut children = n.else_ifs.iter().map(|v| v.into()).collect::<Vec<Node>>();
children.insert(0, n.cond.as_ref().into());
children.push(n.final_else.as_ref().into());
children
}
Node::VariableDeclaration(n) => vec![(&n.declaration).into()],
Node::ReturnStatement(n) => {
vec![(&n.argument).into()]
}
Node::VariableDeclarator(n) => {
vec![(&n.id).into(), (&n.init).into()]
}
Node::UnaryExpression(n) => {
vec![(&n.argument).into()]
}
Node::Parameter(n) => {
vec![(&n.identifier).into()]
}
Node::ObjectProperty(n) => {
vec![(&n.value).into()]
}
Node::ElseIf(n) => {
vec![(&n.cond).into(), n.then_val.as_ref().into()]
}
Node::LabelledExpression(e) => {
vec![(&e.expr).into(), (&e.label).into()]
}
Node::PipeSubstitution(_)
| Node::TagDeclarator(_)
| Node::Identifier(_)
| Node::ImportStatement(_)
| Node::MemberObject(_)
| Node::LiteralIdentifier(_)
| Node::KclNone(_)
| Node::Literal(_) => vec![],
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
macro_rules! kcl {
( $kcl:expr ) => {{
$crate::parsing::top_level_parse($kcl).unwrap()
}};
}
#[test]
fn count_crows() {
let program = kcl!(
"\
const crow1 = 1
const crow2 = 2
fn crow3() {
const crow4 = 3
crow5()
}
"
);
#[derive(Debug, Default)]
struct CountCrows {
n: Box<Mutex<usize>>,
}
impl<'tree> Visitor<'tree> for &CountCrows {
type Error = ();
fn visit_node(&self, node: Node<'tree>) -> Result<bool, Self::Error> {
if let Node::VariableDeclarator(vd) = node {
if vd.id.name.starts_with("crow") {
*self.n.lock().unwrap() += 1;
}
}
for child in node.children().iter() {
if !child.visit(*self)? {
return Ok(false);
}
}
Ok(true)
}
}
let prog: Node = (&program).into();
let count_crows: CountCrows = Default::default();
Visitable::visit(&prog, &count_crows).unwrap();
assert_eq!(*count_crows.n.lock().unwrap(), 4);
}
}

View File

@ -1,329 +1,55 @@
use anyhow::Result; use anyhow::Result;
use super::ast_visitor::{Visitable, Visitor};
use crate::{ use crate::{
parsing::ast::types::{ parsing::ast::types::{NodeRef, Program},
BinaryPart, BodyItem, Expr, IfExpression, LiteralIdentifier, MemberExpression, MemberObject, NodeRef,
ObjectExpression, ObjectProperty, Parameter, Program, UnaryExpression, VariableDeclarator,
},
walk::Node, walk::Node,
}; };
/// Walker is implemented by things that are able to walk an AST tree to /// *DEPRECATED* Walk trait.
/// produce lints. This trait is implemented automatically for a few of the ///
/// common types, but can be manually implemented too. /// This was written before [Visitor], which is the better way to traverse
/// a AST.
///
/// This trait continues to exist in order to not change all the linter
/// as we refine the walk code.
///
/// This, internally, uses the new [Visitor] trait, and is only provided as
/// a stub until we migrate all existing code off this trait.
pub trait Walker<'a> { pub trait Walker<'a> {
/// Walk will visit every element of the AST. /// Walk will visit every element of the AST, recursing through the
/// whole tree.
fn walk(&self, n: Node<'a>) -> Result<bool>; fn walk(&self, n: Node<'a>) -> Result<bool>;
} }
impl<'a, FnT> Walker<'a> for FnT impl<'tree, VisitorT> Walker<'tree> for VisitorT
where where
FnT: Fn(Node<'a>) -> Result<bool>, VisitorT: Visitor<'tree>,
VisitorT: Clone,
anyhow::Error: From<VisitorT::Error>,
VisitorT::Error: Send,
VisitorT::Error: Sync,
{ {
fn walk(&self, n: Node<'a>) -> Result<bool> { fn walk(&self, n: Node<'tree>) -> Result<bool> {
self(n) if !n.visit(self.clone())? {
return Ok(false);
}
for child in n.children() {
if !Self::walk(self, child)? {
return Ok(false);
}
}
Ok(true)
} }
} }
/// Run the Walker against all [Node]s in a [Program]. /// Run the Walker against all [Node]s in a [Program].
pub fn walk<'a, WalkT>(prog: NodeRef<'a, Program>, f: &WalkT) -> Result<bool> pub fn walk<'a, WalkT>(prog: NodeRef<'a, Program>, f: WalkT) -> Result<bool>
where where
WalkT: Walker<'a>, WalkT: Walker<'a>,
{ {
if !f.walk(prog.into())? { let prog: Node = prog.into();
return Ok(false); f.walk(prog)
}
for bi in &prog.body {
if !walk_body_item(bi, f)? {
return Ok(false);
}
}
Ok(true)
}
fn walk_variable_declarator<'a, WalkT>(node: NodeRef<'a, VariableDeclarator>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !f.walk((&node.id).into())? {
return Ok(false);
}
walk_value(&node.init, f)
}
fn walk_parameter<'a, WalkT>(node: &'a Parameter, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
f.walk((&node.identifier).into())
}
fn walk_member_object<'a, WalkT>(node: &'a MemberObject, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
f.walk(node.into())
}
fn walk_literal_identifier<'a, WalkT>(node: &'a LiteralIdentifier, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
f.walk(node.into())
}
fn walk_member_expression<'a, WalkT>(node: NodeRef<'a, MemberExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !walk_member_object(&node.object, f)? {
return Ok(false);
}
walk_literal_identifier(&node.property, f)
}
fn walk_binary_part<'a, WalkT>(node: &'a BinaryPart, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
match node {
BinaryPart::Literal(lit) => f.walk(lit.as_ref().into()),
BinaryPart::Identifier(id) => f.walk(id.as_ref().into()),
BinaryPart::BinaryExpression(be) => f.walk(be.as_ref().into()),
BinaryPart::CallExpression(ce) => f.walk(ce.as_ref().into()),
BinaryPart::CallExpressionKw(ce) => f.walk(ce.as_ref().into()),
BinaryPart::UnaryExpression(ue) => walk_unary_expression(ue, f),
BinaryPart::MemberExpression(me) => walk_member_expression(me, f),
BinaryPart::IfExpression(e) => walk_if_expression(e, f),
}
}
// TODO: Rename this to walk_expr
fn walk_value<'a, WalkT>(node: &'a Expr, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
match node {
Expr::Literal(lit) => f.walk(lit.as_ref().into()),
Expr::TagDeclarator(tag) => f.walk(tag.as_ref().into()),
Expr::Identifier(id) => {
// sometimes there's a bare Identifier without a Value::Identifier.
f.walk(id.as_ref().into())
}
Expr::BinaryExpression(be) => {
if !f.walk(be.as_ref().into())? {
return Ok(false);
}
if !walk_binary_part(&be.left, f)? {
return Ok(false);
}
walk_binary_part(&be.right, f)
}
Expr::FunctionExpression(fe) => {
if !f.walk(fe.as_ref().into())? {
return Ok(false);
}
for arg in &fe.params {
if !walk_parameter(arg, f)? {
return Ok(false);
}
}
walk(&fe.body, f)
}
Expr::CallExpression(ce) => {
if !f.walk(ce.as_ref().into())? {
return Ok(false);
}
if !f.walk((&ce.callee).into())? {
return Ok(false);
}
for e in &ce.arguments {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::CallExpressionKw(ce) => {
if !f.walk(ce.as_ref().into())? {
return Ok(false);
}
if !f.walk((&ce.callee).into())? {
return Ok(false);
}
if let Some(ref e) = ce.unlabeled {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
for e in &ce.arguments {
if !walk_value::<WalkT>(&e.arg, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::PipeExpression(pe) => {
if !f.walk(pe.as_ref().into())? {
return Ok(false);
}
for e in &pe.body {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::PipeSubstitution(ps) => f.walk(ps.as_ref().into()),
Expr::ArrayExpression(ae) => {
if !f.walk(ae.as_ref().into())? {
return Ok(false);
}
for e in &ae.elements {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::ArrayRangeExpression(are) => {
if !f.walk(are.as_ref().into())? {
return Ok(false);
}
if !walk_value::<WalkT>(&are.start_element, f)? {
return Ok(false);
}
if !walk_value::<WalkT>(&are.end_element, f)? {
return Ok(false);
}
Ok(true)
}
Expr::ObjectExpression(oe) => walk_object_expression(oe, f),
Expr::MemberExpression(me) => walk_member_expression(me, f),
Expr::UnaryExpression(ue) => walk_unary_expression(ue, f),
Expr::IfExpression(e) => walk_if_expression(e, f),
Expr::None(_) => Ok(true),
}
}
/// Walk through an [ObjectProperty].
fn walk_object_property<'a, WalkT>(node: NodeRef<'a, ObjectProperty>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
walk_value(&node.value, f)
}
/// Walk through an [ObjectExpression].
fn walk_object_expression<'a, WalkT>(node: NodeRef<'a, ObjectExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
for prop in &node.properties {
if !walk_object_property(prop, f)? {
return Ok(false);
}
}
Ok(true)
}
/// Walk through an [IfExpression].
fn walk_if_expression<'a, WalkT>(node: NodeRef<'a, IfExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !walk_value(&node.cond, f)? {
return Ok(false);
}
for else_if in &node.else_ifs {
if !walk_value(&else_if.cond, f)? {
return Ok(false);
}
if !walk(&else_if.then_val, f)? {
return Ok(false);
}
}
let final_else = &(*node.final_else);
if !f.walk(final_else.into())? {
return Ok(false);
}
Ok(true)
}
/// walk through an [UnaryExpression].
fn walk_unary_expression<'a, WalkT>(node: NodeRef<'a, UnaryExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
walk_binary_part(&node.argument, f)
}
/// walk through a [BodyItem].
fn walk_body_item<'a, WalkT>(node: &'a BodyItem, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
// We don't walk a BodyItem since it's an enum itself.
match node {
BodyItem::ImportStatement(xs) => {
if !f.walk(xs.as_ref().into())? {
return Ok(false);
}
Ok(true)
}
BodyItem::ExpressionStatement(xs) => {
if !f.walk(xs.into())? {
return Ok(false);
}
walk_value(&xs.expression, f)
}
BodyItem::VariableDeclaration(vd) => {
if !f.walk(vd.as_ref().into())? {
return Ok(false);
}
walk_variable_declarator(&vd.declaration, f)
}
BodyItem::ReturnStatement(rs) => {
if !f.walk(rs.into())? {
return Ok(false);
}
walk_value(&rs.argument, f)
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -345,10 +71,10 @@ const bar = 2
" "
); );
walk(&program, &|node| { walk(&program, |node| {
if let Node::VariableDeclarator(vd) = node { if let Node::VariableDeclarator(vd) = node {
if vd.id.name == "foo" { if vd.id.name == "foo" {
return Ok(false); return Ok::<bool, anyhow::Error>(false);
} }
panic!("walk didn't stop"); panic!("walk didn't stop");
} }

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