Compare commits

...

55 Commits

Author SHA1 Message Date
157b76cc78 KCL refactor: combine two fields into one struct (#4689) 2024-12-06 19:11:31 -06:00
cf957d880e KCL refactor: Fix argument names (#4690)
Does not change behaviour. It just clarifies whether JS is passing a string containing KCL source code, or containing a JSON-stringified KCL AST.
2024-12-06 17:20:06 -06:00
dfc3d19677 remove clearScene from TS side (#4684)
* updates

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

* fix lint

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

* add failing tests

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

* more tests

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

* the scene is cleared

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

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

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

* pull thru

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

* set that we switched files

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

* updates

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

* updates

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

* updates

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

* fix two dirties

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

* fix

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

---------

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

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

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

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

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

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

* fix: add missing TokenType case

* fix: incorrect fn args after merge

* fix: clippy lint

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

---------

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

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

* Remove unnecessary Vec from VariableDeclaration

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

* Parse export import

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

* Factor out an execution module

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

* imports: constants, globs, export import

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

* test fixups

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

---------

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

* Add .eslintcache to ignores
2024-12-06 12:28:58 -05:00
441d957228 start of cache: don't re-execute on whitespace / top level code comment changes (#4663)
* start

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

working for whitespace

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

pull thru

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

fix wasm

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

pull thru to js start

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

actually use the cache in ts

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

rust owns clearing the scene

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

fixes

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

empty

stupid log

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 tests

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

updatez

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

updates

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

save the state

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

save the state

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

updates

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

use the old memory

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

cleanup to use the old exec state

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

fices

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

updates;

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

fixes

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

fmt

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

updates

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

fixes

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

updates

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

cleanup

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

fixes

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

* rebase and compile

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

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

* fix the lsp to use the cache

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

* add comment

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

* use a global static instead;

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

* fix rust test

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

* cleanup more

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

* cleanups

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

* cleanup the api even more

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>

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

* bust the cache on unit changes

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>

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

* stupid codespell

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 03:51:06 +00:00
9e57034873 AST: Allow KCL fn params to have defaults and labels (#4676)
Pure refactor, should not change any behaviour.

Previously, optional parameters in KCL function calls always set the parameter to KclNone. 

As of this PR, they can be set to KCL literals in addition to KCL none. However the parser does not actually ever use this (that'll be in a follow-up PR).

Also adds a `labeled: bool` to all parameters, which is always true. But it lays the groundwork for the unlabeled first parameter in a follow-up PR.
2024-12-06 03:04:40 +00:00
eb96d6539c Surface warnings to frontend and LSP (#4603)
* Send multiple errors and warnings to the frontend and LSP

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

* Refactor the parser to use CompilationError for parsing errors rather than KclError

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

* Refactoring: move CompilationError, etc.

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

* Integrate compilation errors with the frontend and CodeMirror

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

* Fix tests

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

* Review comments

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

* Fix module id/source range stuff

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

* More test fixups

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-06 13:57:31 +13:00
513c76ecc8 KCL: Remove stdlib written in KCL (#4673)
We don't have any of these, and I don't think it's
worth the complexity. The goal was to let us write
KCL stdlib functions in KCL not Rust. But who cares
really. We can always put this back if we need it.
2024-12-05 23:59:37 +00:00
51d9449280 Fix broken test from previous PR (#4674)
* Fix broken test from previous PR

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

* void

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 23:39:40 +00:00
6366bc4766 KCL: Keyword function calls for stdlib (#4647)
Part of https://github.com/KittyCAD/modeling-app/issues/4600

Adds support for keyword arguments to the stdlib, and calling stdlib functions with keyword arguments.

So far, I've changed one function: `rem`. Previously you would have used `rem(7, 2)` but now it's `rem(7, divisor: 2)`.

This is a proof-of-concept. If it's approved, we will:

1. Support closures with keyword arguments, and calling them
2. Move the rest of the stdlib to use kw arguments
2024-12-05 14:27:51 -06:00
7a21918223 Cleaner nightly release notes (#4668)
* Bump: Improve and fix nightly release notes

* Clean up after manual push

* Consistency tweak
2024-12-05 15:14:58 -05:00
8072f1db63 Add support for line comments in playwright-secrets.env (#4671) 2024-12-05 19:45:33 +00:00
18e1855fa9 Bump indexmap from 2.6.0 to 2.7.0 in /src/wasm-lib (#4622)
Bumps [indexmap](https://github.com/indexmap-rs/indexmap) from 2.6.0 to 2.7.0.
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/master/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.6.0...2.7.0)

---
updated-dependencies:
- dependency-name: indexmap
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 17:44:35 +00:00
7be53c7d4a Bump KCL, KCL test server, derive-docs (#4670) 2024-12-05 17:34:43 +00:00
2bf20988ef Fix to never have undefined iteration order and lint against it (#4665) 2024-12-05 17:09:35 +00:00
1495cc6d18 Fix default planes to be created in deterministic order (#4664)
* Fix default planes to be created in deterministic order

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

* Trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 16:04:04 +00:00
f876e6ca3c Bump and release kcl-lib 0.2.28 (#4669)
bump kcl version
2024-12-05 15:03:55 +00:00
60a0c811ab Move parsing files around (#4626)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-05 17:56:49 +13:00
cab0c1e6a1 Add list of commits as changelog between nightly builds (#4654) 2024-12-04 19:06:17 -05:00
417d720b22 Point-and-click Loft (#4605)
* 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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-04 22:24:16 +00:00
77293952c0 fix: upon entering sketch mode, axis do not rotate. (#4572)
* fix: upon entering sketch mode, axis do not rotate.

* fix: camera settings has the up vector to set, this should now work
2024-12-04 15:57:17 -06:00
ea3d604b73 Allow standard planes to appear in the artifact graph (#4594) 2024-12-04 21:06:02 +00:00
023a659491 Make some fields of lint::Discovered public again; update kittycad (#4658)
* Make some fields of lint::Discovered public again; update kittycad

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

* Empty commit to try to unstick CI

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-05 10:05:23 +13:00
dd3a2b14f9 Bump hashbrown from 0.15.0 to 0.15.2 (#4659) 2024-12-04 20:40:46 +00:00
424b409cc1 Bump and release kcl-lib 0.2.27 (#4643)
* bump and release kcl-lib

* update snapshot

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

* empty commit

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-03 17:58:21 -05:00
82a58e69c2 Bump wasm-pack from 0.13.0 to 0.13.1 (#4630) 2024-12-03 16:27:16 -06:00
max
776b420031 Rename addFillet files to addEdgeTreatment (#4644)
* rename

* update references
2024-12-04 08:30:02 +11:00
1087d4223b Bump happy-dom from 15.10.2 to 15.11.7 (#4625)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 15.10.2 to 15.11.7.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v15.10.2...v15.11.7)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 15:40:35 -05:00
089d6df889 Update fn syntax in module docs (#4641) 2024-12-03 15:14:32 -05:00
efb067af58 Update fn syntax in module docs (#4641) 2024-12-03 19:47:21 +00:00
2aa27eab01 Bump happy-dom from 15.10.2 to 15.11.7 (#4625)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 15.10.2 to 15.11.7.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v15.10.2...v15.11.7)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 19:40:36 +00:00
9c47ac5b57 Update fn syntax in KCL Types doc (#4640)
* update fn syntax

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

* Trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-03 18:17:02 +00:00
5ae1aecd74 Reduce Python API surface area to what is necessary for kcl.py (#4637)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-03 17:34:58 +13:00
68ae7e98f9 Refactor SourceRange and ModuleId to make them better encapsualated (#4636)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-03 16:39:51 +13:00
56771d561a Bump rustls from 0.23.13 to 0.23.19 and rustls-pki-types (#4632) 2024-12-03 15:49:15 +13:00
f09411817c KCL AST: Call functions with keyword arguments (#4599)
Call expressions only, haven't done function expressions yet.

Part of https://github.com/KittyCAD/modeling-app/issues/4600
2024-12-02 21:23:18 +00:00
max
bed7ae3b8b Refactor addFillet into addEdgeTreatment Function Supporting Chamfers (#4593)
* refactor code mod and tests

* tsc

* make lint happy

* remove dumby data

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

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-12-02 21:43:59 +01:00
c43510732c KCL docs: Remove FunctionExpression (#4633)
KCL functions are a weird edge case, and the `FunctionExpression` field should not be included in its public API. That field is only there for implementation details, it shouldn't be exposed to users.

What's worse is that `FunctionExpression` includes a `Program` so every single AST node wound up being included in our docs.
2024-12-02 14:36:49 -06:00
51f0b669a4 fix: only count something as a directory if it has children (#4595)
* fix: only count something as a directory if it has children

* fix: playwright tests

* fix: return 0 if you cant find the projectfolder

* fix: remove folder count from e2e tests since it is unused currently

---------

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
2024-12-02 15:16:43 -05:00
3cbedcd3e7 Bump clap from 4.5.20 to 4.5.21 in /src/wasm-lib (#4623)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.20 to 4.5.21.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.20...clap_complete-v4.5.21)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 09:28:57 -08:00
5d2fa43150 Bump dawidd6/action-download-artifact from 6 to 7 (#4621)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 6 to 7.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 09:27:57 -08:00
ec49b0752e Remove auto creation of the draft release (#4588)
Fixes #4586
2024-12-02 10:30:24 -05:00
3b171fb881 Update nightly and release icons from Figma (#4597)
* Update nightly and windows icons

* Remove icon.ico, keep only icon.png, see https://www.electron.build/icons.html#windows-nsis

* Revert "Remove icon.ico, keep only icon.png, see https://www.electron.build/icons.html#windows-nsis"

This reverts commit b97f81b07d.

* Update windows icons

* Reset windows ico

* Test ico no margin

* Converted with freeconvert

* Use convertico.com for conversion
2024-12-02 10:24:14 -05:00
0548409da0 Bump to Rust 1.83 (#4604) 2024-11-28 18:31:11 +00:00
dd052b35fd KCL: Remove unnecessary 'optional: bool' field on CallExpression (#4584)
It was put there in the original KCL JS-to-Rust rewrite, and I don't think it's used at all.
2024-11-27 18:01:42 -06:00
46be4e7eef Log simple performance metrics (#4596)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-11-28 11:27:17 +13:00
412d1b7a99 Update KCL Types doc (#4591)
* use `=` instead of `:`, fix formatting

* remove `%` from `segEnd` and `segLen` calls
2024-11-27 11:14:59 -05:00
cfdd22af74 Add ability to immediately enter sketch mode by double-clicking an existing sketch (#4573)
* Implement the functionality

* Another fmt

* Fix handler to not rely on modelingMachine's context,
because that creates an implicit race

* Write an E2E test

* Fix tsc and fmt

* Use artifactGraph helpers for more concise code

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

* Fix up imports and whatnot from commit 2bfc5f5c

* Make early return more clear with curly braces

* Whoops should have linted

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-11-27 10:08:23 -05:00
68a11e7aa5 Remove the lexer from KCL's API (#4589)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-11-27 03:30:28 +00:00
3139e18dc7 Make = and => optional in function declarations (#4577)
* Make `=` and `=>` optional in function declarations

And requires `:` for return types

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

* Tests

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

* Format types in function decls

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

* Require  in anon function decls

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-11-27 15:46:58 +13:00
494 changed files with 114101 additions and 136418 deletions

View File

@ -362,6 +362,17 @@ jobs:
- name: List artifacts - name: List artifacts
run: "ls -R out" run: "ls -R out"
- name: Set more complete nightly release notes
if: ${{ env.IS_NIGHTLY == 'true' }}
run: |
# Note: prefered going this way instead of a full clone in the checkout step,
# see https://github.com/actions/checkout/issues/1471
git fetch --prune --unshallow --tags
export TAG="nightly-${VERSION}"
export PREVIOUS_TAG=$(git describe --tags --match="nightly-v[0-9]*" --abbrev=0)
export NOTES=$(./scripts/get-nightly-changelog.sh)
yarn files:set-notes
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
uses: 'google-github-actions/auth@v2.1.7' uses: 'google-github-actions/auth@v2.1.7'
@ -383,12 +394,13 @@ jobs:
parent: false parent: false
destination: 'dl.kittycad.io/releases/modeling-app/nightly' destination: 'dl.kittycad.io/releases/modeling-app/nightly'
- name: Create draft release - name: Tag nightly commit
uses: softprops/action-gh-release@v2 if: ${{ env.IS_NIGHTLY == 'true' }}
if: ${{ env.IS_RELEASE == 'true' }} uses: actions/github-script@v7
with: with:
name: ${{ env.VERSION }} script: |
tag_name: ${{ env.VERSION }} const { VERSION } = process.env
draft: true const { owner, repo } = context.repo
generate_release_notes: true const { sha } = context
files: 'out/Zoo*' const ref = `refs/tags/nightly-${VERSION}`
github.rest.git.createRef({ owner, repo, sha, ref })

View File

@ -68,7 +68,7 @@ jobs:
- name: Download Wasm Cache - name: Download Wasm Cache
id: download-wasm id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false' if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v7
continue-on-error: true continue-on-error: true
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
@ -255,7 +255,7 @@ jobs:
- name: Download Wasm Cache - name: Download Wasm Cache
id: download-wasm id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false' if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v7
continue-on-error: true continue-on-error: true
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}

View File

@ -132,6 +132,12 @@ jobs:
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async
- name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
files: 'out/Zoo*'
announce_release: announce_release:
needs: [publish-apps-release] needs: [publish-apps-release]

1
.gitignore vendored
View File

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

View File

@ -99,7 +99,7 @@ yarn tron:start
This will start the application and hot-reload on changes. This will start the application and hot-reload on changes.
Devtools can be opened with the usual Cmd/Ctrl-Shift-I. Devtools can be opened with the usual Cmd-Opt-I (Mac) or Ctrl-Shift-I (Linux and Windows).
To build, run `yarn tron:package`. To build, run `yarn tron:package`.
@ -136,7 +136,7 @@ https://github.com/KittyCAD/modeling-app/issues/new
#### 2. Push a new tag #### 2. Push a new tag
Create a new tag and push it to the repo (eg. `v0.28.0` for `$VERSION`) Create a new tag and push it to the repo. The `semantic-release.sh` script will automatically bump the minor part, which we use the most. For instance going from `v0.27.0` to `v0.28.0`.
``` ```
VERSION=$(./scripts/semantic-release.sh) VERSION=$(./scripts/semantic-release.sh)
@ -146,16 +146,14 @@ git push origin --tags
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts. This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts.
Once the workflow succeeds, a draft release will be created at https://github.com/KittyCAD/modeling-app/releases. The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)).
#### 3. Manually test artifacts from the Cut Release PR #### 3. Manually test artifacts
##### Release builds ##### Release builds
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.). The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.).
Alternatively, the draft release will also include these builds.
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue. Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
##### Updater-test builds ##### Updater-test builds
@ -178,9 +176,11 @@ If the prompt doesn't show up, start the app in command line to grab the electro
#### 4. Publish the release #### 4. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases, paste in the changelog discussed in the issue, and publish the draft release created by the `build-apps` workflow from step 2. Head over to https://github.com/KittyCAD/modeling-app/releases/new, pick the newly created tag and type it in the _Release title_ field as well.
A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter. On success, the files will be uploaded to the public bucket and the announcement on Discord will be sent. Hit _Generate release notes_ as a starting point to discuss the changelog in the issue. Once done, make sure _Set as the latest release_ is checked, and hit _Publish release_.
A new `publish-apps-release` will kick in and you should be able to find it [here](https://github.com/KittyCAD/modeling-app/actions?query=event%3Arelease). On success, the files will be uploaded to the public bucket as well as to the GitHub release, and the announcement on Discord will be sent.
#### 5. Close the issue #### 5. Close the issue
@ -450,3 +450,9 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
## KCL ## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
### Logging
To display logging (to the terminal or console) set `ZOO_LOG=1`. This will log some warnings and simple performance metrics. To view these in test runs, use `-- --nocapture`.
To enable memory metrics, build with `--features dhat-heap`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -58,7 +58,7 @@ mountingPlate = extrude(thickness, mountingPlateSketch)
```js ```js
// Sketch on the face of a chamfer. // Sketch on the face of a chamfer.
fn cube = (pos, scale) => { fn cube(pos, scale) {
sg = startSketchOn('XY') sg = startSketchOn('XY')
|> startProfileAt(pos, %) |> startProfileAt(pos, %)
|> line([0, scale], %) |> line([0, scale], %)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -37,7 +37,7 @@ assertEqual(n, 3, 0.0001, "5/2 = 2.5, rounded up makes 3")
startSketchOn('XZ') startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 2 }, %) |> circle({ center = [0, 0], radius = 2 }, %)
|> extrude(5, %) |> extrude(5, %)
|> patternTransform(n, (id) => { |> patternTransform(n, fn(id) {
return { translate = [4 * id, 0, 0] } return { translate = [4 * id, 0, 0] }
}, %) }, %)
``` ```

View File

@ -29,7 +29,7 @@ map(array: [KclValue], map_fn: FunctionParam) -> [KclValue]
```js ```js
r = 10 // radius r = 10 // radius
fn drawCircle = (id) => { fn drawCircle(id) {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
} }
@ -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], (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

@ -12,7 +12,7 @@ to other modules.
``` ```
// util.kcl // util.kcl
export fn increment = (x) => { export fn increment(x) {
return x + 1 return x + 1
} }
``` ```
@ -37,11 +37,11 @@ Multiple functions can be exported in a file.
``` ```
// util.kcl // util.kcl
export fn increment = (x) => { export fn increment(x) {
return x + 1 return x + 1
} }
export fn decrement = (x) => { export fn decrement(x) {
return x - 1 return x - 1
} }
``` ```

File diff suppressed because one or more lines are too long

View File

@ -30,7 +30,7 @@ patternTransform2d(total_instances: u32, transform_function: FunctionParam, soli
```js ```js
// Each instance will be shifted along the X axis. // Each instance will be shifted along the X axis.
fn transform = (id) => { fn transform(id) {
return { translate = [4 * id, 0] } return { translate = [4 * id, 0] }
} }

View File

@ -30,14 +30,14 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
```js ```js
// This function adds two numbers. // This function adds two numbers.
fn add = (a, b) => { fn add(a, b) {
return a + b return a + b
} }
// This function adds an array of numbers. // This function adds an array of numbers.
// It uses the `reduce` function, to call the `add` function on every // It uses the `reduce` function, to call the `add` function on every
// element of the `arr` parameter. The starting value is 0. // element of the `arr` parameter. The starting value is 0.
fn sum = (arr) => { fn sum(arr) {
return reduce(arr, 0, add) return reduce(arr, 0, add)
} }
@ -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, (i, result_so_far) {
return i + result_so_far return i + result_so_far
}) })
@ -74,7 +74,7 @@ assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
```js ```js
// Declare a function that sketches a decagon. // Declare a function that sketches a decagon.
fn decagon = (radius) => { fn decagon(radius) {
// Each side of the decagon is turned this many degrees from the previous angle. // Each side of the decagon is turned this many degrees from the previous angle.
stepAngle = 1 / 10 * tau() stepAngle = 1 / 10 * tau()
@ -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, (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 one or more lines are too long

View File

@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
fn cylinder = (radius, tag) => { fn cylinder(radius, tag) {
return startSketchAt([0, 0]) return startSketchAt([0, 0])
|> circle({ |> circle({
radius = radius, radius = radius,

View File

@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
fn cylinder = (radius, tag) => { fn cylinder(radius, tag) {
return startSketchAt([0, 0]) return startSketchAt([0, 0])
|> circle({ |> circle({
radius = radius, radius = radius,

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,7 @@ If you want to get a value from an array you can use the index like so:
An object is defined with `{}` braces. Here is an example object: An object is defined with `{}` braces. Here is an example object:
``` ```
myObj = {a: 0, b: "thing"} myObj = { a = 0, b = "thing" }
``` ```
We support two different ways of getting properties from objects, you can call We support two different ways of getting properties from objects, you can call
@ -54,7 +54,7 @@ We also have support for defining your own functions. Functions can take in any
type of argument. Below is an example of the syntax: type of argument. Below is an example of the syntax:
``` ```
fn myFn = (x) => { fn myFn(x) {
return x return x
} }
``` ```
@ -90,12 +90,12 @@ startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %) - 90, segAng(rectangleSegmentA001) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %), segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001, %) -segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -118,17 +118,17 @@ use the tag `rectangleSegmentA001` in any function or expression in the file.
However if the code was written like this: However if the code was written like this:
``` ```
fn rect = (origin) => { fn rect(origin) {
return startSketchOn('XZ') return startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %) - 90, segAng(rectangleSegmentA001) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %), segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001, %) -segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -146,17 +146,17 @@ Tags are accessible through the sketch group they are declared in.
For example the following code works. For example the following code works.
``` ```
fn rect = (origin) => { fn rect(origin) {
return startSketchOn('XZ') return startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %) - 90, segAng(rectangleSegmentA001) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %), segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001, %) -segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -167,7 +167,10 @@ myRect = rect([20, 0])
myRect myRect
|> extrude(10, %) |> extrude(10, %)
|> fillet({radius: 0.5, tags: [myRect.tags.rectangleSegmentA001]}, %) |> fillet({
radius = 0.5,
tags = [myRect.tags.rectangleSegmentA001]
}, %)
``` ```
See how we use the tag `rectangleSegmentA001` in the `fillet` function outside See how we use the tag `rectangleSegmentA001` in the `fillet` function outside

View File

@ -1,161 +0,0 @@
---
title: "BinaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Add two numbers.
**enum:** `+`
----
Subtract two numbers.
**enum:** `-`
----
Multiply two numbers.
**enum:** `*`
----
Divide two numbers.
**enum:** `/`
----
Modulo two numbers.
**enum:** `%`
----
Raise a number to a power.
**enum:** `^`
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
----

View File

@ -1,161 +0,0 @@
---
title: "BinaryPart"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,97 +0,0 @@
---
title: "BodyItem"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
| `path` |`string`| | No |
| `raw_path` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ExpressionStatement`| | No |
| `expression` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `VariableDeclaration`| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ReturnStatement`| | No |
| `argument` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,41 +0,0 @@
---
title: "CommentStyle"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Like // foo
**enum:** `line`
----
Like /* foo */
**enum:** `block`
----

View File

@ -1,24 +0,0 @@
---
title: "ElseIf"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,16 +0,0 @@
---
title: "EnvironmentRef"
excerpt: "An index pointing to an environment."
layout: manual
---
An index pointing to an environment.
**Type:** `integer` (`uint`)

View File

@ -1,318 +0,0 @@
---
title: "Expr"
excerpt: "An expression can be evaluated to yield a single KCL value."
layout: manual
---
An expression can be evaluated to yield a single KCL value.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| An expression can be evaluated to yield a single KCL value. | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
| `value` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`FunctionExpression`](/docs/kcl/types/FunctionExpression)| | No |
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| An expression can be evaluated to yield a single KCL value. | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeExpression`| | No |
| `body` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeSubstitution`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayExpression`| | No |
| `elements` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ObjectExpression`| | No |
| `properties` |`[` [`ObjectProperty`](/docs/kcl/types/ObjectProperty) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| An expression can be evaluated to yield a single KCL value. | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| An expression can be evaluated to yield a single KCL value. | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application).
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `None`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,24 +0,0 @@
---
title: "FunctionExpression"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,23 +0,0 @@
---
title: "Identifier"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,24 +0,0 @@
---
title: "ImportItem"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,16 +0,0 @@
---
title: "ItemVisibility"
excerpt: ""
layout: manual
---
**enum:** `default`, `export`

View File

@ -317,7 +317,6 @@ Data for an imported geometry.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Function`| | No | | `type` |enum: `Function`| | No |
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No | | `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -1,56 +0,0 @@
---
title: "LiteralIdentifier"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,47 +0,0 @@
---
title: "LiteralValue"
excerpt: ""
layout: manual
---
**This schema accepts any of the following:**
**Type:** `number` (`double`)
----
**Type:** `string`
----
**Type:** `boolean`
----

View File

@ -1,57 +0,0 @@
---
title: "MemberObject"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,22 +0,0 @@
---
title: "NonCodeMeta"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `nonCodeNodes` |`object`| | No |
| `startNodes` |`[` [`NonCodeNode`](/docs/kcl/types/NonCodeNode) `]`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -1,23 +0,0 @@
---
title: "NonCodeNode"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `value` |[`NonCodeValue`](/docs/kcl/types/NonCodeValue)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,86 +0,0 @@
---
title: "NonCodeValue"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `inlineComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment. An example of this is the following: ```python,no_run /* This is a block comment */ 1 + 1 ``` Now this is important. The block comment is attached to the next line. This is always the case. Also the block comment doesn't have a new line above it. If it did it would be a `NewLineBlockComment`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `blockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment that has a new line above it. The user explicitly added a new line above the block comment.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLineBlockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLine`| | No |
----

View File

@ -1,24 +0,0 @@
---
title: "ObjectProperty"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `key` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `value` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,23 +0,0 @@
---
title: "Parameter"
excerpt: "Parameter of a KCL function."
layout: manual
---
Parameter of a KCL function.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `identifier` |[`Identifier`](/docs/kcl/types/Identifier)| The parameter's label or name. | No |
| `optional` |`boolean`| Is the parameter optional? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -1,26 +0,0 @@
---
title: "Program"
excerpt: "A KCL program top level, or function body."
layout: manual
---
A KCL program top level, or function body.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `body` |`[` [`BodyItem`](/docs/kcl/types/BodyItem) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| A KCL program top level, or function body. | No |
| `shebang` |[`Shebang`](/docs/kcl/types/Shebang)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,23 +0,0 @@
---
title: "Shebang"
excerpt: "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```"
layout: manual
---
A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `content` |`string`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,15 +0,0 @@
---
title: "Uint"
excerpt: ""
layout: manual
---
**Type:** `integer` (`uint32`)

View File

@ -1,41 +0,0 @@
---
title: "UnaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Negate a number.
**enum:** `-`
----
Negate a boolean.
**enum:** `!`
----

View File

@ -1,24 +0,0 @@
---
title: "VariableDeclarator"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |[`Identifier`](/docs/kcl/types/Identifier)| The identifier of the variable. | No |
| `init` |[`Expr`](/docs/kcl/types/Expr)| The value of the variable. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,41 +0,0 @@
---
title: "VariableKind"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Declare a named constant.
**enum:** `const`
----
Declare a function.
**enum:** `fn`
----

View File

@ -458,8 +458,8 @@ test.describe('Editor tests', () => {
/* add the following code to the editor ($ error is not a valid line) /* add the following code to the editor ($ error is not a valid line)
$ error $ error
const topAng = 30 topAng = 30
const bottomAng = 25 bottomAng = 25
*/ */
await u.codeLocator.click() await u.codeLocator.click()
await page.keyboard.type('$ error') await page.keyboard.type('$ error')
@ -474,12 +474,14 @@ test.describe('Editor tests', () => {
await page.keyboard.type('bottomAng = 25') await page.keyboard.type('bottomAng = 25')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// error in guter // error in gutter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token: $').first()).toBeVisible() await expect(
page.getByText('Tag names must not be empty').first()
).toBeVisible()
// select the line that's causing the error and delete it // select the line that's causing the error and delete it
await page.getByText('$ error').click() await page.getByText('$ error').click()
@ -518,7 +520,10 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
}) })
test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => { // TODO currently multiple source ranges are not supported
test.skip('error with 2 source ranges gets 2 diagnostics', async ({
page,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(

View File

@ -45,7 +45,6 @@ test.describe('integrations tests', () => {
{ {
title: 'test-sample', title: 'test-sample',
fileCount: 1, fileCount: 1,
folderCount: 1,
}, },
], ],
sortBy: 'last-modified-desc', sortBy: 'last-modified-desc',
@ -233,7 +232,6 @@ test.describe('when using the file tree to', () => {
{ {
title: projectName, title: projectName,
fileCount: 2, fileCount: 2,
folderCount: 2, // TODO: This is a pre-existing bug, there are no folders within the project
}, },
], ],
sortBy: 'last-modified-desc', sortBy: 'last-modified-desc',

View File

@ -4,7 +4,6 @@ import { expect } from '@playwright/test'
interface ProjectCardState { interface ProjectCardState {
title: string title: string
fileCount: number fileCount: number
folderCount: number
} }
interface HomePageState { interface HomePageState {
@ -61,15 +60,13 @@ export class HomePageFixture {
const projectCards = await this.projectCard.all() const projectCards = await this.projectCard.all()
const projectCardStates: Array<ProjectCardState> = [] const projectCardStates: Array<ProjectCardState> = []
for (const projectCard of projectCards) { for (const projectCard of projectCards) {
const [title, fileCount, folderCount] = await Promise.all([ const [title, fileCount] = await Promise.all([
(await projectCard.locator(this.projectCardTitle).textContent()) || '', (await projectCard.locator(this.projectCardTitle).textContent()) || '',
Number(await projectCard.locator(this.projectCardFile).textContent()), Number(await projectCard.locator(this.projectCardFile).textContent()),
Number(await projectCard.locator(this.projectCardFolder).textContent()),
]) ])
projectCardStates.push({ projectCardStates.push({
title: title, title: title,
fileCount, fileCount,
folderCount,
}) })
} }
return projectCardStates return projectCardStates

View File

@ -28,6 +28,7 @@ type SceneSerialised = {
type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean> type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean>
type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean> type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean>
type DragFromHandler = ( type DragFromHandler = (
dragParams: mouseDragFromParams dragParams: mouseDragFromParams
@ -68,7 +69,7 @@ export class SceneFixture {
x: number, x: number,
y: number, y: number,
{ steps }: { steps: number } = { steps: 20 } { steps }: { steps: number } = { steps: 20 }
): [ClickHandler, MoveHandler] => ): [ClickHandler, MoveHandler, DblClickHandler] =>
[ [
(clickParams?: mouseParams) => { (clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) { if (clickParams?.pixelDiff) {
@ -90,6 +91,16 @@ export class SceneFixture {
} }
return this.page.mouse.move(x, y, { steps }) return this.page.mouse.move(x, y, { steps })
}, },
(clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() => this.page.mouse.dblclick(x, y),
clickParams.pixelDiff
)
}
return this.page.mouse.dblclick(x, y)
},
] as const ] as const
makeDragHelpers = ( makeDragHelpers = (
x: number, x: number,

View File

@ -6,6 +6,7 @@ export class ToolbarFixture {
public page: Page public page: Page
extrudeButton!: Locator extrudeButton!: Locator
loftButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
@ -26,6 +27,7 @@ export class ToolbarFixture {
reConstruct = (page: Page) => { reConstruct = (page: Page) => {
this.page = page this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
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

@ -552,6 +552,82 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
}) })
}) })
test(`Verify user can double-click to edit a sketch`, async ({
app,
editor,
toolbar,
scene,
}) => {
const initialCode = `closedSketch = startSketchOn('XZ')
|> circle({ center = [8, 5], radius = 2 }, %)
openSketch = startSketchOn('XY')
|> startProfileAt([-5, 0], %)
|> lineTo([0, 5], %)
|> xLine(5, %)
|> tangentialArcTo([10, 0], %)
`
await app.initialise(initialCode)
const pointInsideCircle = {
x: app.viewPortSize.width * 0.63,
y: app.viewPortSize.height * 0.5,
}
const pointOnPathAfterSketching = {
x: app.viewPortSize.width * 0.58,
y: app.viewPortSize.height * 0.5,
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_clickOpenPath, moveToOpenPath, dblClickOpenPath] =
scene.makeMouseHelpers(
pointOnPathAfterSketching.x,
pointOnPathAfterSketching.y
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_clickCircle, moveToCircle, dblClickCircle] = scene.makeMouseHelpers(
pointInsideCircle.x,
pointInsideCircle.y
)
const exitSketch = async () => {
await test.step(`Exit sketch mode`, async () => {
await toolbar.exitSketchBtn.click()
await expect(toolbar.exitSketchBtn).not.toBeVisible()
await expect(toolbar.startSketchBtn).toBeEnabled()
})
}
await test.step(`Double-click on the closed sketch`, async () => {
await moveToCircle()
await dblClickCircle()
await expect(toolbar.startSketchBtn).not.toBeVisible()
await expect(toolbar.exitSketchBtn).toBeVisible()
await editor.expectState({
activeLines: [`|>circle({center=[8,5],radius=2},%)`],
highlightedCode: 'circle({center=[8,5],radius=2},%)',
diagnostics: [],
})
})
await exitSketch()
await test.step(`Double-click on the open sketch`, async () => {
await moveToOpenPath()
await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15)
// There is a full execution after exiting sketch that clears the scene.
await app.page.waitForTimeout(500)
await dblClickOpenPath()
await expect(toolbar.startSketchBtn).not.toBeVisible()
await expect(toolbar.exitSketchBtn).toBeVisible()
// Wait for enter sketch mode to complete
await app.page.waitForTimeout(500)
await editor.expectState({
activeLines: [`|>xLine(5,%)`],
highlightedCode: 'xLine(5,%)',
diagnostics: [],
})
})
})
test(`Offset plane point-and-click`, async ({ test(`Offset plane point-and-click`, async ({
app, app,
scene, scene,
@ -601,3 +677,94 @@ test(`Offset plane point-and-click`, async ({
await scene.expectPixelColor([74, 74, 74], testPoint, 15) await scene.expectPixelColor([74, 74, 74], testPoint, 15)
}) })
}) })
const loftPointAndClickCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
]
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({
app,
page,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %)
`
await app.initialise(initialCode)
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(
testPoint.x,
testPoint.y + 80
)
const loftDeclaration = 'loft001 = loft([sketch001, sketch002])'
await test.step(`Look for the white of the sketch001 shape`, async () => {
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
async function selectSketches() {
await clickOnSketch1()
await page.keyboard.down('Shift')
await clickOnSketch2()
await app.page.waitForTimeout(500)
await page.keyboard.up('Shift')
}
if (!shouldPreselect) {
await test.step(`Go through the command bar flow without preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: { Selection: '' },
highlightedHeaderArg: 'selection',
commandName: 'Loft',
})
await selectSketches()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
} else {
await test.step(`Preselect the two sketches`, async () => {
await selectSketches()
})
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
}
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(loftDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [loftDeclaration],
highlightedCode: '',
})
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
})
})
})

View File

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

View File

@ -550,7 +550,7 @@ sketch001 = startSketchAt([-0, -0])
const u = await getUtils(page) const u = await getUtils(page)
// Constants and locators // Constants and locators
const planeColor: [number, number, number] = [170, 220, 170] const planeColor: [number, number, number] = [161, 220, 155]
const bgColor: [number, number, number] = [27, 27, 27] const bgColor: [number, number, number] = [27, 27, 27]
const middlePixelIsColor = async (color: [number, number, number]) => { const middlePixelIsColor = async (color: [number, number, number]) => {
return u.getGreatestPixDiff({ x: 600, y: 250 }, color) return u.getGreatestPixDiff({ x: 600, y: 250 }, color)

View File

@ -7,6 +7,8 @@ try {
.split('\n') .split('\n')
.filter((line) => line && line.length > 1) .filter((line) => line && line.length > 1)
.forEach((line) => { .forEach((line) => {
// Allow line comments.
if (line.trimStart().startsWith('#')) return
const [key, value] = line.split('=') const [key, value] = line.split('=')
// prefer env vars over secrets file // prefer env vars over secrets file
secrets[key] = process.env[key] || (value as any).replaceAll('"', '') secrets[key] = process.env[key] || (value as any).replaceAll('"', '')

View File

@ -943,6 +943,110 @@ sketch002 = startSketchOn(extrude001, 'END')
`.replace(/\s/g, '') `.replace(/\s/g, '')
) )
}) })
/* TODO: once we fix bug turn on.
test('empty-scene default-planes act as expected when spaces in file', async ({
page,
browserName,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await u.openAndClearDebugPanel()
// Fill with spaces
await u.codeLocator.fill(`
`)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
})
test('empty-scene default-planes act as expected when only code comments in file', async ({
page,
browserName,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await u.openAndClearDebugPanel()
// Fill with spaces
await u.codeLocator.fill(`// this is a code comments ya nerds
`)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
})*/
test('empty-scene default-planes act as expected', async ({ test('empty-scene default-planes act as expected', async ({
page, page,
browserName, browserName,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -23,7 +23,7 @@ test.describe('Test toggling perspective', () => {
y: screenHeight * 0.4, y: screenHeight * 0.4,
} }
const backgroundColor: [number, number, number] = [29, 29, 29] const backgroundColor: [number, number, number] = [29, 29, 29]
const xzPlaneColor: [number, number, number] = [50, 50, 99] const xzPlaneColor: [number, number, number] = [82, 55, 96]
const locationToHaveColor = async (color: [number, number, number]) => { const locationToHaveColor = async (color: [number, number, number]) => {
return u.getGreatestPixDiff(checkedScreenLocation, color) return u.getGreatestPixDiff(checkedScreenLocation, color)
} }

View File

@ -158,6 +158,7 @@
"@electron/rebuild": "^3.6.0", "@electron/rebuild": "^3.6.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1", "@lezer/generator": "^1.7.1",
"@nabla/vite-plugin-eslint": "^2.0.5",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.46.1",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2", "@testing-library/react": "^15.0.2",
@ -170,7 +171,7 @@
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4", "@types/react": "^18.3.4",
"@types/react-dom": "^18.2.25", "@types/react-dom": "^18.3.1",
"@types/react-modal": "^3.16.3", "@types/react-modal": "^3.16.3",
"@types/three": "^0.163.0", "@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
@ -192,7 +193,7 @@
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.30.0",
"eslint-plugin-suggest-no-throw": "^1.0.0", "eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^15.10.2", "happy-dom": "^15.11.7",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"husky": "^9.1.5", "husky": "^9.1.5",
"kill-port": "^2.0.1", "kill-port": "^2.0.1",
@ -207,12 +208,11 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^5.4.6", "vite": "^5.4.6",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"vitest-webgl-canvas-mock": "^1.1.0", "vitest-webgl-canvas-mock": "^1.1.0",
"wasm-pack": "^0.13.0", "wasm-pack": "^0.13.1",
"ws": "^8.17.0", "ws": "^8.17.0",
"yarn": "^1.22.22" "yarn": "^1.22.22"
} }

View File

@ -0,0 +1,5 @@
#!/bin/bash
echo "## What's Changed"
git log ${PREVIOUS_TAG}..HEAD --oneline --pretty=format:%s | grep -v Bump | awk '{print "* "toupper(substr($0,0,1))substr($0,2)}'
echo ""
echo "**Full Changelog**: https://github.com/KittyCAD/modeling-app/compare/${PREVIOUS_TAG}...${TAG}"

View File

@ -155,7 +155,6 @@ export class CameraControls {
this.camera.zoom = camProps.zoom || 1 this.camera.zoom = camProps.zoom || 1
} }
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
console.log('doing this thing', camProps)
this.update(true) this.update(true)
} }
@ -273,14 +272,26 @@ export class CameraControls {
camSettings.center.y, camSettings.center.y,
camSettings.center.z camSettings.center.z
) )
const quat = new Quaternion( const orientation = new Quaternion(
camSettings.orientation.x, camSettings.orientation.x,
camSettings.orientation.y, camSettings.orientation.y,
camSettings.orientation.z, camSettings.orientation.z,
camSettings.orientation.w camSettings.orientation.w
).invert() ).invert()
this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat)) const newUp = new Vector3(
camSettings.up.x,
camSettings.up.y,
camSettings.up.z
)
this.camera.quaternion.set(
orientation.x,
orientation.y,
orientation.z,
orientation.w
)
this.camera.up.copy(newUp)
this.camera.updateProjectionMatrix()
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) { if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
this.useOrthographicCamera() this.useOrthographicCamera()
} }
@ -1164,7 +1175,7 @@ export class CameraControls {
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
} }
if (this.syncDirection === 'clientToEngine' || forceUpdate) if (this.syncDirection === 'clientToEngine' || forceUpdate) {
this.throttledUpdateEngineCamera({ this.throttledUpdateEngineCamera({
quaternion: this.camera.quaternion, quaternion: this.camera.quaternion,
position: this.camera.position, position: this.camera.position,
@ -1172,6 +1183,7 @@ export class CameraControls {
isPerspective: this.isPerspective, isPerspective: this.isPerspective,
target: this.target, target: this.target,
}) })
}
this.deferReactUpdate(this.reactCameraProperties) this.deferReactUpdate(this.reactCameraProperties)
Object.values(this._camChangeCallbacks).forEach((cb) => cb()) Object.values(this._camChangeCallbacks).forEach((cb) => cb())
} }

View File

@ -29,6 +29,9 @@ import {
Expr, Expr,
parse, parse,
recast, recast,
defaultSourceRange,
resultIsOk,
ProgramMemory,
} from 'lang/wasm' } from 'lang/wasm'
import { CustomIcon, CustomIconName } from 'components/CustomIcon' import { CustomIcon, CustomIconName } from 'components/CustomIcon'
import { ConstrainInfo } from 'lang/std/stdTypes' import { ConstrainInfo } from 'lang/std/stdTypes'
@ -412,14 +415,15 @@ export async function deleteSegment({
if (err(modifiedAst)) return Promise.reject(modifiedAst) if (err(modifiedAst)) return Promise.reject(modifiedAst)
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
modifiedAst = parse(newCode) const pResult = parse(newCode)
if (err(modifiedAst)) return Promise.reject(modifiedAst) if (err(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
modifiedAst = pResult.program
const testExecute = await executeAst({ const testExecute = await executeAst({
ast: modifiedAst, ast: modifiedAst,
idGenerator: kclManager.execState.idGenerator,
useFakeExecutor: true,
engineCommandManager: engineCommandManager, engineCommandManager: engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride: ProgramMemory.empty(),
}) })
if (testExecute.errors.length) { if (testExecute.errors.length) {
toast.error('Segment tag used outside of current Sketch. Could not delete.') toast.error('Segment tag used outside of current Sketch. Could not delete.')
@ -590,7 +594,9 @@ const ConstraintSymbol = ({
if (err(_node)) return if (err(_node)) return
const node = _node.node const node = _node.node
const range: SourceRange = node ? [node.start, node.end] : [0, 0] const range: SourceRange = node
? [node.start, node.end, true]
: defaultSourceRange()
if (_type === 'intersectionTag') return null if (_type === 'intersectionTag') return null
@ -612,7 +618,7 @@ const ConstraintSymbol = ({
editorManager.setHighlightRange([range]) editorManager.setHighlightRange([range])
}} }}
onMouseLeave={() => { onMouseLeave={() => {
editorManager.setHighlightRange([[0, 0]]) editorManager.setHighlightRange([defaultSourceRange()])
}} }}
// disabled={isConstrained || !convertToVarEnabled} // disabled={isConstrained || !convertToVarEnabled}
// 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?
@ -627,10 +633,12 @@ const ConstraintSymbol = ({
}) })
} else if (isConstrained) { } else if (isConstrained) {
try { try {
const parsed = parse(recast(kclManager.ast)) const pResult = parse(recast(kclManager.ast))
if (trap(parsed)) return Promise.reject(parsed) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
const _node1 = getNodeFromPath<CallExpression>( const _node1 = getNodeFromPath<CallExpression>(
parsed, pResult.program!,
pathToNode, pathToNode,
'CallExpression', 'CallExpression',
true true

View File

@ -48,6 +48,9 @@ import {
VariableDeclarator, VariableDeclarator,
sketchFromKclValue, sketchFromKclValue,
sketchFromKclValueOptional, sketchFromKclValueOptional,
defaultSourceRange,
sourceRangeFromRust,
resultIsOk,
} from 'lang/wasm' } from 'lang/wasm'
import { import {
engineCommandManager, engineCommandManager,
@ -495,10 +498,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
const sketch = sketchFromPathToNode({ const sketch = sketchFromPathToNode({
@ -530,7 +532,7 @@ export class SceneEntities {
const segPathToNode = getNodePathFromSourceRange( const segPathToNode = getNodePathFromSourceRange(
maybeModdedAst, maybeModdedAst,
sketch.start.__geoMeta.sourceRange sourceRangeFromRust(sketch.start.__geoMeta.sourceRange)
) )
if (sketch?.paths?.[0]?.type !== 'Circle') { if (sketch?.paths?.[0]?.type !== 'Circle') {
const _profileStart = createProfileStartHandle({ const _profileStart = createProfileStartHandle({
@ -552,7 +554,7 @@ export class SceneEntities {
sketch.paths.forEach((segment, index) => { sketch.paths.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange( let segPathToNode = getNodePathFromSourceRange(
maybeModdedAst, maybeModdedAst,
segment.__geoMeta.sourceRange sourceRangeFromRust(segment.__geoMeta.sourceRange)
) )
if ( if (
draftExpressionsIndices && draftExpressionsIndices &&
@ -561,12 +563,12 @@ export class SceneEntities {
const previousSegment = sketch.paths[index - 1] || sketch.start const previousSegment = sketch.paths[index - 1] || sketch.start
const previousSegmentPathToNode = getNodePathFromSourceRange( const previousSegmentPathToNode = getNodePathFromSourceRange(
maybeModdedAst, maybeModdedAst,
previousSegment.__geoMeta.sourceRange sourceRangeFromRust(previousSegment.__geoMeta.sourceRange)
) )
const bodyIndex = previousSegmentPathToNode[1][0] const bodyIndex = previousSegmentPathToNode[1][0]
segPathToNode = getNodePathFromSourceRange( segPathToNode = getNodePathFromSourceRange(
truncatedAst, truncatedAst,
segment.__geoMeta.sourceRange sourceRangeFromRust(segment.__geoMeta.sourceRange)
) )
segPathToNode[1][0] = bodyIndex segPathToNode[1][0] = bodyIndex
} }
@ -575,7 +577,10 @@ export class SceneEntities {
index <= draftExpressionsIndices.end && index <= draftExpressionsIndices.end &&
index >= draftExpressionsIndices.start index >= draftExpressionsIndices.start
const isSelected = selectionRanges?.graphSelections.some((selection) => const isSelected = selectionRanges?.graphSelections.some((selection) =>
isOverlap(selection?.codeRef?.range, segment.__geoMeta.sourceRange) isOverlap(
selection?.codeRef?.range,
sourceRangeFromRust(segment.__geoMeta.sourceRange)
)
) )
let seg: Group let seg: Group
@ -657,13 +662,11 @@ export class SceneEntities {
} }
updateAstAndRejigSketch = async ( updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
modifiedAst: Node<Program> | Error, modifiedAst: Node<Program>,
forward: [number, number, number], forward: [number, number, number],
up: [number, number, number], up: [number, number, number],
origin: [number, number, number] origin: [number, number, number]
) => { ) => {
if (err(modifiedAst)) return modifiedAst
const nextAst = await kclManager.updateAst(modifiedAst, false) const nextAst = await kclManager.updateAst(modifiedAst, false)
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners() sceneInfra.resetMouseListeners()
@ -698,8 +701,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = const variableDeclarationName = _node1.node?.declaration.id?.name || ''
_node1.node?.declarations?.[0]?.id?.name || ''
const sg = sketchFromKclValue( const sg = sketchFromKclValue(
kclManager.programMemory.get(variableDeclarationName), kclManager.programMemory.get(variableDeclarationName),
@ -721,8 +723,9 @@ export class SceneEntities {
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
}) })
if (trap(mod)) return Promise.reject(mod) if (trap(mod)) return Promise.reject(mod)
const modifiedAst = parse(recast(mod.modifiedAst)) const pResult = parse(recast(mod.modifiedAst))
if (trap(modifiedAst)) return Promise.reject(modifiedAst) if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
const modifiedAst = pResult.program
const draftExpressionsIndices = { start: index, end: index } const draftExpressionsIndices = { start: index, end: index }
@ -898,10 +901,9 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = const variableDeclarationName = _node1.node?.declaration.id?.name || ''
_node1.node?.declarations?.[0]?.id?.name || '' const startSketchOn = _node1.node?.declaration
const startSketchOn = _node1.node?.declarations const startSketchOnInit = startSketchOn?.init
const startSketchOnInit = startSketchOn?.[0]?.init
const tags: [string, string, string] = [ const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'), findUniqueName(_ast, 'rectangleSegmentA'),
@ -909,14 +911,14 @@ export class SceneEntities {
findUniqueName(_ast, 'rectangleSegmentC'), findUniqueName(_ast, 'rectangleSegmentC'),
] ]
startSketchOn[0].init = createPipeExpression([ startSketchOn.init = createPipeExpression([
startSketchOnInit, startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags), ...getRectangleCallExpressions(rectangleOrigin, tags),
]) ])
let _recastAst = parse(recast(_ast)) const pResult = parse(recast(_ast))
if (trap(_recastAst)) return Promise.reject(_recastAst) if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
_ast = _recastAst _ast = pResult.program
const { programMemoryOverride, truncatedAst } = await this.setupSketch({ const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode, sketchPathToNode,
@ -939,7 +941,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return Promise.reject(_node) if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0] const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1] const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
@ -950,10 +952,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
@ -989,7 +990,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
if (sketchInit.type !== 'PipeExpression') { if (sketchInit.type !== 'PipeExpression') {
return return
@ -998,9 +999,10 @@ export class SceneEntities {
updateRectangleSketch(sketchInit, x, y, tags[0]) updateRectangleSketch(sketchInit, x, y, tags[0])
const newCode = recast(_ast) const newCode = recast(_ast)
let _recastAst = parse(newCode) const pResult = parse(newCode)
if (trap(_recastAst)) return if (trap(pResult) || !resultIsOk(pResult))
_ast = _recastAst return Promise.reject(pResult)
_ast = pResult.program
// Update the primary AST and unequip the rectangle tool // Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
@ -1013,10 +1015,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: _ast, ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
@ -1055,10 +1056,9 @@ export class SceneEntities {
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
// startSketchOn already exists // startSketchOn already exists
const variableDeclarationName = const variableDeclarationName = _node1.node?.declaration.id?.name || ''
_node1.node?.declarations?.[0]?.id?.name || '' const startSketchOn = _node1.node?.declaration
const startSketchOn = _node1.node?.declarations const startSketchOnInit = startSketchOn?.init
const startSketchOnInit = startSketchOn?.[0]?.init
const tags: [string, string, string] = [ const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'), findUniqueName(_ast, 'rectangleSegmentA'),
@ -1066,14 +1066,14 @@ export class SceneEntities {
findUniqueName(_ast, 'rectangleSegmentC'), findUniqueName(_ast, 'rectangleSegmentC'),
] ]
startSketchOn[0].init = createPipeExpression([ startSketchOn.init = createPipeExpression([
startSketchOnInit, startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags), ...getRectangleCallExpressions(rectangleOrigin, tags),
]) ])
let _recastAst = parse(recast(_ast)) const pResult = parse(recast(_ast))
if (trap(_recastAst)) return Promise.reject(_recastAst) if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
_ast = _recastAst _ast = pResult.program
const { programMemoryOverride, truncatedAst } = await this.setupSketch({ const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode, sketchPathToNode,
@ -1096,7 +1096,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return Promise.reject(_node) if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0] const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1] const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
@ -1114,10 +1114,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
@ -1153,7 +1152,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
if (sketchInit.type === 'PipeExpression') { if (sketchInit.type === 'PipeExpression') {
updateCenterRectangleSketch( updateCenterRectangleSketch(
@ -1165,9 +1164,10 @@ export class SceneEntities {
rectangleOrigin[1] rectangleOrigin[1]
) )
let _recastAst = parse(recast(_ast)) const pResult = parse(recast(_ast))
if (trap(_recastAst)) return if (trap(pResult) || !resultIsOk(pResult))
_ast = _recastAst return Promise.reject(pResult)
_ast = pResult.program
// Update the primary AST and unequip the rectangle tool // Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
@ -1180,10 +1180,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: _ast, ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
@ -1222,12 +1221,11 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = const variableDeclarationName = _node1.node?.declaration.id?.name || ''
_node1.node?.declarations?.[0]?.id?.name || '' const startSketchOn = _node1.node?.declaration
const startSketchOn = _node1.node?.declarations const startSketchOnInit = startSketchOn?.init
const startSketchOnInit = startSketchOn?.[0]?.init
startSketchOn[0].init = createPipeExpression([ startSketchOn.init = createPipeExpression([
startSketchOnInit, startSketchOnInit,
createCallExpressionStdLib('circle', [ createCallExpressionStdLib('circle', [
createObjectExpression({ createObjectExpression({
@ -1241,9 +1239,9 @@ export class SceneEntities {
]), ]),
]) ])
let _recastAst = parse(recast(_ast)) const pResult = parse(recast(_ast))
if (trap(_recastAst)) return Promise.reject(_recastAst) if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
_ast = _recastAst _ast = pResult.program
// do a quick mock execution to get the program memory up-to-date // do a quick mock execution to get the program memory up-to-date
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
@ -1269,7 +1267,7 @@ export class SceneEntities {
) )
let modded = structuredClone(truncatedAst) let modded = structuredClone(truncatedAst)
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node.declaration.init
const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0] const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0]
const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1] const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1]
@ -1299,10 +1297,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: modded, ast: modded,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
@ -1338,7 +1335,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declaration.init
let modded = structuredClone(_ast) let modded = structuredClone(_ast)
if (sketchInit.type === 'PipeExpression') { if (sketchInit.type === 'PipeExpression') {
@ -1365,9 +1362,10 @@ export class SceneEntities {
const newCode = recast(modded) const newCode = recast(modded)
if (err(newCode)) return if (err(newCode)) return
let _recastAst = parse(newCode) const pResult = parse(newCode)
if (trap(_recastAst)) return Promise.reject(_recastAst) if (trap(pResult) || !resultIsOk(pResult))
_ast = _recastAst return Promise.reject(pResult)
_ast = pResult.program
// Update the primary AST and unequip the rectangle tool // Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
@ -1660,7 +1658,7 @@ export class SceneEntities {
kclManager.programMemory, kclManager.programMemory,
{ {
type: 'sourceRange', type: 'sourceRange',
sourceRange: [node.start, node.end], sourceRange: [node.start, node.end, true],
}, },
getChangeSketchInput() getChangeSketchInput()
) )
@ -1683,10 +1681,9 @@ export class SceneEntities {
codeManager.updateCodeEditor(code) codeManager.updateCodeEditor(code)
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
@ -1750,7 +1747,7 @@ export class SceneEntities {
): (() => SegmentOverlayPayload | null) => { ): (() => SegmentOverlayPayload | null) => {
const segPathToNode = getNodePathFromSourceRange( const segPathToNode = getNodePathFromSourceRange(
modifiedAst, modifiedAst,
segment.__geoMeta.sourceRange sourceRangeFromRust(segment.__geoMeta.sourceRange)
) )
const sgPaths = sketch.paths const sgPaths = sketch.paths
const originalPathToNodeStr = JSON.stringify(segPathToNode) const originalPathToNodeStr = JSON.stringify(segPathToNode)
@ -1901,8 +1898,10 @@ export class SceneEntities {
SEGMENT_BODIES_PLUS_PROFILE_START SEGMENT_BODIES_PLUS_PROFILE_START
) )
if (parent?.userData?.pathToNode) { if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast)) const pResult = parse(recast(kclManager.ast))
if (trap(updatedAst)) return if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
const updatedAst = pResult.program
const _node = getNodeFromPath<Node<CallExpression>>( const _node = getNodeFromPath<Node<CallExpression>>(
updatedAst, updatedAst,
parent.userData.pathToNode, parent.userData.pathToNode,
@ -1910,7 +1909,7 @@ export class SceneEntities {
) )
if (trap(_node, { suppress: true })) return if (trap(_node, { suppress: true })) return
const node = _node.node const node = _node.node
editorManager.setHighlightRange([[node.start, node.end]]) editorManager.setHighlightRange([[node.start, node.end, true]])
const yellow = 0xffff00 const yellow = 0xffff00
colorSegment(selected, yellow) colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE) const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -1955,10 +1954,10 @@ export class SceneEntities {
}) })
return return
} }
editorManager.setHighlightRange([[0, 0]]) editorManager.setHighlightRange([defaultSourceRange()])
}, },
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => { onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
editorManager.setHighlightRange([[0, 0]]) editorManager.setHighlightRange([defaultSourceRange()])
const parent = getParentGroup( const parent = getParentGroup(
selected, selected,
SEGMENT_BODIES_PLUS_PROFILE_START SEGMENT_BODIES_PLUS_PROFILE_START
@ -2057,7 +2056,7 @@ function prepareTruncatedMemoryAndAst(
'VariableDeclaration' 'VariableDeclaration'
) )
if (err(_node)) return _node if (err(_node)) return _node
const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || '' const variableDeclarationName = _node.node?.declaration.id?.name || ''
const sg = sketchFromKclValue( const sg = sketchFromKclValue(
programMemory.get(variableDeclarationName), programMemory.get(variableDeclarationName),
variableDeclarationName variableDeclarationName
@ -2082,28 +2081,30 @@ function prepareTruncatedMemoryAndAst(
]) ])
} }
;( ;(
(_ast.body[bodyIndex] as VariableDeclaration).declarations[0] (_ast.body[bodyIndex] as VariableDeclaration).declaration
.init as PipeExpression .init as PipeExpression
).body.push(newSegment) ).body.push(newSegment)
// update source ranges to section we just added. // update source ranges to section we just added.
// hacks like this wouldn't be needed if the AST put pathToNode info in memory/sketch segments // hacks like this wouldn't be needed if the AST put pathToNode info in memory/sketch segments
const updatedSrcRangeAst = parse(recast(_ast)) // get source ranges correct since unfortunately we still rely on them const pResult = parse(recast(_ast)) // get source ranges correct since unfortunately we still rely on them
if (err(updatedSrcRangeAst)) return updatedSrcRangeAst if (trap(pResult) || !resultIsOk(pResult))
return Error('Unexpected compilation error')
const updatedSrcRangeAst = pResult.program
const lastPipeItem = ( const lastPipeItem = (
(updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration) (updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration).declaration
.declarations[0].init as PipeExpression .init as PipeExpression
).body.slice(-1)[0] ).body.slice(-1)[0]
;( ;(
(_ast.body[bodyIndex] as VariableDeclaration).declarations[0] (_ast.body[bodyIndex] as VariableDeclaration).declaration
.init as PipeExpression .init as PipeExpression
).body.slice(-1)[0].start = lastPipeItem.start ).body.slice(-1)[0].start = lastPipeItem.start
_ast.end = lastPipeItem.end _ast.end = lastPipeItem.end
const varDec = _ast.body[bodyIndex] as Node<VariableDeclaration> const varDec = _ast.body[bodyIndex] as Node<VariableDeclaration>
varDec.end = lastPipeItem.end varDec.end = lastPipeItem.end
const declarator = varDec.declarations[0] const declarator = varDec.declaration
declarator.end = lastPipeItem.end declarator.end = lastPipeItem.end
const init = declarator.init as Node<PipeExpression> const init = declarator.init as Node<PipeExpression>
init.end = lastPipeItem.end init.end = lastPipeItem.end
@ -2140,7 +2141,7 @@ function prepareTruncatedMemoryAndAst(
if (node.type !== 'VariableDeclaration') { if (node.type !== 'VariableDeclaration') {
continue continue
} }
const name = node.declarations[0].id.name const name = node.declaration.id.name
const memoryItem = programMemory.get(name) const memoryItem = programMemory.get(name)
if (!memoryItem) { if (!memoryItem) {
continue continue

View File

@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react'
import { trap } from 'lib/trap' import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections' import { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph' import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange } from 'lang/wasm'
export function AstExplorer() { export function AstExplorer() {
const { context } = useModelingContext() const { context } = useModelingContext()
@ -46,7 +47,7 @@ export function AstExplorer() {
<div <div
className="h-full relative" className="h-full relative"
onMouseLeave={(e) => { onMouseLeave={(e) => {
editorManager.setHighlightRange([[0, 0]]) editorManager.setHighlightRange([defaultSourceRange()])
}} }}
> >
<pre className="text-xs"> <pre className="text-xs">
@ -115,15 +116,19 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : '' hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`} }`}
onMouseEnter={(e) => { onMouseEnter={(e) => {
editorManager.setHighlightRange([[obj?.start || 0, obj.end]]) editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
e.stopPropagation() e.stopPropagation()
}} }}
onMouseMove={(e) => { onMouseMove={(e) => {
e.stopPropagation() e.stopPropagation()
editorManager.setHighlightRange([[obj?.start || 0, obj.end]]) editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
}} }}
onClick={(e) => { onClick={(e) => {
const range: [number, number] = [obj?.start || 0, obj.end || 0] const range: [number, number, boolean] = [
obj?.start || 0,
obj.end || 0,
true,
]
const idInfo = codeToIdSelections([ const idInfo = codeToIdSelections([
{ codeRef: codeRefFromRange(range, kclManager.ast) }, { codeRef: codeRefFromRange(range, kclManager.ast) },
])[0] ])[0]

View File

@ -1,5 +1,11 @@
import { useEffect, useState, useRef } from 'react' import { useEffect, useState, useRef } from 'react'
import { parse, BinaryPart, Expr, ProgramMemory } from '../lang/wasm' import {
parse,
BinaryPart,
Expr,
ProgramMemory,
resultIsOk,
} from '../lang/wasm'
import { import {
createIdentifier, createIdentifier,
createLiteral, createLiteral,
@ -141,8 +147,9 @@ export function useCalc({
useEffect(() => { useEffect(() => {
try { try {
const code = `const __result__ = ${value}` const code = `const __result__ = ${value}`
const ast = parse(code) const pResult = parse(code)
if (trap(ast)) return if (trap(pResult) || !resultIsOk(pResult)) return
const ast = pResult.program
const _programMem: ProgramMemory = ProgramMemory.empty() const _programMem: ProgramMemory = ProgramMemory.empty()
for (const { key, value } of availableVarInfo.variables) { for (const { key, value } of availableVarInfo.variables) {
const error = _programMem.set(key, { const error = _programMem.set(key, {
@ -156,18 +163,17 @@ export function useCalc({
executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, // We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride: kclManager.programMemory.clone(), programMemoryOverride: kclManager.programMemory.clone(),
idGenerator: kclManager.execState.idGenerator,
}).then(({ execState }) => { }).then(({ execState }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__' a.declaration.id?.name === '__result__'
) )
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declaration.init
const result = execState.memory?.get('__result__')?.value const result = execState.memory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)

View File

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

View File

@ -50,6 +50,7 @@ import {
isSketchPipe, isSketchPipe,
Selections, Selections,
updateSelections, updateSelections,
canLoftSelection,
} 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'
@ -66,7 +67,7 @@ import {
sketchOnOffsetPlane, sketchOnOffsetPlane,
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { Program, parse, recast } from 'lang/wasm' import { Program, parse, recast, resultIsOk } from 'lang/wasm'
import { import {
doesSceneHaveSweepableSketch, doesSceneHaveSweepableSketch,
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -82,7 +83,7 @@ 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'
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet' import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment'
import { import {
ExportIntent, ExportIntent,
EngineConnectionStateType, EngineConnectionStateType,
@ -569,6 +570,21 @@ export const ModelingMachineProvider = ({
if (err(canSweep)) return false if (err(canSweep)) return false
return canSweep return canSweep
}, },
'has valid loft selection': ({ context: { selectionRanges } }) => {
const hasNoSelection =
selectionRanges.graphSelections.length === 0 ||
isRangeBetweenCharacters(selectionRanges) ||
isSelectionLastLine(selectionRanges, codeManager.code)
if (hasNoSelection) {
const count = 2
return doesSceneHaveSweepableSketch(kclManager.ast, count)
}
const canLoft = canLoftSelection(selectionRanges)
if (err(canLoft)) return false
return canLoft
},
'has valid selection for deletion': ({ 'has valid selection for deletion': ({
context: { selectionRanges }, context: { selectionRanges },
}) => { }) => {
@ -576,8 +592,10 @@ export const ModelingMachineProvider = ({
if (selectionRanges.graphSelections.length <= 0) return false if (selectionRanges.graphSelections.length <= 0) return false
return true return true
}, },
'has valid fillet selection': ({ context: { selectionRanges } }) => { 'has valid edge treatment selection': ({
return hasValidFilletSelection({ context: { selectionRanges },
}) => {
return hasValidEdgeTreatmentSelection({
selectionRanges, selectionRanges,
ast: kclManager.ast, ast: kclManager.ast,
code: codeManager.code, code: codeManager.code,
@ -594,15 +612,11 @@ export const ModelingMachineProvider = ({
) )
}, },
'Has exportable geometry': () => { 'Has exportable geometry': () => {
if ( if (!kclManager.hasErrors() && kclManager.ast.body.length > 0)
kclManager.kclErrors.length === 0 &&
kclManager.ast.body.length > 0
)
return true return true
else { else {
let errorMessage = 'Unable to Export ' let errorMessage = 'Unable to Export '
if (kclManager.kclErrors.length > 0) if (kclManager.hasErrors()) errorMessage += 'due to KCL Errors'
errorMessage += 'due to KCL Errors'
else if (kclManager.ast.body.length === 0) else if (kclManager.ast.body.length === 0)
errorMessage += 'due to Empty Scene' errorMessage += 'due to Empty Scene'
console.error(errorMessage) console.error(errorMessage)
@ -720,7 +734,11 @@ export const ModelingMachineProvider = ({
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
selectionRanges, selectionRanges,
}) })
const _modifiedAst = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap( const updatedPathToNode = updatePathToNodeFromMap(
@ -761,7 +779,10 @@ export const ModelingMachineProvider = ({
constraint: 'setVertDistance', constraint: 'setVertDistance',
selectionRanges, selectionRanges,
}) })
const _modifiedAst = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap( const updatedPathToNode = updatePathToNodeFromMap(
@ -809,7 +830,10 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
angleOrLength: 'setAngle', angleOrLength: 'setAngle',
})) }))
const _modifiedAst = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (err(_modifiedAst)) return Promise.reject(_modifiedAst) if (err(_modifiedAst)) return Promise.reject(_modifiedAst)
if (!sketchDetails) if (!sketchDetails)
@ -851,7 +875,10 @@ export const ModelingMachineProvider = ({
await applyConstraintAngleLength({ await applyConstraintAngleLength({
selectionRanges, selectionRanges,
}) })
const _modifiedAst = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap( const updatedPathToNode = updatePathToNodeFromMap(
@ -891,7 +918,10 @@ export const ModelingMachineProvider = ({
await applyConstraintIntersect({ await applyConstraintIntersect({
selectionRanges, selectionRanges,
}) })
const _modifiedAst = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap( const updatedPathToNode = updatePathToNodeFromMap(
@ -932,7 +962,10 @@ export const ModelingMachineProvider = ({
constraint: 'xAbs', constraint: 'xAbs',
selectionRanges, selectionRanges,
}) })
const _modifiedAst = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap( const updatedPathToNode = updatePathToNodeFromMap(
@ -973,7 +1006,10 @@ export const ModelingMachineProvider = ({
constraint: 'yAbs', constraint: 'yAbs',
selectionRanges, selectionRanges,
}) })
const _modifiedAst = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap( const updatedPathToNode = updatePathToNodeFromMap(
@ -1014,9 +1050,10 @@ export const ModelingMachineProvider = ({
const { variableName } = await getVarNameModal({ const { variableName } = await getVarNameModal({
valueName: data?.variableName || 'var', valueName: data?.variableName || 'var',
}) })
let parsed = parse(recast(kclManager.ast)) let pResult = parse(recast(kclManager.ast))
if (trap(parsed)) return Promise.reject(parsed) if (trap(pResult) || !resultIsOk(pResult))
parsed = parsed as Node<Program> return Promise.reject(new Error('Unexpected compilation error'))
let parsed = pResult.program
const { modifiedAst: _modifiedAst, pathToReplacedNode } = const { modifiedAst: _modifiedAst, pathToReplacedNode } =
moveValueIntoNewVariablePath( moveValueIntoNewVariablePath(
@ -1025,7 +1062,11 @@ export const ModelingMachineProvider = ({
data?.pathToNode || [], data?.pathToNode || [],
variableName variableName
) )
parsed = parse(recast(_modifiedAst)) pResult = parse(recast(_modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
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 (!pathToReplacedNode)

View File

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

View File

@ -1,6 +1,6 @@
import { processMemory } from './MemoryPane' import { processMemory } from './MemoryPane'
import { enginelessExecutor } from '../../../lib/testHelpers' import { enginelessExecutor } from '../../../lib/testHelpers'
import { initPromise, parse, ProgramMemory } from '../../../lang/wasm' import { assertParse, initPromise, ProgramMemory } from '../../../lang/wasm'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
@ -28,12 +28,16 @@ describe('processMemory', () => {
|> lineTo([0.98, 5.16], %) |> lineTo([0.98, 5.16], %)
|> lineTo([2.15, 4.32], %) |> lineTo([2.15, 4.32], %)
// |> rx(90, %)` // |> rx(90, %)`
const ast = parse(code) const ast = assertParse(code)
const execState = await enginelessExecutor(ast, ProgramMemory.empty()) const execState = await enginelessExecutor(ast, ProgramMemory.empty())
const output = processMemory(execState.memory) const output = processMemory(execState.memory)
expect(output.myVar).toEqual(5) expect(output.myVar).toEqual(5)
expect(output.otherVar).toEqual(3) expect(output.otherVar).toEqual(3)
expect(output).toEqual({ expect(output).toEqual({
HALF_TURN: 180,
QUARTER_TURN: 90,
THREE_QUARTER_TURN: 270,
ZERO: 0,
myVar: 5, myVar: 5,
myFn: '__function(a)__', myFn: '__function(a)__',
otherVar: 3, otherVar: 3,

View File

@ -90,7 +90,7 @@ export const sidebarPanes: SidebarPane[] = [
keybinding: 'Shift + C', keybinding: 'Shift + C',
showBadge: { showBadge: {
value: ({ kclContext }) => { value: ({ kclContext }) => {
return kclContext.errors.length return kclContext.diagnostics.length
}, },
onClick: (e) => { onClick: (e) => {
e.preventDefault() e.preventDefault()

View File

@ -53,7 +53,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
settings: settings.context, settings: settings.context,
platform: getPlatformString(), platform: getPlatformString(),
}), }),
[kclContext.errors, settings.context] [kclContext.diagnostics, settings.context]
) )
const sidebarActions: SidebarAction[] = [ const sidebarActions: SidebarAction[] = [

View File

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

View File

@ -18,6 +18,8 @@ import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { err, reportRejection } from 'lib/trap'
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
enum StreamState { enum StreamState {
Playing = 'playing', Playing = 'playing',
@ -280,12 +282,49 @@ export const Stream = () => {
} }
} }
/**
* On double-click of sketch entities we automatically enter sketch mode with the selected sketch,
* allowing for quick editing of sketches. TODO: This should be moved to a more central place.
*/
const enterSketchModeIfSelectingSketch: MouseEventHandler<HTMLDivElement> = (
e
) => {
if (
!isNetworkOkay ||
!videoRef.current ||
state.matches('Sketch') ||
state.matches({ idle: 'showPlanes' }) ||
sceneInfra.camControls.wasDragging === true ||
!btnName(e.nativeEvent).left
) {
return
}
sendSelectEventToEngine(e, videoRef.current)
.then(({ entity_id }) => {
if (!entity_id) {
// No entity selected. This is benign
return
}
const path = getArtifactOfTypes(
{ key: entity_id, types: ['path', 'solid2D', 'segment'] },
engineCommandManager.artifactGraph
)
if (err(path)) {
return path
}
sceneInfra.modelingSend({ type: 'Enter sketch' })
})
.catch(reportRejection)
}
return ( return (
<div <div
className="absolute inset-0 z-0" className="absolute inset-0 z-0"
id="stream" id="stream"
data-testid="stream" data-testid="stream"
onClick={handleMouseUp} onClick={handleMouseUp}
onDoubleClick={enterSketchModeIfSelectingSketch}
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()} onContextMenuCapture={(e) => e.preventDefault()}
> >

View File

@ -40,7 +40,10 @@ export function removeConstrainingValuesInfo({
otherSelections: [], otherSelections: [],
graphSelections: nodes.map( graphSelections: nodes.map(
(node): Selection => ({ (node): Selection => ({
codeRef: codeRefFromRange([node.start, node.end], kclManager.ast), codeRef: codeRefFromRange(
[node.start, node.end, true],
kclManager.ast
),
}) })
), ),
} }

View File

@ -139,7 +139,9 @@ export default class EditorManager {
} }
setHighlightRange(range: Array<Selection['codeRef']['range']>): void { setHighlightRange(range: Array<Selection['codeRef']['range']>): void {
this._highlightRange = range this._highlightRange = range.map((s): [number, number] => {
return [s[0], s[1]]
})
const selectionsWithSafeEnds = range.map((s): [number, number] => { const selectionsWithSafeEnds = range.map((s): [number, number] => {
const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1]) const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1])

View File

@ -18,7 +18,7 @@ import {
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities' import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { CallExpression } from 'lang/wasm' import { CallExpression, defaultSourceRange } from 'lang/wasm'
import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine' import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine'
export function useEngineConnectionSubscriptions() { export function useEngineConnectionSubscriptions() {
@ -46,7 +46,7 @@ export function useEngineConnectionSubscriptions() {
(editorManager.highlightRange[0][0] !== 0 && (editorManager.highlightRange[0][0] !== 0 &&
editorManager.highlightRange[0][1] !== 0) editorManager.highlightRange[0][1] !== 0)
) { ) {
editorManager.setHighlightRange([[0, 0]]) editorManager.setHighlightRange([defaultSourceRange()])
} }
}, },
}) })
@ -201,7 +201,7 @@ export function useEngineConnectionSubscriptions() {
const { z_axis, y_axis, origin } = faceInfo const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange( const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast, kclManager.ast,
err(codeRef) ? [0, 0] : codeRef.range err(codeRef) ? defaultSourceRange() : codeRef.range
) )
const getEdgeCutMeta = (): null | EdgeCutInfo => { const getEdgeCutMeta = (): null | EdgeCutInfo => {

View File

@ -1,15 +1,15 @@
import { KCLError } from './errors'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom' import { useLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
const KclContext = createContext({ const KclContext = createContext({
code: codeManager?.code || '', code: codeManager?.code || '',
programMemory: kclManager?.programMemory, programMemory: kclManager?.programMemory,
ast: kclManager?.ast, ast: kclManager?.ast,
isExecuting: kclManager?.isExecuting, isExecuting: kclManager?.isExecuting,
errors: kclManager?.kclErrors, diagnostics: kclManager?.diagnostics,
logs: kclManager?.logs, logs: kclManager?.logs,
wasmInitFailed: kclManager?.wasmInitFailed, wasmInitFailed: kclManager?.wasmInitFailed,
}) })
@ -32,7 +32,7 @@ export function KclContextProvider({
const [programMemory, setProgramMemory] = useState(kclManager.programMemory) const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
const [ast, setAst] = useState(kclManager.ast) const [ast, setAst] = useState(kclManager.ast)
const [isExecuting, setIsExecuting] = useState(false) const [isExecuting, setIsExecuting] = useState(false)
const [errors, setErrors] = useState<KCLError[]>([]) const [diagnostics, setErrors] = useState<Diagnostic[]>([])
const [logs, setLogs] = useState<string[]>([]) const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false) const [wasmInitFailed, setWasmInitFailed] = useState(false)
@ -57,7 +57,7 @@ export function KclContextProvider({
programMemory, programMemory,
ast, ast,
isExecuting, isExecuting,
errors, diagnostics,
logs, logs,
wasmInitFailed, wasmInitFailed,
}} }}

View File

@ -1,6 +1,10 @@
import { executeAst, lintAst } from 'lang/langHelpers' import { executeAst, lintAst } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { KCLError, kclErrorsToDiagnostics } from './errors' import {
KCLError,
complilationErrorsToDiagnostics,
kclErrorsToDiagnostics,
} from './errors'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager } from './std/engineConnection'
import { err } from 'lib/trap' import { err } from 'lib/trap'
@ -8,6 +12,7 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { import {
CallExpression, CallExpression,
clearSceneAndBustCache,
emptyExecState, emptyExecState,
ExecState, ExecState,
initPromise, initPromise,
@ -51,11 +56,12 @@ export class KclManager {
private _programMemory: ProgramMemory = ProgramMemory.empty() private _programMemory: ProgramMemory = ProgramMemory.empty()
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty() lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
private _logs: string[] = [] private _logs: string[] = []
private _lints: Diagnostic[] = [] private _diagnostics: Diagnostic[] = []
private _kclErrors: KCLError[] = []
private _isExecuting = false private _isExecuting = false
private _executeIsStale: ExecuteArgs | null = null private _executeIsStale: ExecuteArgs | null = null
private _wasmInitFailed = true private _wasmInitFailed = true
private _hasErrors = false
private _switchedFiles = false
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
@ -63,7 +69,7 @@ export class KclManager {
private _astCallBack: (arg: Node<Program>) => void = () => {} private _astCallBack: (arg: Node<Program>) => void = () => {}
private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {} private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {}
private _logsCallBack: (arg: string[]) => void = () => {} private _logsCallBack: (arg: string[]) => void = () => {}
private _kclErrorsCallBack: (arg: KCLError[]) => void = () => {} private _kclErrorsCallBack: (errors: Diagnostic[]) => void = () => {}
private _wasmInitFailedCallback: (arg: boolean) => void = () => {} private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
private _executeCallback: () => void = () => {} private _executeCallback: () => void = () => {}
@ -75,6 +81,10 @@ export class KclManager {
this._astCallBack(ast) this._astCallBack(ast)
} }
set switchedFiles(switchedFiles: boolean) {
this._switchedFiles = switchedFiles
}
get programMemory() { get programMemory() {
return this._programMemory return this._programMemory
} }
@ -84,7 +94,7 @@ export class KclManager {
this._programMemoryCallBack(programMemory) this._programMemoryCallBack(programMemory)
} }
set execState(execState) { private set execState(execState) {
this._execState = execState this._execState = execState
this.programMemory = execState.memory this.programMemory = execState.memory
} }
@ -101,38 +111,28 @@ export class KclManager {
this._logsCallBack(logs) this._logsCallBack(logs)
} }
get lints() { get diagnostics() {
return this._lints return this._diagnostics
} }
set lints(lints) { set diagnostics(ds) {
if (lints === this._lints) return if (ds === this._diagnostics) return
this._lints = lints this._diagnostics = ds
// Run the lints through the diagnostics.
this.kclErrors = this._kclErrors
}
get kclErrors() {
return this._kclErrors
}
set kclErrors(kclErrors) {
if (kclErrors === this._kclErrors && this.lints.length === 0) return
this._kclErrors = kclErrors
this.setDiagnosticsForCurrentErrors() this.setDiagnosticsForCurrentErrors()
this._kclErrorsCallBack(kclErrors) }
addDiagnostics(ds: Diagnostic[]) {
if (ds.length === 0) return
this.diagnostics = this.diagnostics.concat(ds)
}
hasErrors(): boolean {
return this._hasErrors
} }
setDiagnosticsForCurrentErrors() { setDiagnosticsForCurrentErrors() {
let diagnostics = kclErrorsToDiagnostics(this.kclErrors) editorManager?.setDiagnostics(this.diagnostics)
if (this.lints.length > 0) { this._kclErrorsCallBack(this.diagnostics)
diagnostics = diagnostics.concat(this.lints)
}
editorManager?.setDiagnostics(diagnostics)
}
addKclErrors(kclErrors: KCLError[]) {
if (kclErrors.length === 0) return
this.kclErrors = this.kclErrors.concat(kclErrors)
} }
get isExecuting() { get isExecuting() {
@ -172,8 +172,12 @@ export class KclManager {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ensureWasmInit().then(() => { this.ensureWasmInit().then(async () => {
this.ast = this.safeParse(codeManager.code) || this.ast await this.safeParse(codeManager.code).then((ast) => {
if (ast) {
this.ast = ast
}
})
}) })
} }
@ -188,7 +192,7 @@ export class KclManager {
setProgramMemory: (arg: ProgramMemory) => void setProgramMemory: (arg: ProgramMemory) => void
setAst: (arg: Node<Program>) => void setAst: (arg: Node<Program>) => void
setLogs: (arg: string[]) => void setLogs: (arg: string[]) => void
setKclErrors: (arg: KCLError[]) => void setKclErrors: (errors: Diagnostic[]) => void
setIsExecuting: (arg: boolean) => void setIsExecuting: (arg: boolean) => void
setWasmInitFailed: (arg: boolean) => void setWasmInitFailed: (arg: boolean) => void
}) { }) {
@ -217,20 +221,50 @@ export class KclManager {
} }
} }
safeParse(code: string): Node<Program> | null { // (jess) I'm not in love with this, but it ensures we clear the scene and
const ast = parse(code) // bust the cache on
this.lints = [] // errors from parsing when opening new files.
this.kclErrors = [] // Why not just clear the cache on all parse errors, you ask? well its actually
if (!err(ast)) return ast // really nice to keep the cache on parse errors within the same file, and
const kclerror: KCLError = ast as KCLError // only bust on engine errors esp if they take a long time to execute and
// you hit the wrong key!
private async checkIfSwitchedFilesShouldClear() {
// If we were switching files and we hit an error on parse we need to bust
// the cache and clear the scene.
if (this._hasErrors && this._switchedFiles) {
await clearSceneAndBustCache(this.engineCommandManager)
} else if (this._switchedFiles) {
// Reset the switched files boolean.
this._switchedFiles = false
}
}
this.addKclErrors([kclerror]) async safeParse(code: string): Promise<Node<Program> | null> {
// TODO: re-eval if session should end? const result = parse(code)
if (kclerror.msg === 'file is empty') this.diagnostics = []
this.engineCommandManager?.endSession() this._hasErrors = false
if (err(result)) {
const kclerror: KCLError = result as KCLError
this.diagnostics = kclErrorsToDiagnostics([kclerror])
this._hasErrors = true
await this.checkIfSwitchedFilesShouldClear()
return null return null
} }
this.addDiagnostics(complilationErrorsToDiagnostics(result.errors))
this.addDiagnostics(complilationErrorsToDiagnostics(result.warnings))
if (result.errors.length > 0) {
this._hasErrors = true
await this.checkIfSwitchedFilesShouldClear()
return null
}
return result.program
}
async ensureWasmInit() { async ensureWasmInit() {
try { try {
await initPromise await initPromise
@ -267,19 +301,16 @@ export class KclManager {
this._cancelTokens.set(currentExecutionId, false) this._cancelTokens.set(currentExecutionId, false)
this.isExecuting = true this.isExecuting = true
// Make sure we clear before starting again. End session will do this.
this.engineCommandManager?.endSession()
await this.ensureWasmInit() await this.ensureWasmInit()
const { logs, errors, execState, isInterrupted } = await executeAst({ const { logs, errors, execState, isInterrupted } = await executeAst({
ast, ast,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
}) })
// Program was not interrupted, setup the scene // Program was not interrupted, setup the scene
// Do not send send scene commands if the program was interrupted, go to clean up // Do not send send scene commands if the program was interrupted, go to clean up
if (!isInterrupted) { if (!isInterrupted) {
this.lints = await lintAst({ ast: ast }) this.addDiagnostics(await lintAst({ ast: ast }))
sceneInfra.modelingSend({ type: 'code edit during sketch' }) sceneInfra.modelingSend({ type: 'code edit during sketch' })
setSelectionFilterToDefault(execState.memory, this.engineCommandManager) setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
@ -321,9 +352,7 @@ export class KclManager {
this.logs = logs this.logs = logs
// Do not add the errors since the program was interrupted and the error is not a real KCL error // Do not add the errors since the program was interrupted and the error is not a real KCL error
this.addKclErrors(isInterrupted ? [] : errors) this.addDiagnostics(isInterrupted ? [] : kclErrorsToDiagnostics(errors))
// Reset the next ID index so that we reuse the previous IDs next time.
execState.idGenerator.nextId = 0
this.execState = execState this.execState = execState
if (!errors.length) { if (!errors.length) {
this.lastSuccessfulProgramMemory = execState.memory this.lastSuccessfulProgramMemory = execState.memory
@ -355,7 +384,7 @@ export class KclManager {
console.error(newCode) console.error(newCode)
return return
} }
const newAst = this.safeParse(newCode) const newAst = await this.safeParse(newCode)
if (!newAst) { if (!newAst) {
this.clearAst() this.clearAst()
return return
@ -364,13 +393,13 @@ export class KclManager {
const { logs, errors, execState } = await executeAst({ const { logs, errors, execState } = await executeAst({
ast: newAst, ast: newAst,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
useFakeExecutor: true, // We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride: ProgramMemory.empty(),
}) })
this._logs = logs this._logs = logs
this._kclErrors = errors this.addDiagnostics(kclErrorsToDiagnostics(errors))
this._execState = execState this._execState = execState
this._programMemory = execState.memory this._programMemory = execState.memory
if (!errors.length) { if (!errors.length) {
@ -398,7 +427,7 @@ export class KclManager {
...artifact, ...artifact,
codeRef: { codeRef: {
...artifact.codeRef, ...artifact.codeRef,
range: [node.start, node.end], range: [node.start, node.end, true],
}, },
}) })
} }
@ -410,7 +439,7 @@ export class KclManager {
}) })
} }
async executeCode(zoomToFit?: boolean): Promise<void> { async executeCode(zoomToFit?: boolean): Promise<void> {
const ast = this.safeParse(codeManager.code) const ast = await this.safeParse(codeManager.code)
if (!ast) { if (!ast) {
this.clearAst() this.clearAst()
return return
@ -418,9 +447,9 @@ export class KclManager {
this.ast = { ...ast } this.ast = { ...ast }
return this.executeAst({ zoomToFit }) return this.executeAst({ zoomToFit })
} }
format() { async format() {
const originalCode = codeManager.code const originalCode = codeManager.code
const ast = this.safeParse(originalCode) const ast = await this.safeParse(originalCode)
if (!ast) { if (!ast) {
this.clearAst() this.clearAst()
return return
@ -460,7 +489,7 @@ export class KclManager {
const newCode = recast(ast) const newCode = recast(ast)
if (err(newCode)) return Promise.reject(newCode) if (err(newCode)) return Promise.reject(newCode)
const astWithUpdatedSource = this.safeParse(newCode) const astWithUpdatedSource = await this.safeParse(newCode)
if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast')) if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast'))
let returnVal: Selections | undefined = undefined let returnVal: Selections | undefined = undefined
@ -490,7 +519,7 @@ export class KclManager {
if (start && end) { if (start && end) {
returnVal.graphSelections.push({ returnVal.graphSelections.push({
codeRef: { codeRef: {
range: [start, end], range: [start, end, true],
pathToNode: path, pathToNode: path,
}, },
}) })

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { parse, initPromise } from './wasm' import { assertParse, initPromise } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
beforeAll(async () => { beforeAll(async () => {
@ -14,7 +14,7 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %)` // |> rx(45, %)`
const execState = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(assertParse(code))
// @ts-ignore // @ts-ignore
const sketch001 = execState.memory.get('mySketch001') const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
@ -67,7 +67,7 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)
|> extrude(2, %)` |> extrude(2, %)`
const execState = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(assertParse(code))
// @ts-ignore // @ts-ignore
const sketch001 = execState.memory.get('mySketch001') const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
@ -147,7 +147,7 @@ const sk2 = startSketchOn('XY')
|> extrude(2, %) |> extrude(2, %)
` `
const execState = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(assertParse(code))
const programMemory = execState.memory const programMemory = execState.memory
// @ts-ignore // @ts-ignore
const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')] const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')]

View File

@ -8,20 +8,14 @@ describe('test kclErrToDiagnostic', () => {
message: '', message: '',
kind: 'semantic', kind: 'semantic',
msg: 'Semantic error', msg: 'Semantic error',
sourceRanges: [ sourceRange: [0, 1, true],
[0, 1, 0],
[2, 3, 0],
],
}, },
{ {
name: '', name: '',
message: '', message: '',
kind: 'type', kind: 'type',
msg: 'Type error', msg: 'Type error',
sourceRanges: [ sourceRange: [4, 5, true],
[4, 5, 0],
[6, 7, 0],
],
}, },
] ]
const diagnostics = kclErrorsToDiagnostics(errors) const diagnostics = kclErrorsToDiagnostics(errors)
@ -32,24 +26,12 @@ describe('test kclErrToDiagnostic', () => {
message: 'Semantic error', message: 'Semantic error',
severity: 'error', severity: 'error',
}, },
{
from: 2,
to: 3,
message: 'Semantic error',
severity: 'error',
},
{ {
from: 4, from: 4,
to: 5, to: 5,
message: 'Type error', message: 'Type error',
severity: 'error', severity: 'error',
}, },
{
from: 6,
to: 7,
message: 'Type error',
severity: 'error',
},
]) ])
}) })
}) })

View File

@ -1,88 +1,90 @@
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint' import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
import { posToOffset } from '@kittycad/codemirror-lsp-client' import { posToOffset } from '@kittycad/codemirror-lsp-client'
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol' import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
import { Text } from '@codemirror/state' import { Text } from '@codemirror/state'
import { EditorView } from 'codemirror'
const TOP_LEVEL_MODULE_ID = 0 import { SourceRange } from 'lang/wasm'
type ExtractKind<T> = T extends { kind: infer K } ? K : never type ExtractKind<T> = T extends { kind: infer K } ? K : never
export class KCLError extends Error { export class KCLError extends Error {
kind: ExtractKind<RustKclError> | 'name' kind: ExtractKind<RustKclError> | 'name'
sourceRanges: [number, number, number][] sourceRange: SourceRange
msg: string msg: string
constructor( constructor(
kind: ExtractKind<RustKclError> | 'name', kind: ExtractKind<RustKclError> | 'name',
msg: string, msg: string,
sourceRanges: [number, number, number][] sourceRange: SourceRange
) { ) {
super() super()
this.kind = kind this.kind = kind
this.msg = msg this.msg = msg
this.sourceRanges = sourceRanges this.sourceRange = sourceRange
Object.setPrototypeOf(this, KCLError.prototype) Object.setPrototypeOf(this, KCLError.prototype)
} }
} }
export class KCLLexicalError extends KCLError { export class KCLLexicalError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) { constructor(msg: string, sourceRange: SourceRange) {
super('lexical', msg, sourceRanges) super('lexical', msg, sourceRange)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
export class KCLInternalError extends KCLError { export class KCLInternalError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) { constructor(msg: string, sourceRange: SourceRange) {
super('internal', msg, sourceRanges) super('internal', msg, sourceRange)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
export class KCLSyntaxError extends KCLError { export class KCLSyntaxError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) { constructor(msg: string, sourceRange: SourceRange) {
super('syntax', msg, sourceRanges) super('syntax', msg, sourceRange)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
export class KCLSemanticError extends KCLError { export class KCLSemanticError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) { constructor(msg: string, sourceRange: SourceRange) {
super('semantic', msg, sourceRanges) super('semantic', msg, sourceRange)
Object.setPrototypeOf(this, KCLSemanticError.prototype) Object.setPrototypeOf(this, KCLSemanticError.prototype)
} }
} }
export class KCLTypeError extends KCLError { export class KCLTypeError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) { constructor(msg: string, sourceRange: SourceRange) {
super('type', msg, sourceRanges) super('type', msg, sourceRange)
Object.setPrototypeOf(this, KCLTypeError.prototype) Object.setPrototypeOf(this, KCLTypeError.prototype)
} }
} }
export class KCLUnimplementedError extends KCLError { export class KCLUnimplementedError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) { constructor(msg: string, sourceRange: SourceRange) {
super('unimplemented', msg, sourceRanges) super('unimplemented', msg, sourceRange)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype) Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
} }
} }
export class KCLUnexpectedError extends KCLError { export class KCLUnexpectedError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) { constructor(msg: string, sourceRange: SourceRange) {
super('unexpected', msg, sourceRanges) super('unexpected', msg, sourceRange)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype) Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
} }
} }
export class KCLValueAlreadyDefined extends KCLError { export class KCLValueAlreadyDefined extends KCLError {
constructor(key: string, sourceRanges: [number, number, number][]) { constructor(key: string, sourceRange: SourceRange) {
super('name', `Key ${key} was already defined elsewhere`, sourceRanges) super('name', `Key ${key} was already defined elsewhere`, sourceRange)
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype) Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
} }
} }
export class KCLUndefinedValueError extends KCLError { export class KCLUndefinedValueError extends KCLError {
constructor(key: string, sourceRanges: [number, number, number][]) { constructor(key: string, sourceRange: SourceRange) {
super('name', `Key ${key} has not been defined`, sourceRanges) super('name', `Key ${key} has not been defined`, sourceRange)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype) Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
} }
} }
@ -99,27 +101,14 @@ export function lspDiagnosticsToKclErrors(
.flatMap( .flatMap(
({ range, message }) => ({ range, message }) =>
new KCLError('unexpected', message, [ new KCLError('unexpected', message, [
[
posToOffset(doc, range.start)!, posToOffset(doc, range.start)!,
posToOffset(doc, range.end)!, posToOffset(doc, range.end)!,
TOP_LEVEL_MODULE_ID, true,
],
]) ])
) )
.filter(({ sourceRanges }) => {
const [from, to, moduleId] = sourceRanges[0]
return (
from !== null &&
to !== null &&
from !== undefined &&
to !== undefined &&
// Filter out errors that are not from the top-level module.
moduleId === TOP_LEVEL_MODULE_ID
)
})
.sort((a, b) => { .sort((a, b) => {
const c = a.sourceRanges[0][0] const c = a.sourceRange[0]
const d = b.sourceRanges[0][0] const d = b.sourceRange[0]
switch (true) { switch (true) {
case c < d: case c < d:
return -1 return -1
@ -137,17 +126,48 @@ export function lspDiagnosticsToKclErrors(
export function kclErrorsToDiagnostics( export function kclErrorsToDiagnostics(
errors: KCLError[] errors: KCLError[]
): CodeMirrorDiagnostic[] { ): CodeMirrorDiagnostic[] {
return errors?.flatMap((err) => { return errors
const sourceRanges: CodeMirrorDiagnostic[] = err.sourceRanges ?.filter((err) => err.sourceRange[2])
// Filter out errors that are not from the top-level module. .map((err) => {
.filter(([_start, _end, moduleId]) => moduleId === TOP_LEVEL_MODULE_ID) return {
.map(([from, to]) => { from: err.sourceRange[0],
return { from, to, message: err.msg, severity: 'error' } to: err.sourceRange[1],
}) message: err.msg,
// Make sure we didn't filter out all the source ranges. severity: 'error',
if (sourceRanges.length === 0) { }
sourceRanges.push({ from: 0, to: 0, message: err.msg, severity: 'error' }) })
}
export function complilationErrorsToDiagnostics(
errors: CompilationError[]
): CodeMirrorDiagnostic[] {
return errors
?.filter((err) => err.sourceRange[2] === 0)
.map((err) => {
let severity: any = 'error'
if (err.severity === 'Warning') {
severity = 'warning'
}
let actions
const suggestion = err.suggestion
if (suggestion) {
actions = [
{
name: suggestion.title,
apply: (view: EditorView, from: number, to: number) => {
view.dispatch({
changes: { from, to, insert: suggestion.insert },
})
},
},
]
}
return {
from: err.sourceRange[0],
to: err.sourceRange[1],
message: err.message,
severity,
actions,
} }
return sourceRanges
}) })
} }

View File

@ -1,7 +1,7 @@
import fs from 'node:fs' import fs from 'node:fs'
import { import {
parse, assertParse,
ProgramMemory, ProgramMemory,
Sketch, Sketch,
initPromise, initPromise,
@ -472,7 +472,7 @@ describe('Testing Errors', () => {
const theExtrude = startSketchOn('XY') const theExtrude = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([-2.4, 5], %) |> line([-2.4, 5], %)
|> line([-0.76], myVarZ, %) |> line(myVarZ, %)
|> line([5,5], %) |> line([5,5], %)
|> close(%) |> close(%)
|> extrude(4, %)` |> extrude(4, %)`
@ -480,7 +480,7 @@ const theExtrude = startSketchOn('XY')
new KCLError( new KCLError(
'undefined_value', 'undefined_value',
'memory item key `myVarZ` is not defined', 'memory item key `myVarZ` is not defined',
[[129, 135, 0]] [129, 135, true]
) )
) )
}) })
@ -492,7 +492,7 @@ async function exe(
code: string, code: string,
programMemory: ProgramMemory = ProgramMemory.empty() programMemory: ProgramMemory = ProgramMemory.empty()
) { ) {
const ast = parse(code) const ast = assertParse(code)
const execState = await enginelessExecutor(ast, programMemory) const execState = await enginelessExecutor(ast, programMemory)
return execState.memory return execState.memory

View File

@ -1,5 +1,5 @@
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst' import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
import { Identifier, parse, initPromise, Parameter } from './wasm' import { Identifier, assertParse, initPromise, Parameter } from './wasm'
import { err } from 'lib/trap' import { err } from 'lib/trap'
beforeAll(async () => { beforeAll(async () => {
@ -17,19 +17,19 @@ const sk3 = startSketchAt([0, 0])
` `
const subStr = 'lineTo([3, 4], %, $yo)' const subStr = 'lineTo([3, 4], %, $yo)'
const lineToSubstringIndex = code.indexOf(subStr) const lineToSubstringIndex = code.indexOf(subStr)
const sourceRange: [number, number] = [ const sourceRange: [number, number, boolean] = [
lineToSubstringIndex, lineToSubstringIndex,
lineToSubstringIndex + subStr.length, lineToSubstringIndex + subStr.length,
true,
] ]
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const nodePath = getNodePathFromSourceRange(ast, sourceRange) const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const _node = getNodeFromPath<any>(ast, nodePath) const _node = getNodeFromPath<any>(ast, nodePath)
if (err(_node)) throw _node if (err(_node)) throw _node
const { node } = _node const { node } = _node
expect([node.start, node.end]).toEqual(sourceRange) expect([node.start, node.end, true]).toEqual(sourceRange)
expect(node.type).toBe('CallExpression') expect(node.type).toBe('CallExpression')
}) })
it('gets path right for function definition params', () => { it('gets path right for function definition params', () => {
@ -45,13 +45,13 @@ const sk3 = startSketchAt([0, 0])
const b1 = cube([0,0], 10)` const b1 = cube([0,0], 10)`
const subStr = 'pos, scale' const subStr = 'pos, scale'
const subStrIndex = code.indexOf(subStr) const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number] = [ const sourceRange: [number, number, boolean] = [
subStrIndex, subStrIndex,
subStrIndex + 'pos'.length, subStrIndex + 'pos'.length,
true,
] ]
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const nodePath = getNodePathFromSourceRange(ast, sourceRange) const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const _node = getNodeFromPath<Parameter>(ast, nodePath) const _node = getNodeFromPath<Parameter>(ast, nodePath)
if (err(_node)) throw _node if (err(_node)) throw _node
@ -60,8 +60,7 @@ const b1 = cube([0,0], 10)`
expect(nodePath).toEqual([ expect(nodePath).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['params', 'FunctionExpression'], ['params', 'FunctionExpression'],
[0, 'index'], [0, 'index'],
@ -82,13 +81,13 @@ const b1 = cube([0,0], 10)`
const b1 = cube([0,0], 10)` const b1 = cube([0,0], 10)`
const subStr = 'scale, 0' const subStr = 'scale, 0'
const subStrIndex = code.indexOf(subStr) const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number] = [ const sourceRange: [number, number, boolean] = [
subStrIndex, subStrIndex,
subStrIndex + 'scale'.length, subStrIndex + 'scale'.length,
true,
] ]
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const nodePath = getNodePathFromSourceRange(ast, sourceRange) const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const _node = getNodeFromPath<Identifier>(ast, nodePath) const _node = getNodeFromPath<Identifier>(ast, nodePath)
if (err(_node)) throw _node if (err(_node)) throw _node
@ -96,14 +95,12 @@ const b1 = cube([0,0], 10)`
expect(nodePath).toEqual([ expect(nodePath).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'FunctionExpression'], ['body', 'FunctionExpression'],
['body', 'FunctionExpression'], ['body', 'FunctionExpression'],
[0, 'index'], [0, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'PipeExpression'], ['body', 'PipeExpression'],
[2, 'index'], [2, 'index'],

View File

@ -1,6 +1,5 @@
import { parse, initPromise, programMemoryInit } from './wasm' import { assertParse, initPromise, programMemoryInit } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { assert } from 'vitest'
// These unit tests makes web requests to a public github repository. // These unit tests makes web requests to a public github repository.
interface KclSampleFile { interface KclSampleFile {
@ -58,8 +57,7 @@ describe('Test KCL Samples from public Github repository', () => {
files.forEach((file: KclSampleFile) => { files.forEach((file: KclSampleFile) => {
it(`should parse ${file.filename} without errors`, async () => { it(`should parse ${file.filename} without errors`, async () => {
const code = await getKclSampleCodeFromGithub(file.filename) const code = await getKclSampleCodeFromGithub(file.filename)
const parsed = parse(code) assertParse(code)
assert(!(parsed instanceof Error))
}, 1000) }, 1000)
}) })
}) })
@ -71,9 +69,8 @@ describe('Test KCL Samples from public Github repository', () => {
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file: KclSampleFile = files[i] const file: KclSampleFile = files[i]
const code = await getKclSampleCodeFromGithub(file.filename) const code = await getKclSampleCodeFromGithub(file.filename)
const parsed = parse(code) const ast = assertParse(code)
assert(!(parsed instanceof Error)) await enginelessExecutor(ast, programMemoryInit())
await enginelessExecutor(parsed, programMemoryInit())
} }
}, },
files.length * 1000 files.length * 1000

View File

@ -2,7 +2,6 @@ import {
Program, Program,
_executor, _executor,
ProgramMemory, ProgramMemory,
programMemoryInit,
kclLint, kclLint,
emptyExecState, emptyExecState,
ExecState, ExecState,
@ -11,7 +10,6 @@ import { enginelessExecutor } from 'lib/testHelpers'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
import { KCLError } from 'lang/errors' import { KCLError } from 'lang/errors'
import { Diagnostic } from '@codemirror/lint' import { Diagnostic } from '@codemirror/lint'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ToolTip = export type ToolTip =
@ -49,15 +47,13 @@ export const toolTips: Array<ToolTip> = [
export async function executeAst({ export async function executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor = false, // If you set programMemoryOverride we assume you mean mock mode. Since that
// is the only way to go about it.
programMemoryOverride, programMemoryOverride,
idGenerator,
}: { }: {
ast: Node<Program> ast: Node<Program>
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean
programMemoryOverride?: ProgramMemory programMemoryOverride?: ProgramMemory
idGenerator?: IdGenerator
isInterrupted?: boolean isInterrupted?: boolean
}): Promise<{ }): Promise<{
logs: string[] logs: string[]
@ -66,22 +62,14 @@ export async function executeAst({
isInterrupted: boolean isInterrupted: boolean
}> { }> {
try { try {
if (!useFakeExecutor) { const execState = await (programMemoryOverride
engineCommandManager.endSession() ? enginelessExecutor(ast, programMemoryOverride)
// eslint-disable-next-line @typescript-eslint/no-floating-promises : _executor(ast, engineCommandManager))
engineCommandManager.startNewSession()
} await engineCommandManager.waitForAllCommands(
const execState = await (useFakeExecutor programMemoryOverride !== undefined
? enginelessExecutor(ast, programMemoryOverride || programMemoryInit()) )
: _executor(
ast,
programMemoryInit(),
idGenerator,
engineCommandManager,
false
))
await engineCommandManager.waitForAllCommands(useFakeExecutor)
return { return {
logs: [], logs: [],
errors: [], errors: [],

View File

@ -1,4 +1,4 @@
import { parse, recast, initPromise, Identifier } from './wasm' import { assertParse, recast, initPromise, Identifier } from './wasm'
import { import {
createLiteral, createLiteral,
createIdentifier, createIdentifier,
@ -82,11 +82,11 @@ describe('Testing createVariableDeclaration', () => {
it('should create a variable declaration', () => { it('should create a variable declaration', () => {
const result = createVariableDeclaration('myVar', createLiteral(5)) const result = createVariableDeclaration('myVar', createLiteral(5))
expect(result.type).toBe('VariableDeclaration') expect(result.type).toBe('VariableDeclaration')
expect(result.declarations[0].type).toBe('VariableDeclarator') expect(result.declaration.type).toBe('VariableDeclarator')
expect(result.declarations[0].id.type).toBe('Identifier') expect(result.declaration.id.type).toBe('Identifier')
expect(result.declarations[0].id.name).toBe('myVar') expect(result.declaration.id.name).toBe('myVar')
expect(result.declarations[0].init.type).toBe('Literal') expect(result.declaration.init.type).toBe('Literal')
expect((result.declarations[0].init as any).value).toBe(5) expect((result.declaration.init as any).value).toBe(5)
}) })
}) })
describe('Testing createPipeExpression', () => { describe('Testing createPipeExpression', () => {
@ -146,10 +146,13 @@ function giveSketchFnCallTagTestHelper(
// giveSketchFnCallTag inputs and outputs an ast, which is very verbose for testing // giveSketchFnCallTag inputs and outputs an ast, which is very verbose for testing
// this wrapper changes the input and output to code // this wrapper changes the input and output to code
// making it more of an integration test, but easier to read the test intention is the goal // making it more of an integration test, but easier to read the test intention is the goal
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const start = code.indexOf(searchStr) const start = code.indexOf(searchStr)
const range: [number, number] = [start, start + searchStr.length] const range: [number, number, boolean] = [
start,
start + searchStr.length,
true,
]
const sketchRes = giveSketchFnCallTag(ast, range) const sketchRes = giveSketchFnCallTag(ast, range)
if (err(sketchRes)) throw sketchRes if (err(sketchRes)) throw sketchRes
const { modifiedAst, tag, isTagExisting } = sketchRes const { modifiedAst, tag, isTagExisting } = sketchRes
@ -221,14 +224,13 @@ part001 = startSketchOn('XY')
|> angledLine([jkl(yo) + 2, 3.09], %) |> angledLine([jkl(yo) + 2, 3.09], %)
yo2 = hmm([identifierGuy + 5])` yo2 = hmm([identifierGuy + 5])`
it('should move a binary expression into a new variable', async () => { it('should move a binary expression into a new variable', async () => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('100 + 100') + 1 const startIndex = code.indexOf('100 + 100') + 1
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex, true],
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -236,14 +238,13 @@ yo2 = hmm([identifierGuy + 5])`
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`) expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
}) })
it('should move a value into a new variable', async () => { it('should move a value into a new variable', async () => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('2.8') + 1 const startIndex = code.indexOf('2.8') + 1
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex, true],
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -251,14 +252,13 @@ yo2 = hmm([identifierGuy + 5])`
expect(newCode).toContain(`line([newVar, 0], %)`) expect(newCode).toContain(`line([newVar, 0], %)`)
}) })
it('should move a callExpression into a new variable', async () => { it('should move a callExpression into a new variable', async () => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('def(') const startIndex = code.indexOf('def(')
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex, true],
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -266,14 +266,13 @@ yo2 = hmm([identifierGuy + 5])`
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`) expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
}) })
it('should move a binary expression with call expression into a new variable', async () => { it('should move a binary expression with call expression into a new variable', async () => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('jkl(') + 1 const startIndex = code.indexOf('jkl(') + 1
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex, true],
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -281,14 +280,13 @@ yo2 = hmm([identifierGuy + 5])`
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`) expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
}) })
it('should move a identifier into a new variable', async () => { it('should move a identifier into a new variable', async () => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('identifierGuy +') + 1 const startIndex = code.indexOf('identifierGuy +') + 1
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex, true],
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -305,19 +303,20 @@ describe('testing sketchOnExtrudedFace', () => {
|> line([8.62, -9.57], %) |> line([8.62, -9.57], %)
|> close(%) |> close(%)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const segmentSnippet = `line([9.7, 9.19], %)` const segmentSnippet = `line([9.7, 9.19], %)`
const segmentRange: [number, number] = [ const segmentRange: [number, number, boolean] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length, code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
] ]
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange) const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)` const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number] = [ const extrudeRange: [number, number, boolean] = [
code.indexOf(extrudeSnippet), code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length, code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
] ]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
@ -345,18 +344,19 @@ sketch001 = startSketchOn(part001, seg01)`)
|> line([8.62, -9.57], %) |> line([8.62, -9.57], %)
|> close(%) |> close(%)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const segmentSnippet = `close(%)` const segmentSnippet = `close(%)`
const segmentRange: [number, number] = [ const segmentRange: [number, number, boolean] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length, code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
] ]
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange) const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)` const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number] = [ const extrudeRange: [number, number, boolean] = [
code.indexOf(extrudeSnippet), code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length, code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
] ]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
@ -384,18 +384,19 @@ sketch001 = startSketchOn(part001, seg01)`)
|> line([8.62, -9.57], %) |> line([8.62, -9.57], %)
|> close(%) |> close(%)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const sketchSnippet = `startProfileAt([3.58, 2.06], %)` const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
const sketchRange: [number, number] = [ const sketchRange: [number, number, boolean] = [
code.indexOf(sketchSnippet), code.indexOf(sketchSnippet),
code.indexOf(sketchSnippet) + sketchSnippet.length, code.indexOf(sketchSnippet) + sketchSnippet.length,
true,
] ]
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange) const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const extrudeSnippet = `extrude(5 + 7, %)` const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number] = [ const extrudeRange: [number, number, boolean] = [
code.indexOf(extrudeSnippet), code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length, code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
] ]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
@ -432,18 +433,19 @@ sketch001 = startSketchOn(part001, 'END')`)
|> line([-17.67, 0.85], %) |> line([-17.67, 0.85], %)
|> close(%) |> close(%)
part001 = extrude(5 + 7, sketch001)` part001 = extrude(5 + 7, sketch001)`
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const segmentSnippet = `line([4.99, -0.46], %)` const segmentSnippet = `line([4.99, -0.46], %)`
const segmentRange: [number, number] = [ const segmentRange: [number, number, boolean] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length, code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
] ]
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange) const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, sketch001)` const extrudeSnippet = `extrude(5 + 7, sketch001)`
const extrudeRange: [number, number] = [ const extrudeRange: [number, number, boolean] = [
code.indexOf(extrudeSnippet), code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length, code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
] ]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
@ -466,13 +468,13 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|> line([306.21, 198.82], %) |> line([306.21, 198.82], %)
|> line([306.21, 198.85], %, $a) |> line([306.21, 198.85], %, $a)
|> line([306.21, 198.87], %)` |> line([306.21, 198.87], %)`
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = 'line([306.21, 198.85], %, $a)' const lineOfInterest = 'line([306.21, 198.85], %, $a)'
const range: [number, number] = [ const range: [number, number, boolean] = [
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
] ]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
const modifiedAst = deleteSegmentFromPipeExpression( const modifiedAst = deleteSegmentFromPipeExpression(
@ -544,13 +546,13 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
], ],
])(`%s`, async (_, line, [replace1, replace2]) => { ])(`%s`, async (_, line, [replace1, replace2]) => {
const code = makeCode(line) const code = makeCode(line)
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = line const lineOfInterest = line
const range: [number, number] = [ const range: [number, number, boolean] = [
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
] ]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
const dependentSegments = findUsesOfTagInPipe(ast, pathToNode) const dependentSegments = findUsesOfTagInPipe(ast, pathToNode)
@ -632,14 +634,14 @@ describe('Testing removeSingleConstraintInfo', () => {
], ],
['tangentialArcTo([3.14 + 0, 13.14], %)', 'arrayIndex', 1], ['tangentialArcTo([3.14 + 0, 13.14], %)', 'arrayIndex', 1],
] as const)('stdlib fn: %s', async (expectedFinish, key, value) => { ] as const)('stdlib fn: %s', async (expectedFinish, key, value) => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '(' const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [ const range: [number, number, boolean] = [
code.indexOf(lineOfInterest) + 1, code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
] ]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
let argPosition: SimplifiedArgDetails let argPosition: SimplifiedArgDetails
@ -686,14 +688,14 @@ describe('Testing removeSingleConstraintInfo', () => {
['angledLineToX([12.14 + 0, 12], %)', 'arrayIndex', 1], ['angledLineToX([12.14 + 0, 12], %)', 'arrayIndex', 1],
['angledLineToY([30, 10.14 + 0], %)', 'arrayIndex', 0], ['angledLineToY([30, 10.14 + 0], %)', 'arrayIndex', 0],
])('stdlib fn: %s', async (expectedFinish, key, value) => { ])('stdlib fn: %s', async (expectedFinish, key, value) => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '(' const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [ const range: [number, number, boolean] = [
code.indexOf(lineOfInterest) + 1, code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
] ]
let argPosition: SimplifiedArgDetails let argPosition: SimplifiedArgDetails
if (key === 'arrayIndex' && typeof value === 'number') { if (key === 'arrayIndex' && typeof value === 'number') {
@ -883,14 +885,14 @@ sketch002 = startSketchOn({
'%s', '%s',
async (name, { codeBefore, codeAfter, lineOfInterest, type }) => { async (name, { codeBefore, codeAfter, lineOfInterest, type }) => {
// const lineOfInterest = 'line([-2.94, 2.7], %)' // const lineOfInterest = 'line([-2.94, 2.7], %)'
const ast = parse(codeBefore) const ast = assertParse(codeBefore)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
// deleteFromSelection // deleteFromSelection
const range: [number, number] = [ const range: [number, number, boolean] = [
codeBefore.indexOf(lineOfInterest), codeBefore.indexOf(lineOfInterest),
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length, codeBefore.indexOf(lineOfInterest) + lineOfInterest.length,
true,
] ]
const artifact = { type } as Artifact const artifact = { type } as Artifact
const newAst = await deleteFromSelection( const newAst = await deleteFromSelection(

View File

@ -66,8 +66,7 @@ export function startSketchOnDefault(
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[sketchIndex, 'index'], [sketchIndex, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
] ]
@ -94,7 +93,7 @@ export function addStartProfileAt(
return new Error('variableDeclaration.init.type !== PipeExpression') return new Error('variableDeclaration.init.type !== PipeExpression')
} }
const _node = { ...node } const _node = { ...node }
const init = variableDeclaration.declarations[0].init const init = variableDeclaration.declaration.init
const startProfileAt = createCallExpressionStdLib('startProfileAt', [ const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createArrayExpression([ createArrayExpression([
createLiteral(roundOff(at[0])), createLiteral(roundOff(at[0])),
@ -105,7 +104,7 @@ export function addStartProfileAt(
if (init.type === 'PipeExpression') { if (init.type === 'PipeExpression') {
init.body.splice(1, 0, startProfileAt) init.body.splice(1, 0, startProfileAt)
} else { } else {
variableDeclaration.declarations[0].init = createPipeExpression([ variableDeclaration.declaration.init = createPipeExpression([
init, init,
startProfileAt, startProfileAt,
]) ])
@ -149,8 +148,7 @@ export function addSketchTo(
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[sketchIndex, 'index'], [sketchIndex, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
] ]
if (axis !== 'xy') { if (axis !== 'xy') {
@ -333,8 +331,7 @@ export function extrudeSketch(
const pathToExtrudeArg: PathToNode = [ const pathToExtrudeArg: PathToNode = [
['body', ''], ['body', ''],
[sketchIndexInBody + 1, 'index'], [sketchIndexInBody + 1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -346,6 +343,36 @@ export function extrudeSketch(
} }
} }
export function loftSketches(
node: Node<Program>,
declarators: VariableDeclarator[]
): {
modifiedAst: Node<Program>
pathToNode: PathToNode
} {
const modifiedAst = structuredClone(node)
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.LOFT)
const elements = declarators.map((d) => createIdentifier(d.id.name))
const loft = createCallExpressionStdLib('loft', [
createArrayExpression(elements),
])
const declaration = createVariableDeclaration(name, loft)
modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst,
pathToNode,
}
}
export function revolveSketch( export function revolveSketch(
node: Node<Program>, node: Node<Program>,
pathToNode: PathToNode, pathToNode: PathToNode,
@ -429,8 +456,7 @@ export function revolveSketch(
const pathToRevolveArg: PathToNode = [ const pathToRevolveArg: PathToNode = [
['body', ''], ['body', ''],
[sketchIndexInBody + 1, 'index'], [sketchIndexInBody + 1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -516,8 +542,7 @@ export function sketchOnExtrudedFace(
const newpathToNode: PathToNode = [ const newpathToNode: PathToNode = [
['body', ''], ['body', ''],
[expressionIndex + 1, 'index'], [expressionIndex + 1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
] ]
@ -554,8 +579,7 @@ export function addOffsetPlane({
const pathToNode: PathToNode = [ const pathToNode: PathToNode = [
['body', ''], ['body', ''],
[modifiedAst.body.length - 1, 'index'], [modifiedAst.body.length - 1, 'index'],
['declarations', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -727,7 +751,6 @@ export function createCallExpressionStdLib(
name, name,
}, },
optional: false,
arguments: args, arguments: args,
} }
} }
@ -749,7 +772,6 @@ export function createCallExpression(
name, name,
}, },
optional: false,
arguments: args, arguments: args,
} }
} }
@ -794,8 +816,7 @@ export function createVariableDeclaration(
end: 0, end: 0,
moduleId: 0, moduleId: 0,
declarations: [ declaration: {
{
type: 'VariableDeclarator', type: 'VariableDeclarator',
start: 0, start: 0,
end: 0, end: 0,
@ -804,7 +825,6 @@ export function createVariableDeclaration(
id: createIdentifier(varName), id: createIdentifier(varName),
init, init,
}, },
],
visibility, visibility,
kind, kind,
} }
@ -1091,7 +1111,7 @@ export async function deleteFromSelection(
traverse(astClone, { traverse(astClone, {
enter: (node, path) => { enter: (node, path) => {
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
const dec = node.declarations[0] const dec = node.declaration
if ( if (
dec.init.type === 'CallExpression' && dec.init.type === 'CallExpression' &&
(dec.init.callee.name === 'extrude' || (dec.init.callee.name === 'extrude' ||
@ -1126,7 +1146,7 @@ export async function deleteFromSelection(
enter: (node, path) => { enter: (node, path) => {
;(async () => { ;(async () => {
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
currentVariableName = node.declarations[0].id.name currentVariableName = node.declaration.id.name
} }
if ( if (
// match startSketchOn(${extrudeNameToDelete}) // match startSketchOn(${extrudeNameToDelete})

View File

@ -1,5 +1,5 @@
import { import {
parse, assertParse,
recast, recast,
initPromise, initPromise,
PathToNode, PathToNode,
@ -10,18 +10,21 @@ import {
VariableDeclarator, VariableDeclarator,
} from '../wasm' } from '../wasm'
import { import {
EdgeTreatmentType,
getPathToExtrudeForSegmentSelection, getPathToExtrudeForSegmentSelection,
hasValidFilletSelection, hasValidEdgeTreatmentSelection,
isTagUsedInFillet, isTagUsedInEdgeTreatment,
modifyAstWithFilletAndTag, modifyAstWithEdgeTreatmentAndTag,
} from './addFillet' FilletParameters,
ChamferParameters,
EdgeTreatmentParameters,
} from './addEdgeTreatment'
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 { 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 { KclCommandValue } from 'lib/commandTypes'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
import { codeRefFromRange } from 'lang/std/artifactGraph' import { codeRefFromRange } from 'lang/std/artifactGraph'
@ -75,9 +78,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
code: string, code: string,
expectedExtrudeSnippet: string expectedExtrudeSnippet: string
): CallExpression | PipeExpression | Error { ): CallExpression | PipeExpression | Error {
const extrudeRange: [number, number] = [ const extrudeRange: [number, number, boolean] = [
code.indexOf(expectedExtrudeSnippet), code.indexOf(expectedExtrudeSnippet),
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length, code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
true,
] ]
const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange) const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
const expectedExtrudeNodeResult = getNodeFromPath< const expectedExtrudeNodeResult = getNodeFromPath<
@ -106,14 +110,13 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
} }
// ast // ast
const astOrError = parse(code) const ast = assertParse(code)
if (err(astOrError)) return new Error('AST not found')
const ast = astOrError
// selection // selection
const segmentRange: [number, number] = [ const segmentRange: [number, number, boolean] = [
code.indexOf(selectedSegmentSnippet), code.indexOf(selectedSegmentSnippet),
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length, code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
true,
] ]
const selection: Selections = { const selection: Selections = {
graphSelections: [ graphSelections: [
@ -253,34 +256,24 @@ extrude003 = extrude(-15, sketch003)`
}) })
}) })
const runModifyAstCloneWithFilletAndTag = async ( const runModifyAstCloneWithEdgeTreatmentAndTag = async (
code: string, code: string,
selectionSnippets: Array<string>, selectionSnippets: Array<string>,
radiusValue: number, parameters: EdgeTreatmentParameters,
expectedCode: string expectedCode: string
) => { ) => {
// ast // ast
const astOrError = parse(code) const ast = assertParse(code)
if (err(astOrError)) {
return new Error('AST not found')
}
const ast = astOrError
// selection // selection
const segmentRanges: Array<[number, number]> = selectionSnippets.map( const segmentRanges: Array<[number, number, boolean]> = selectionSnippets.map(
(selectionSnippet) => [ (selectionSnippet) => [
code.indexOf(selectionSnippet), code.indexOf(selectionSnippet),
code.indexOf(selectionSnippet) + selectionSnippet.length, code.indexOf(selectionSnippet) + selectionSnippet.length,
true,
] ]
) )
// radius
const radius: KclCommandValue = {
valueAst: createLiteral(radiusValue),
valueText: radiusValue.toString(),
valueCalculated: radiusValue.toString(),
}
// executeAst // executeAst
await kclManager.executeAst({ ast }) await kclManager.executeAst({ ast })
const artifactGraph = engineCommandManager.artifactGraph const artifactGraph = engineCommandManager.artifactGraph
@ -299,8 +292,8 @@ const runModifyAstCloneWithFilletAndTag = async (
otherSelections: [], otherSelections: [],
} }
// apply fillet to selection // apply edge treatment to seleciton
const result = modifyAstWithFilletAndTag(ast, selection, radius) const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
if (err(result)) { if (err(result)) {
return result return result
} }
@ -310,8 +303,41 @@ const runModifyAstCloneWithFilletAndTag = async (
expect(newCode).toContain(expectedCode) expect(newCode).toContain(expectedCode)
} }
describe('Testing applyFilletToSelection', () => { const createFilletParameters = (radiusValue: number): FilletParameters => ({
it('should add a fillet to a specific segment', async () => { type: EdgeTreatmentType.Fillet,
radius: {
valueAst: createLiteral(radiusValue),
valueText: radiusValue.toString(),
valueCalculated: radiusValue.toString(),
},
})
const createChamferParameters = (lengthValue: number): ChamferParameters => ({
type: EdgeTreatmentType.Chamfer,
length: {
valueAst: createLiteral(lengthValue),
valueText: lengthValue.toString(),
valueCalculated: lengthValue.toString(),
},
})
// Iterate tests over all edge treatment types
Object.values(EdgeTreatmentType).forEach(
(edgeTreatmentType: EdgeTreatmentType) => {
// create parameters based on the edge treatment type
let parameterName: string
let parameters: EdgeTreatmentParameters
if (edgeTreatmentType === EdgeTreatmentType.Fillet) {
parameterName = 'radius'
parameters = createFilletParameters(3)
} else if (edgeTreatmentType === EdgeTreatmentType.Chamfer) {
parameterName = 'length'
parameters = createChamferParameters(3)
} else {
// Handle future edge treatments
return new Error(`Unsupported edge treatment type: ${edgeTreatmentType}`)
}
// run tests
describe(`Testing modifyAstCloneWithEdgeTreatmentAndTag with ${edgeTreatmentType}s`, () => {
it(`should add a ${edgeTreatmentType} to a specific segment`, async () => {
const code = `sketch001 = startSketchOn('XY') const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %) |> line([20, 0], %)
@ -321,7 +347,6 @@ describe('Testing applyFilletToSelection', () => {
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001)` extrude001 = extrude(-15, sketch001)`
const segmentSnippets = ['line([0, -20], %)'] const segmentSnippets = ['line([0, -20], %)']
const radiusValue = 3
const expectedCode = `sketch001 = startSketchOn('XY') const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %) |> line([20, 0], %)
@ -330,16 +355,16 @@ extrude001 = extrude(-15, sketch001)`
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> fillet({ radius = 3, tags = [seg01] }, %)` |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
await runModifyAstCloneWithFilletAndTag( await runModifyAstCloneWithEdgeTreatmentAndTag(
code, code,
segmentSnippets, segmentSnippets,
radiusValue, parameters,
expectedCode expectedCode
) )
}) })
it('should add a fillet to the sketch pipe', async () => { it(`should add a ${edgeTreatmentType} to the sketch pipe`, async () => {
const code = `sketch001 = startSketchOn('XY') const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %) |> line([20, 0], %)
@ -349,7 +374,6 @@ extrude001 = extrude(-15, sketch001)
|> close(%) |> close(%)
|> extrude(-15, %)` |> extrude(-15, %)`
const segmentSnippets = ['line([0, -20], %)'] const segmentSnippets = ['line([0, -20], %)']
const radiusValue = 3
const expectedCode = `sketch001 = startSketchOn('XY') const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %) |> line([20, 0], %)
@ -358,16 +382,16 @@ extrude001 = extrude(-15, sketch001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
|> extrude(-15, %) |> extrude(-15, %)
|> fillet({ radius = 3, tags = [seg01] }, %)` |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
await runModifyAstCloneWithFilletAndTag( await runModifyAstCloneWithEdgeTreatmentAndTag(
code, code,
segmentSnippets, segmentSnippets,
radiusValue, parameters,
expectedCode expectedCode
) )
}) })
it('should add a fillet to an already tagged segment', async () => { it(`should add a ${edgeTreatmentType} to an already tagged segment`, async () => {
const code = `sketch001 = startSketchOn('XY') const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %) |> line([20, 0], %)
@ -377,7 +401,6 @@ extrude001 = extrude(-15, sketch001)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001)` extrude001 = extrude(-15, sketch001)`
const segmentSnippets = ['line([0, -20], %, $seg01)'] const segmentSnippets = ['line([0, -20], %, $seg01)']
const radiusValue = 3
const expectedCode = `sketch001 = startSketchOn('XY') const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %) |> line([20, 0], %)
@ -386,16 +409,16 @@ extrude001 = extrude(-15, sketch001)`
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> fillet({ radius = 3, tags = [seg01] }, %)` |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
await runModifyAstCloneWithFilletAndTag( await runModifyAstCloneWithEdgeTreatmentAndTag(
code, code,
segmentSnippets, segmentSnippets,
radiusValue, parameters,
expectedCode expectedCode
) )
}) })
it('should add a fillet with existing tag on other segment', async () => { it(`should add a ${edgeTreatmentType} with existing tag on other segment`, async () => {
const code = `sketch001 = startSketchOn('XY') const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01) |> line([20, 0], %, $seg01)
@ -405,7 +428,6 @@ extrude001 = extrude(-15, sketch001)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001)` extrude001 = extrude(-15, sketch001)`
const segmentSnippets = ['line([-20, 0], %)'] const segmentSnippets = ['line([-20, 0], %)']
const radiusValue = 3
const expectedCode = `sketch001 = startSketchOn('XY') const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01) |> line([20, 0], %, $seg01)
@ -414,16 +436,16 @@ extrude001 = extrude(-15, sketch001)`
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> fillet({ radius = 3, tags = [seg02] }, %)` |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg02] }, %)`
await runModifyAstCloneWithFilletAndTag( await runModifyAstCloneWithEdgeTreatmentAndTag(
code, code,
segmentSnippets, segmentSnippets,
radiusValue, parameters,
expectedCode expectedCode
) )
}) })
it('should add a fillet with existing fillet on other segment', async () => { it(`should add a ${edgeTreatmentType} with existing fillet on other segment`, async () => {
const code = `sketch001 = startSketchOn('XY') const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01) |> line([20, 0], %, $seg01)
@ -434,7 +456,6 @@ extrude001 = extrude(-15, sketch001)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> fillet({ radius = 5, tags = [seg01] }, %)` |> fillet({ radius = 5, tags = [seg01] }, %)`
const segmentSnippets = ['line([-20, 0], %)'] const segmentSnippets = ['line([-20, 0], %)']
const radiusValue = 3
const expectedCode = `sketch001 = startSketchOn('XY') const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01) |> line([20, 0], %, $seg01)
@ -444,16 +465,45 @@ extrude001 = extrude(-15, sketch001)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> fillet({ radius = 5, tags = [seg01] }, %) |> fillet({ radius = 5, tags = [seg01] }, %)
|> fillet({ radius = 3, tags = [seg02] }, %)` |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg02] }, %)`
await runModifyAstCloneWithFilletAndTag( await runModifyAstCloneWithEdgeTreatmentAndTag(
code, code,
segmentSnippets, segmentSnippets,
radiusValue, parameters,
expectedCode expectedCode
) )
}) })
it('should add a fillet to two segments of a single extrusion', async () => { it(`should add a ${edgeTreatmentType} with existing chamfer on other segment`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01)
|> line([0, -20], %)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %)`
const segmentSnippets = ['line([-20, 0], %)']
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01)
|> line([0, -20], %)
|> line([-20, 0], %, $seg02)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %)
|> ${edgeTreatmentType}({ ${parameterName}: 3, tags: [seg02] }, %)`
await runModifyAstCloneWithEdgeTreatmentAndTag(
code,
segmentSnippets,
parameters,
expectedCode
)
})
it(`should add a ${edgeTreatmentType} to two segments of a single extrusion`, async () => {
const code = `sketch001 = startSketchOn('XY') const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %) |> line([20, 0], %)
@ -463,7 +513,6 @@ extrude001 = extrude(-15, sketch001)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001)` extrude001 = extrude(-15, sketch001)`
const segmentSnippets = ['line([20, 0], %)', 'line([-20, 0], %)'] const segmentSnippets = ['line([20, 0], %)', 'line([-20, 0], %)']
const radiusValue = 3
const expectedCode = `sketch001 = startSketchOn('XY') const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01) |> line([20, 0], %, $seg01)
@ -472,16 +521,16 @@ extrude001 = extrude(-15, sketch001)`
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> fillet({ radius = 3, tags = [seg01, seg02] }, %)` |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01, seg02] }, %)`
await runModifyAstCloneWithFilletAndTag( await runModifyAstCloneWithEdgeTreatmentAndTag(
code, code,
segmentSnippets, segmentSnippets,
radiusValue, parameters,
expectedCode expectedCode
) )
}) })
it('should add fillets to two bodies', async () => { it(`should add ${edgeTreatmentType}s to two bodies`, async () => {
const code = `sketch001 = startSketchOn('XY') const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %) |> line([20, 0], %)
@ -503,7 +552,6 @@ extrude002 = extrude(-25, sketch002)` // <--- body 2
'line([-20, 0], %)', 'line([-20, 0], %)',
'line([0, -15], %)', 'line([0, -15], %)',
] ]
const radiusValue = 3
const expectedCode = `sketch001 = startSketchOn('XY') const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01) |> line([20, 0], %, $seg01)
@ -512,7 +560,7 @@ extrude002 = extrude(-25, sketch002)` // <--- body 2
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> fillet({ radius = 3, tags = [seg01, seg02] }, %) |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01, seg02] }, %)
sketch002 = startSketchOn('XY') sketch002 = startSketchOn('XY')
|> startProfileAt([30, 10], %) |> startProfileAt([30, 10], %)
|> line([15, 0], %) |> line([15, 0], %)
@ -521,18 +569,20 @@ sketch002 = startSketchOn('XY')
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude002 = extrude(-25, sketch002) extrude002 = extrude(-25, sketch002)
|> fillet({ radius = 3, tags = [seg03] }, %)` // <-- able to add a new one |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg03] }, %)` // <-- able to add a new one
await runModifyAstCloneWithFilletAndTag( await runModifyAstCloneWithEdgeTreatmentAndTag(
code, code,
segmentSnippets, segmentSnippets,
radiusValue, parameters,
expectedCode expectedCode
) )
}) })
}) })
}
)
describe('Testing isTagUsedInFillet', () => { describe('Testing isTagUsedInEdgeTreatment', () => {
const code = `sketch001 = startSketchOn('XZ') const code = `sketch001 = startSketchOn('XZ')
|> startProfileAt([7.72, 4.13], %) |> startProfileAt([7.72, 4.13], %)
|> line([7.11, 3.48], %, $seg01) |> line([7.11, 3.48], %, $seg01)
@ -550,12 +600,12 @@ extrude001 = extrude(-5, sketch001)
}, %) }, %)
` `
it('should correctly identify getOppositeEdge and baseEdge edges', () => { it('should correctly identify getOppositeEdge and baseEdge edges', () => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) return
const lineOfInterest = `line([7.11, 3.48], %, $seg01)` const lineOfInterest = `line([7.11, 3.48], %, $seg01)`
const range: [number, number] = [ const range: [number, number, boolean] = [
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
] ]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return if (err(pathToNode)) return
@ -565,16 +615,16 @@ extrude001 = extrude(-5, sketch001)
'CallExpression' 'CallExpression'
) )
if (err(callExp)) return if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node }) const edges = isTagUsedInEdgeTreatment({ ast, callExp: callExp.node })
expect(edges).toEqual(['getOppositeEdge', 'baseEdge']) expect(edges).toEqual(['getOppositeEdge', 'baseEdge'])
}) })
it('should correctly identify getPreviousAdjacentEdge edges', () => { it('should correctly identify getPreviousAdjacentEdge edges', () => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) return
const lineOfInterest = `line([-6.37, 3.88], %, $seg02)` const lineOfInterest = `line([-6.37, 3.88], %, $seg02)`
const range: [number, number] = [ const range: [number, number, boolean] = [
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
] ]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return if (err(pathToNode)) return
@ -584,16 +634,16 @@ extrude001 = extrude(-5, sketch001)
'CallExpression' 'CallExpression'
) )
if (err(callExp)) return if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node }) const edges = isTagUsedInEdgeTreatment({ ast, callExp: callExp.node })
expect(edges).toEqual(['getPreviousAdjacentEdge']) expect(edges).toEqual(['getPreviousAdjacentEdge'])
}) })
it('should correctly identify no edges', () => { it('should correctly identify no edges', () => {
const ast = parse(code) const ast = assertParse(code)
if (err(ast)) return
const lineOfInterest = `line([-3.29, -13.85], %)` const lineOfInterest = `line([-3.29, -13.85], %)`
const range: [number, number] = [ const range: [number, number, boolean] = [
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
] ]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return if (err(pathToNode)) return
@ -603,7 +653,7 @@ extrude001 = extrude(-5, sketch001)
'CallExpression' 'CallExpression'
) )
if (err(callExp)) return if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node }) const edges = isTagUsedInEdgeTreatment({ ast, callExp: callExp.node })
expect(edges).toEqual([]) expect(edges).toEqual([])
}) })
}) })
@ -614,19 +664,15 @@ describe('Testing button states', () => {
segmentSnippet: string, segmentSnippet: string,
expectedState: boolean expectedState: boolean
) => { ) => {
// ast const ast = assertParse(code)
const astOrError = parse(code)
if (err(astOrError)) {
return new Error('AST not found')
}
const ast = astOrError
const range: [number, number] = segmentSnippet const range: [number, number, boolean] = segmentSnippet
? [ ? [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length, code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
] ]
: [ast.end, ast.end] // empty line in the end of the code : [ast.end, ast.end, true] // empty line in the end of the code
const selectionRanges: Selections = { const selectionRanges: Selections = {
graphSelections: [ graphSelections: [
@ -638,7 +684,7 @@ describe('Testing button states', () => {
} }
// state // state
const buttonState = hasValidFilletSelection({ const buttonState = hasValidEdgeTreatmentSelection({
ast, ast,
selectionRanges, selectionRanges,
code, code,

View File

@ -44,32 +44,49 @@ import {
} from 'lib/singletons' } from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
// Apply Fillet To Selection // Edge Treatment Types
export enum EdgeTreatmentType {
Chamfer = 'chamfer',
Fillet = 'fillet',
}
export function applyFilletToSelection( export interface ChamferParameters {
type: EdgeTreatmentType.Chamfer
length: KclCommandValue
}
export interface FilletParameters {
type: EdgeTreatmentType.Fillet
radius: KclCommandValue
}
export type EdgeTreatmentParameters = ChamferParameters | FilletParameters
// Apply Edge Treatment (Fillet or Chamfer) To Selection
export function applyEdgeTreatmentToSelection(
ast: Node<Program>, ast: Node<Program>,
selection: Selections, selection: Selections,
radius: KclCommandValue parameters: EdgeTreatmentParameters
): void | Error { ): void | Error {
// 1. clone and modify with fillet and tag // 1. clone and modify with edge treatment and tag
const result = modifyAstWithFilletAndTag(ast, selection, radius) const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
if (err(result)) return result if (err(result)) return result
const { modifiedAst, pathToFilletNode } = result const { modifiedAst, pathToEdgeTreatmentNode } = result
// 2. update ast // 2. update ast
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
updateAstAndFocus(modifiedAst, pathToFilletNode) updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
} }
export function modifyAstWithFilletAndTag( export function modifyAstWithEdgeTreatmentAndTag(
ast: Node<Program>, ast: Node<Program>,
selections: Selections, selections: Selections,
radius: KclCommandValue parameters: EdgeTreatmentParameters
): { modifiedAst: Node<Program>; pathToFilletNode: Array<PathToNode> } | Error { ):
| { modifiedAst: Node<Program>; pathToEdgeTreatmentNode: Array<PathToNode> }
| Error {
let clonedAst = structuredClone(ast) let clonedAst = structuredClone(ast)
const clonedAstForGetExtrude = structuredClone(ast) const clonedAstForGetExtrude = structuredClone(ast)
const astResult = insertRadiusIntoAst(clonedAst, radius) const astResult = insertParametersIntoAst(clonedAst, parameters)
if (err(astResult)) return astResult if (err(astResult)) return astResult
const artifactGraph = engineCommandManager.artifactGraph const artifactGraph = engineCommandManager.artifactGraph
@ -119,21 +136,26 @@ export function modifyAstWithFilletAndTag(
} }
} }
// Step 2: Apply fillet(s) for each extrude node (body) // Step 2: Apply edge treatments for each extrude node (body)
let pathToFilletNodes: Array<PathToNode> = [] let pathToEdgeTreatmentNodes: Array<PathToNode> = []
for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) { for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) {
// Create a fillet expression with multiple tags // Create an edge treatment expression with multiple tags
const radiusValue =
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
// edge treatment parameter
const parameterResult = getParameterNameAndValue(parameters)
if (err(parameterResult)) return parameterResult
const { parameterName, parameterValue } = parameterResult
// tag calls
const tagCalls = tagInfos.map(({ tag, artifact }) => { const tagCalls = tagInfos.map(({ tag, artifact }) => {
return getEdgeTagCall(tag, artifact) return getEdgeTagCall(tag, artifact)
}) })
const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges) const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges)
const filletCall = createCallExpressionStdLib('fillet', [ // edge treatment call
const edgeTreatmentCall = createCallExpressionStdLib(parameters.type, [
createObjectExpression({ createObjectExpression({
radius: radiusValue, [parameterName]: parameterValue,
tags: createArrayExpression(tagCalls), tags: createArrayExpression(tagCalls),
}), }),
createPipeSubstitution(), createPipeSubstitution(),
@ -147,64 +169,89 @@ export function modifyAstWithFilletAndTag(
if (err(locatedExtrudeDeclarator)) return locatedExtrudeDeclarator if (err(locatedExtrudeDeclarator)) return locatedExtrudeDeclarator
const { extrudeDeclarator } = locatedExtrudeDeclarator const { extrudeDeclarator } = locatedExtrudeDeclarator
// Modify the extrude expression to include this fillet expression // Modify the extrude expression to include this edge treatment expression
// CallExpression - no fillet // CallExpression - no edge treatment
// PipeExpression - fillet exists or extrude in sketch pipe // PipeExpression - edge treatment exists or body in sketch pipe
let pathToFilletNode: PathToNode = [] let pathToEdgeTreatmentNode: PathToNode
if (extrudeDeclarator.init.type === 'CallExpression') { if (extrudeDeclarator.init.type === 'CallExpression') {
// 1. case when no fillet exists // 1. case when no edge treatment exists
// modify ast with new fillet call by mutating the extrude node // modify ast with new edge treatment call by mutating the extrude node
extrudeDeclarator.init = createPipeExpression([ extrudeDeclarator.init = createPipeExpression([
extrudeDeclarator.init, extrudeDeclarator.init,
filletCall, edgeTreatmentCall,
]) ])
// get path to the fillet node // get path to the edge treatment node
pathToFilletNode = getPathToNodeOfFilletLiteral( pathToEdgeTreatmentNode = getPathToNodeOfEdgeTreatmentLiteral(
pathToExtrudeNode, pathToExtrudeNode,
extrudeDeclarator, extrudeDeclarator,
firstTag firstTag,
parameters
) )
pathToFilletNodes.push(pathToFilletNode) pathToEdgeTreatmentNodes.push(pathToEdgeTreatmentNode)
} else if (extrudeDeclarator.init.type === 'PipeExpression') { } else if (extrudeDeclarator.init.type === 'PipeExpression') {
// 2. case when fillet exists or extrude in sketch pipe // 2. case when edge treatment exists or extrude in sketch pipe
// mutate the extrude node with the new fillet call // mutate the extrude node with the new edge treatment call
extrudeDeclarator.init.body.push(filletCall) extrudeDeclarator.init.body.push(edgeTreatmentCall)
// get path to the fillet node // get path to the edge treatment node
pathToFilletNode = getPathToNodeOfFilletLiteral( pathToEdgeTreatmentNode = getPathToNodeOfEdgeTreatmentLiteral(
pathToExtrudeNode, pathToExtrudeNode,
extrudeDeclarator, extrudeDeclarator,
firstTag firstTag,
parameters
) )
pathToFilletNodes.push(pathToFilletNode) pathToEdgeTreatmentNodes.push(pathToEdgeTreatmentNode)
} else { } else {
return new Error('Unsupported extrude type.') return new Error('Unsupported extrude type.')
} }
} }
return { modifiedAst: clonedAst, pathToFilletNode: pathToFilletNodes } return {
modifiedAst: clonedAst,
pathToEdgeTreatmentNode: pathToEdgeTreatmentNodes,
}
} }
function insertRadiusIntoAst( function insertParametersIntoAst(
ast: Program, ast: Program,
radius: KclCommandValue parameters: EdgeTreatmentParameters
): { ast: Program } | Error { ): { ast: Program } | Error {
try { try {
// Validate and update AST
if (
'variableName' in radius &&
radius.variableName &&
radius.insertIndex !== undefined
) {
const newAst = structuredClone(ast) const newAst = structuredClone(ast)
newAst.body.splice(radius.insertIndex, 0, radius.variableDeclarationAst)
return { ast: newAst } // handle radius parameter
if (
parameters.type === EdgeTreatmentType.Fillet &&
'variableName' in parameters.radius &&
parameters.radius.variableName &&
parameters.radius.insertIndex !== undefined
) {
newAst.body.splice(
parameters.radius.insertIndex,
0,
parameters.radius.variableDeclarationAst
)
} }
return { ast } // handle length parameter
if (
parameters.type === EdgeTreatmentType.Chamfer &&
'variableName' in parameters.length &&
parameters.length.variableName &&
parameters.length.insertIndex !== undefined
) {
newAst.body.splice(
parameters.length.insertIndex,
0,
parameters.length.variableDeclarationAst
)
}
// handle upcoming parameters here (for blend, bevel, etc.)
return { ast: newAst }
} catch (error) { } catch (error) {
return new Error(`Failed to handle AST: ${(error as Error).message}`) return new Error(`Failed to handle AST: ${(error as Error).message}`)
} }
@ -226,7 +273,7 @@ export function getPathToExtrudeForSegmentSelection(
'VariableDeclaration' 'VariableDeclaration'
) )
if (err(varDecNode)) return varDecNode if (err(varDecNode)) return varDecNode
const sketchVar = varDecNode.node.declarations[0].id.name const sketchVar = varDecNode.node.declaration.id.name
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
kclManager.programMemory.get(sketchVar), kclManager.programMemory.get(sketchVar),
@ -248,10 +295,10 @@ export function getPathToExtrudeForSegmentSelection(
async function updateAstAndFocus( async function updateAstAndFocus(
modifiedAst: Node<Program>, modifiedAst: Node<Program>,
pathToFilletNode: Array<PathToNode> pathToEdgeTreatmentNode: Array<PathToNode>
) { ) {
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode, focusPath: pathToEdgeTreatmentNode,
}) })
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst) await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
@ -320,7 +367,7 @@ function locateExtrudeDeclarator(
if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall
const { node: extrudeVarDecl } = nodeOfExtrudeCall const { node: extrudeVarDecl } = nodeOfExtrudeCall
const extrudeDeclarator = extrudeVarDecl.declarations[0] const extrudeDeclarator = extrudeVarDecl.declaration
if (!extrudeDeclarator) { if (!extrudeDeclarator) {
return new Error('Extrude Declarator not found.') return new Error('Extrude Declarator not found.')
} }
@ -340,27 +387,38 @@ function locateExtrudeDeclarator(
return { extrudeDeclarator } return { extrudeDeclarator }
} }
function getPathToNodeOfFilletLiteral( function getPathToNodeOfEdgeTreatmentLiteral(
pathToExtrudeNode: PathToNode, pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator, extrudeDeclarator: VariableDeclarator,
tag: Identifier | CallExpression tag: Identifier | CallExpression,
parameters: EdgeTreatmentParameters
): PathToNode { ): PathToNode {
let pathToFilletObj: PathToNode = [] let pathToEdgeTreatmentObj: PathToNode = []
let inFillet = false let inEdgeTreatment = false
traverse(extrudeDeclarator.init, { traverse(extrudeDeclarator.init, {
enter(node, path) { enter(node, path) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') { if (
inFillet = true node.type === 'CallExpression' &&
node.callee.name === parameters.type
) {
inEdgeTreatment = true
} }
if (inFillet && node.type === 'ObjectExpression') { if (inEdgeTreatment && node.type === 'ObjectExpression') {
if (!hasTag(node, tag)) return false if (!hasTag(node, tag)) return false
pathToFilletObj = getPathToRadiusLiteral(node, path) pathToEdgeTreatmentObj = getPathToEdgeTreatmentParameterLiteral(
node,
path,
parameters
)
} }
}, },
leave(node) { leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') { if (
inFillet = false node.type === 'CallExpression' &&
node.callee.name === parameters.type
) {
inEdgeTreatment = false
} }
}, },
}) })
@ -375,7 +433,7 @@ function getPathToNodeOfFilletLiteral(
return [ return [
...pathToExtrudeNode.slice(0, indexOfPipeExpression), ...pathToExtrudeNode.slice(0, indexOfPipeExpression),
...pathToFilletObj, ...pathToEdgeTreatmentObj,
] ]
} }
@ -408,23 +466,62 @@ function hasTag(
}) })
} }
function getPathToRadiusLiteral(node: ObjectExpression, path: any): PathToNode { function getPathToEdgeTreatmentParameterLiteral(
let pathToFilletObj = path node: ObjectExpression,
path: any,
parameters: EdgeTreatmentParameters
): PathToNode {
let pathToEdgeTreatmentObj = path
const parameterResult = getParameterNameAndValue(parameters)
if (err(parameterResult)) return pathToEdgeTreatmentObj
const { parameterName } = parameterResult
node.properties.forEach((prop, index) => { node.properties.forEach((prop, index) => {
if (prop.key.name === 'radius') { if (prop.key.name === parameterName) {
pathToFilletObj.push( pathToEdgeTreatmentObj.push(
['properties', 'ObjectExpression'], ['properties', 'ObjectExpression'],
[index, 'index'], [index, 'index'],
['value', 'Property'] ['value', 'Property']
) )
} }
}) })
return pathToFilletObj return pathToEdgeTreatmentObj
}
function getParameterNameAndValue(
parameters: EdgeTreatmentParameters
): { parameterName: string; parameterValue: Expr } | Error {
if (parameters.type === EdgeTreatmentType.Fillet) {
const parameterValue =
'variableName' in parameters.radius
? parameters.radius.variableIdentifierAst
: parameters.radius.valueAst
return { parameterName: 'radius', parameterValue }
} else if (parameters.type === EdgeTreatmentType.Chamfer) {
const parameterValue =
'variableName' in parameters.length
? parameters.length.variableIdentifierAst
: parameters.length.valueAst
return { parameterName: 'length', parameterValue }
} else {
return new Error('Unsupported edge treatment type}')
}
}
// Type Guards
function isEdgeTreatmentType(name: string): name is EdgeTreatmentType {
return name === EdgeTreatmentType.Chamfer || name === EdgeTreatmentType.Fillet
}
function isEdgeType(name: string): name is EdgeTypes {
return (
name === 'getNextAdjacentEdge' ||
name === 'getPreviousAdjacentEdge' ||
name === 'getOppositeEdge'
)
} }
// Button states // Button states
export const hasValidEdgeTreatmentSelection = ({
export const hasValidFilletSelection = ({
selectionRanges, selectionRanges,
ast, ast,
code, code,
@ -433,11 +530,14 @@ export const hasValidFilletSelection = ({
ast: Node<Program> ast: Node<Program>
code: string code: string
}) => { }) => {
// check if there is anything filletable in the scene // check if there is anything valid for the edge treatment in the scene
let extrudeExists = false let extrudeExists = false
traverse(ast, { traverse(ast, {
enter(node) { enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'extrude') { if (
node.type === 'CallExpression' &&
(node.callee.name === 'extrude' || node.callee.name === 'revolve')
) {
extrudeExists = true extrudeExists = true
} }
}, },
@ -494,32 +594,39 @@ export const hasValidFilletSelection = ({
}, },
}) })
// check if tag is used in fillet // check if tag is used in edge treatment
if (tagExists && selection.artifact) { if (tagExists && selection.artifact) {
// create tag call // create tag call
let tagCall: Expr = getEdgeTagCall(tag, selection.artifact) let tagCall: Expr = getEdgeTagCall(tag, selection.artifact)
// check if tag is used in fillet // check if tag is used in edge treatment
let inFillet = false let inEdgeTreatment = false
let tagUsedInFillet = false let tagUsedInEdgeTreatment = false
traverse(ast, { traverse(ast, {
enter(node) { enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') { if (
inFillet = true node.type === 'CallExpression' &&
isEdgeTreatmentType(node.callee.name)
) {
inEdgeTreatment = true
} }
if (inFillet && node.type === 'ObjectExpression') { if (inEdgeTreatment && node.type === 'ObjectExpression') {
if (hasTag(node, tagCall)) { if (hasTag(node, tagCall)) {
tagUsedInFillet = true tagUsedInEdgeTreatment = true
} }
} }
}, },
leave(node) { leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') { if (
inFillet = false node.type === 'CallExpression' &&
isEdgeTreatmentType(node.callee.name)
) {
inEdgeTreatment = false
} }
}, },
}) })
if (tagUsedInFillet) { if (tagUsedInEdgeTreatment) {
return false return false
} }
} }
@ -533,7 +640,7 @@ type EdgeTypes =
| 'getPreviousAdjacentEdge' | 'getPreviousAdjacentEdge'
| 'getOppositeEdge' | 'getOppositeEdge'
export const isTagUsedInFillet = ({ export const isTagUsedInEdgeTreatment = ({
ast, ast,
callExp, callExp,
}: { }: {
@ -543,16 +650,21 @@ export const isTagUsedInFillet = ({
const tag = getTagFromCallExpression(callExp) const tag = getTagFromCallExpression(callExp)
if (err(tag)) return [] if (err(tag)) return []
let inFillet = false let inEdgeTreatment = false
let inObj = false let inObj = false
let inTagHelper: EdgeTypes | '' = '' let inTagHelper: EdgeTypes | '' = ''
const edges: Array<EdgeTypes> = [] const edges: Array<EdgeTypes> = []
traverse(ast, { traverse(ast, {
enter: (node) => { enter: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') { // Check if we are entering an edge treatment call
inFillet = true if (
node.type === 'CallExpression' &&
isEdgeTreatmentType(node.callee.name)
) {
inEdgeTreatment = true
} }
if (inFillet && node.type === 'ObjectExpression') { if (inEdgeTreatment && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => { node.properties.forEach((prop) => {
if ( if (
prop.key.name === 'tags' && prop.key.name === 'tags' &&
@ -564,17 +676,15 @@ export const isTagUsedInFillet = ({
} }
if ( if (
inObj && inObj &&
inFillet && inEdgeTreatment &&
node.type === 'CallExpression' && node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' || isEdgeType(node.callee.name)
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) { ) {
inTagHelper = node.callee.name inTagHelper = node.callee.name
} }
if ( if (
inObj && inObj &&
inFillet && inEdgeTreatment &&
!inTagHelper && !inTagHelper &&
node.type === 'Identifier' && node.type === 'Identifier' &&
node.name === tag node.name === tag
@ -583,7 +693,7 @@ export const isTagUsedInFillet = ({
} }
if ( if (
inObj && inObj &&
inFillet && inEdgeTreatment &&
inTagHelper && inTagHelper &&
node.type === 'Identifier' && node.type === 'Identifier' &&
node.name === tag node.name === tag
@ -592,10 +702,13 @@ export const isTagUsedInFillet = ({
} }
}, },
leave: (node) => { leave: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') { if (
inFillet = false node.type === 'CallExpression' &&
isEdgeTreatmentType(node.callee.name)
) {
inEdgeTreatment = false
} }
if (inFillet && node.type === 'ObjectExpression') { if (inEdgeTreatment && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => { node.properties.forEach((prop) => {
if ( if (
prop.key.name === 'tags' && prop.key.name === 'tags' &&
@ -607,11 +720,9 @@ export const isTagUsedInFillet = ({
} }
if ( if (
inObj && inObj &&
inFillet && inEdgeTreatment &&
node.type === 'CallExpression' && node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' || isEdgeType(node.callee.name)
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) { ) {
inTagHelper = '' inTagHelper = ''
} }

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