Compare commits

...

55 Commits

Author SHA1 Message Date
6399e9ce6b A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-18 05:30:06 +00:00
11c1f55ac2 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-18 05:29:25 +00:00
13834d3477 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) 2024-10-18 05:23:25 +00:00
728e624d42 2024-10-18 01:19:15 -04:00
785002fa4e Live reload on file tree changes and project settings changes (#4142)
* Reload FileTree and File when changed externally

* Added tests

* Make file and project creation a bit more reliable
2024-10-17 23:42:24 -04:00
f650281855 KCL: Fix duplicate 'type' key 2024-10-17 20:37:05 -07:00
9f6999829a Fix broken digest code (#4206)
I broke the typescript bindings in https://github.com/KittyCAD/modeling-app/pull/4193 -- basically the `digest: Option<Digest>` fields previously allowed Typescript to not set a value for the `digest` key at all, but somehow my change made it required in the Typescript.

Fix is to apply `#[ts(optional)]`, see docs at https://docs.rs/ts-rs/latest/ts_rs/trait.TS.html#struct-field-attributes
2024-10-17 20:36:40 -07:00
a14bbaa237 Update machine-api spec (#4205)
* YOYO NEW API SPEC!

* New machine-api types

* empty

* Update .codespellrc

* Update .codespellrc

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-17 20:01:34 -07:00
0706624381 KCL: Refactor array-indexing code (#4173)
Neater code, and better error messages.
2024-10-17 16:29:27 -07:00
ef0ae5e06e Add syntax highlighting for comparison operators (#4182)
* Add syntax highlighting for comparison operators

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

* Confirm

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-17 23:29:13 +00:00
a010743abb KCL: Skip serializing default values for AST nodes (#4193)
This will make our snapshots and JSON representations easier to read (making our tests less verbose).
2024-10-17 16:22:40 -07:00
057ee479c3 Update machine-api spec (#4201)
* YOYO NEW API SPEC!

* New machine-api types

* empt

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2024-10-17 16:03:55 -07:00
7218efc489 Fix weird machine api behavior/add status (#4186)
* YOYO NEW API SPEC!

* fies

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

* add status

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

* pass disabled

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

* disabled

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

* add nozzle diameter

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

* ypdates

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

* update types

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

* update types

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

* update types

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>
Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
2024-10-17 15:30:46 -07:00
b6dd6e7dd0 Release kcl-test-server 0.1.14 (#4200) 2024-10-17 15:29:05 -07:00
47af18f533 Split artifacts per arch and re-enable updater for nightly builds (#4118)
* Split builds per os and per architecture to minimize artifacts size
Fixes #3920

* Add missing if case; Add nightly build updater support

* Fix linux x86_64

* Improve upload filtering

* Attempt to fix matrix

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

* Test release builds

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

* Clean up

* Clean up win arm64 builds

* Add temp fake-publish to check on artifacts

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

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

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

* Trigger CI

* Fix win arm

* Clean up

* Remove forced release builds

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-17 15:08:31 -07:00
0505220dac KCL refactor: Move compute_digest into its own module (#4176)
* KCL refactor: Move compute_digest into its own module

* Move if-else execute into execute.rs
2024-10-17 12:30:44 -07:00
f7711b71d6 Add notes around updater-test (#4134)
* Add notes around updater-test

* Fix typos
2024-10-17 10:43:24 -07:00
0255fde5fe Release KCL v0.2.22 (#4187) 2024-10-17 09:23:08 -07:00
ebade29ed0 Bump image from 0.25.2 to 0.25.3 in /src/wasm-lib (#4180)
Bumps [image](https://github.com/image-rs/image) from 0.25.2 to 0.25.3.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.2...v0.25.3)

---
updated-dependencies:
- dependency-name: image
  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-10-16 23:35:55 -07:00
582d37e51b Bump uuid from 1.10.0 to 1.11.0 in /src/wasm-lib (#4179)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.10.0...1.11.0)

---
updated-dependencies:
- dependency-name: uuid
  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-10-16 23:35:47 -07:00
4ef9429842 Bump proc-macro2 from 1.0.86 to 1.0.88 in /src/wasm-lib (#4178)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.86 to 1.0.88.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.86...1.0.88)

---
updated-dependencies:
- dependency-name: proc-macro2
  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-10-16 23:35:33 -07:00
0577b6a984 internal: KCL modules, part 1 (#4149)
Addresses #4080. (Not ready to close it yet.)

# Important

Requires a fix for #4147 before it can work in ZMA.

# Overview

```kcl
// numbers.kcl
export fn inc = (x) => {
  return x + 1
}
```

```kcl
import inc from "numbers.kcl"

answer = inc(41)
```

This also implements multiple imports with optional renaming.

```kcl
import inc, dec from "numbers.kcl"
import identity as id, length as len from "utils.kcl"
```

Note: Imported files _must_ be in the same directory.

Things for a follow-up PR:

- #4147. Currently, we cannot read files in WebAssembly, i.e. ZMA.
- Docs
- Should be an error to `import` anywhere besides the top level. Needs parser restructuring to track the context of a "function body".
- Should be an error to have `export` anywhere besides the top level. It has no effect, but we should tell people it's not valid instead of silently ignoring it.
- Error message for cycle detection is funky because the Rust side doesn't actually know the name of the first file. Message will say "b -> a -> b" instead of "a -> b -> a" when "a" is the top-level file.
- Cache imported files so that they don't need to be re-parsed and re-executed.
2024-10-16 21:48:33 -07:00
7d44de0c12 KCL refactor: Move execution into its own file (#4174)
types.rs is really, really big. I've moved executing AST nodes into
their own file.
2024-10-16 14:33:03 -07:00
f7d5313588 CI: Remove rust-cache from static analysis (#4175)
* CI: Remove rust-cache from static analysis

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

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

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

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-16 14:31:42 -07:00
bd4783e885 Bump web-sys from 0.3.70 to 0.3.72 in /src/wasm-lib (#4153)
Bumps [web-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.70 to 0.3.72.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: web-sys
  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-10-16 10:12:37 -07:00
8794696b26 Bump pyo3 from 0.22.4 to 0.22.5 in /src/wasm-lib (#4170)
Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.22.4 to 0.22.5.
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.22.4...v0.22.5)

---
updated-dependencies:
- dependency-name: pyo3
  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-10-16 10:11:17 -07:00
1c2e415c70 Permitting whitespace before colon (#4171)
Fixes #4150 by ignoring whitespace before a colon just like how whitespace after a colon is already ignored.
Added snapshot parser::parser_impl::snapshot_tests::bh for a kcl snippet that has whitespace before a colon.

Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-10-16 10:00:52 -07:00
248ef8ebb3 Implement dynamic ranges (#4151)
Closes #4021

Allows array ranges (e.g., `[0..10]`) to take expression instead of just numeric literals as their start and end values. Both expressions are required (we don't support `[0..]`, etc.).

I've created a new kind of expression in the AST. The alternative was to represent the internals of an array as some kind of pattern which could initially be fully explicit or ranges. I figured the chosen version was simpler and easier to extend to open ranges, whereas the latter would be easier to extend to mixed ranges or other patterns. I chose simpler, it'll be easy enough to refactor if necessary.

Parsing is tested implicitly by the tests of execution and unparsing.

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2024-10-16 06:58:04 -07:00
fbac9935fe Bugfix: double map (#4169)
Previously, map was wrapping KCL values in a JSON object unnecessarily.  The new `double_map` test would emit this error:

```
Syntax(KclErrorDetails {
 source_ranges: [SourceRange([31, 32])],
 message: "Invalid number: {\"type\":\"UserVal\",\"value\":1.0,\"__meta\":[{\"sourceRange\":[31,36]}]}"
})
```

In other words, the second `map` statement is being passed an array of JSON STRINGS, not an array of numbers.
The strings contain JSON stringified representations of user values which are numbers.

Bug is now fixed.
2024-10-15 15:58:04 -07:00
b4c171a347 Bump pyo3 from 0.22.3 to 0.22.4 in /src/wasm-lib (#4154)
Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.22.3 to 0.22.4.
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.22.3...v0.22.4)

---
updated-dependencies:
- dependency-name: pyo3
  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-10-15 15:10:59 -07:00
0811d9fa4e Rename things to be clearer about expression vs. value (#4163) 2024-10-15 17:33:20 -04:00
1efc2b9762 KCL: Pattern repetitions now pattern instances (#4162)
Josh Gomez requests pattern calculations take the total number of instances,
not the number of extra repetitions to do. This is how we designed the
patternTransform API, but we didn't do that for patternLinear/Circular.
2024-10-15 13:25:03 -07:00
d361bda180 Add collapsible element to updater notification to show changelog, open if there breaking changes (#4051)
* Add collapsible element to updater toast notification showing release notes

* Temp create release artifacts to test updater

* Fix tsc error

* Fix some styling, make release notes not appear if no notes are present

* Add component tests

* Remove test release builds

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2024-10-15 07:30:00 -04:00
1d3ade114f KCL: Comparison operators (#4025)
Add `==, !=, >, >=, <, <=` to the language parser, executor and tests.

Closes https://github.com/KittyCAD/modeling-app/issues/4020

Currently these comparison operators are associative, allowing users to chain them, e.g. (x <= y <= z). This should not be allowed, will do in a follow-up. See https://github.com/KittyCAD/modeling-app/issues/4155
2024-10-14 10:46:39 -07:00
max
3382b66075 Add support for opposite and adjacent edges for fillets (#4103)
* add support for opposite and adjacent edges

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

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

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

* update playwright

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

* update unit tests

* enable button state checker for selections

* typos

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

* trigger ci

* fix typo

* remove leave(node)

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* typo

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

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

* pull getEdgeTagCall  into a utility function

* mask model-state-indicator

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

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

* Rerun CI

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

* Rerun CI

* screenshot fix

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-10-11 23:25:51 +02:00
5e8b5c254d Update machine-api spec (#4121)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-11 12:49:32 -07:00
b99b2d9a96 Bump js-sys from 0.3.71 to 0.3.72 in /src/wasm-lib (#4139)
Bumps [js-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.71 to 0.3.72.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: js-sys
  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-10-11 12:09:48 -07:00
81041661c7 bump test server 2024-10-11 14:31:21 -04:00
9d99b5be7f bump kcl (#4144) 2024-10-11 14:26:46 -04:00
85a39109f8 Don't mutate the user's codeBasedSelections when they apply a multi-selection constraint (#4141)
* Don't mutate the user's `codeBasedSelections` when they apply a multi-selection constraint

* Fix to not mutate the input parameter

* Format

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-10-11 12:53:33 -04:00
23c2aa948a Fix bug with selection order on multi-select constraints (#4138)
* Sort selections in order of appearance before applying `transformSecondarySketchLinesTagFirst`

* Add an integration test for this sorting behavior

* Remove console logs from test
2024-10-11 09:49:58 -04:00
1fd4aa9ede Upload release-notes.md on release (#4123)
* Upload release-notes.md on release
Contributes to #3984

* Test release builds with updater-test-release-notes

* Change to releaseNotesFile conf, tested locally

* Clean up for merge
2024-10-11 06:16:39 -04:00
e8a9fb7f55 Revert "Sort selections in order of appearance before applying transformSecondarySketchLinesTagFirst"
This reverts commit b1ccc6df0f.
2024-10-10 20:35:15 -04:00
cc4345b7c3 Revert "Add an integration test for this sorting behavior"
This reverts commit 6035e834c2.
2024-10-10 20:35:07 -04:00
6035e834c2 Add an integration test for this sorting behavior 2024-10-10 20:32:52 -04:00
b1ccc6df0f Sort selections in order of appearance before applying transformSecondarySketchLinesTagFirst 2024-10-10 20:32:35 -04:00
9563bd322c Bump js-sys from 0.3.70 to 0.3.71 in /src/wasm-lib (#4133)
Bumps [js-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.70 to 0.3.71.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: js-sys
  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-10-10 10:04:30 -07:00
1e35c03dc8 Bump wasm-bindgen-futures from 0.4.43 to 0.4.44 in /src/wasm-lib (#4132)
Bumps [wasm-bindgen-futures](https://github.com/rustwasm/wasm-bindgen) from 0.4.43 to 0.4.44.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: wasm-bindgen-futures
  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-10-10 10:04:13 -07:00
7caa0aff7b Add a loading toast when auto-updater download begins (#3995)
* Add a loading toast when update download begins

* Temporarily turn on artifact builds to test

* fmt

* Oops forgot to rename where I used a type

* Update src/index.tsx

* Update src/index.tsx

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

* Add an `update-error` listener

* Flatten autoupdater hooks

* Revert "Temporarily turn on artifact builds to test"

This reverts commit e2e1991977.

* Switch branch to build-publish-apps on to this one

* Make the loading toast not attempt to display a version number

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

* fmt

* Update .github/workflows/build-test-publish-apps.yml

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2024-10-10 12:16:45 -04:00
accbc1fc3b Remove a few E2E workflows for test setup that have proven flaky on Windows (#4135)
* Remove a few E2E workflows for test setup that have proven flaky on Windows

* fix lint
2024-10-10 11:38:24 -04:00
05b21f100c Update README.md (#4130)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-10-10 12:59:19 +13:00
0fb5ff7f10 Change artifact IDs to be stable across KCL executions (#4101)
* Add ID generator to ExecState

* Change default plane IDs to be hardcoded

* Fix lint warning

* Add exposing ID generator as output of executor

* Change to use generated definition of ExecState in TS

* Fix IdGenerator to use camel case in TS

* Fix TS type errors

* Add exposing id_generator parameter

* Add using the previously generated ID generator

* wip: Add display of feature tree in debug pane

* Remove artifact graph augmentation

* Change default planes to use id generator instead of hardcoded UUIDs

* Fix to reuse previously generated IDs

* Add e2e test

* Change feature tree to be collapsed by default

* Remove debug prints

* Fix unit test to use execState

* Fix type to be more general

* Remove outdated comment

* Update derive-docs output

* Fix object display component to be more general

* Remove unused ArtifactId type

* Fix test to be less brittle

* Remove codeRef and pathToNode from display

* Fix to remove test.only

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

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

* Move plane conversion code to be next to type

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

This reverts commit 3455cc951b.

* Rename file

* Rename components and add doc comments

* Revive the collapse button

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

* Confirm

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

* Confirm

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

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

* Confirm

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-09 19:38:40 -04:00
e525b319d0 Bump clap from 4.5.19 to 4.5.20 in /src/wasm-lib (#4122)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.19 to 4.5.20.
- [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.19...clap_complete-v4.5.20)

---
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-10-09 09:43:53 -07:00
01c6774c54 Nadro/2608/sketch mode scene state improvements (#3866)
* bug: fixing multiple state issues with the engine and modeling app to enable/disable planes/axis/delete code

* fix: yarn tsc fmt lint xgen

* fix: adding a comment back that I deleted on accident

* fix: adding formatting back?

* fix: reverting syntax

* fix: removing click line tool because the line tool is automatically selected. Clicking this will exit

* fix: Fixed a E2E test that had a line tool workflow with no points

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-10-09 09:33:20 -05:00
b745cec079 KCL docs: Better docs for KclValue (#4096)
* KCL docs: Better docs for KclValue

* Update docs

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-10-09 03:56:38 +00:00
266 changed files with 8502 additions and 4901 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts

View File

@ -25,6 +25,7 @@ jobs:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows) runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs: outputs:
version: ${{ steps.export_version.outputs.version }} version: ${{ steps.export_version.outputs.version }}
notes: ${{ steps.export_version.outputs.notes }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -53,22 +54,46 @@ jobs:
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json # TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
- name: Generate release notes
env:
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
run: |
echo "$NOTES" > release-notes.md
cat release-notes.md
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: prepared-files name: prepared-files
path: | path: |
package.json package.json
src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg/wasm_lib*
release-notes.md
- id: export_version - id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT" run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for nightly
if: ${{ github.event_name == 'schedule' }}
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' }}
with:
name: prepared-files-nightly
path: |
electron-builder.yml
- id: export_notes
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for updater test - name: Prepare electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }} if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: | run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with: with:
name: prepared-files-updater-test name: prepared-files-updater-test
path: | path: |
@ -80,7 +105,13 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-14, windows-2022, ubuntu-22.04] include:
- os: macos-14
platform: mac
- os: windows-2022
platform: win
- os: ubuntu-22.04
platform: linux
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
@ -107,6 +138,17 @@ jobs:
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
mkdir src/wasm-lib/pkg mkdir src/wasm-lib/pkg
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
cp prepared-files/release-notes.md release-notes.md
- uses: actions/download-artifact@v3
if: ${{ github.event_name == 'schedule' }}
name: prepared-files-nightly
- name: Copy updated electron-builder.yml file for nightly build
if: ${{ github.event_name == 'schedule' }}
run: |
ls -R prepared-files-nightly
cp prepared-files-nightly/electron-builder.yml electron-builder.yml
- name: Sync node version and setup cache - name: Sync node version and setup cache
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -152,9 +194,27 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: out-${{ matrix.os }} name: out-arm64-${{ matrix.platform }}
# first two will pick both Zoo Modeling App-$VERSION-arm64-win.exe and Zoo Modeling App-$VERSION-win.exe
path: |
out/*-${{ env.VERSION_NO_V }}-win.*
out/*-${{ env.VERSION_NO_V }}-arm64-win.*
out/*-arm64-mac.*
out/*-arm64-linux.*
- uses: actions/upload-artifact@v3
with:
name: out-x64-${{ matrix.platform }}
path: |
out/*-x64-win.*
out/*-x64-mac.*
out/*-x86_64-linux.*
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: out-yml
path: | path: |
out/Zoo*.*
out/latest*.yml out/latest*.yml
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back # TODO: add the 'Build for Mac TestFlight (nightly)' stage back
@ -176,10 +236,20 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }} if: ${{ env.CUT_RELEASE_PR == 'true' }}
with: with:
name: updater-test-${{ matrix.os }} name: updater-test-arm64-${{ matrix.platform }}
path: | path: |
out/Zoo*.* out/*-arm64-win.exe
out/latest*.yml out/*-arm64-mac.dmg
out/*-arm64-linux.AppImage
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-x64-${{ matrix.platform }}
path: |
out/*-x64-win.exe
out/*-x64-mac.dmg
out/*-x86_64-linux.AppImage
publish-apps-release: publish-apps-release:
@ -192,7 +262,7 @@ jobs:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }} PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }} NOTES: ${{ needs.prepare-files.outputs.notes }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }} BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }} WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }} URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
@ -201,17 +271,37 @@ jobs:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: out-windows-2022 name: out-arm64-win
path: out path: out
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: out-macos-14 name: out-x64-win
path: out path: out
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: out-ubuntu-22.04 name: out-arm64-mac
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-mac
path: out
- uses: actions/download-artifact@v3
with:
name: out-arm64-linux
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-linux
path: out
- uses: actions/download-artifact@v3
with:
name: out-yml
path: out path: out
- name: Generate the download static endpoint - name: Generate the download static endpoint

View File

@ -37,10 +37,6 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm - run: yarn build:wasm
yarn-tsc: yarn-tsc:
@ -70,10 +66,6 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn lint - run: yarn lint
python-codespell: python-codespell:
@ -101,11 +93,6 @@ jobs:
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm - run: yarn build:wasm
- run: yarn simpleserver:bg - run: yarn simpleserver:bg

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2023 The Zoo Authors ALL YOUR BASE BELONG TO LEE
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -57,7 +57,7 @@ yarn install
followed by: followed by:
``` ```
yarn build:wasm-dev yarn build:wasm
``` ```
or if you have the gh cli installed or if you have the gh cli installed
@ -66,15 +66,15 @@ or if you have the gh cli installed
./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle ./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle
``` ```
That will build the WASM binary and put in the `public` dir (though gitignored) That will build the WASM binary and put in the `public` dir (though gitignored).
finally, to run the web app only, run: Finally, to run the web app only, run:
``` ```
yarn start yarn start
``` ```
If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens ofcourse, then navigate to localhost:3000 again. Note that navigating to localhost:3000/signin removes your token so you will need to set the token again. If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens of course, then navigate to localhost:3000 again. Note that navigating to `localhost:3000/signin` removes your token so you will need to set the token again.
### Development environment variables ### Development environment variables
@ -91,13 +91,13 @@ Third-Party Cookies".
## Desktop ## Desktop
To spin up the desktop app, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then To spin up the desktop app, `yarn install` and `yarn build:wasm` need to have been done before hand then
``` ```
yarn electron:start yarn tron:start
``` ```
This will start the application and hot-reload on changed. 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/Ctrl-Shift-I.
@ -158,11 +158,29 @@ The PR may then serve as a place to discuss the human-readable changelog and ext
#### 3. Manually test artifacts from the Cut Release PR #### 3. Manually test artifacts from the Cut Release PR
The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch. ##### Release builds
The release builds can be found under the `out-{platform}` zip, at the very bottom of the `build-publish-apps` summary page for each commit on this branch.
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR. Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR.
The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows). ##### Updater-test builds
The other `build-publish-apps` output in Cut Release PRs is `updater-test-{platform}`. As we don't have a way to test this fully automatically, we have a semi-automated process. For macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
```
# Windows (PowerShell)
& 'C:\Program Files\Zoo Modeling App\Zoo Modeling App.exe'
# macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux
./Zoo Modeling App-{version}-{arch}-linux.AppImage
```
#### 4. Merge the Cut Release PR #### 4. Merge the Cut Release PR
@ -334,7 +352,16 @@ Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testin
```bash ```bash
cd src/wasm-lib cd src/wasm-lib
cargo test KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1
```
Where `XXX` is an API token from the production engine (NOT the dev environment).
We recommend using [nextest](https://nexte.st/) to run the Rust tests (its faster and is used in CI). Once installed, run the tests using
```
cd src/wasm-lib
KITTYCAD_API_TOKEN=XXX cargo run nextest
``` ```
### Mapping CI CD jobs to local commands ### Mapping CI CD jobs to local commands

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ exampleSketch = startSketchOn('XZ')
|> close(%) |> close(%)
|> patternCircular2d({ |> patternCircular2d({
center: [0, 0], center: [0, 0],
repetitions: 12, instances: 13,
arcDegrees: 360, arcDegrees: 360,
rotateDuplicates: true rotateDuplicates: true
}, %) }, %)

View File

@ -35,7 +35,7 @@ example = extrude(-5, exampleSketch)
|> patternCircular3d({ |> patternCircular3d({
axis: [1, -1, 0], axis: [1, -1, 0],
center: [10, -20, 0], center: [10, -20, 0],
repetitions: 10, instances: 11,
arcDegrees: 360, arcDegrees: 360,
rotateDuplicates: true rotateDuplicates: true
}, %) }, %)

View File

@ -32,7 +32,7 @@ exampleSketch = startSketchOn('XZ')
|> circle({ center: [0, 0], radius: 1 }, %) |> circle({ center: [0, 0], radius: 1 }, %)
|> patternLinear2d({ |> patternLinear2d({
axis: [1, 0], axis: [1, 0],
repetitions: 6, instances: 7,
distance: 4 distance: 4
}, %) }, %)

View File

@ -38,7 +38,7 @@ exampleSketch = startSketchOn('XZ')
example = extrude(1, exampleSketch) example = extrude(1, exampleSketch)
|> patternLinear3d({ |> patternLinear3d({
axis: [1, 0, 1], axis: [1, 0, 1],
repetitions: 6, instances: 7,
distance: 6 distance: 6
}, %) }, %)
``` ```

View File

@ -18,12 +18,12 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `array` | [`[KclValue]`](/docs/kcl/types/KclValue) | | Yes | | `array` | [`[KclValue]`](/docs/kcl/types/KclValue) | | Yes |
| `start` | [`KclValue`](/docs/kcl/types/KclValue) | A memory item. | Yes | | `start` | [`KclValue`](/docs/kcl/types/KclValue) | Any KCL value. | Yes |
| `reduce_fn` | `FunctionParam` | | Yes | | `reduce_fn` | `FunctionParam` | | Yes |
### Returns ### Returns
[`KclValue`](/docs/kcl/types/KclValue) - A memory item. [`KclValue`](/docs/kcl/types/KclValue) - Any KCL value.
### Examples ### Examples
@ -32,7 +32,7 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
fn decagon = (radius) => { fn decagon = (radius) => {
step = 1 / 10 * tau() step = 1 / 10 * tau()
sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius]) sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius])
return reduce([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], sketch001, (i, sg) => { return reduce([1..10], sketch001, (i, sg) => {
x = cos(step * i) * radius x = cos(step * i) * radius
y = sin(step * i) * radius y = sin(step * i) * radius
return lineTo([x, y], sg) return lineTo([x, y], sg)

File diff suppressed because it is too large Load Diff

View File

@ -82,6 +82,78 @@ Raise a number to a power.
----
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

@ -18,6 +18,27 @@ layout: manual
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | 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 |
----
**Type:** `object`
## Properties ## Properties
| Property | Type | Description | Required | | Property | Type | Description | Required |
@ -45,6 +66,7 @@ layout: manual
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | | `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | | `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No | | `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | 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 | | `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

@ -16,7 +16,7 @@ Data for a circular pattern on a 2D sketch.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No | | `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No | | `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No | | `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No | | `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 3D model.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No | | `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `axis` |`[number, number, number]`| The axis around which to make the pattern. This is a 3D vector. | No | | `axis` |`[number, number, number]`| The axis around which to make the pattern. This is a 3D vector. | No |
| `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No | | `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No | | `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |

View File

@ -197,6 +197,27 @@ An expression can be evaluated to yield a single KCL value.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | 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 |
----
**Type:** `object`
## Properties ## Properties
| Property | Type | Description | Required | | Property | Type | Description | Required |

View File

@ -0,0 +1,24 @@
---
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 |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | 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

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

View File

@ -1,10 +1,10 @@
--- ---
title: "KclValue" title: "KclValue"
excerpt: "A memory item." excerpt: "Any KCL value."
layout: manual layout: manual
--- ---
A memory item. Any KCL value.
@ -80,7 +80,7 @@ A plane.
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Plane`| | No | | `type` |enum: `Plane`| | No |
| `id` |`string`| The id of the plane. | No | | `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A memory item. | No | | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
@ -183,8 +183,8 @@ 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)| A memory item. | No | | `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| A memory item. | 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

@ -16,7 +16,7 @@ Data for a linear pattern on a 2D sketch.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No | | `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No | | `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No | | `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No |

View File

@ -16,7 +16,7 @@ Data for a linear pattern on a 3D model.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No | | `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No | | `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number, number]`| The axis of the pattern. | No | | `axis` |`[number, number, number]`| The axis of the pattern. | No |

View File

@ -313,3 +313,45 @@ test(
await electronApp.close() await electronApp.close()
} }
) )
test(
'external change of file contents are reflected in editor',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const PROJECT_DIR_NAME = 'lee-was-here'
const {
electronApp,
page,
dir: projectsDir,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const aProjectDir = join(dir, PROJECT_DIR_NAME)
await fsp.mkdir(aProjectDir, { recursive: true })
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await test.step('Open the project', async () => {
await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible()
await page.getByText(PROJECT_DIR_NAME).click()
await u.waitForPageLoad()
})
await u.openFilePanel()
await u.openKclCodePanel()
await test.step('Write to file externally and check for changed content', async () => {
const content = 'ha he ho ho ha blap scap be dap'
await fsp.writeFile(
join(projectsDir, PROJECT_DIR_NAME, 'main.kcl'),
content
)
await u.editorTextMatches(content)
})
await electronApp.close()
}
)

View File

@ -0,0 +1,80 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
function countNewlines(input: string): number {
let count = 0
for (const char of input) {
if (char === '\n') {
count++
}
}
return count
}
test.describe('Debug pane', () => {
test('Artifact IDs in the artifact graph are stable across code edits', async ({
page,
context,
}) => {
const code = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([1, 1], %)
`
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const tree = page.getByTestId('debug-feature-tree')
const segment = tree.locator('li', {
hasText: 'segIds:',
hasNotText: 'paths:',
})
await test.step('Test setup', async () => {
await u.waitForAuthSkipAppStart()
await u.openKclCodePanel()
await u.openDebugPanel()
// Set the code in the code editor.
await u.codeLocator.click()
await page.keyboard.type(code, { delay: 0 })
// Scroll to the feature tree.
await tree.scrollIntoViewIfNeeded()
// Expand the feature tree.
await tree.getByText('Feature Tree').click()
// Just expanded the details, making the element taller, so scroll again.
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
})
// Extract the artifact IDs from the debug feature tree.
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
// The artifact ID should include a UUID.
expect(initialSegmentIds).toMatch(
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/
)
await test.step('Move cursor to the bottom of the code editor', async () => {
// Focus on the code editor.
await u.codeLocator.click()
// Make sure the cursor is at the end of the code.
const lines = countNewlines(code) + 1
for (let i = 0; i < lines; i++) {
await page.keyboard.press('ArrowDown')
}
})
await test.step('Enter a comment', async () => {
await page.keyboard.type('|> line([2, 2], %)', { delay: 0 })
// Wait for keyboard input debounce and updated artifact graph.
await page.waitForTimeout(1000)
})
const newSegmentIds = await segment.innerText()
// Strip off the closing bracket.
const initialIds = initialSegmentIds.slice(0, initialSegmentIds.length - 1)
expect(newSegmentIds.slice(0, initialIds.length)).toEqual(initialIds)
})
})

View File

@ -960,4 +960,171 @@ _test.describe('Deleting items from the file pane', () => {
'TODO - delete folder we are in, with no main.kcl', 'TODO - delete folder we are in, with no main.kcl',
async () => {} async () => {}
) )
// Copied from tests above.
_test(
`external deletion of project navigates back home`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
// Point of divergence. Delete the project folder and see if it goes back
// to the home view.
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(join(projectsDirName, TEST_PROJECT_NAME), {
recursive: true,
force: true,
})
}
)
await _test.step('Check the app is back on the home view', async () => {
const projectsDirLink = page.getByText('Loaded from')
await _expect(projectsDirLink).toBeVisible()
})
await electronApp.close()
}
)
// Similar to the above
_test(
`external deletion of file in sub-directory updates the file tree and recreates it on code editor typing`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(
join(
projectsDirName,
TEST_PROJECT_NAME,
'folderToDelete',
'someFileWithin.kcl'
)
)
}
)
await _test.step('Check the file is gone in the file tree', async () => {
await _expect(
page.getByTestId('file-pane-scroll-container')
).not.toContainText('someFileWithin.kcl')
})
await _test.step(
'Check the file is back in the file tree after typing in code editor',
async () => {
await u.pasteCodeInEditor('hello = 1')
await _expect(
page.getByTestId('file-pane-scroll-container')
).toContainText('someFileWithin.kcl')
}
)
await electronApp.close()
}
)
}) })

View File

@ -1,4 +1,4 @@
import { test, expect, Page } from '@playwright/test' import { test, expect } from '@playwright/test'
import { import {
doExport, doExport,
executorInputPath, executorInputPath,
@ -618,31 +618,30 @@ test(
'Deleting projects, can delete individual project, can still create projects after deleting all', 'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectData = [
['router-template-slate', 'cylinder.kcl'],
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['lego', 'lego.kcl'],
]
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => {
// Do these serially to ensure the order is correct
for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true })
await fsp.copyFile(
executorInputPath(file),
join(dir, name, `main.kcl`)
)
// Wait 1s between each project to ensure the order is correct
await new Promise((r) => setTimeout(r, 1_000))
}
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
const createProjectAndRenameItTest = async ({
name,
page,
}: {
name: string
page: Page
}) => {
await test.step(`Create and rename project ${name}`, async () => {
await createProjectAndRenameIt({ name, page })
})
}
// we need to create the folders so that the order is correct
// creating them ahead of time with fs tools means they all have the same timestamp
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
await createProjectAndRenameItTest({ name: 'bracket', page })
await createProjectAndRenameItTest({ name: 'lego', page })
await test.step('delete the middle project, i.e. the bracket project', async () => { await test.step('delete the middle project, i.e. the bracket project', async () => {
const project = page.getByText('bracket') const project = page.getByText('bracket')
@ -744,8 +743,26 @@ test(
'Can sort projects on home page', 'Can sort projects on home page',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectData = [
['router-template-slate', 'cylinder.kcl'],
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['lego', 'lego.kcl'],
]
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => {
// Do these serially to ensure the order is correct
for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true })
await fsp.copyFile(
executorInputPath(file),
join(dir, name, `main.kcl`)
)
// Wait 1s between each project to ensure the order is correct
await new Promise((r) => setTimeout(r, 1_000))
}
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -753,24 +770,6 @@ test(
page.on('console', console.log) page.on('console', console.log)
const createProjectAndRenameItTest = async ({
name,
page,
}: {
name: string
page: Page
}) => {
await test.step(`Create and rename project ${name}`, async () => {
await createProjectAndRenameIt({ name, page })
})
}
// we need to create the folders so that the order is correct
// creating them ahead of time with fs tools means they all have the same timestamp
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
await createProjectAndRenameItTest({ name: 'bracket', page })
await createProjectAndRenameItTest({ name: 'lego', page })
await test.step('should be shorted by modified initially', async () => { await test.step('should be shorted by modified initially', async () => {
const lastModifiedButton = page.getByRole('button', { const lastModifiedButton = page.getByRole('button', {
name: 'Last Modified', name: 'Last Modified',

View File

@ -521,7 +521,6 @@ test(
const startXPx = 600 const startXPx = 600
// Equip the rectangle tool // Equip the rectangle tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page await page
.getByRole('button', { name: 'rectangle Corner rectangle', exact: true }) .getByRole('button', { name: 'rectangle Corner rectangle', exact: true })
.click() .click()
@ -670,6 +669,7 @@ test.describe(
// screen shot should show the sketch // screen shot should show the sketch
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
}) })
// exit sketch // exit sketch
@ -687,6 +687,7 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same. // second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
}) })
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -463,6 +463,9 @@ export async function getUtils(page: Page, test_?: typeof test) {
return test_?.step( return test_?.step(
`Create and select project with text "${hasText}"`, `Create and select project with text "${hasText}"`,
async () => { async () => {
// Without this, we get unreliable project creation. It's probably
// due to a race between the FS being read and clicking doing something.
await page.waitForTimeout(100)
await page.getByTestId('home-new-file').click() await page.getByTestId('home-new-file').click()
const projectLinksPost = page.getByTestId('project-link') const projectLinksPost = page.getByTestId('project-link')
await projectLinksPost.filter({ hasText }).click() await projectLinksPost.filter({ hasText }).click()
@ -492,6 +495,11 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFile: async (name: string) => { createNewFile: async (name: string) => {
return test?.step(`Create a file named ${name}`, async () => { return test?.step(`Create a file named ${name}`, async () => {
// If the application is in the middle of connecting a stream
// then creating a new file won't work in the end.
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByTestId('create-file-button').click() await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name) await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')

View File

@ -1208,6 +1208,12 @@ extrude001 = extrude(50, sketch001)
test('Deselecting line tool should mean nothing happens on click', async ({ test('Deselecting line tool should mean nothing happens on click', async ({
page, page,
}) => { }) => {
/**
* If the line tool is clicked when the state is 'No Points' it will exit Sketch mode.
* This is the same exact workflow as pressing ESC.
*
* To continue to test this workflow, we now enter sketch mode and place a single point before exiting the line tool.
*/
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -1228,6 +1234,7 @@ extrude001 = extrude(50, sketch001)
200 200
) )
// Clicks the XZ Plane in the page
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
@ -1236,6 +1243,11 @@ extrude001 = extrude(50, sketch001)
await page.waitForTimeout(600) await page.waitForTimeout(600)
// Place a point because the line tool will exit if no points are pressed
await page.mouse.click(650, 200)
await page.waitForTimeout(600)
// Code before exiting the tool
let previousCodeContent = await page.locator('.cm-content').innerText() let previousCodeContent = await page.locator('.cm-content').innerText()
// deselect the line tool by clicking it // deselect the line tool by clicking it

View File

@ -9,7 +9,7 @@ import {
executorInputPath, executorInputPath,
} from './test-utils' } from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes' import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants' import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import { import {
TEST_SETTINGS_KEY, TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED, TEST_SETTINGS_CORRUPTED,
@ -445,6 +445,58 @@ test.describe('Testing settings', () => {
} }
) )
test(
'project settings reload on external change',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const {
electronApp,
page,
dir: projectDirName,
} = await setupElectron({
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
const logoLink = page.getByTestId('app-logo')
const projectDirLink = page.getByText('Loaded from')
await test.step('Wait for project view', async () => {
await expect(projectDirLink).toBeVisible()
})
const projectLinks = page.getByTestId('project-link')
const oldCount = await projectLinks.count()
await page.getByRole('button', { name: 'New project' }).click()
await expect(projectLinks).toHaveCount(oldCount + 1)
await projectLinks.filter({ hasText: 'project-000' }).first().click()
const changeColorFs = async (color: string) => {
const tempSettingsFilePath = join(
projectDirName,
'project-000',
PROJECT_SETTINGS_FILE_NAME
)
await fsp.writeFile(
tempSettingsFilePath,
`[settings.app]\nthemeColor = "${color}"`
)
}
await test.step('Check the color is first starting as we expect', async () => {
await expect(logoLink).toHaveCSS('--primary-hue', '264.5')
})
await test.step('Check color of logo changed', async () => {
await changeColorFs('99')
await expect(logoLink).toHaveCSS('--primary-hue', '99')
})
await electronApp.close()
}
)
test( test(
`Closing settings modal should go back to the original file being viewed`, `Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' }, { tag: '@electron' },

View File

@ -73,3 +73,5 @@ publish:
- provider: generic - provider: generic
url: https://dl.zoo.dev/releases/modeling-app url: https://dl.zoo.dev/releases/modeling-app
channel: latest channel: latest
releaseInfo:
releaseNotesFile: release-notes.md

13
interface.d.ts vendored
View File

@ -20,9 +20,10 @@ export interface IElectronAPI {
version: typeof process.env.version version: typeof process.env.version
watchFileOn: ( watchFileOn: (
path: string, path: string,
key: string,
callback: (eventType: string, path: string) => void callback: (eventType: string, path: string) => void
) => void ) => void
watchFileOff: (path: string) => void watchFileOff: (path: string, key: string) => void
readFile: (path: string) => ReturnType<fs.readFile> readFile: (path: string) => ReturnType<fs.readFile>
writeFile: ( writeFile: (
path: string, path: string,
@ -67,11 +68,15 @@ export interface IElectronAPI {
} }
} }
kittycad: (access: string, args: any) => any kittycad: (access: string, args: any) => any
listMachines: () => Promise<MachinesListing> listMachines: (machineApiIp: string) => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null> getMachineApiIp: () => Promise<string | null>
onUpdateDownloaded: ( onUpdateDownloadStart: (
callback: (value: string) => void callback: (value: { version: string }) => void
) => Electron.IpcRenderer ) => Electron.IpcRenderer
onUpdateDownloaded: (
callback: (value: { version: string; releaseNotes: string }) => void
) => Electron.IpcRenderer
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
appRestart: () => void appRestart: () => void
} }

View File

@ -36,38 +36,297 @@
"description": "Extra machine-specific information regarding a connected machine.", "description": "Extra machine-specific information regarding a connected machine.",
"oneOf": [ "oneOf": [
{ {
"additionalProperties": false,
"properties": { "properties": {
"Moonraker": { "type": {
"type": "object" "enum": [
"moonraker"
],
"type": "string"
} }
}, },
"required": [ "required": [
"Moonraker" "type"
], ],
"type": "object" "type": "object"
}, },
{ {
"additionalProperties": false,
"properties": { "properties": {
"Usb": { "type": {
"type": "object" "enum": [
"usb"
],
"type": "string"
} }
}, },
"required": [ "required": [
"Usb" "type"
], ],
"type": "object" "type": "object"
}, },
{ {
"additionalProperties": false,
"properties": { "properties": {
"Bambu": { "current_stage": {
"type": "object" "allOf": [
{
"$ref": "#/components/schemas/Stage"
}
],
"description": "The current stage of the machine as defined by Bambu which can include errors, etc.",
"nullable": true
},
"nozzle_diameter": {
"allOf": [
{
"$ref": "#/components/schemas/NozzleDiameter"
}
],
"description": "The nozzle diameter of the machine."
},
"type": {
"enum": [
"bambu"
],
"type": "string"
} }
}, },
"required": [ "required": [
"Bambu" "nozzle_diameter",
"type"
],
"type": "object"
}
]
},
"FdmHardwareConfiguration": {
"description": "Configuration for a FDM-based printer.",
"properties": {
"filaments": {
"description": "The filaments the printer has access to.",
"items": {
"$ref": "#/components/schemas/Filament"
},
"type": "array"
},
"nozzle_diameter": {
"description": "Diameter of the extrusion nozzle, in mm.",
"format": "double",
"type": "number"
}
},
"required": [
"filaments",
"nozzle_diameter"
],
"type": "object"
},
"Filament": {
"description": "Information about the filament being used in a FDM printer.",
"properties": {
"color": {
"description": "The color (as hex without the `#`) of the filament, this is likely specific to the manufacturer.",
"maxLength": 6,
"minLength": 6,
"nullable": true,
"type": "string"
},
"material": {
"allOf": [
{
"$ref": "#/components/schemas/FilamentMaterial"
}
],
"description": "The material that the filament is made of."
},
"name": {
"description": "The name of the filament, this is likely specfic to the manufacturer.",
"nullable": true,
"type": "string"
}
},
"required": [
"material"
],
"type": "object"
},
"FilamentMaterial": {
"description": "The material that the filament is made of.",
"oneOf": [
{
"description": "Polylactic acid based plastics",
"properties": {
"type": {
"enum": [
"pla"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "Pla support",
"properties": {
"type": {
"enum": [
"pla_support"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "acrylonitrile butadiene styrene based plastics",
"properties": {
"type": {
"enum": [
"abs"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "polyethylene terephthalate glycol based plastics",
"properties": {
"type": {
"enum": [
"petg"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "unsuprisingly, nylon based",
"properties": {
"type": {
"enum": [
"nylon"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "thermoplastic polyurethane based urethane material",
"properties": {
"type": {
"enum": [
"tpu"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "polyvinyl alcohol based material",
"properties": {
"type": {
"enum": [
"pva"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "high impact polystyrene based material",
"properties": {
"type": {
"enum": [
"hips"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "composite material with stuff in other stuff, something like PLA mixed with carbon fiber, kevlar, or fiberglass",
"properties": {
"type": {
"enum": [
"composite"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
]
},
"HardwareConfiguration": {
"description": "The hardware configuration of a machine.",
"oneOf": [
{
"description": "No configuration is possible. This isn't the same conceptually as an `Option<HardwareConfiguration>`, because this indicates we positively know there is no possible configuration changes that are possible with this method of manufcture.",
"properties": {
"type": {
"enum": [
"none"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "Hardware configuration specific to FDM based printers",
"properties": {
"config": {
"allOf": [
{
"$ref": "#/components/schemas/FdmHardwareConfiguration"
}
],
"description": "The configuration for the FDM printer."
},
"type": {
"enum": [
"fdm"
],
"type": "string"
}
},
"required": [
"config",
"type"
], ],
"type": "object" "type": "object"
} }
@ -85,6 +344,14 @@
"description": "Additional, per-machine information which is specific to the underlying machine type.", "description": "Additional, per-machine information which is specific to the underlying machine type.",
"nullable": true "nullable": true
}, },
"hardware_configuration": {
"allOf": [
{
"$ref": "#/components/schemas/HardwareConfiguration"
}
],
"description": "Information about how the Machine is currently configured."
},
"id": { "id": {
"description": "Machine Identifier (ID) for the specific Machine.", "description": "Machine Identifier (ID) for the specific Machine.",
"type": "string" "type": "string"
@ -114,6 +381,12 @@
"description": "Maximum part size that can be manufactured by this device. This may be some sort of theoretical upper bound, getting close to this limit seems like maybe a bad idea.\n\nThis may be `None` if the maximum size is not knowable by the Machine API.\n\nWhat \"close\" means is up to you!", "description": "Maximum part size that can be manufactured by this device. This may be some sort of theoretical upper bound, getting close to this limit seems like maybe a bad idea.\n\nThis may be `None` if the maximum size is not knowable by the Machine API.\n\nWhat \"close\" means is up to you!",
"nullable": true "nullable": true
}, },
"progress": {
"description": "Progress of the current print, if printing.",
"format": "double",
"nullable": true,
"type": "number"
},
"state": { "state": {
"allOf": [ "allOf": [
{ {
@ -124,6 +397,7 @@
} }
}, },
"required": [ "required": [
"hardware_configuration",
"id", "id",
"machine_type", "machine_type",
"make_model", "make_model",
@ -157,57 +431,111 @@
"oneOf": [ "oneOf": [
{ {
"description": "If a print state can not be resolved at this time, an Unknown may be returned.", "description": "If a print state can not be resolved at this time, an Unknown may be returned.",
"enum": [
"Unknown"
],
"type": "string"
},
{
"description": "Idle, and ready for another job.",
"enum": [
"Idle"
],
"type": "string"
},
{
"description": "Running a job -- 3D printing or CNC-ing a part.",
"enum": [
"Running"
],
"type": "string"
},
{
"description": "Machine is currently offline or unreachable.",
"enum": [
"Offline"
],
"type": "string"
},
{
"description": "Job is underway but halted, waiting for some action to take place.",
"enum": [
"Paused"
],
"type": "string"
},
{
"description": "Job is finished, but waiting manual action to move back to Idle.",
"enum": [
"Complete"
],
"type": "string"
},
{
"additionalProperties": false,
"description": "The printer has failed and is in an unknown state that may require manual attention to resolve. The inner value is a human readable description of what specifically has failed.",
"properties": { "properties": {
"Failed": { "state": {
"nullable": true, "enum": [
"unknown"
],
"type": "string" "type": "string"
} }
}, },
"required": [ "required": [
"Failed" "state"
],
"type": "object"
},
{
"description": "Idle, and ready for another job.",
"properties": {
"state": {
"enum": [
"idle"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"description": "Running a job -- 3D printing or CNC-ing a part.",
"properties": {
"state": {
"enum": [
"running"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"description": "Machine is currently offline or unreachable.",
"properties": {
"state": {
"enum": [
"offline"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"description": "Job is underway but halted, waiting for some action to take place.",
"properties": {
"state": {
"enum": [
"paused"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"description": "Job is finished, but waiting manual action to move back to Idle.",
"properties": {
"state": {
"enum": [
"complete"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"description": "The printer has failed and is in an unknown state that may require manual attention to resolve. The inner value is a human readable description of what specifically has failed.",
"properties": {
"message": {
"description": "A human-readable message describing the failure.",
"nullable": true,
"type": "string"
},
"state": {
"enum": [
"failed"
],
"type": "string"
}
},
"required": [
"state"
], ],
"type": "object" "type": "object"
} }
@ -219,21 +547,54 @@
{ {
"description": "Use light to cure a resin to build up layers.", "description": "Use light to cure a resin to build up layers.",
"enum": [ "enum": [
"Stereolithography" "stereolithography"
], ],
"type": "string" "type": "string"
}, },
{ {
"description": "Fused Deposition Modeling, layers of melted plastic.", "description": "Fused Deposition Modeling, layers of melted plastic.",
"enum": [ "enum": [
"FusedDeposition" "fused_deposition"
], ],
"type": "string" "type": "string"
}, },
{ {
"description": "\"Computer numerical control\" - machine that grinds away material from a hunk of material to construct a part.", "description": "\"Computer numerical control\" - machine that grinds away material from a hunk of material to construct a part.",
"enum": [ "enum": [
"Cnc" "cnc"
],
"type": "string"
}
]
},
"NozzleDiameter": {
"description": "A nozzle diameter.",
"oneOf": [
{
"description": "0.2mm.",
"enum": [
"0.2"
],
"type": "string"
},
{
"description": "0.4mm.",
"enum": [
"0.4"
],
"type": "string"
},
{
"description": "0.6mm.",
"enum": [
"0.6"
],
"type": "string"
},
{
"description": "0.8mm.",
"enum": [
"0.8"
], ],
"type": "string" "type": "string"
} }
@ -284,6 +645,15 @@
"machine_id": { "machine_id": {
"description": "The machine id to print to.", "description": "The machine id to print to.",
"type": "string" "type": "string"
},
"slicer_configuration": {
"allOf": [
{
"$ref": "#/components/schemas/SlicerConfiguration"
}
],
"description": "Requested design-specific slicer configurations.",
"nullable": true
} }
}, },
"required": [ "required": [
@ -292,6 +662,283 @@
], ],
"type": "object" "type": "object"
}, },
"SlicerConfiguration": {
"description": "The slicer configuration is a set of parameters that are passed to the slicer to control how the gcode is generated.",
"properties": {
"filament_idx": {
"description": "The filament to use for the print.",
"format": "uint",
"minimum": 0,
"nullable": true,
"type": "integer"
}
},
"type": "object"
},
"Stage": {
"description": "The print stage. These come from: https://github.com/SoftFever/OrcaSlicer/blob/431978baf17961df90f0d01871b0ad1d839d7f5d/src/slic3r/GUI/DeviceManager.cpp#L78",
"oneOf": [
{
"description": "Nothing.",
"enum": [
"nothing"
],
"type": "string"
},
{
"description": "Empty.",
"enum": [
"empty"
],
"type": "string"
},
{
"description": "Auto bed leveling.",
"enum": [
"auto_bed_leveling"
],
"type": "string"
},
{
"description": "Heatbed preheating.",
"enum": [
"heatbed_preheating"
],
"type": "string"
},
{
"description": "Sweeping XY mech mode.",
"enum": [
"sweeping_xy_mech_mode"
],
"type": "string"
},
{
"description": "Changing filament.",
"enum": [
"changing_filament"
],
"type": "string"
},
{
"description": "M400 pause.",
"enum": [
"m400_pause"
],
"type": "string"
},
{
"description": "Paused due to filament runout.",
"enum": [
"paused_due_to_filament_runout"
],
"type": "string"
},
{
"description": "Heating hotend.",
"enum": [
"heating_hotend"
],
"type": "string"
},
{
"description": "Calibrating extrusion.",
"enum": [
"calibrating_extrusion"
],
"type": "string"
},
{
"description": "Scanning bed surface.",
"enum": [
"scanning_bed_surface"
],
"type": "string"
},
{
"description": "Inspecting first layer.",
"enum": [
"inspecting_first_layer"
],
"type": "string"
},
{
"description": "Identifying build plate type.",
"enum": [
"identifying_build_plate_type"
],
"type": "string"
},
{
"description": "Calibrating micro lidar.",
"enum": [
"calibrating_micro_lidar"
],
"type": "string"
},
{
"description": "Homing toolhead.",
"enum": [
"homing_toolhead"
],
"type": "string"
},
{
"description": "Cleaning nozzle tip.",
"enum": [
"cleaning_nozzle_tip"
],
"type": "string"
},
{
"description": "Checking extruder temperature.",
"enum": [
"checking_extruder_temperature"
],
"type": "string"
},
{
"description": "Printing was paused by the user.",
"enum": [
"printing_was_paused_by_the_user"
],
"type": "string"
},
{
"description": "Pause of front cover falling.",
"enum": [
"pause_of_front_cover_falling"
],
"type": "string"
},
{
"description": "Calibrating micro lidar.",
"enum": [
"calibrating_micro_lidar2"
],
"type": "string"
},
{
"description": "Calibrating extrusion flow.",
"enum": [
"calibrating_extrusion_flow"
],
"type": "string"
},
{
"description": "Paused due to nozzle temperature malfunction.",
"enum": [
"paused_due_to_nozzle_temperature_malfunction"
],
"type": "string"
},
{
"description": "Paused due to heat bed temperature malfunction.",
"enum": [
"paused_due_to_heat_bed_temperature_malfunction"
],
"type": "string"
},
{
"description": "Filament unloading.",
"enum": [
"filament_unloading"
],
"type": "string"
},
{
"description": "Skip step pause.",
"enum": [
"skip_step_pause"
],
"type": "string"
},
{
"description": "Filament loading.",
"enum": [
"filament_loading"
],
"type": "string"
},
{
"description": "Motor noise calibration.",
"enum": [
"motor_noise_calibration"
],
"type": "string"
},
{
"description": "Paused due to AMS lost.",
"enum": [
"paused_due_to_ams_lost"
],
"type": "string"
},
{
"description": "Paused due to low speed of the heat break fan.",
"enum": [
"paused_due_to_low_speed_of_the_heat_break_fan"
],
"type": "string"
},
{
"description": "Paused due to chamber temperature control error.",
"enum": [
"paused_due_to_chamber_temperature_control_error"
],
"type": "string"
},
{
"description": "Cooling chamber.",
"enum": [
"cooling_chamber"
],
"type": "string"
},
{
"description": "Paused by the Gcode inserted by the user.",
"enum": [
"paused_by_the_gcode_inserted_by_the_user"
],
"type": "string"
},
{
"description": "Motor noise showoff.",
"enum": [
"motor_noise_showoff"
],
"type": "string"
},
{
"description": "Nozzle filament covered detected pause.",
"enum": [
"nozzle_filament_covered_detected_pause"
],
"type": "string"
},
{
"description": "Cutter error pause.",
"enum": [
"cutter_error_pause"
],
"type": "string"
},
{
"description": "First layer error pause.",
"enum": [
"first_layer_error_pause"
],
"type": "string"
},
{
"description": "Nozzle clog pause.",
"enum": [
"nozzle_clog_pause"
],
"type": "string"
}
]
},
"Volume": { "Volume": {
"description": "Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum.\n\nAll measurements are in millimeters.", "description": "Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum.\n\nAll measurements are in millimeters.",
"properties": { "properties": {
@ -425,6 +1072,34 @@
] ]
} }
}, },
"/metrics": {
"get": {
"operationId": "get_metrics",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"title": "String",
"type": "string"
}
}
},
"description": "successful operation"
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
},
"summary": "List available machines and their statuses",
"tags": [
"hidden"
]
}
},
"/ping": { "/ping": {
"get": { "get": {
"operationId": "ping", "operationId": "ping",
@ -492,6 +1167,13 @@
} }
}, },
"tags": [ "tags": [
{
"description": "Hidden API endpoints that should not show up in the docs.",
"externalDocs": {
"url": "https://docs.zoo.dev/api/machines"
},
"name": "hidden"
},
{ {
"description": "Utilities for making parts and discovering machines.", "description": "Utilities for making parts and discovering machines.",
"externalDocs": { "externalDocs": {

View File

@ -408,6 +408,7 @@ export async function deleteSegment({
const testExecute = await executeAst({ const testExecute = await executeAst({
ast: modifiedAst, ast: modifiedAst,
idGenerator: kclManager.execState.idGenerator,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: engineCommandManager, engineCommandManager: engineCommandManager,
}) })

View File

@ -391,12 +391,14 @@ export class SceneEntities {
const { truncatedAst, programMemoryOverride, variableDeclarationName } = const { truncatedAst, programMemoryOverride, variableDeclarationName } =
prepared prepared
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
const sketch = sketchFromPathToNode({ const sketch = sketchFromPathToNode({
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
ast: maybeModdedAst, ast: maybeModdedAst,
@ -801,12 +803,14 @@ export class SceneEntities {
updateRectangleSketch(sketchInit, x, y, tags[0]) updateRectangleSketch(sketchInit, x, y, tags[0])
} }
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName), programMemory.get(variableDeclarationName),
@ -848,12 +852,14 @@ export class SceneEntities {
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish rectangle' }) sceneInfra.modelingSend({ type: 'Finish rectangle' })
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: _ast, ast: _ast,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
// Prepare to update the THREEjs scene // Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
@ -965,12 +971,14 @@ export class SceneEntities {
modded = moddedResult.modifiedAst modded = moddedResult.modifiedAst
} }
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: modded, ast: modded,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName), programMemory.get(variableDeclarationName),
@ -1317,12 +1325,14 @@ export class SceneEntities {
// don't want to mod the user's code yet as they have't committed to the change yet // don't want to mod the user's code yet as they have't committed to the change yet
// plus this would be the truncated ast being recast, it would be wrong // plus this would be the truncated ast being recast, it would be wrong
codeManager.updateCodeEditor(code) codeManager.updateCodeEditor(code)
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const maybeSketch = programMemory.get(variableDeclarationName) const maybeSketch = programMemory.get(variableDeclarationName)

View File

@ -157,7 +157,7 @@ export function useCalc({
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: kclManager.programMemory.clone(), programMemoryOverride: kclManager.programMemory.clone(),
}).then(({ programMemory }) => { }).then(({ execState }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
@ -166,7 +166,7 @@ export function useCalc({
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.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

@ -0,0 +1,111 @@
import { isArray, isNonNullable } from 'lib/utils'
import { useRef, useState } from 'react'
type Primitive = string | number | bigint | boolean | symbol | null | undefined
export type GenericObj = {
type?: string
[key: string]: GenericObj | Primitive | Array<GenericObj | Primitive>
}
/**
* Display an array of objects or primitives for debug purposes. Nullable values
* are displayed so that relative indexes are preserved.
*/
export function DebugDisplayArray({
arr,
filterKeys,
}: {
arr: Array<GenericObj | Primitive>
filterKeys: string[]
}) {
return (
<>
{arr.map((obj, index) => {
return (
<div className="my-2" key={index}>
{obj && typeof obj === 'object' ? (
<DebugDisplayObj obj={obj} filterKeys={filterKeys} />
) : isNonNullable(obj) ? (
<span>{obj.toString()}</span>
) : (
<span>{obj}</span>
)}
</div>
)
})}
</>
)
}
/**
* Display an object as a tree for debug purposes. Nullable values are omitted.
* The only other property treated specially is the type property, which is
* assumed to be a string.
*/
export function DebugDisplayObj({
obj,
filterKeys,
}: {
obj: GenericObj
filterKeys: string[]
}) {
const ref = useRef<HTMLPreElement>(null)
const hasCursor = false
const [isCollapsed, setIsCollapsed] = useState(false)
return (
<pre
ref={ref}
className={`ml-2 border-l border-violet-600 pl-1 ${
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`}
>
{isCollapsed ? (
<button
className="m-0 p-0 border-0"
onClick={() => setIsCollapsed(false)}
>
{'>'}type: {obj.type}
</button>
) : (
<span className="flex">
<button
className="m-0 p-0 border-0 mb-auto"
onClick={() => setIsCollapsed(true)}
>
{'⬇️'}
</button>
<ul className="inline-block">
{Object.entries(obj).map(([key, value]) => {
if (filterKeys.includes(key)) {
return null
} else if (isArray(value)) {
return (
<li key={key}>
{`${key}: [`}
<DebugDisplayArray arr={value} filterKeys={filterKeys} />
{']'}
</li>
)
} else if (typeof value === 'object' && value !== null) {
return (
<li key={key}>
{key}:
<DebugDisplayObj obj={value} filterKeys={filterKeys} />
</li>
)
} else if (isNonNullable(value)) {
return (
<li key={key}>
{key}: {value.toString()}
</li>
)
}
return null
})}
</ul>
</span>
)}
</pre>
)
}

View File

@ -0,0 +1,45 @@
import { useMemo } from 'react'
import { engineCommandManager } from 'lib/singletons'
import {
ArtifactGraph,
expandPlane,
PlaneArtifactRich,
} from 'lang/std/artifactGraph'
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
export function DebugFeatureTree() {
const featureTree = useMemo(() => {
return computeTree(engineCommandManager.artifactGraph)
}, [engineCommandManager.artifactGraph])
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
return (
<details data-testid="debug-feature-tree" className="relative">
<summary>Feature Tree</summary>
{featureTree.length > 0 ? (
<pre className="text-xs">
<DebugDisplayArray arr={featureTree} filterKeys={filterKeys} />
</pre>
) : (
<p>(Empty)</p>
)}
</details>
)
}
function computeTree(artifactGraph: ArtifactGraph): GenericObj[] {
let items: GenericObj[] = []
const planes: PlaneArtifactRich[] = []
for (const artifact of artifactGraph.values()) {
if (artifact.type === 'plane') {
planes.push(expandPlane(artifact, artifactGraph))
}
}
const extraRichPlanes: GenericObj[] = planes.map((plane) => {
return plane as any as GenericObj
})
items = items.concat(extraRichPlanes)
return items
}

View File

@ -2,7 +2,7 @@ import type { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { Dispatch, useCallback, useEffect, useRef, useState } from 'react' import { Dispatch, useCallback, useRef, useState } from 'react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { Disclosure } from '@headlessui/react' import { Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -13,7 +13,6 @@ import { sortProject } from 'lib/desktopFS'
import { FILE_EXT } from 'lib/constants' import { FILE_EXT } from 'lib/constants'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
@ -21,6 +20,8 @@ import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
import { ContextMenu, ContextMenuItem } from './ContextMenu' import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project' import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})` return `calc(1rem * ${level + 1})`
@ -131,6 +132,23 @@ const FileTreeItem = ({
const isCurrentFile = fileOrDir.path === currentFile?.path const isCurrentFile = fileOrDir.path === currentFile?.path
const itemRef = useRef(null) const itemRef = useRef(null)
// Since every file or directory gets its own FileTreeItem, we can do this.
// Because subtrees only render when they are opened, that means this
// only listens when they open. Because this acts like a useEffect, when
// the ReactNodes are destroyed, so is this listener :)
useFileSystemWatcher(
async (eventType, path) => {
// Don't try to read a file that was removed.
if (isCurrentFile && eventType !== 'unlink') {
let code = await window.electron.readFile(path)
code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code)
}
fileSend({ type: 'Refresh' })
},
[fileOrDir.path]
)
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path) const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
const removeCurrentItemFromRenaming = useCallback( const removeCurrentItemFromRenaming = useCallback(
() => () =>
@ -154,6 +172,13 @@ const FileTreeItem = ({
}) })
}, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend]) }, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend])
const clickDirectory = () => {
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) { function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
if (e.metaKey && e.key === 'Backspace') { if (e.metaKey && e.key === 'Backspace') {
// Open confirmation dialog // Open confirmation dialog
@ -242,18 +267,8 @@ const FileTreeItem = ({
} }
style={{ paddingInlineStart: getIndentationCSS(level) }} style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => e.currentTarget.focus()} onClick={(e) => e.currentTarget.focus()}
onClickCapture={(e) => onClickCapture={clickDirectory}
fileSend({ onFocusCapture={clickDirectory}
type: 'Set selected directory',
directory: fileOrDir,
})
}
onFocusCapture={(e) =>
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()} onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
> >
@ -469,27 +484,30 @@ export const FileTreeInner = ({
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { send: fileSend, context: fileContext } = useFileContext() const { send: fileSend, context: fileContext } = useFileContext()
const { send: modelingSend } = useModelingContext() const { send: modelingSend } = useModelingContext()
const documentHasFocus = useDocumentHasFocus()
// Refresh the file tree when the document gets focus // Refresh the file tree when there are changes.
useEffect(() => { useFileSystemWatcher(
fileSend({ type: 'Refresh' }) async (eventType, path) => {
}, [documentHasFocus]) fileSend({ type: 'Refresh' })
},
[loaderData?.project?.path, fileContext.selectedDirectory.path].filter(
(x: string | undefined) => x !== undefined
)
)
const clickDirectory = () => {
fileSend({
type: 'Set selected directory',
directory: fileContext.project,
})
}
return ( return (
<div <div
className="overflow-auto pb-12 absolute inset-0" className="overflow-auto pb-12 absolute inset-0"
data-testid="file-pane-scroll-container" data-testid="file-pane-scroll-container"
> >
<ul <ul className="m-0 p-0 text-sm" onClickCapture={clickDirectory}>
className="m-0 p-0 text-sm"
onClickCapture={(e) => {
fileSend({
type: 'Set selected directory',
directory: fileContext.project,
})
}}
>
{sortProject(fileContext.project?.children || []).map((fileOrDir) => ( {sortProject(fileContext.project?.children || []).map((fileOrDir) => (
<FileTreeItem <FileTreeItem
project={fileContext.project} project={fileContext.project}

View File

@ -149,6 +149,13 @@ export const ModelingMachineProvider = ({
}, },
'sketch exit execute': ({ context: { store } }) => { 'sketch exit execute': ({ context: { store } }) => {
;(async () => { ;(async () => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') { if (cameraProjection.current === 'perspective') {

View File

@ -1,3 +1,4 @@
import { DebugFeatureTree } from 'components/DebugFeatureTree'
import { AstExplorer } from '../../AstExplorer' import { AstExplorer } from '../../AstExplorer'
import { EngineCommands } from '../../EngineCommands' import { EngineCommands } from '../../EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp' import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
@ -12,6 +13,7 @@ export const DebugPane = () => {
<EngineCommands /> <EngineCommands />
<CamDebugSettings /> <CamDebugSettings />
<AstExplorer /> <AstExplorer />
<DebugFeatureTree />
</div> </div>
</section> </section>
) )

View File

@ -29,8 +29,8 @@ describe('processMemory', () => {
|> lineTo([2.15, 4.32], %) |> lineTo([2.15, 4.32], %)
// |> rx(90, %)` // |> rx(90, %)`
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast, ProgramMemory.empty()) const execState = await enginelessExecutor(ast, ProgramMemory.empty())
const output = processMemory(programMemory) 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({

View File

@ -11,6 +11,8 @@ export const NetworkMachineIndicator = ({
}) => { }) => {
const machineCount = machineManager.machineCount() const machineCount = machineManager.machineCount()
const reason = machineManager.noMachinesReason() const reason = machineManager.noMachinesReason()
const machines = machineManager.machines
console.log('react machines', machines)
return isDesktop() ? ( return isDesktop() ? (
<Popover className="relative"> <Popover className="relative">
@ -46,20 +48,29 @@ export const NetworkMachineIndicator = ({
</div> </div>
{machineCount > 0 && ( {machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80"> <ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{Object.entries(machineManager.machines).map( {machines.map((machine) => {
([hostname, machine]) => ( return (
<li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}> <li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className=""> <p className="">{machine.id.toUpperCase()}</p>
{machine.make_model.model || <p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
machine.make_model.manufacturer || {machine.make_model.model}
'Unknown Machine'}
</p> </p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs"> <p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Hostname {hostname} {machine.state.state.toUpperCase()}
{machine.state.state === 'failed' && machine.state.message
? ': ' + machine.state.message
: ''}
</p> </p>
{machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter && (
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Nozzle Diameter: {machine.extra.nozzle_diameter}
</p>
)}
</li> </li>
) )
)} })}
</ul> </ul>
)} )}
</Popover.Panel> </Popover.Panel>

View File

@ -221,6 +221,19 @@ export const SettingsAuthProviderBase = ({
useFileSystemWatcher( useFileSystemWatcher(
async () => { async () => {
// If there is a projectPath but it no longer exists it means
// it was exterally removed. If we let the code past this condition
// execute it will recreate the directory due to code in
// loadAndValidateSettings trying to recreate files. I do not
// wish to change the behavior in case anything else uses it.
// Go home.
if (loadedProject?.project?.path) {
if (!window.electron.exists(loadedProject?.project?.path)) {
navigate(PATHS.HOME)
return
}
}
const data = await loadAndValidateSettings(loadedProject?.project?.path) const data = await loadAndValidateSettings(loadedProject?.project?.path)
settingsSend({ settingsSend({
type: 'Set all settings', type: 'Set all settings',
@ -228,7 +241,9 @@ export const SettingsAuthProviderBase = ({
doNotPersist: true, doNotPersist: true,
}) })
}, },
settingsPath ? [settingsPath] : [] [settingsPath, loadedProject?.project?.path].filter(
(x: string | undefined) => x !== undefined
)
) )
// Add settings commands to the command bar // Add settings commands to the command bar

View File

@ -0,0 +1,153 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { ToastUpdate } from './ToastUpdate'
describe('ToastUpdate tests', () => {
const testData = {
version: '0.255.255',
files: [
{
url: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
size: 141277345,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.zip',
sha512:
'b+ugdg7A4LhYYJaFkPRxh1RvmGGMlPJJj7inkLg9PwRtCnR9ePMlktj2VRciXF1iLh59XW4bLc4dK1dFQHMULA==',
size: 135278259,
},
{
url: 'Zoo Modeling App-0.255.255-x64-mac.dmg',
sha512:
'gCUqww05yj8OYwPiTq6bo5GbkpngSbXGtenmDD7+kUm0UyVK8WD3dMAfQJtGNG5HY23aHCHe9myE2W4mbZGmiQ==',
size: 146004232,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.dmg',
sha512:
'ND871ayf81F1ZT+iWVLYTc2jdf/Py6KThuxX2QFWz14ebmIbJPL07lNtxQOexOFiuk0MwRhlCy1RzOSG1b9bmw==',
size: 140021522,
},
],
path: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
releaseNotes:
'## Some markdown release notes\n\n- This is a list item\n- This is another list item\n\n```javascript\nconsole.log("Hello, world!")\n```\n',
releaseDate: '2024-10-09T11:57:59.133Z',
} as const
test('Happy path: renders the toast with good data', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={testData.releaseNotes}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.getByTestId('release-notes')
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(restartButton).toBeEnabled()
fireEvent.click(restartButton)
expect(onRestart.mock.calls).toHaveLength(1)
expect(dismissButton).toBeEnabled()
fireEvent.click(dismissButton)
expect(onDismiss.mock.calls).toHaveLength(1)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(releaseNotes).toHaveTextContent('Release notes')
const releaseNotesListItems = screen.getAllByRole('listitem')
expect(releaseNotesListItems.map((el) => el.textContent)).toEqual([
'This is a list item',
'This is another list item',
])
})
test('Happy path: renders the breaking changes notice', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- This is a list item
- This is another list item with a breaking change
- This is a list item
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const releaseNotes = screen.getByText('Release notes', {
selector: 'summary',
})
const listItemContents = screen
.getAllByRole('listitem')
.map((el) => el.textContent)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(listItemContents).toEqual([
'This is a list item',
'This is another list item with a breaking change',
'This is a list item',
])
})
test('Missing release notes: renders the toast without release notes', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={''}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.queryByText(/release notes/i, {
selector: 'details > summary',
})
const releaseNotesListItem = screen.queryByRole('listitem', {
name: /this is a list item/i,
})
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(releaseNotes).not.toBeInTheDocument()
expect(releaseNotesListItem).not.toBeInTheDocument()
expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled()
})
})

View File

@ -1,14 +1,23 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
export function ToastUpdate({ export function ToastUpdate({
version, version,
releaseNotes,
onRestart, onRestart,
onDismiss,
}: { }: {
version: string version: string
releaseNotes?: string
onRestart: () => void onRestart: () => void
onDismiss: () => void
}) { }) {
const containsBreakingChanges = releaseNotes
?.toLocaleLowerCase()
.includes('breaking')
return ( return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md"> <div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90"> <div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -19,7 +28,7 @@ export function ToastUpdate({
> >
v{version} v{version}
</span> </span>
<span className="ml-4 text-md text-bold"> <p className="ml-4 text-md text-bold">
A new update has downloaded and will be available next time you A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '} start the app. You can view the release notes{' '}
<a <a
@ -32,15 +41,39 @@ export function ToastUpdate({
> >
here on GitHub. here on GitHub.
</a> </a>
</span> </p>
</div> </div>
{releaseNotes && (
<details
className="my-4 border border-chalkboard-30 dark:border-chalkboard-60 rounded"
open={containsBreakingChanges}
data-testid="release-notes"
>
<summary className="p-2 select-none cursor-pointer">
Release notes
{containsBreakingChanges && (
<strong className="text-destroy-50"> (Breaking changes)</strong>
)}
</summary>
<div
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, {
gfm: true,
breaks: true,
sanitize: true,
}),
}}
></div>
</details>
)}
<div className="flex justify-between gap-8"> <div className="flex justify-between gap-8">
<ActionButton <ActionButton
Element="button" Element="button"
iconStart={{ iconStart={{
icon: 'arrowRotateRight', icon: 'arrowRotateRight',
}} }}
name="Restart app now" name="restart"
onClick={onRestart} onClick={onRestart}
> >
Restart app now Restart app now
@ -50,9 +83,10 @@ export function ToastUpdate({
iconStart={{ iconStart={{
icon: 'checkmark', icon: 'checkmark',
}} }}
name="Got it" name="dismiss"
onClick={() => { onClick={() => {
toast.dismiss() toast.dismiss()
onDismiss()
}} }}
> >
Got it Got it

View File

@ -1,6 +1,9 @@
import { styleTags, tags as t } from '@lezer/highlight' import { styleTags, tags as t } from '@lezer/highlight'
export const kclHighlight = styleTags({ export const kclHighlight = styleTags({
'import export': t.moduleKeyword,
ImportItemAs: t.definitionKeyword,
ImportFrom: t.moduleKeyword,
'fn var let const': t.definitionKeyword, 'fn var let const': t.definitionKeyword,
'if else': t.controlKeyword, 'if else': t.controlKeyword,
return: t.controlKeyword, return: t.controlKeyword,
@ -8,7 +11,7 @@ export const kclHighlight = styleTags({
nil: t.null, nil: t.null,
'AddOp MultOp ExpOp': t.arithmeticOperator, 'AddOp MultOp ExpOp': t.arithmeticOperator,
BangOp: t.logicOperator, BangOp: t.logicOperator,
CompOp: t.logicOperator, CompOp: t.compareOperator,
'Equals Arrow': t.definitionOperator, 'Equals Arrow': t.definitionOperator,
PipeOperator: t.controlOperator, PipeOperator: t.controlOperator,
String: t.string, String: t.string,

View File

@ -15,8 +15,9 @@
} }
statement[@isGroup=Statement] { statement[@isGroup=Statement] {
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } | ImportStatement { kw<"import"> ImportItems ImportFrom String } |
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } | FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ReturnStatement { kw<"return"> expression } | ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression } ExpressionStatement { expression }
} }
@ -25,6 +26,9 @@ ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")"
Body { "{" statement* "}" } Body { "{" statement* "}" }
ImportItems { commaSep1NoTrailingComma<ImportItem> }
ImportItem { identifier (ImportItemAs identifier)? }
expression[@isGroup=Expression] { expression[@isGroup=Expression] {
String | String |
Number | Number |
@ -74,6 +78,8 @@ kw<term> { @specialize[@name={term}]<identifier, term> }
commaSep<term> { (term ("," term)*)? ","? } commaSep<term> { (term ("," term)*)? ","? }
commaSep1NoTrailingComma<term> { term ("," term)* }
@tokens { @tokens {
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' } String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
@ -84,7 +90,7 @@ commaSep<term> { (term ("," term)*)? ","? }
MultOp { "/" | "*" | "\\" } MultOp { "/" | "*" | "\\" }
ExpOp { "^" } ExpOp { "^" }
BangOp { "!" } BangOp { "!" }
CompOp { $[<>] "="? | "!=" | "==" } CompOp { "==" | "!=" | "<=" | ">=" | "<" | ">" }
Equals { "=" } Equals { "=" }
Arrow { "=>" } Arrow { "=>" }
PipeOperator { "|>" } PipeOperator { "|>" }
@ -106,6 +112,9 @@ commaSep<term> { (term ("," term)*)? ","? }
Shebang { "#!" ![\n]* } Shebang { "#!" ![\n]* }
ImportItemAs { "as" }
ImportFrom { "from" }
"(" ")" "(" ")"
"{" "}" "{" "}"
"[" "]" "[" "]"

View File

@ -12,35 +12,51 @@ type Path = string
// watcher.addListener(() => { ... }). // watcher.addListener(() => { ... }).
export const useFileSystemWatcher = ( export const useFileSystemWatcher = (
callback: (path: Path) => Promise<void>, callback: (eventType: string, path: Path) => Promise<void>,
dependencyArray: Path[] paths: Path[]
): void => { ): void => {
// Track a ref to the callback. This is how we get the callback updated // Used to track this instance of useFileSystemWatcher.
// across the NodeJS<->Browser boundary. // Assign to ref so it doesn't change between renders.
const callbackRef = useRef<{ fn: (path: Path) => Promise<void> }>({ const key = useRef(Math.random().toString())
fn: async (_path) => {},
}) const [output, setOutput] = useState<
{ eventType: string; path: string } | undefined
>(undefined)
// Used to track if paths list changes.
const [pathsTracked, setPathsTracked] = useState<Path[]>([])
useEffect(() => { useEffect(() => {
callbackRef.current.fn = callback if (!output) return
}, [callback]) callback(output.eventType, output.path).catch(reportRejection)
}, [output])
// Used to track if dependencyArrray changes.
const [dependencyArrayTracked, setDependencyArrayTracked] = useState<Path[]>(
[]
)
// On component teardown obliterate all watchers. // On component teardown obliterate all watchers.
useEffect(() => { useEffect(() => {
// The hook is useless on web. // The hook is useless on web.
if (!isDesktop()) return if (!isDesktop()) return
const cbWatcher = (eventType: string, path: string) => {
setOutput({ eventType, path })
}
for (let path of pathsTracked) {
// Because functions don't retain refs between NodeJS-Browser I need to
// pass an identifying key so we can later remove it.
// A way to think of the function call is:
// "For this path, add a new handler with this key"
// "There can be many keys (functions) per path"
// Again if refs were preserved, we wouldn't need to do this. Keys
// gives us uniqueness.
window.electron.watchFileOn(path, key.current, cbWatcher)
}
return () => { return () => {
for (let path of dependencyArray) { for (let path of pathsTracked) {
window.electron.watchFileOff(path) window.electron.watchFileOff(path, key.current)
} }
} }
}, []) }, [pathsTracked])
function difference<T>(l1: T[], l2: T[]): [T[], T[]] { function difference<T>(l1: T[], l2: T[]): [T[], T[]] {
return [ return [
@ -49,8 +65,7 @@ export const useFileSystemWatcher = (
] ]
} }
const hasDiff = const hasDiff = difference(paths, pathsTracked)[0].length !== 0
difference(dependencyArray, dependencyArrayTracked)[0].length !== 0
// Removing 1 watcher at a time is only possible because in a filesystem, // Removing 1 watcher at a time is only possible because in a filesystem,
// a path is unique (there can never be two paths with the same name). // a path is unique (there can never be two paths with the same name).
@ -61,19 +76,8 @@ export const useFileSystemWatcher = (
if (!hasDiff) return if (!hasDiff) return
const [pathsRemoved, pathsRemaining] = difference( const [, pathsRemaining] = difference(pathsTracked, paths)
dependencyArrayTracked, const [pathsAdded] = difference(paths, pathsTracked)
dependencyArray setPathsTracked(pathsRemaining.concat(pathsAdded))
)
for (let path of pathsRemoved) {
window.electron.watchFileOff(path)
}
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
for (let path of pathsAdded) {
window.electron.watchFileOn(path, (_eventType: string, path: Path) => {
callbackRef.current.fn(path).catch(reportRejection)
})
}
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
}, [hasDiff]) }, [hasDiff])
} }

View File

@ -293,6 +293,24 @@ code {
which lets you use them with @apply in your CSS, and get which lets you use them with @apply in your CSS, and get
autocomplete in classNames in your JSX. autocomplete in classNames in your JSX.
*/ */
.parsed-markdown ul,
.parsed-markdown ol {
@apply list-outside pl-4 lg:pl-8 my-2;
}
.parsed-markdown ul li {
@apply list-disc;
}
.parsed-markdown li p {
@apply inline;
}
.parsed-markdown code {
@apply px-1 py-0.5 rounded-sm;
@apply bg-chalkboard-20 text-chalkboard-80;
@apply dark:bg-chalkboard-80 dark:text-chalkboard-30;
}
} }
#code-mirror-override .cm-scroller, #code-mirror-override .cm-scroller,

View File

@ -8,6 +8,7 @@ import ModalContainer from 'react-modal-promise'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { AppStreamProvider } from 'AppState' import { AppStreamProvider } from 'AppState'
import { ToastUpdate } from 'components/ToastUpdate' import { ToastUpdate } from 'components/ToastUpdate'
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env' // import { DEV } from 'env'
@ -53,17 +54,35 @@ root.render(
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals() reportWebVitals()
isDesktop() && if (isDesktop()) {
window.electron.onUpdateDownloaded((version: string) => { // Listen for update download progress to begin
// to show a loading toast.
window.electron.onUpdateDownloadStart(() => {
const message = `Downloading app update...`
console.log(message)
toast.loading(message, { id: AUTO_UPDATER_TOAST_ID })
})
// Listen for update download errors to show
// an error toast and clear the loading toast.
window.electron.onUpdateError(({ error }) => {
console.error(error)
toast.error('An error occurred while downloading the update.', {
id: AUTO_UPDATER_TOAST_ID,
})
})
window.electron.onUpdateDownloaded(({ version, releaseNotes }) => {
const message = `A new update (${version}) was downloaded and will be available next time you open the app.` const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
console.log(message) console.log(message)
toast.custom( toast.custom(
ToastUpdate({ ToastUpdate({
version, version,
releaseNotes,
onRestart: () => { onRestart: () => {
window.electron.appRestart() window.electron.appRestart()
}, },
onDismiss: () => {},
}), }),
{ duration: 30000 } { duration: 30000, id: AUTO_UPDATER_TOAST_ID }
) )
}) })
}

View File

@ -8,6 +8,8 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { import {
CallExpression, CallExpression,
emptyExecState,
ExecState,
initPromise, initPromise,
parse, parse,
PathToNode, PathToNode,
@ -38,10 +40,9 @@ export class KclManager {
nonCodeMeta: { nonCodeMeta: {
nonCodeNodes: {}, nonCodeNodes: {},
start: [], start: [],
digest: null,
}, },
digest: null,
} }
private _execState: ExecState = emptyExecState()
private _programMemory: ProgramMemory = ProgramMemory.empty() private _programMemory: ProgramMemory = ProgramMemory.empty()
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty() lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
private _logs: string[] = [] private _logs: string[] = []
@ -72,11 +73,21 @@ export class KclManager {
get programMemory() { get programMemory() {
return this._programMemory return this._programMemory
} }
set programMemory(programMemory) { // This is private because callers should be setting the entire execState.
private set programMemory(programMemory) {
this._programMemory = programMemory this._programMemory = programMemory
this._programMemoryCallBack(programMemory) this._programMemoryCallBack(programMemory)
} }
set execState(execState) {
this._execState = execState
this.programMemory = execState.memory
}
get execState() {
return this._execState
}
get logs() { get logs() {
return this._logs return this._logs
} }
@ -195,9 +206,7 @@ export class KclManager {
nonCodeMeta: { nonCodeMeta: {
nonCodeNodes: {}, nonCodeNodes: {},
start: [], start: [],
digest: null,
}, },
digest: null,
} }
} }
@ -253,8 +262,9 @@ export class KclManager {
// Make sure we clear before starting again. End session will do this. // Make sure we clear before starting again. End session will do this.
this.engineCommandManager?.endSession() this.engineCommandManager?.endSession()
await this.ensureWasmInit() await this.ensureWasmInit()
const { logs, errors, programMemory, isInterrupted } = await executeAst({ const { logs, errors, execState, isInterrupted } = await executeAst({
ast, ast,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
}) })
@ -264,7 +274,7 @@ export class KclManager {
this.lints = await lintAst({ ast: ast }) this.lints = await lintAst({ ast: ast })
sceneInfra.modelingSend({ type: 'code edit during sketch' }) sceneInfra.modelingSend({ type: 'code edit during sketch' })
defaultSelectionFilter(programMemory, this.engineCommandManager) defaultSelectionFilter(execState.memory, this.engineCommandManager)
if (args.zoomToFit) { if (args.zoomToFit) {
let zoomObjectId: string | undefined = '' let zoomObjectId: string | undefined = ''
@ -295,12 +305,20 @@ export class KclManager {
this._cancelTokens.delete(currentExecutionId) this._cancelTokens.delete(currentExecutionId)
return return
} }
// Exit sketch mode if the AST is empty
if (this._isAstEmpty(ast)) {
await this.disableSketchMode()
}
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.addKclErrors(isInterrupted ? [] : errors)
this.programMemory = programMemory // Reset the next ID index so that we reuse the previous IDs next time.
execState.idGenerator.nextId = 0
this.execState = execState
if (!errors.length) { if (!errors.length) {
this.lastSuccessfulProgramMemory = programMemory this.lastSuccessfulProgramMemory = execState.memory
} }
this.ast = { ...ast } this.ast = { ...ast }
this._executeCallback() this._executeCallback()
@ -338,17 +356,19 @@ export class KclManager {
await codeManager.writeToFile() await codeManager.writeToFile()
this._ast = { ...newAst } this._ast = { ...newAst }
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, execState } = await executeAst({
ast: newAst, ast: newAst,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
}) })
this._logs = logs this._logs = logs
this._kclErrors = errors this._kclErrors = errors
this._programMemory = programMemory this._execState = execState
this._programMemory = execState.memory
if (!errors.length) { if (!errors.length) {
this.lastSuccessfulProgramMemory = programMemory this.lastSuccessfulProgramMemory = execState.memory
} }
if (updates !== 'artifactRanges') return if (updates !== 'artifactRanges') return
@ -553,6 +573,24 @@ export class KclManager {
defaultSelectionFilter() { defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager) defaultSelectionFilter(this.programMemory, this.engineCommandManager)
} }
/**
* We can send a single command of 'enable_sketch_mode' or send this in a batched request.
* When there is no code in the KCL editor we should be sending 'sketch_mode_disable' since any previous half finished
* code could leave the state of the application in sketch mode on the engine side.
*/
async disableSketchMode() {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
}
// Determines if there is no KCL code which means it is executing a blank KCL file
_isAstEmpty(ast: Program) {
return ast.start === 0 && ast.end === 0 && ast.body.length === 0
}
} }
function defaultSelectionFilter( function defaultSelectionFilter(

File diff suppressed because it is too large Load Diff

View File

@ -14,9 +14,9 @@ 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 programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.get('mySketch001') const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'UserVal', type: 'UserVal',
__meta: [{ sourceRange: [46, 71] }], __meta: [{ sourceRange: [46, 71] }],
@ -68,9 +68,9 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)
|> extrude(2, %)` |> extrude(2, %)`
const programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.get('mySketch001') const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'Solid', type: 'Solid',
id: expect.any(String), id: expect.any(String),
@ -148,9 +148,10 @@ const sk2 = startSketchOn('XY')
|> extrude(2, %) |> extrude(2, %)
` `
const programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
const programMemory = execState.memory
// @ts-ignore // @ts-ignore
const geos = [programMemory?.get('theExtrude'), programMemory?.get('sk2')] const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')]
expect(geos).toEqual([ expect(geos).toEqual([
{ {
type: 'Solid', type: 'Solid',
@ -171,7 +172,6 @@ const sk2 = startSketchOn('XY')
start: 114, start: 114,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'p', value: 'p',
digest: null,
}, },
id: expect.any(String), id: expect.any(String),
sourceRange: [95, 117], sourceRange: [95, 117],
@ -222,7 +222,6 @@ const sk2 = startSketchOn('XY')
start: 114, start: 114,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'p', value: 'p',
digest: null,
}, },
__geoMeta: { __geoMeta: {
id: expect.any(String), id: expect.any(String),
@ -265,7 +264,6 @@ const sk2 = startSketchOn('XY')
start: 417, start: 417,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'o', value: 'o',
digest: null,
}, },
id: expect.any(String), id: expect.any(String),
sourceRange: [399, 420], sourceRange: [399, 420],
@ -316,7 +314,6 @@ const sk2 = startSketchOn('XY')
start: 417, start: 417,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'o', value: 'o',
digest: null,
}, },
__geoMeta: { __geoMeta: {
id: expect.any(String), id: expect.any(String),

View File

@ -73,7 +73,6 @@ const newVar = myVar + 1`
start: 89, start: 89,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'myPath', value: 'myPath',
digest: null,
}, },
}, },
{ {
@ -99,7 +98,6 @@ const newVar = myVar + 1`
start: 143, start: 143,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'rightPath', value: 'rightPath',
digest: null,
}, },
}, },
]) ])
@ -201,7 +199,6 @@ const newVar = myVar + 1`
start: 109, start: 109,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'myPath', value: 'myPath',
digest: null,
}, },
}, },
{ {
@ -443,6 +440,6 @@ async function exe(
) { ) {
const ast = parse(code) const ast = parse(code)
const result = await enginelessExecutor(ast, programMemory) const execState = await enginelessExecutor(ast, programMemory)
return result return execState.memory
} }

View File

@ -4,11 +4,14 @@ import {
ProgramMemory, ProgramMemory,
programMemoryInit, programMemoryInit,
kclLint, kclLint,
emptyExecState,
ExecState,
} from 'lang/wasm' } from 'lang/wasm'
import { enginelessExecutor } from 'lib/testHelpers' 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'
export type ToolTip = export type ToolTip =
| 'lineTo' | 'lineTo'
@ -47,16 +50,18 @@ export async function executeAst({
engineCommandManager, engineCommandManager,
useFakeExecutor = false, useFakeExecutor = false,
programMemoryOverride, programMemoryOverride,
idGenerator,
}: { }: {
ast: Program ast: Program
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean useFakeExecutor?: boolean
programMemoryOverride?: ProgramMemory programMemoryOverride?: ProgramMemory
idGenerator?: IdGenerator
isInterrupted?: boolean isInterrupted?: boolean
}): Promise<{ }): Promise<{
logs: string[] logs: string[]
errors: KCLError[] errors: KCLError[]
programMemory: ProgramMemory execState: ExecState
isInterrupted: boolean isInterrupted: boolean
}> { }> {
try { try {
@ -65,15 +70,21 @@ export async function executeAst({
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
} }
const programMemory = await (useFakeExecutor const execState = await (useFakeExecutor
? enginelessExecutor(ast, programMemoryOverride || programMemoryInit()) ? enginelessExecutor(ast, programMemoryOverride || programMemoryInit())
: _executor(ast, programMemoryInit(), engineCommandManager, false)) : _executor(
ast,
programMemoryInit(),
idGenerator,
engineCommandManager,
false
))
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
return { return {
logs: [], logs: [],
errors: [], errors: [],
programMemory, execState,
isInterrupted: false, isInterrupted: false,
} }
} catch (e: any) { } catch (e: any) {
@ -89,7 +100,7 @@ export async function executeAst({
return { return {
errors: [e], errors: [e],
logs: [], logs: [],
programMemory: ProgramMemory.empty(), execState: emptyExecState(),
isInterrupted, isInterrupted,
} }
} else { } else {
@ -97,7 +108,7 @@ export async function executeAst({
return { return {
logs: [e], logs: [e],
errors: [], errors: [],
programMemory: ProgramMemory.empty(), execState: emptyExecState(),
isInterrupted, isInterrupted,
} }
} }

View File

@ -100,15 +100,15 @@ describe('Testing findUniqueName', () => {
it('should find a unique name', () => { it('should find a unique name', () => {
const result = findUniqueName( const result = findUniqueName(
JSON.stringify([ JSON.stringify([
{ type: 'Identifier', name: 'yo01', start: 0, end: 0, digest: null }, { type: 'Identifier', name: 'yo01', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0, digest: null }, { type: 'Identifier', name: 'yo02', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0, digest: null }, { type: 'Identifier', name: 'yo03', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0, digest: null }, { type: 'Identifier', name: 'yo04', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0, digest: null }, { type: 'Identifier', name: 'yo05', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0, digest: null }, { type: 'Identifier', name: 'yo06', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0, digest: null }, { type: 'Identifier', name: 'yo07', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0, digest: null }, { type: 'Identifier', name: 'yo08', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0, digest: null }, { type: 'Identifier', name: 'yo09', start: 0, end: 0 },
] satisfies Identifier[]), ] satisfies Identifier[]),
'yo', 'yo',
2 2
@ -123,8 +123,7 @@ describe('Testing addSketchTo', () => {
body: [], body: [],
start: 0, start: 0,
end: 0, end: 0,
nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null }, nonCodeMeta: { nonCodeNodes: {}, start: [] },
digest: null,
}, },
'yz' 'yz'
) )
@ -220,11 +219,11 @@ 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 = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = 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,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -235,11 +234,11 @@ yo2 = hmm([identifierGuy + 5])`
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 = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = 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,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -250,11 +249,11 @@ yo2 = hmm([identifierGuy + 5])`
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 = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = 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,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -265,11 +264,11 @@ yo2 = hmm([identifierGuy + 5])`
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 = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = 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,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -280,11 +279,11 @@ yo2 = hmm([identifierGuy + 5])`
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 = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = 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,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -465,7 +464,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|> line([306.21, 198.87], %)` |> line([306.21, 198.87], %)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = 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] = [
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
@ -475,7 +474,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
const modifiedAst = deleteSegmentFromPipeExpression( const modifiedAst = deleteSegmentFromPipeExpression(
[], [],
ast, ast,
programMemory, execState.memory,
code, code,
pathToNode pathToNode
) )
@ -543,7 +542,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
const code = makeCode(line) const code = makeCode(line)
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = line const lineOfInterest = line
const range: [number, number] = [ const range: [number, number] = [
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
@ -554,7 +553,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
const modifiedAst = deleteSegmentFromPipeExpression( const modifiedAst = deleteSegmentFromPipeExpression(
dependentSegments, dependentSegments,
ast, ast,
programMemory, execState.memory,
code, code,
pathToNode pathToNode
) )
@ -632,7 +631,7 @@ describe('Testing removeSingleConstraintInfo', () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = 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] = [
code.indexOf(lineOfInterest) + 1, code.indexOf(lineOfInterest) + 1,
@ -661,7 +660,7 @@ describe('Testing removeSingleConstraintInfo', () => {
pathToNode, pathToNode,
argPosition, argPosition,
ast, ast,
programMemory execState.memory
) )
if (!mod) return new Error('mod is undefined') if (!mod) return new Error('mod is undefined')
const recastCode = recast(mod.modifiedAst) const recastCode = recast(mod.modifiedAst)
@ -686,7 +685,7 @@ describe('Testing removeSingleConstraintInfo', () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = 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] = [
code.indexOf(lineOfInterest) + 1, code.indexOf(lineOfInterest) + 1,
@ -711,7 +710,7 @@ describe('Testing removeSingleConstraintInfo', () => {
pathToNode, pathToNode,
argPosition, argPosition,
ast, ast,
programMemory execState.memory
) )
if (!mod) return new Error('mod is undefined') if (!mod) return new Error('mod is undefined')
const recastCode = recast(mod.modifiedAst) const recastCode = recast(mod.modifiedAst)
@ -882,7 +881,7 @@ sketch002 = startSketchOn({
// const lineOfInterest = 'line([-2.94, 2.7], %)' // const lineOfInterest = 'line([-2.94, 2.7], %)'
const ast = parse(codeBefore) const ast = parse(codeBefore)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
// deleteFromSelection // deleteFromSelection
const range: [number, number] = [ const range: [number, number] = [
@ -895,7 +894,7 @@ sketch002 = startSketchOn({
range, range,
type, type,
}, },
programMemory, execState.memory,
async () => { async () => {
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
return { return {

View File

@ -241,7 +241,6 @@ export function mutateObjExpProp(
value: updateWith, value: updateWith,
start: 0, start: 0,
end: 0, end: 0,
digest: null,
}) })
} }
} }
@ -501,6 +500,7 @@ export function sketchOnExtrudedFace(
createIdentifier(extrudeName ? extrudeName : oldSketchName), createIdentifier(extrudeName ? extrudeName : oldSketchName),
_tag, _tag,
]), ]),
undefined,
'const' 'const'
) )
@ -578,7 +578,6 @@ export function createLiteral(value: string | number): Literal {
end: 0, end: 0,
value, value,
raw: `${value}`, raw: `${value}`,
digest: null,
} }
} }
@ -587,7 +586,7 @@ export function createTagDeclarator(value: string): TagDeclarator {
type: 'TagDeclarator', type: 'TagDeclarator',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
value, value,
} }
} }
@ -597,7 +596,7 @@ export function createIdentifier(name: string): Identifier {
type: 'Identifier', type: 'Identifier',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
name, name,
} }
} }
@ -607,7 +606,6 @@ export function createPipeSubstitution(): PipeSubstitution {
type: 'PipeSubstitution', type: 'PipeSubstitution',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
} }
} }
@ -623,12 +621,11 @@ export function createCallExpressionStdLib(
type: 'Identifier', type: 'Identifier',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
name, name,
}, },
optional: false, optional: false,
arguments: args, arguments: args,
digest: null,
} }
} }
@ -644,12 +641,11 @@ export function createCallExpression(
type: 'Identifier', type: 'Identifier',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
name, name,
}, },
optional: false, optional: false,
arguments: args, arguments: args,
digest: null,
} }
} }
@ -660,7 +656,7 @@ export function createArrayExpression(
type: 'ArrayExpression', type: 'ArrayExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(), nonCodeMeta: nonCodeMetaEmpty(),
elements, elements,
} }
@ -673,7 +669,7 @@ export function createPipeExpression(
type: 'PipeExpression', type: 'PipeExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
body, body,
nonCodeMeta: nonCodeMetaEmpty(), nonCodeMeta: nonCodeMetaEmpty(),
} }
@ -682,23 +678,25 @@ export function createPipeExpression(
export function createVariableDeclaration( export function createVariableDeclaration(
varName: string, varName: string,
init: VariableDeclarator['init'], init: VariableDeclarator['init'],
visibility: VariableDeclaration['visibility'] = 'default',
kind: VariableDeclaration['kind'] = 'const' kind: VariableDeclaration['kind'] = 'const'
): VariableDeclaration { ): VariableDeclaration {
return { return {
type: 'VariableDeclaration', type: 'VariableDeclaration',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
declarations: [ declarations: [
{ {
type: 'VariableDeclarator', type: 'VariableDeclarator',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
id: createIdentifier(varName), id: createIdentifier(varName),
init, init,
}, },
], ],
visibility,
kind, kind,
} }
} }
@ -710,14 +708,14 @@ export function createObjectExpression(properties: {
type: 'ObjectExpression', type: 'ObjectExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(), nonCodeMeta: nonCodeMetaEmpty(),
properties: Object.entries(properties).map(([key, value]) => ({ properties: Object.entries(properties).map(([key, value]) => ({
type: 'ObjectProperty', type: 'ObjectProperty',
start: 0, start: 0,
end: 0, end: 0,
key: createIdentifier(key), key: createIdentifier(key),
digest: null,
value, value,
})), })),
} }
@ -731,7 +729,7 @@ export function createUnaryExpression(
type: 'UnaryExpression', type: 'UnaryExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
operator, operator,
argument, argument,
} }
@ -746,7 +744,7 @@ export function createBinaryExpression([left, operator, right]: [
type: 'BinaryExpression', type: 'BinaryExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
operator, operator,
left, left,
right, right,
@ -1136,5 +1134,5 @@ export async function deleteFromSelection(
} }
const nonCodeMetaEmpty = () => { const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, start: [], digest: null } return { nonCodeNodes: {}, start: [] }
} }

View File

@ -620,7 +620,7 @@ describe('Testing button states', () => {
it('should return true when body exists and segment is selected', async () => { it('should return true when body exists and segment is selected', async () => {
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true) await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
}) })
it('hould return false when body exists and not a segment is selected', async () => { it('should return false when body exists and not a segment is selected', async () => {
await runButtonStateTest(codeWithBody, `close(%)`, false) await runButtonStateTest(codeWithBody, `close(%)`, false)
}) })
}) })

View File

@ -1,5 +1,7 @@
import { import {
CallExpression, CallExpression,
Expr,
Identifier,
ObjectExpression, ObjectExpression,
PathToNode, PathToNode,
Program, Program,
@ -27,7 +29,7 @@ import {
sketchLineHelperMap, sketchLineHelperMap,
} from '../std/sketch' } from '../std/sketch'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections' import { Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { import {
ArtifactGraph, ArtifactGraph,
@ -66,7 +68,10 @@ export function modifyAstCloneWithFilletAndTag(
const artifactGraph = engineCommandManager.artifactGraph const artifactGraph = engineCommandManager.artifactGraph
// Step 1: modify ast with tags and group them by extrude nodes (bodies) // Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map<PathToNode, string[]> = new Map() const extrudeToTagsMap: Map<
PathToNode,
Array<{ tag: string; selectionType: string }>
> = new Map()
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
for (const selectionRange of selection.codeBasedSelections) { for (const selectionRange of selection.codeBasedSelections) {
@ -74,6 +79,7 @@ export function modifyAstCloneWithFilletAndTag(
codeBasedSelections: [selectionRange], codeBasedSelections: [selectionRange],
otherSelections: [], otherSelections: [],
} }
const selectionType = singleSelection.codeBasedSelections[0].type
const result = getPathToExtrudeForSegmentSelection( const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude, clonedAstForGetExtrude,
@ -89,6 +95,7 @@ export function modifyAstCloneWithFilletAndTag(
) )
if (err(tagResult)) return tagResult if (err(tagResult)) return tagResult
const { tag } = tagResult const { tag } = tagResult
const tagInfo = { tag, selectionType }
// Group tags by their corresponding extrude node // Group tags by their corresponding extrude node
const extrudeKey = JSON.stringify(pathToExtrudeNode) const extrudeKey = JSON.stringify(pathToExtrudeNode)
@ -96,23 +103,29 @@ export function modifyAstCloneWithFilletAndTag(
if (lookupMap.has(extrudeKey)) { if (lookupMap.has(extrudeKey)) {
const existingPath = lookupMap.get(extrudeKey) const existingPath = lookupMap.get(extrudeKey)
if (!existingPath) return new Error('Path to extrude node not found.') if (!existingPath) return new Error('Path to extrude node not found.')
extrudeToTagsMap.get(existingPath)?.push(tag) extrudeToTagsMap.get(existingPath)?.push(tagInfo)
} else { } else {
lookupMap.set(extrudeKey, pathToExtrudeNode) lookupMap.set(extrudeKey, pathToExtrudeNode)
extrudeToTagsMap.set(pathToExtrudeNode, [tag]) extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo])
} }
} }
// Step 2: Apply fillet(s) for each extrude node (body) // Step 2: Apply fillet(s) for each extrude node (body)
let pathToFilletNodes: Array<PathToNode> = [] let pathToFilletNodes: Array<PathToNode> = []
for (const [pathToExtrudeNode, tags] of extrudeToTagsMap.entries()) { for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) {
// Create a fillet expression with multiple tags // Create a fillet expression with multiple tags
const radiusValue = const radiusValue =
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst 'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
const tagCalls = tagInfos.map(({ tag, selectionType }) => {
return getEdgeTagCall(tag, selectionType)
})
const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges)
const filletCall = createCallExpressionStdLib('fillet', [ const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({ createObjectExpression({
radius: radiusValue, radius: radiusValue,
tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))), tags: createArrayExpression(tagCalls),
}), }),
createPipeSubstitution(), createPipeSubstitution(),
]) ])
@ -144,7 +157,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral( pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode, pathToExtrudeNode,
extrudeDeclarator, extrudeDeclarator,
tags[0] firstTag
) )
pathToFilletNodes.push(pathToFilletNode) pathToFilletNodes.push(pathToFilletNode)
} else if (extrudeDeclarator.init.type === 'PipeExpression') { } else if (extrudeDeclarator.init.type === 'PipeExpression') {
@ -165,7 +178,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral( pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode, pathToExtrudeNode,
extrudeDeclarator, extrudeDeclarator,
tags[0] firstTag
) )
pathToFilletNodes.push(pathToFilletNode) pathToFilletNodes.push(pathToFilletNode)
} else { } else {
@ -276,6 +289,21 @@ function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag } return { modifiedAst: astClone, tag }
} }
function getEdgeTagCall(
tag: string,
selectionType: string
): Identifier | CallExpression {
let tagCall: Expr = createIdentifier(tag)
// Modify the tag based on selectionType
if (selectionType === 'edge') {
tagCall = createCallExpressionStdLib('getOppositeEdge', [tagCall])
} else if (selectionType === 'adjacent-edge') {
tagCall = createCallExpressionStdLib('getNextAdjacentEdge', [tagCall])
}
return tagCall
}
function locateExtrudeDeclarator( function locateExtrudeDeclarator(
node: Program, node: Program,
pathToExtrudeNode: PathToNode pathToExtrudeNode: PathToNode
@ -311,7 +339,7 @@ function locateExtrudeDeclarator(
function getPathToNodeOfFilletLiteral( function getPathToNodeOfFilletLiteral(
pathToExtrudeNode: PathToNode, pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator, extrudeDeclarator: VariableDeclarator,
tag: string tag: Identifier | CallExpression
): PathToNode { ): PathToNode {
let pathToFilletObj: PathToNode = [] let pathToFilletObj: PathToNode = []
let inFillet = false let inFillet = false
@ -347,12 +375,30 @@ function getPathToNodeOfFilletLiteral(
] ]
} }
function hasTag(node: ObjectExpression, tag: string): boolean { function hasTag(
node: ObjectExpression,
tag: Identifier | CallExpression
): boolean {
return node.properties.some((prop) => { return node.properties.some((prop) => {
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') { if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some( // if selection is a base edge:
(element) => element.type === 'Identifier' && element.name === tag if (tag.type === 'Identifier') {
) return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag.name
)
}
// if selection is an adjacent or opposite edge:
if (tag.type === 'CallExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'CallExpression' &&
element.callee.name === tag.callee.name && // edge location
element.arguments[0].type === 'Identifier' &&
tag.arguments[0].type === 'Identifier' &&
element.arguments[0].name === tag.arguments[0].name // tag name
)
}
} }
return false return false
}) })
@ -383,7 +429,7 @@ export const hasValidFilletSelection = ({
ast: Program ast: Program
code: string code: string
}) => { }) => {
// case 0: check if there is anything filletable in the scene // check if there is anything filletable in the scene
let extrudeExists = false let extrudeExists = false
traverse(ast, { traverse(ast, {
enter(node) { enter(node) {
@ -394,65 +440,88 @@ export const hasValidFilletSelection = ({
}) })
if (!extrudeExists) return false if (!extrudeExists) return false
// case 1: nothing selected, test whether the extrusion exists // check if nothing is selected
if (selectionRanges) { if (selectionRanges.codeBasedSelections.length === 0) {
if (selectionRanges.codeBasedSelections.length === 0) { return true
return true
}
const range0 = selectionRanges.codeBasedSelections[0].range[0]
const codeLength = code.length
if (range0 === codeLength) {
return true
}
} }
// case 2: sketch segment selected, test whether it is extruded // check if selection is last string in code
// TODO: add loft / sweep check if (selectionRanges.codeBasedSelections[0].range[0] === code.length) {
if (selectionRanges.codeBasedSelections.length > 0) { return true
const isExtruded = hasSketchPipeBeenExtruded( }
selectionRanges.codeBasedSelections[0],
ast // selection exists:
for (const selection of selectionRanges.codeBasedSelections) {
// check if all selections are in sketchLineHelperMap
const path = getNodePathFromSourceRange(ast, selection.range)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
path,
'CallExpression'
) )
if (isExtruded) { if (err(segmentNode)) return false
const pathToSelectedNode = getNodePathFromSourceRange( if (segmentNode.node.type !== 'CallExpression') {
ast, return false
selectionRanges.codeBasedSelections[0].range }
) if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
const segmentNode = getNodeFromPath<CallExpression>(
ast,
pathToSelectedNode,
'CallExpression'
)
if (err(segmentNode)) return false
if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name
if (segmentName in sketchLineHelperMap) {
// Add check whether the tag exists at all:
if (!(segmentNode.node.arguments.length === 3)) return true
// If the tag exists, check if it is already filleted
const edges = isTagUsedInFillet({
ast,
callExp: segmentNode.node,
})
// edge has already been filleted
if (
['edge', 'default'].includes(
selectionRanges.codeBasedSelections[0].type
) &&
edges.includes('baseEdge')
)
return false
return true
} else {
return false
}
}
} else {
return false return false
} }
}
return canFilletSelection(selectionRanges) // check if selection is extruded
// TODO: option 1 : extrude is in the sketch pipe
// option 2: extrude is outside the sketch pipe
const extrudeExists = hasSketchPipeBeenExtruded(selection, ast)
if (err(extrudeExists)) {
return false
}
if (!extrudeExists) {
return false
}
// check if tag exists for the selection
let tagExists = false
let tag = ''
traverse(segmentNode.node, {
enter(node) {
if (node.type === 'TagDeclarator') {
tagExists = true
tag = node.value
}
},
})
// check if tag is used in fillet
if (tagExists) {
// create tag call
let tagCall: Expr = getEdgeTagCall(tag, selection.type)
// check if tag is used in fillet
let inFillet = false
let tagUsedInFillet = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
if (hasTag(node, tagCall)) {
tagUsedInFillet = true
}
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
if (tagUsedInFillet) {
return false
}
}
}
return true
} }
type EdgeTypes = type EdgeTypes =

View File

@ -45,11 +45,11 @@ variableBelowShouldNotBeIncluded = 3
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7 const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const { variables, bodyPath, insertIndex } = findAllPreviousVariables( const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
ast, ast,
programMemory, execState.memory,
[rangeStart, rangeStart] [rangeStart, rangeStart]
) )
expect(variables).toEqual([ expect(variables).toEqual([
@ -351,11 +351,11 @@ part001 = startSketchAt([-1.41, 3.46])
const ast = parse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const result = hasExtrudeSketch({ const result = hasExtrudeSketch({
ast, ast,
selection: { type: 'default', range: [100, 101] }, selection: { type: 'default', range: [100, 101] },
programMemory, programMemory: execState.memory,
}) })
expect(result).toEqual(true) expect(result).toEqual(true)
}) })
@ -370,11 +370,11 @@ part001 = startSketchAt([-1.41, 3.46])
const ast = parse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const result = hasExtrudeSketch({ const result = hasExtrudeSketch({
ast, ast,
selection: { type: 'default', range: [100, 101] }, selection: { type: 'default', range: [100, 101] },
programMemory, programMemory: execState.memory,
}) })
expect(result).toEqual(true) expect(result).toEqual(true)
}) })
@ -383,11 +383,11 @@ part001 = startSketchAt([-1.41, 3.46])
const ast = parse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const result = hasExtrudeSketch({ const result = hasExtrudeSketch({
ast, ast,
selection: { type: 'default', range: [10, 11] }, selection: { type: 'default', range: [10, 11] },
programMemory, programMemory: execState.memory,
}) })
expect(result).toEqual(false) expect(result).toEqual(false)
}) })

View File

@ -28,6 +28,7 @@ import {
getConstraintType, getConstraintType,
} from './std/sketchcombos' } from './std/sketchcombos'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
/** /**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -120,7 +121,12 @@ export function getNodeFromPathCurry(
} }
function moreNodePathFromSourceRange( function moreNodePathFromSourceRange(
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement, node:
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement,
sourceRange: Selection['range'], sourceRange: Selection['range'],
previousPath: PathToNode = [['body', '']] previousPath: PathToNode = [['body', '']]
): PathToNode { ): PathToNode {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 613 KiB

View File

@ -117,11 +117,11 @@ describe('testing changeSketchArguments', () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) return ast if (err(ast)) return ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
const changeSketchArgsRetVal = changeSketchArguments( const changeSketchArgsRetVal = changeSketchArguments(
ast, ast,
programMemory, execState.memory,
{ {
type: 'sourceRange', type: 'sourceRange',
sourceRange: [sourceStart, sourceStart + lineToChange.length], sourceRange: [sourceStart, sourceStart + lineToChange.length],
@ -150,12 +150,12 @@ mySketch001 = startSketchOn('XY')
const ast = parse(code) const ast = parse(code)
if (err(ast)) return ast if (err(ast)) return ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
expect(sourceStart).toBe(89) expect(sourceStart).toBe(89)
const newSketchLnRetVal = addNewSketchLn({ const newSketchLnRetVal = addNewSketchLn({
node: ast, node: ast,
programMemory, programMemory: execState.memory,
input: { input: {
type: 'straight-segment', type: 'straight-segment',
from: [0, 0], from: [0, 0],
@ -186,7 +186,7 @@ mySketch001 = startSketchOn('XY')
const modifiedAst2 = addCloseToPipe({ const modifiedAst2 = addCloseToPipe({
node: ast, node: ast,
programMemory, programMemory: execState.memory,
pathToNode: [ pathToNode: [
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
@ -230,7 +230,7 @@ describe('testing addTagForSketchOnFace', () => {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const sketchOnFaceRetVal = addTagForSketchOnFace( const sketchOnFaceRetVal = addTagForSketchOnFace(
{ {
// previousProgramMemory: programMemory, // redundant? // previousProgramMemory: execState.memory, // redundant?
pathToNode, pathToNode,
node: ast, node: ast,
}, },

View File

@ -1823,11 +1823,10 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
start: 0, start: 0,
end: 0, end: 0,
body: [], body: [],
digest: null,
nonCodeMeta: { nonCodeMeta: {
start: [], start: [],
nonCodeNodes: [], nonCodeNodes: [],
digest: null,
}, },
}, },
pathToNode, pathToNode,

View File

@ -34,7 +34,7 @@ async function testingSwapSketchFnCall({
const ast = parse(inputCode) const ast = parse(inputCode)
if (err(ast)) return Promise.reject(ast) if (err(ast)) return Promise.reject(ast)
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const selections = { const selections = {
codeBasedSelections: [range], codeBasedSelections: [range],
otherSelections: [], otherSelections: [],
@ -45,7 +45,7 @@ async function testingSwapSketchFnCall({
return Promise.reject(new Error('transformInfos undefined')) return Promise.reject(new Error('transformInfos undefined'))
const ast2 = transformAstSketchLines({ const ast2 = transformAstSketchLines({
ast, ast,
programMemory, programMemory: execState.memory,
selectionRanges: selections, selectionRanges: selections,
transformInfos, transformInfos,
referenceSegName: '', referenceSegName: '',
@ -360,10 +360,10 @@ part001 = startSketchOn('XY')
|> line([2.14, 1.35], %) // normal-segment |> line([2.14, 1.35], %) // normal-segment
|> xLine(3.54, %)` |> xLine(3.54, %)`
it('normal case works', async () => { it('normal case works', async () => {
const programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
const index = code.indexOf('// normal-segment') - 7 const index = code.indexOf('// normal-segment') - 7
const sg = sketchFromKclValue( const sg = sketchFromKclValue(
programMemory.get('part001'), execState.memory.get('part001'),
'part001' 'part001'
) as Sketch ) as Sketch
const _segment = getSketchSegmentFromSourceRange(sg, [index, index]) const _segment = getSketchSegmentFromSourceRange(sg, [index, index])
@ -377,10 +377,10 @@ part001 = startSketchOn('XY')
}) })
}) })
it('verify it works when the segment is in the `start` property', async () => { it('verify it works when the segment is in the `start` property', async () => {
const programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
const index = code.indexOf('// segment-in-start') - 7 const index = code.indexOf('// segment-in-start') - 7
const _segment = getSketchSegmentFromSourceRange( const _segment = getSketchSegmentFromSourceRange(
sketchFromKclValue(programMemory.get('part001'), 'part001') as Sketch, sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch,
[index, index] [index, index]
) )
if (err(_segment)) throw _segment if (err(_segment)) throw _segment

View File

@ -9,7 +9,7 @@ import {
getConstraintLevelFromSourceRange, getConstraintLevelFromSourceRange,
} from './sketchcombos' } from './sketchcombos'
import { ToolTip } from 'lang/langHelpers' import { ToolTip } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
@ -96,6 +96,86 @@ function makeSelections(
} }
describe('testing transformAstForSketchLines for equal length constraint', () => { describe('testing transformAstForSketchLines for equal length constraint', () => {
describe(`should always reorder selections to have the base selection first`, () => {
const inputScript = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([5, 5], %)
|> line([-2, 5], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
const expectedModifiedScript = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([5, 5], %, $seg01)
|> angledLine([112, segLen(seg01)], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
const selectLine = (script: string, lineNumber: number): Selection => {
const lines = script.split('\n')
const codeBeforeLine = lines.slice(0, lineNumber).join('\n').length
const line = lines.find((_, i) => i === lineNumber)
if (!line) {
throw new Error(
`line index ${lineNumber} not found in test sample, friend`
)
}
const start = codeBeforeLine + line.indexOf('|> ' + 5)
const range: [number, number] = [start, start]
return {
type: 'default',
range,
}
}
async function applyTransformation(
inputCode: string,
selectionRanges: Selections['codeBasedSelections']
) {
const ast = parse(inputCode)
if (err(ast)) return Promise.reject(ast)
const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos(
makeSelections(selectionRanges.slice(1)),
ast,
'equalLength'
)
const transformedSelection = makeSelections(selectionRanges)
const newAst = transformSecondarySketchLinesTagFirst({
ast,
selectionRanges: transformedSelection,
transformInfos,
programMemory: execState.memory,
})
if (err(newAst)) return Promise.reject(newAst)
const newCode = recast(newAst.modifiedAst)
return newCode
}
it(`Should reorder when user selects first-to-last`, async () => {
const selectionRanges: Selections['codeBasedSelections'] = [
selectLine(inputScript, 3),
selectLine(inputScript, 4),
]
const newCode = await applyTransformation(inputScript, selectionRanges)
expect(newCode).toBe(expectedModifiedScript)
})
it(`Should reorder when user selects last-to-first`, async () => {
const selectionRanges: Selections['codeBasedSelections'] = [
selectLine(inputScript, 4),
selectLine(inputScript, 3),
]
const newCode = await applyTransformation(inputScript, selectionRanges)
expect(newCode).toBe(expectedModifiedScript)
})
})
const inputScript = `myVar = 3 const inputScript = `myVar = 3
myVar2 = 5 myVar2 = 5
myVar3 = 6 myVar3 = 6
@ -220,7 +300,7 @@ part001 = startSketchOn('XY')
} }
}) })
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos( const transformInfos = getTransformInfos(
makeSelections(selectionRanges.slice(1)), makeSelections(selectionRanges.slice(1)),
ast, ast,
@ -231,7 +311,7 @@ part001 = startSketchOn('XY')
ast, ast,
selectionRanges: makeSelections(selectionRanges), selectionRanges: makeSelections(selectionRanges),
transformInfos, transformInfos,
programMemory, programMemory: execState.memory,
}) })
if (err(newAst)) return Promise.reject(newAst) if (err(newAst)) return Promise.reject(newAst)
@ -311,7 +391,7 @@ part001 = startSketchOn('XY')
} }
}) })
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos( const transformInfos = getTransformInfos(
makeSelections(selectionRanges), makeSelections(selectionRanges),
ast, ast,
@ -322,7 +402,7 @@ part001 = startSketchOn('XY')
ast, ast,
selectionRanges: makeSelections(selectionRanges), selectionRanges: makeSelections(selectionRanges),
transformInfos, transformInfos,
programMemory, programMemory: execState.memory,
referenceSegName: '', referenceSegName: '',
}) })
if (err(newAst)) return Promise.reject(newAst) if (err(newAst)) return Promise.reject(newAst)
@ -373,7 +453,7 @@ part001 = startSketchOn('XY')
} }
}) })
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos( const transformInfos = getTransformInfos(
makeSelections(selectionRanges), makeSelections(selectionRanges),
ast, ast,
@ -384,7 +464,7 @@ part001 = startSketchOn('XY')
ast, ast,
selectionRanges: makeSelections(selectionRanges), selectionRanges: makeSelections(selectionRanges),
transformInfos, transformInfos,
programMemory, programMemory: execState.memory,
referenceSegName: '', referenceSegName: '',
}) })
if (err(newAst)) return Promise.reject(newAst) if (err(newAst)) return Promise.reject(newAst)
@ -470,7 +550,7 @@ async function helperThing(
} }
}) })
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos( const transformInfos = getTransformInfos(
makeSelections(selectionRanges.slice(1)), makeSelections(selectionRanges.slice(1)),
ast, ast,
@ -481,7 +561,7 @@ async function helperThing(
ast, ast,
selectionRanges: makeSelections(selectionRanges), selectionRanges: makeSelections(selectionRanges),
transformInfos, transformInfos,
programMemory, programMemory: execState.memory,
}) })
if (err(newAst)) return Promise.reject(newAst) if (err(newAst)) return Promise.reject(newAst)

View File

@ -1559,7 +1559,15 @@ export function transformSecondarySketchLinesTagFirst({
} }
| Error { | Error {
// let node = structuredClone(ast) // let node = structuredClone(ast)
const primarySelection = selectionRanges.codeBasedSelections[0].range
// We need to sort the selections by their start position
// so that we can process them in dependency order and not write invalid KCL.
const sortedCodeBasedSelections =
selectionRanges.codeBasedSelections.toSorted(
(a, b) => a.range[0] - b.range[0]
)
const primarySelection = sortedCodeBasedSelections[0].range
const secondarySelections = sortedCodeBasedSelections.slice(1)
const _tag = giveSketchFnCallTag(ast, primarySelection, forceSegName) const _tag = giveSketchFnCallTag(ast, primarySelection, forceSegName)
if (err(_tag)) return _tag if (err(_tag)) return _tag
@ -1569,7 +1577,7 @@ export function transformSecondarySketchLinesTagFirst({
ast: modifiedAst, ast: modifiedAst,
selectionRanges: { selectionRanges: {
...selectionRanges, ...selectionRanges,
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1), codeBasedSelections: secondarySelections,
}, },
referencedSegmentRange: primarySelection, referencedSegmentRange: primarySelection,
transformInfos, transformInfos,

View File

@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
offset: ${offset}, offset: ${offset},
}, %, $yo2) }, %, $yo2)
intersect = segEndX(yo2)` intersect = segEndX(yo2)`
const mem = await enginelessExecutor(parse(code('-1'))) const execState = await enginelessExecutor(parse(code('-1')))
expect(mem.get('intersect')?.value).toBe(1 + Math.sqrt(2)) expect(execState.memory.get('intersect')?.value).toBe(1 + Math.sqrt(2))
const noOffset = await enginelessExecutor(parse(code('0'))) const noOffset = await enginelessExecutor(parse(code('0')))
expect(noOffset.get('intersect')?.value).toBeCloseTo(1) expect(noOffset.memory.get('intersect')?.value).toBeCloseTo(1)
}) })
}) })

View File

@ -37,6 +37,11 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { DeepPartial } from 'lib/types' import { DeepPartial } from 'lib/types'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Sketch } from '../wasm-lib/kcl/bindings/Sketch' import { Sketch } from '../wasm-lib/kcl/bindings/Sketch'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
import { ExecState as RawExecState } from '../wasm-lib/kcl/bindings/ExecState'
import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory'
import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
import { Environment } from '../wasm-lib/kcl/bindings/Environment'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr' export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -136,29 +141,46 @@ export const parse = (code: string | Error): Program | Error => {
export type PathToNode = [string | number, string][] export type PathToNode = [string | number, string][]
interface Memory { export interface ExecState {
[key: string]: KclValue memory: ProgramMemory
idGenerator: IdGenerator
} }
type EnvironmentRef = number /**
* Create an empty ExecState. This is useful on init to prevent needing an
* Option.
*/
export function emptyExecState(): ExecState {
return {
memory: ProgramMemory.empty(),
idGenerator: defaultIdGenerator(),
}
}
function execStateFromRaw(raw: RawExecState): ExecState {
return {
memory: ProgramMemory.fromRaw(raw.memory),
idGenerator: raw.idGenerator,
}
}
export function defaultIdGenerator(): IdGenerator {
return {
nextId: 0,
ids: [],
}
}
interface Memory {
[key: string]: KclValue | undefined
}
const ROOT_ENVIRONMENT_REF: EnvironmentRef = 0 const ROOT_ENVIRONMENT_REF: EnvironmentRef = 0
interface Environment {
bindings: Memory
parent: EnvironmentRef | null
}
function emptyEnvironment(): Environment { function emptyEnvironment(): Environment {
return { bindings: {}, parent: null } return { bindings: {}, parent: null }
} }
interface RawProgramMemory {
environments: Environment[]
currentEnv: EnvironmentRef
return: KclValue | null
}
/** /**
* This duplicates logic in Rust. The hope is to keep ProgramMemory internals * This duplicates logic in Rust. The hope is to keep ProgramMemory internals
* isolated from the rest of the TypeScript code so that we can move it to Rust * isolated from the rest of the TypeScript code so that we can move it to Rust
@ -217,7 +239,7 @@ export class ProgramMemory {
while (true) { while (true) {
const env = this.environments[envRef] const env = this.environments[envRef]
if (env.bindings.hasOwnProperty(name)) { if (env.bindings.hasOwnProperty(name)) {
return env.bindings[name] return env.bindings[name] ?? null
} }
if (!env.parent) { if (!env.parent) {
break break
@ -260,6 +282,7 @@ export class ProgramMemory {
} }
for (const [name, value] of Object.entries(env.bindings)) { for (const [name, value] of Object.entries(env.bindings)) {
if (value === undefined) continue
// Check the predicate. // Check the predicate.
if (!predicate(value)) { if (!predicate(value)) {
continue continue
@ -293,6 +316,7 @@ export class ProgramMemory {
while (true) { while (true) {
const env = this.environments[envRef] const env = this.environments[envRef]
for (const [name, value] of Object.entries(env.bindings)) { for (const [name, value] of Object.entries(env.bindings)) {
if (value === undefined) continue
// Don't include shadowed variables. // Don't include shadowed variables.
if (!map.has(name)) { if (!map.has(name)) {
map.set(name, value) map.set(name, value)
@ -356,9 +380,10 @@ export function sketchFromKclValue(
export const executor = async ( export const executor = async (
node: Program, node: Program,
programMemory: ProgramMemory | Error = ProgramMemory.empty(), programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
isMock: boolean = false isMock: boolean = false
): Promise<ProgramMemory> => { ): Promise<ExecState> => {
if (err(programMemory)) return Promise.reject(programMemory) if (err(programMemory)) return Promise.reject(programMemory)
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
@ -366,6 +391,7 @@ export const executor = async (
const _programMemory = await _executor( const _programMemory = await _executor(
node, node,
programMemory, programMemory,
idGenerator,
engineCommandManager, engineCommandManager,
isMock isMock
) )
@ -378,9 +404,10 @@ export const executor = async (
export const _executor = async ( export const _executor = async (
node: Program, node: Program,
programMemory: ProgramMemory | Error = ProgramMemory.empty(), programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
isMock: boolean isMock: boolean
): Promise<ProgramMemory> => { ): Promise<ExecState> => {
if (err(programMemory)) return Promise.reject(programMemory) if (err(programMemory)) return Promise.reject(programMemory)
try { try {
@ -392,15 +419,17 @@ export const _executor = async (
baseUnit = baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm' (await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
} }
const memory: RawProgramMemory = await execute_wasm( const execState: RawExecState = await execute_wasm(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemory.toRaw()), JSON.stringify(programMemory.toRaw()),
JSON.stringify(idGenerator),
baseUnit, baseUnit,
engineCommandManager, engineCommandManager,
fileSystemManager, fileSystemManager,
undefined,
isMock isMock
) )
return ProgramMemory.fromRaw(memory) return execStateFromRaw(execState)
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
const parsed: RustKclError = JSON.parse(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())

3
src/lib/codeEditor.ts Normal file
View File

@ -0,0 +1,3 @@
export const normalizeLineEndings = (str: string, normalized = '\n') => {
return str.replace(/\r?\n/g, normalized)
}

View File

@ -190,10 +190,17 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
options: () => { options: () => {
return Object.entries(machineManager.machines).map( return Object.entries(machineManager.machines).map(
([_, machine]) => ({ ([_, machine]) => ({
name: `${machine.id} (${ name:
machine.make_model.model || machine.make_model.manufacturer `${machine.id} (${
}) via ${machineManager.machineApiIp || 'the local network'}`, machine.make_model.model || machine.make_model.manufacturer
}) (${machine.state.state})` +
(machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter
? ` - Nozzle Diameter: ${machine.extra.nozzle_diameter}`
: ''),
isCurrent: false, isCurrent: false,
disabled: machine.state.state !== 'idle',
value: machine as components['schemas']['MachineInfoResponse'], value: machine as components['schemas']['MachineInfoResponse'],
}) })
) )

View File

@ -102,3 +102,6 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json', 'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json',
localFallback: '/kcl-samples-manifest-fallback.json', localFallback: '/kcl-samples-manifest-fallback.json',
} as const } as const
/** Toast id for the app auto-updater toast */
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'

View File

@ -55,6 +55,23 @@ export interface paths {
patch?: never patch?: never
trace?: never trace?: never
} }
'/metrics': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** List available machines and their statuses */
get: operations['get_metrics']
put?: never
post?: never
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/ping': { '/ping': {
parameters: { parameters: {
query?: never query?: never
@ -102,18 +119,96 @@ export interface components {
/** @description Extra machine-specific information regarding a connected machine. */ /** @description Extra machine-specific information regarding a connected machine. */
ExtraMachineInfoResponse: ExtraMachineInfoResponse:
| { | {
Moonraker: Record<string, never> /** @enum {string} */
type: 'moonraker'
} }
| { | {
Usb: Record<string, never> /** @enum {string} */
type: 'usb'
} }
| { | {
Bambu: Record<string, never> /** @description The current stage of the machine as defined by Bambu which can include errors, etc. */
current_stage?: components['schemas']['Stage'] | null
/** @description The nozzle diameter of the machine. */
nozzle_diameter: components['schemas']['NozzleDiameter']
/** @enum {string} */
type: 'bambu'
}
/** @description Configuration for a FDM-based printer. */
FdmHardwareConfiguration: {
/** @description The filaments the printer has access to. */
filaments: components['schemas']['Filament'][]
/**
* Format: double
* @description Diameter of the extrusion nozzle, in mm.
*/
nozzle_diameter: number
}
/** @description Information about the filament being used in a FDM printer. */
Filament: {
/** @description The color (as hex without the `#`) of the filament, this is likely specific to the manufacturer. */
color?: string | null
/** @description The material that the filament is made of. */
material: components['schemas']['FilamentMaterial']
/** @description The name of the filament, this is likely specfic to the manufacturer. */
name?: string | null
}
/** @description The material that the filament is made of. */
FilamentMaterial:
| {
/** @enum {string} */
type: 'pla'
}
| {
/** @enum {string} */
type: 'pla_support'
}
| {
/** @enum {string} */
type: 'abs'
}
| {
/** @enum {string} */
type: 'petg'
}
| {
/** @enum {string} */
type: 'nylon'
}
| {
/** @enum {string} */
type: 'tpu'
}
| {
/** @enum {string} */
type: 'pva'
}
| {
/** @enum {string} */
type: 'hips'
}
| {
/** @enum {string} */
type: 'composite'
}
/** @description The hardware configuration of a machine. */
HardwareConfiguration:
| {
/** @enum {string} */
type: 'none'
}
| {
/** @description The configuration for the FDM printer. */
config: components['schemas']['FdmHardwareConfiguration']
/** @enum {string} */
type: 'fdm'
} }
/** @description Information regarding a connected machine. */ /** @description Information regarding a connected machine. */
MachineInfoResponse: { MachineInfoResponse: {
/** @description Additional, per-machine information which is specific to the underlying machine type. */ /** @description Additional, per-machine information which is specific to the underlying machine type. */
extra?: components['schemas']['ExtraMachineInfoResponse'] | null extra?: components['schemas']['ExtraMachineInfoResponse'] | null
/** @description Information about how the Machine is currently configured. */
hardware_configuration: components['schemas']['HardwareConfiguration']
/** @description Machine Identifier (ID) for the specific Machine. */ /** @description Machine Identifier (ID) for the specific Machine. */
id: string id: string
/** @description Information regarding the method of manufacture. */ /** @description Information regarding the method of manufacture. */
@ -126,6 +221,11 @@ export interface components {
* *
* What "close" means is up to you! */ * What "close" means is up to you! */
max_part_volume?: components['schemas']['Volume'] | null max_part_volume?: components['schemas']['Volume'] | null
/**
* Format: double
* @description Progress of the current print, if printing.
*/
progress?: number | null
/** @description Status of the printer -- be it printing, idle, or unreachable. This may dictate if a machine is capable of taking a new job. */ /** @description Status of the printer -- be it printing, idle, or unreachable. This may dictate if a machine is capable of taking a new job. */
state: components['schemas']['MachineState'] state: components['schemas']['MachineState']
} }
@ -140,17 +240,40 @@ export interface components {
} }
/** @description Current state of the machine -- be it printing, idle or offline. This can be used to determine if a printer is in the correct state to take a new job. */ /** @description Current state of the machine -- be it printing, idle or offline. This can be used to determine if a printer is in the correct state to take a new job. */
MachineState: MachineState:
| 'Unknown'
| 'Idle'
| 'Running'
| 'Offline'
| 'Paused'
| 'Complete'
| { | {
Failed: string | null /** @enum {string} */
state: 'unknown'
}
| {
/** @enum {string} */
state: 'idle'
}
| {
/** @enum {string} */
state: 'running'
}
| {
/** @enum {string} */
state: 'offline'
}
| {
/** @enum {string} */
state: 'paused'
}
| {
/** @enum {string} */
state: 'complete'
}
| {
/** @description A human-readable message describing the failure. */
message?: string | null
/** @enum {string} */
state: 'failed'
} }
/** @description Specific technique by which this Machine takes a design, and produces a real-world 3D object. */ /** @description Specific technique by which this Machine takes a design, and produces a real-world 3D object. */
MachineType: 'Stereolithography' | 'FusedDeposition' | 'Cnc' MachineType: 'stereolithography' | 'fused_deposition' | 'cnc'
/** @description A nozzle diameter. */
NozzleDiameter: '0.2' | '0.4' | '0.6' | '0.8'
/** @description The response from the `/ping` endpoint. */ /** @description The response from the `/ping` endpoint. */
Pong: { Pong: {
/** @description The pong response. */ /** @description The pong response. */
@ -169,7 +292,56 @@ export interface components {
job_name: string job_name: string
/** @description The machine id to print to. */ /** @description The machine id to print to. */
machine_id: string machine_id: string
/** @description Requested design-specific slicer configurations. */
slicer_configuration?: components['schemas']['SlicerConfiguration'] | null
} }
/** @description The slicer configuration is a set of parameters that are passed to the slicer to control how the gcode is generated. */
SlicerConfiguration: {
/**
* Format: uint
* @description The filament to use for the print.
*/
filament_idx?: number | null
}
/** @description The print stage. These come from: https://github.com/SoftFever/OrcaSlicer/blob/431978baf17961df90f0d01871b0ad1d839d7f5d/src/slic3r/GUI/DeviceManager.cpp#L78 */
Stage:
| 'nothing'
| 'empty'
| 'auto_bed_leveling'
| 'heatbed_preheating'
| 'sweeping_xy_mech_mode'
| 'changing_filament'
| 'm400_pause'
| 'paused_due_to_filament_runout'
| 'heating_hotend'
| 'calibrating_extrusion'
| 'scanning_bed_surface'
| 'inspecting_first_layer'
| 'identifying_build_plate_type'
| 'calibrating_micro_lidar'
| 'homing_toolhead'
| 'cleaning_nozzle_tip'
| 'checking_extruder_temperature'
| 'printing_was_paused_by_the_user'
| 'pause_of_front_cover_falling'
| 'calibrating_micro_lidar2'
| 'calibrating_extrusion_flow'
| 'paused_due_to_nozzle_temperature_malfunction'
| 'paused_due_to_heat_bed_temperature_malfunction'
| 'filament_unloading'
| 'skip_step_pause'
| 'filament_loading'
| 'motor_noise_calibration'
| 'paused_due_to_ams_lost'
| 'paused_due_to_low_speed_of_the_heat_break_fan'
| 'paused_due_to_chamber_temperature_control_error'
| 'cooling_chamber'
| 'paused_by_the_gcode_inserted_by_the_user'
| 'motor_noise_showoff'
| 'nozzle_filament_covered_detected_pause'
| 'cutter_error_pause'
| 'first_layer_error_pause'
| 'nozzle_clog_pause'
/** @description Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum. /** @description Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum.
* *
* All measurements are in millimeters. */ * All measurements are in millimeters. */
@ -278,6 +450,28 @@ export interface operations {
'5XX': components['responses']['Error'] '5XX': components['responses']['Error']
} }
} }
get_metrics: {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
requestBody?: never
responses: {
/** @description successful operation */
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': string
}
}
'4XX': components['responses']['Error']
'5XX': components['responses']['Error']
}
}
ping: { ping: {
parameters: { parameters: {
query?: never query?: never

View File

@ -85,7 +85,11 @@ export class MachineManager {
return return
} }
this._machines = await window.electron.listMachines() if (this._machineApiIp === null) {
return
}
this._machines = await window.electron.listMachines(this._machineApiIp)
} }
private async updateMachineApiIp(): Promise<void> { private async updateMachineApiIp(): Promise<void> {

View File

@ -14,6 +14,7 @@ import { codeManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
import { getProjectInfo } from './desktop' import { getProjectInfo } from './desktop'
import { createSettings } from './settings/initialSettings' import { createSettings } from './settings/initialSettings'
import { normalizeLineEndings } from 'lib/codeEditor'
// The root loader simply resolves the settings and any errors that // The root loader simply resolves the settings and any errors that
// occurred during the settings load // occurred during the settings load
@ -182,7 +183,3 @@ export const homeLoader: LoaderFunction = async (): Promise<
} }
return {} return {}
} }
const normalizeLineEndings = (str: string, normalized = '\n') => {
return str.replace(/\r?\n/g, normalized)
}

View File

@ -1,4 +1,11 @@
import { Program, ProgramMemory, _executor, SourceRange } from '../lang/wasm' import {
Program,
ProgramMemory,
_executor,
SourceRange,
ExecState,
defaultIdGenerator,
} from '../lang/wasm'
import { import {
EngineCommandManager, EngineCommandManager,
EngineCommandManagerEvents, EngineCommandManagerEvents,
@ -9,6 +16,7 @@ import { v4 as uuidv4 } from 'uuid'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { toSync } from './utils' import { toSync } from './utils'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
type WebSocketResponse = Models['WebSocketResponse_type'] type WebSocketResponse = Models['WebSocketResponse_type']
@ -77,8 +85,9 @@ class MockEngineCommandManager {
export async function enginelessExecutor( export async function enginelessExecutor(
ast: Program | Error, ast: Program | Error,
pm: ProgramMemory | Error = ProgramMemory.empty() pm: ProgramMemory | Error = ProgramMemory.empty(),
): Promise<ProgramMemory> { idGenerator: IdGenerator = defaultIdGenerator()
): Promise<ExecState> {
if (err(ast)) return Promise.reject(ast) if (err(ast)) return Promise.reject(ast)
if (err(pm)) return Promise.reject(pm) if (err(pm)) return Promise.reject(pm)
@ -88,15 +97,22 @@ export async function enginelessExecutor(
}) as any as EngineCommandManager }) as any as EngineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
mockEngineCommandManager.startNewSession() mockEngineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, mockEngineCommandManager, true) const execState = await _executor(
ast,
pm,
idGenerator,
mockEngineCommandManager,
true
)
await mockEngineCommandManager.waitForAllCommands() await mockEngineCommandManager.waitForAllCommands()
return programMemory return execState
} }
export async function executor( export async function executor(
ast: Program, ast: Program,
pm: ProgramMemory = ProgramMemory.empty() pm: ProgramMemory = ProgramMemory.empty(),
): Promise<ProgramMemory> { idGenerator: IdGenerator = defaultIdGenerator()
): Promise<ExecState> {
const engineCommandManager = new EngineCommandManager() const engineCommandManager = new EngineCommandManager()
engineCommandManager.start({ engineCommandManager.start({
setIsStreamReady: () => {}, setIsStreamReady: () => {},
@ -117,14 +133,15 @@ export async function executor(
toSync(async () => { toSync(async () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const programMemory = await _executor( const execState = await _executor(
ast, ast,
pm, pm,
idGenerator,
engineCommandManager, engineCommandManager,
false false
) )
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
resolve(programMemory) resolve(execState)
}, reportRejection) }, reportRejection)
) )
}) })

View File

@ -295,15 +295,24 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
'break', 'break',
{ {
id: 'line', id: 'line',
onClick: ({ modelingState, modelingSend }) => onClick: ({ modelingState, modelingSend }) => {
modelingSend({ if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) {
type: 'change tool', // Exit the sketch state if there are no points and they press ESC
data: { modelingSend({
tool: !modelingState.matches({ Sketch: 'Line tool' }) type: 'Cancel',
? 'line' })
: 'none', } else {
}, // Exit the tool if there are points and they press ESC
}), modelingSend({
type: 'change tool',
data: {
tool: !modelingState.matches({ Sketch: 'Line tool' })
? 'line'
: 'none',
},
})
}
},
icon: 'line', icon: 'line',
status: 'available', status: 'available',
disabled: (state) => disabled: (state) =>

View File

@ -97,7 +97,7 @@ export function useCalculateKclExpression({
}) })
if (trap(error, { suppress: true })) return if (trap(error, { suppress: true })) return
} }
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
@ -111,7 +111,7 @@ export function useCalculateKclExpression({
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.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)
} }

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