Compare commits

..

108 Commits

Author SHA1 Message Date
c6b80eec68 Fix the linter 2025-02-05 11:42:53 -05:00
1b68f5dc19 Fix FileTree failing 2025-02-05 11:42:53 -05:00
a0aa4802d1 Add docs for modules and import statements (#4435) 2025-02-05 11:42:53 -05:00
746f76ec63 chore: implemented a O(n) unique and added unit tests (#4429) 2025-02-05 11:42:53 -05:00
8e624935c2 Divorce JSON and KCL (#4436)
Removes JSON from the KCL object model. Closes https://github.com/KittyCAD/modeling-app/issues/1130 -- it was filed on Nov 27 last year. Hopefully I close it before its one year anniversary.

Changes:

- Removed the UserVal variant from `enum KclValue`. That variant held JSON data.
- Replaced it with several new variants like Number, String, Array (of KCL values), Object (where keys are String and values are KCL values)
- Added a dedicated Sketch variant to KclValue. We used to have a variant like this, but I removed it as an experimental approach to fix this issue. Eventually I decided to undo it and use the approach of this PR instead.
- Removed the `impl_from_arg_via_json` macro, which implemented conversion from KclValue to Rust types by matching the KclValue to its UserVal variant, grabbing the JSON, then deserializing that into the desired Rust type. 
- Instead, replaced it with manual conversion from KclValue to Rust types, using some convenience macros like `field!`
2025-02-05 11:42:53 -05:00
0a5f22c80a Add basic horizontal and vertical snapping for Line tool (#4465)
* Add on-click snapping behavior

* Fix math error with horizontal/vertical determination

* Move snap constant to where other sketch constants are

* fmt

* Fixing up some of the tests, some still need updating

* A couple other little PW test issues

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

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

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

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

* Fix remaining tests that need updating

* Make `yarn lint` happy

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 11:42:52 -05:00
142db64796 Fix description of angleToMatchLengthX (#4467)
* fix description of angleToMatchLengthX

* Update docs

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

* update doc regen instructions

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

* Re-run CI after snapshots

---------

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
2025-02-05 11:41:42 -05:00
59b0cdc3ac Cut release v0.26.5 (#4477) 2025-02-05 11:41:42 -05:00
4763257dc3 Bug fix: fix autocompletion regression (#4476)
* Add final error-less expectation to autocomplete test

* Fix failing test

* tweak to leave function in place

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2025-02-05 11:41:42 -05:00
b48ba7f081 Internal: Pass through sweep.subType in expandSweep (#4383)
* Internal: Pass through `sweep.subType` in `expandSweep`

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

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

* implement a simple startSketchOn / offsetPlane lint rule (#4384)

* Bump kittycad-modeling-cmds from 0.2.71 to 0.2.72 in /src/wasm-lib (#4356)

Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.71 to 0.2.72.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.71...kittycad-modeling-cmds-0.2.72)

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  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>

* Bump google-github-actions/upload-cloud-storage from 2.2.0 to 2.2.1 (#4364)

Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>

* Internal fix: make `expandPath` not assume path has associated sweep (#4386)

* Add a test that shows current error within `expandPath`

* Make `expandPath` not assume there is an associated sweep artifact

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Bump happy-dom from 14.12.3 to 15.10.1 (#4404)

Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 14.12.3 to 15.10.1.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v14.12.3...v15.10.1)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump @electron-forge/maker-zip from 7.4.0 to 7.5.0 (#4394)

Bumps [@electron-forge/maker-zip](https://github.com/electron/forge) from 7.4.0 to 7.5.0.
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/compare/v7.4.0...v7.5.0)

---
updated-dependencies:
- dependency-name: "@electron-forge/maker-zip"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump uuid from 9.0.1 to 11.0.2 (#4393)

Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.1 to 11.0.2.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.1...v11.0.2)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump validator from 0.18.1 to 0.19.0 in /src/wasm-lib (#4396)

Bumps [validator](https://github.com/Keats/validator) from 0.18.1 to 0.19.0.
- [Changelog](https://github.com/Keats/validator/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Keats/validator/commits)

---
updated-dependencies:
- dependency-name: validator
  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>

* Bump insta from 1.41.0 to 1.41.1 in /src/wasm-lib (#4400)

Bumps [insta](https://github.com/mitsuhiko/insta) from 1.41.0 to 1.41.1.
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.41.0...1.41.1)

---
updated-dependencies:
- dependency-name: insta
  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>

* Bump pyo3 from 0.22.5 to 0.22.6 in /src/wasm-lib (#4398)

Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.22.5 to 0.22.6.
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/v0.22.6/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.22.5...v0.22.6)

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

* Bump thiserror from 1.0.65 to 2.0.0 in /src/wasm-lib (#4397)

Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.65 to 2.0.0.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.65...2.0.0)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump url from 2.5.2 to 2.5.3 in /src/wasm-lib (#4399)

Bumps [url](https://github.com/servo/rust-url) from 2.5.2 to 2.5.3.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.2...v2.5.3)

---
updated-dependencies:
- dependency-name: url
  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>

* Bump @types/ws from 8.5.12 to 8.5.13 (#4395)

Bumps [@types/ws](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws) from 8.5.12 to 8.5.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ws)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* File tree to act more like VS Code's file tree (#4392)

* File tree acts as VS Code's file tree

* Adjust test for new expectations

* Remove screenshot

* Actually remove this screenshot

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

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

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

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

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

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

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Fixing directory/file selection logic to deselect folders properly and always hightlight files.  (#4408)

* File tree acts as VS Code's file tree

* Adjust test for new expectations

* Remove screenshot

* Actually remove this screenshot

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

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

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

* fix: fixed logic for selection

* fix: always show the current file

---------

Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Bump happy-dom from 15.10.1 to 15.10.2 (#4409)

Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 15.10.1 to 15.10.2.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v15.10.1...v15.10.2)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Change Dependabot PR frequency to weekly (#4424)

* Update insta snapshots (#4423)

No meaningful changes, they just added a field to the frontmatter

* Fix KCL source ranges to know which source file they point to (#4418)

* Add ts_rs feature to work with indexmap

* Add feature for schemars to work with indexmap

* Add module ID to intern module paths

* Update code to use new source range with three fields

* Update generated files

* Update docs

* Fix wasm

* Fix TS code to use new SourceRange

* Fix TS tests to use new SourceRange and moduleId

* Fix formatting

* Fix to filter errors and source ranges to only show the top-level module

* Fix to reuse module IDs

* Fix to disallow empty path for import

* Revert unneeded Self change

* Rename field to be clearer

* Fix parser tests

* Update snapshots

* Change to not serialize module_id of 0

* Update snapshots after adding default module_id

* Move module_id functions to separate module

* Fix tests for console errors

* Proposal: module ID = 0 gets skipped when serializing tokens too (#4422)

Just like in AST nodes.

Also I think "is_top_level" communicates intention better than is_default

---------

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>

* Bump anyhow from 1.0.92 to 1.0.93 in /src/wasm-lib (#4417)

Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.92 to 1.0.93.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.92...1.0.93)

---
updated-dependencies:
- dependency-name: anyhow
  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>

* Cut release v0.26.3 (#4427)

* Cut release v0.26.3

* Support new electron-builder env variable requirements for building in release mode

* Bump image from 0.25.3 to 0.25.5 in /src/wasm-lib (#4416)

Bumps [image](https://github.com/image-rs/image) from 0.25.3 to 0.25.5.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.3...v0.25.5)

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

* Nadro/3799/perf (#4145)

* chore: building out perf testing

* chore: adding my printing code for the different formats of the marks

* feat: adding invocation count table

* fix: markOnce iunstead

* fix: typescript additions

* fix: adding more types

* chore: adding telemetry panel as MVP, gonna remove the pane

* chore: view telemetry from command bar in file route and home route

* fix: deleting unused imports

* fix: deleting some unused files

* fix: auto cleanup

* chore: adding other routes, these will need to be moved...

* chore: moving some printing logic around and unit testing some of it

* fix: moving command init

* fix: removing debugging marks

* fix: adding some comments

* fix: fixed a bug with generating the go to page commands

* chore: adding will pages load within the router config

* chore: implementing marks for routes

* fix: auto fixes and checkers

* chore: implemented a route watcher at the root level...

* fix: auto fixes, removing unused code

* chore: timing for syntax highlighting and auto fixes

* fix: didAuth issue and syntax highlighting in the packaged application. Constructor name gets renamed

* fix: fixing typescript checks

* chore: adding mag bar chart icon for telemetry

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

* chore: swapped telemetry icon for stopwatch

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

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

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

* chore: writing telemetry to disk

* fix: auto fixers

* chore: getting args parsed for cli flags and writing telemetry file

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

* chore: swapped mark for markOnce since we infinitely write marks to a JS array... need to solve this run time marking in another way. We only need this for startup right now

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

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

* chore: writing raw marks to disk as well

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

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

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

* fix: cleaned up the testing names

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

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

* Fix fmt and codespell

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

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

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

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

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

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

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

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

* fix: moving this route loader data stuff

* chore: adding comment

* fix: fmt

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

* empty :(

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

* empty :(

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2025-02-05 11:41:41 -05:00
64537a59b8 Remove references to wasm-dev build (#4449) 2025-02-05 11:41:23 -05:00
c24efaf2e4 Fixups 2025-02-05 11:41:23 -05:00
2fb16ed074 Update src/clientSideScene/ClientSideSceneComp.tsx
Remove eslint rule no-floating-promises

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-05 11:41:23 -05:00
82e647db3b Correct any last missing code mods 2025-02-05 11:41:22 -05:00
fa2d0a69bf Fix up code based selections after constraints 2025-02-05 11:40:50 -05:00
e372b2680e If value is falsey then don't try to executeAst 2025-02-05 11:40:50 -05:00
c1c1f817c9 ONLY reload current file on changes. 2025-02-05 11:40:50 -05:00
00c0c993f2 Rename setUp to setup 2025-02-05 11:40:50 -05:00
b505c0be07 Turn error into warning about out of date AST. 2025-02-05 11:40:50 -05:00
6c2d06c2c6 Refactor writeToFile and updateCodeEditor to happen at appropriate times 2025-02-05 11:40:50 -05:00
a4b7dd5182 Reapply "Deflake project settings override on desktop (#4370)" (#4450)
This reverts commit b11040c23c.
2025-02-05 11:40:50 -05:00
0c2ca726d0 Cut release v0.26.4 (#4452)
* Cut release v0.26.4

* Turn of setting persistence test with `fixme`
2025-02-05 11:40:50 -05:00
fcfecf702b Revert "Deflake project settings override on desktop (#4370)" (#4450)
* Revert "Deflake project settings override on desktop (#4370)"

This reverts commit ad1cd56891.

* Part of the revert
2025-02-05 11:40:50 -05:00
347a6ef15a Fix to run cargo tests when generated files change (#4430) 2025-02-05 11:40:50 -05:00
eed4386f76 Release derive-docs (#4446) 2025-02-05 11:40:50 -05:00
14afcba599 Release kcl and kcl-test-server (#4438) 2025-02-05 11:40:50 -05:00
faee6cbc64 Nadro/1919/on drag number fix (#3997)
* fix: fixing on drag number inc/dec massive amount of unit tests

* fix: implemented all scenarios for inc/dec formatting

* fix: deleting unused code

* fix: clearer name

* fix: adding commments

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

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

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

* fix: does this trigger the CI?

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

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

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 11:40:50 -05:00
0673e98fad Nadro/3799/perf (#4145)
* chore: building out perf testing

* chore: adding my printing code for the different formats of the marks

* feat: adding invocation count table

* fix: markOnce iunstead

* fix: typescript additions

* fix: adding more types

* chore: adding telemetry panel as MVP, gonna remove the pane

* chore: view telemetry from command bar in file route and home route

* fix: deleting unused imports

* fix: deleting some unused files

* fix: auto cleanup

* chore: adding other routes, these will need to be moved...

* chore: moving some printing logic around and unit testing some of it

* fix: moving command init

* fix: removing debugging marks

* fix: adding some comments

* fix: fixed a bug with generating the go to page commands

* chore: adding will pages load within the router config

* chore: implementing marks for routes

* fix: auto fixes and checkers

* chore: implemented a route watcher at the root level...

* fix: auto fixes, removing unused code

* chore: timing for syntax highlighting and auto fixes

* fix: didAuth issue and syntax highlighting in the packaged application. Constructor name gets renamed

* fix: fixing typescript checks

* chore: adding mag bar chart icon for telemetry

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

* chore: swapped telemetry icon for stopwatch

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

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

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

* chore: writing telemetry to disk

* fix: auto fixers

* chore: getting args parsed for cli flags and writing telemetry file

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

* chore: swapped mark for markOnce since we infinitely write marks to a JS array... need to solve this run time marking in another way. We only need this for startup right now

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

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

* chore: writing raw marks to disk as well

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

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

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

* fix: cleaned up the testing names

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

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

* Fix fmt and codespell

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

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

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

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

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

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

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

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

* fix: moving this route loader data stuff

* chore: adding comment

* fix: fmt

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

* empty :(

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

* empty :(

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:40:48 -05:00
b4eea5f842 Bump image from 0.25.3 to 0.25.5 in /src/wasm-lib (#4416)
Bumps [image](https://github.com/image-rs/image) from 0.25.3 to 0.25.5.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.3...v0.25.5)

---
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>
2025-02-05 11:39:42 -05:00
2c9eb7f7c0 Cut release v0.26.3 (#4427)
* Cut release v0.26.3

* Support new electron-builder env variable requirements for building in release mode
2025-02-05 11:39:42 -05:00
e259b2e3e8 Bump anyhow from 1.0.92 to 1.0.93 in /src/wasm-lib (#4417)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.92 to 1.0.93.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.92...1.0.93)

---
updated-dependencies:
- dependency-name: anyhow
  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>
2025-02-05 11:39:42 -05:00
91049204c5 Fix KCL source ranges to know which source file they point to (#4418)
* Add ts_rs feature to work with indexmap

* Add feature for schemars to work with indexmap

* Add module ID to intern module paths

* Update code to use new source range with three fields

* Update generated files

* Update docs

* Fix wasm

* Fix TS code to use new SourceRange

* Fix TS tests to use new SourceRange and moduleId

* Fix formatting

* Fix to filter errors and source ranges to only show the top-level module

* Fix to reuse module IDs

* Fix to disallow empty path for import

* Revert unneeded Self change

* Rename field to be clearer

* Fix parser tests

* Update snapshots

* Change to not serialize module_id of 0

* Update snapshots after adding default module_id

* Move module_id functions to separate module

* Fix tests for console errors

* Proposal: module ID = 0 gets skipped when serializing tokens too (#4422)

Just like in AST nodes.

Also I think "is_top_level" communicates intention better than is_default

---------

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2025-02-05 11:39:42 -05:00
0128c67aae Update insta snapshots (#4423)
No meaningful changes, they just added a field to the frontmatter
2025-02-05 11:39:42 -05:00
ecc42b1e9c Change Dependabot PR frequency to weekly (#4424) 2025-02-05 11:39:42 -05:00
31811d0269 Bump happy-dom from 15.10.1 to 15.10.2 (#4409)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 15.10.1 to 15.10.2.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v15.10.1...v15.10.2)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:39:42 -05:00
def5959836 Fixing directory/file selection logic to deselect folders properly and always hightlight files. (#4408)
* File tree acts as VS Code's file tree

* Adjust test for new expectations

* Remove screenshot

* Actually remove this screenshot

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

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

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

* fix: fixed logic for selection

* fix: always show the current file

---------

Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 11:39:42 -05:00
4c7fab405b File tree to act more like VS Code's file tree (#4392)
* File tree acts as VS Code's file tree

* Adjust test for new expectations

* Remove screenshot

* Actually remove this screenshot

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

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

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

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

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

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

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 11:39:41 -05:00
1e12e8d36b Bump @types/ws from 8.5.12 to 8.5.13 (#4395)
Bumps [@types/ws](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws) from 8.5.12 to 8.5.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ws)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:50 -05:00
ad775891a3 Bump url from 2.5.2 to 2.5.3 in /src/wasm-lib (#4399)
Bumps [url](https://github.com/servo/rust-url) from 2.5.2 to 2.5.3.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.2...v2.5.3)

---
updated-dependencies:
- dependency-name: url
  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>
2025-02-05 11:38:50 -05:00
efe207f4d2 Bump thiserror from 1.0.65 to 2.0.0 in /src/wasm-lib (#4397)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.65 to 2.0.0.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.65...2.0.0)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:50 -05:00
01f0162991 Bump pyo3 from 0.22.5 to 0.22.6 in /src/wasm-lib (#4398)
Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.22.5 to 0.22.6.
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/v0.22.6/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.22.5...v0.22.6)

---
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>
2025-02-05 11:38:50 -05:00
2bbf7fad67 Bump insta from 1.41.0 to 1.41.1 in /src/wasm-lib (#4400)
Bumps [insta](https://github.com/mitsuhiko/insta) from 1.41.0 to 1.41.1.
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.41.0...1.41.1)

---
updated-dependencies:
- dependency-name: insta
  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>
2025-02-05 11:38:50 -05:00
98549945a4 Bump validator from 0.18.1 to 0.19.0 in /src/wasm-lib (#4396)
Bumps [validator](https://github.com/Keats/validator) from 0.18.1 to 0.19.0.
- [Changelog](https://github.com/Keats/validator/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Keats/validator/commits)

---
updated-dependencies:
- dependency-name: validator
  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>
2025-02-05 11:38:50 -05:00
315fdc3060 Bump uuid from 9.0.1 to 11.0.2 (#4393)
Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.1 to 11.0.2.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.1...v11.0.2)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:50 -05:00
c7e77e2597 Bump @electron-forge/maker-zip from 7.4.0 to 7.5.0 (#4394)
Bumps [@electron-forge/maker-zip](https://github.com/electron/forge) from 7.4.0 to 7.5.0.
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/compare/v7.4.0...v7.5.0)

---
updated-dependencies:
- dependency-name: "@electron-forge/maker-zip"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:50 -05:00
a48679c014 Bump happy-dom from 14.12.3 to 15.10.1 (#4404)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 14.12.3 to 15.10.1.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v14.12.3...v15.10.1)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:50 -05:00
c5e74866a9 Internal fix: make expandPath not assume path has associated sweep (#4386)
* Add a test that shows current error within `expandPath`

* Make `expandPath` not assume there is an associated sweep artifact

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 11:38:49 -05:00
731cb6c532 Bump google-github-actions/upload-cloud-storage from 2.2.0 to 2.2.1 (#4364)
Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
8a36a4c205 Bump kittycad-modeling-cmds from 0.2.71 to 0.2.72 in /src/wasm-lib (#4356)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.71 to 0.2.72.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.71...kittycad-modeling-cmds-0.2.72)

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  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>
2025-02-05 11:38:49 -05:00
f29f2557de implement a simple startSketchOn / offsetPlane lint rule (#4384) 2025-02-05 11:38:49 -05:00
5f0ffb56c4 Bump @electron-forge/maker-wix from 7.4.0 to 7.5.0 (#4033)
Bumps [@electron-forge/maker-wix](https://github.com/electron/forge) from 7.4.0 to 7.5.0.
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/compare/v7.4.0...v7.5.0)

---
updated-dependencies:
- dependency-name: "@electron-forge/maker-wix"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
70078176b0 KCL: More ways to reference paths (#4387)
Adds new stdlib functions segStart, segStartX, segStartY, segEnd

Part of <https://github.com/KittyCAD/modeling-app/issues/4382>
2025-02-05 11:38:49 -05:00
098fa2b5c9 chore: implementing kclsamples in stand alone unit tests (#4358)
* chore: implementing kclsamples in stand alone unit tests

* fix: fmt, lint, and tsc

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

* fix: fixed program memory and test file pattern. Don't know how to exclude though?

* fix: trying to fix the exclude logic

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

* fix: bump CI

* fix:typo

* fix: had conflicting filters ope, now fixed

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
2755156b84 Bump insta from 1.40.0 to 1.41.0 in /src/wasm-lib (#4331)
Bumps [insta](https://github.com/mitsuhiko/insta) from 1.40.0 to 1.41.0.
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.40.0...1.41.0)

---
updated-dependencies:
- dependency-name: insta
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
a8b3ec660d Bump electron-updater from 6.3.0 to 6.3.9 (#4093)
Bumps [electron-updater](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-updater) from 6.3.0 to 6.3.9.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-updater@6.3.9/packages/electron-updater)

---
updated-dependencies:
- dependency-name: electron-updater
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
3747c6ff0e Bump reqwest from 0.12.8 to 0.12.9 in /src/wasm-lib (#4346)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.8 to 0.12.9.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.8...v0.12.9)

---
updated-dependencies:
- dependency-name: reqwest
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
bc1bc817ba Bump @codemirror/language from 6.10.2 to 6.10.3 (#4357)
Bumps [@codemirror/language](https://github.com/codemirror/language) from 6.10.2 to 6.10.3.
- [Changelog](https://github.com/codemirror/language/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/language/compare/6.10.2...6.10.3)

---
updated-dependencies:
- dependency-name: "@codemirror/language"
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
b415e88746 Bump google-github-actions/auth from 2.1.6 to 2.1.7 (#4363)
Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 2.1.6 to 2.1.7.
- [Release notes](https://github.com/google-github-actions/auth/releases)
- [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/auth/compare/v2.1.6...v2.1.7)

---
updated-dependencies:
- dependency-name: google-github-actions/auth
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
9173e368a2 Bump google-github-actions/setup-gcloud from 2.1.0 to 2.1.2 (#4365)
Bumps [google-github-actions/setup-gcloud](https://github.com/google-github-actions/setup-gcloud) from 2.1.0 to 2.1.2.
- [Release notes](https://github.com/google-github-actions/setup-gcloud/releases)
- [Changelog](https://github.com/google-github-actions/setup-gcloud/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/setup-gcloud/compare/v2.1.0...v2.1.2)

---
updated-dependencies:
- dependency-name: google-github-actions/setup-gcloud
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
25928813e3 Back to regular updater-test URL (#4332)
* Back to regular updater-test URL

* Test: build release

* Revert "Test: build release"

This reverts commit 7ed98cc9ed.
2025-02-05 11:38:49 -05:00
aec9cac7c7 Bump syn from 2.0.85 to 2.0.87 in /src/wasm-lib (#4379)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.85 to 2.0.87.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.85...2.0.87)

---
updated-dependencies:
- dependency-name: syn
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
0e82fbf7b0 Bump anyhow from 1.0.91 to 1.0.92 in /src/wasm-lib (#4378)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.91 to 1.0.92.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.91...1.0.92)

---
updated-dependencies:
- dependency-name: anyhow
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
f5975bbd61 Move tests from no_visuals into simulation tests (#4367)
Now you can properly inspect program memory for the no_visuals tests, instead of relying on a lot of KCL asserts.
2025-02-05 11:38:49 -05:00
765e587f6b Deflake project settings override on desktop (#4370) 2025-02-05 11:38:49 -05:00
6ccd5e22b8 KCL: More simulation tests (#4359)
Demonstrate simulation tests where we don't care about visuals, e.g. the double-map test. 

The `just new-sim-test` now accepts an optional argument, `render_to_png` which can be either  "true" or "false" (defaults to "true"). Tests like double_map that don't render anything can use false, rather than rendering an empty PNG with nothing in it.

This means the [tests under `no_visuals/`](https://github.com/KittyCAD/modeling-app/tree/v0.26.2/src/wasm-lib/tests/executor/inputs/no_visuals) can be entirely replaced by simulation tests. This is much better! For example, I moved `double_map.kcl` from a no_visuals test to a simulation test. Here's the file:

```
fn increment = (i) => {
  return i + 1
}

xs = [0..2]
ys = xs
  |> map(%, increment)
  |> map(%, increment)
```

Previously the `no_visuals` test just checked that the program ran successfully without panicking. Now the simulation test lets you see the value of `xs` and `ys` and immediately see they're correct. If our map logic changes (for example, we have an off-by-one error and don't apply the `map` to the last element) it'll show up in the program memory snapshot.
2025-02-05 11:38:49 -05:00
c8bf82ba04 Snap to origin and axis behavior for profile starts and segments (#4344)
* Visualize draft point when near axes (only works on XY rn due to quaternion rotation issue)

* Slightly better quaternion rotation

* Actually snap new profiles to the X and Y axis

* Add snapping behavior while dragging

* Fix flickering on non-XY planes

* Add some fixture additions to support click-and-drag tests

* Add new test to verify snapping behavior

* Make the editor test fixture auto-open and close as needed

* All feedback except absolute lines

* Use `lineTo` for lines that have snapped

* Get other existing tests passing after switching to `lineTo` when snapping

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

* Re-run CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 11:38:47 -05:00
daad2039ec Added test to ensure array push is immutable (#4361)
added test to ensure array push is immutable
2025-02-05 11:37:31 -05:00
b567f6dfad Refactor source ranges into a generic node type (#4350)
* WIP

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

* Fix formatting

* Fix yarn build:wasm

* Fix ts_rs bindings

* Fix tsc errors

* Fix wasm TS types

* Add minimal failing test

* Rename field to avoid name collisions

* Remove node wrapper around NonCodeMeta

Trying to fix TS unit test errors deserializing JSON AST in Rust.

* Rename Node to BoxNode

* Fix lints

* Fix lint by boxing literals

* Rename UnboxedNode to Node

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

* Update docs

* Update snapshots

* initial trait

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

* update docs

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

* updates

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

* gross hack for TagNode

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

* extend gross hack

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

* fix EnvRef bullshit

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

* Fix to fail parsing when a tag declarator matches a stdlib function name

* Fix test errors after merging main

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

* Confirm

* Change to use simpler map_err

* Add comment

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2025-02-05 11:37:31 -05:00
41e85c77ac KCL: New simulation test pipeline (#4351)
The idea behind this is to test all the various stages of executing KCL
separately, i.e.

 - Start with a program
 - Tokenize it
 - Parse those tokens into an AST
 - Recast the AST
 - Execute the AST, outputting
   - a PNG of the rendered model
   - serialized program memory

Each of these steps reads some input and writes some output to disk.
The output of one step becomes the input to the next step. These
intermediate artifacts are also snapshotted (like expectorate or 2020)
to ensure we're aware of any changes to how KCL works. A change could
be a bug, or it could be harmless, or deliberate, but keeping it checked
into the repo means we can easily track changes.

Note: UUIDs sent back by the engine are currently nondeterministic, so
they would break all the snapshot tests. So, the snapshots use a regex
filter and replace anything that looks like a uuid with [uuid] when
writing program memory to a snapshot. In the future I hope our UUIDs will
be seedable and easy to make deterministic. At that point, we can stop
filtering the UUIDs.

We run this pipeline on many different KCL programs. Each keeps its
inputs (KCL programs), outputs (PNG, program memory snapshot) and
intermediate artifacts (AST, token lists, etc) in that directory.

I also added a new `just` command to easily generate these tests.
You can run `just new-sim-test gear $(cat gear.kcl)` to set up a new
gear test directory and generate all the intermediate artifacts for the
first time. This doesn't need any macros, it just appends some new lines
of normal Rust source code to `tests.rs`, so it's easy to see exactly
what the code is doing.

This uses `cargo insta` for convenient snapshot testing of artifacts
as JSON, and `twenty-twenty` for snapshotting PNGs.

This was heavily inspired by Predrag Gruevski's talk at EuroRust 2024
about deterministic simulation testing, and how it can both reduce bugs
and also reduce testing/CI time. Very grateful to him for chatting with
me about this over the last couple of weeks.
2025-02-05 11:37:31 -05:00
9f615b9d3e Bump serde from 1.0.213 to 1.0.214 in /src/wasm-lib (#4345)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.213 to 1.0.214.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.213...v1.0.214)

---
updated-dependencies:
- dependency-name: serde
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:37:31 -05:00
e8b5618b34 Bump handlebars from 6.1.0 to 6.2.0 in /src/wasm-lib (#4330)
Bumps [handlebars](https://github.com/sunng87/handlebars-rust) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/sunng87/handlebars-rust/releases)
- [Changelog](https://github.com/sunng87/handlebars-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sunng87/handlebars-rust/compare/v6.1.0...v6.2.0)

---
updated-dependencies:
- dependency-name: handlebars
  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>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:37:31 -05:00
799b2d77b4 fix auth test in engine (#4354)
* fix auth test in engine

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

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

* emoty

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 11:37:30 -05:00
7b569f9b4f KCL test for subset of poop chute (#4343)
This would have caught the regression in https://github.com/KittyCAD/modeling-app/pull/4333

which had to be reverted in https://github.com/KittyCAD/modeling-app/pull/4339
2025-02-05 11:37:12 -05:00
0f0c396a0c Bump react-router-dom from 6.26.1 to 6.27.0 (#4286)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.26.1 to 6.27.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@6.27.0/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.27.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  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>
2025-02-05 11:37:12 -05:00
83214a88a3 Tests: remove all timeouts and pasting into editor from file name collision PW test (#4352)
remove all timeouts and pasting into editor from file name collision PW test
2025-02-05 11:37:12 -05:00
dbab7876de Add lsystem.kcl to tests (#4146)
* Add lsystem.kcl to tests

* Reduce iterations

* Fix the user settings flake shit (NOTE TO ALL FUTURE PEOPLE MODELING-APP DOES NOT WAIT FOR I/O IN SOME CASES BEFORE ROUTER NAVIGATION)
2025-02-05 11:37:12 -05:00
6706695502 Fix just lint and yarn script to check all targets (#4348)
* Fix just lint to check all targets

* Fix yarn test:rust to lint all targets

* Remove redundant options

* Change cargo --all to --workspace

* Update readme to use just command
2025-02-05 11:37:12 -05:00
fa1f8d8d02 Deflake settings persistence desktop test by verifying we have written to the disk before continuing (#4349) 2025-02-05 11:37:12 -05:00
b4e59b5c56 Fix CI docs generation after #4329 (#4347)
Fix CI
2025-02-05 11:37:12 -05:00
20495383ac A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-29 15:00:02 +00:00
7f5fb83761 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-29 10:39:18 -04:00
2ac874971e A couple small de-flake efforts for "Add multiple sketches" 2024-10-29 10:39:18 -04:00
230e3132e9 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-29 10:39:18 -04:00
bf9bb4fb22 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-29 10:39:18 -04:00
31e7634669 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) 2024-10-29 10:39:18 -04:00
84c71aa046 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) 2024-10-29 10:39:18 -04:00
721b3e8cbd wip 2024-10-29 10:39:18 -04:00
89309b6ccd A shit ton of stuff 2024-10-29 10:39:17 -04:00
15b163bba8 Fix constraint overlay hidden by labels - show labels on small lines too 2024-10-29 10:38:15 -04:00
60d047ef6a Revert "Bring constraints overlay in front of line labels; leave arrowheads when zooming out"
This reverts commit 251beb5fa6414d3b298d330e41d7ba3d57486f8c.
2024-10-29 10:38:15 -04:00
f105044a47 Bring constraints overlay in front of line labels; leave arrowheads when zooming out 2024-10-29 10:37:28 -04:00
9388e09c47 wip test 2024-10-29 10:19:55 -04:00
d71f2af9bd Trying to fix tests 2024-10-29 10:19:55 -04:00
2bb372de12 wip fixing tests 2024-10-29 10:19:55 -04:00
0a3a8afbbd Type correct everything, make settings deserializer better, fmt tsc lint 2024-10-29 10:19:53 -04:00
351df2f306 Account for old setting 2024-10-29 10:19:09 -04:00
05a2eada9a Make the stream idle option a slider for time 2024-10-29 10:19:08 -04:00
788270d4fc Remove unused Stream component 2024-10-29 10:18:15 -04:00
6845f0c4bc Move the model when panes are open 2024-10-29 10:18:13 -04:00
563096fba4 Close command bar and make open button disabled on pause 2024-10-29 09:58:15 -04:00
35133c4f45 Don't zoom to fit on resume; make indicator better 2024-10-29 09:58:15 -04:00
b78c6508c2 Support restoring zoom; use a counter instead of timeout; account for more input types 2024-10-29 09:58:15 -04:00
08b776134f Camera being restored 2024-10-29 09:58:15 -04:00
cca544189c tsc lint 2024-10-29 09:58:15 -04:00
69754c82a2 Make scene status less flashy 2024-10-29 09:58:15 -04:00
afbee552ee Good basis for idle 2024-10-29 09:58:15 -04:00
b11772b27c wip 2024-10-29 09:58:15 -04:00
6dc87aa4fe wip 2024-10-29 09:58:15 -04:00
119 changed files with 10407 additions and 3322 deletions

View File

@ -5,6 +5,8 @@ on:
paths: paths:
- 'src/wasm-lib/**.rs' - 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs' - 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml' - '**/Cargo.toml'
- '**/Cargo.lock' - '**/Cargo.lock'
- '**/rust-toolchain.toml' - '**/rust-toolchain.toml'
@ -15,6 +17,8 @@ on:
paths: paths:
- 'src/wasm-lib/**.rs' - 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs' - 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml' - '**/Cargo.toml'
- '**/Cargo.lock' - '**/Cargo.lock'
- '**/rust-toolchain.toml' - '**/rust-toolchain.toml'

View File

@ -19,7 +19,7 @@ $(XSTATE_TYPEGENS): $(TS_SRC)
yarn xstate typegen 'src/**/*.ts?(x)' yarn xstate typegen 'src/**/*.ts?(x)'
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES) public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
yarn build:wasm-dev yarn build:wasm
node_modules: package.json yarn.lock node_modules: package.json yarn.lock
yarn install yarn install

View File

@ -110,7 +110,7 @@ Which commands from setup are one off vs need to be run every time?
The following will need to be run when checking out a new commit and guarantees the build is not stale: The following will need to be run when checking out a new commit and guarantees the build is not stale:
```bash ```bash
yarn install yarn install
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build yarn build:wasm
yarn start # or yarn build:local && yarn serve for slower but more production-like build yarn start # or yarn build:local && yarn serve for slower but more production-like build
``` ```

View File

@ -1,10 +1,10 @@
--- ---
title: "angleToMatchLengthX" title: "angleToMatchLengthX"
excerpt: "Compute the angle (in degrees) in o" excerpt: "Returns the angle to match the given length for x."
layout: manual layout: manual
--- ---
Compute the angle (in degrees) in o Returns the angle to match the given length for x.

59
docs/kcl/modules.md Normal file
View File

@ -0,0 +1,59 @@
---
title: "KCL Modules"
excerpt: "Documentation of modules for the KCL language for the Zoo Modeling App."
layout: manual
---
`KCL` allows splitting code up into multiple files. Each file is somewhat
isolated from other files as a separate module.
When you define a function, you can use `export` before it to make it available
to other modules.
```
// util.kcl
export fn increment = (x) => {
return x + 1
}
```
Other files in the project can now import functions that have been exported.
This makes them available to use in another file.
```
// main.kcl
import increment from "util.kcl"
answer = increment(41)
```
Imported files _must_ be in the same project so that units are uniform across
modules. This means that it must be in the same directory.
Import statements must be at the top-level of a file. It is not allowed to have
an `import` statement inside a function or in the body of an if-else.
Multiple functions can be exported in a file.
```
// util.kcl
export fn increment = (x) => {
return x + 1
}
export fn decrement = (x) => {
return x - 1
}
```
When importing, you can import multiple functions at once.
```
import increment, decrement from "util.kcl"
```
Imported symbols can be renamed for convenience or to avoid name collisions.
```
import increment as inc, decrement as dec from "util.kcl"
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

16
docs/kcl/types/KclNone.md Normal file
View File

@ -0,0 +1,16 @@
---
title: "KclNone"
excerpt: "KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application)."
layout: manual
---
KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application).
**Type:** `object`

View File

@ -23,8 +23,110 @@ Any KCL value.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `UserVal`| | No | | `type` |enum: `Uuid`| | No |
| `value` |``| | No | | `value` |`string`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Bool`| | No |
| `value` |`boolean`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Number`| | No |
| `value` |`number`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Int`| | No |
| `value` |`integer`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `String`| | No |
| `value` |`string`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Array`| | No |
| `value` |`[` [`KclValue`](/docs/kcl/types/KclValue) `]`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Object`| | No |
| `value` |`object`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
@ -111,6 +213,38 @@ A face.
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Sketch`](/docs/kcl/types/Sketch)| | No |
| `value` |[`Sketch`](/docs/kcl/types/Sketch)| Any KCL value. | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Sketches`| | No |
| `value` |`[` [`Sketch`](/docs/kcl/types/Sketch) `]`| | No |
---- ----
An solid is a collection of extrude surfaces. An solid is a collection of extrude surfaces.
@ -190,6 +324,23 @@ Data for an imported geometry.
---- ----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`KclNone`](/docs/kcl/types/KclNone)| | No |
| `value` |[`KclNone`](/docs/kcl/types/KclNone)| Any KCL value. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----

View File

@ -67,15 +67,15 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`) |> xLine(${commonPoints.num1}, %)`)
} }
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> xLine(${commonPoints.num1}, %)
|> line([0, ${commonPoints.num1 + 0.01}], %)`) |> yLine(${commonPoints.num1 + 0.01}, %)`)
} else { } else {
await page.waitForTimeout(500) await page.waitForTimeout(500)
} }
@ -84,9 +84,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> xLine(${commonPoints.num1}, %)
|> line([0, ${commonPoints.num1 + 0.01}], %) |> yLine(${commonPoints.num1 + 0.01}, %)
|> lineTo([0, ${commonPoints.num3}], %)`) |> xLine(${commonPoints.num2 * -1}, %)`)
} }
// deselect line tool // deselect line tool
@ -142,9 +142,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
await u.openKclCodePanel() await u.openKclCodePanel()
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %, $seg01) |> xLine(${commonPoints.num1}, %, $seg01)
|> line([0, ${commonPoints.num1 + 0.01}], %) |> yLine(${commonPoints.num1 + 0.01}, %)
|> angledLine([180, segLen(seg01)], %)`) |> xLine(-segLen(seg01), %)`)
} }
test.describe('Basic sketch', () => { test.describe('Basic sketch', () => {

View File

@ -694,6 +694,9 @@ test.describe('Editor tests', () => {
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([3.14, 12], %) |> startProfileAt([3.14, 12], %)
|> xLine(5, %) // lin`) |> xLine(5, %) // lin`)
// expect there to be no KCL errors
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0)
}) })
test('with tab to accept the completion', async ({ page }) => { test('with tab to accept the completion', async ({ page }) => {

View File

@ -452,7 +452,7 @@ sketch002 = startSketchOn(extrude001, seg03)
) )
}) })
test(`Verify axis and origin snapping`, async ({ test(`Verify axis, origin, and horizontal snapping`, async ({
app, app,
editor, editor,
toolbar, toolbar,
@ -505,7 +505,7 @@ test(`Verify axis and origin snapping`, async ({
const expectedCodeSnippets = { const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`, pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
segmentOnXAxis: `lineTo([${xAxisSloppy.kcl[0]}, ${xAxisSloppy.kcl[1]}], %)`, segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`, afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`, afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
} }

View File

@ -115,7 +115,7 @@ test.describe('Sketch tests', () => {
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %) |> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %) |> xLine(12.73, %)
|> tangentialArcTo([24.95, -5.38], %)` |> tangentialArcTo([24.95, -5.38], %)`
) )
}) })
@ -156,7 +156,7 @@ test.describe('Sketch tests', () => {
await expect.poll(u.normalisedEditorCode, { timeout: 1000 }) await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %) |> startProfileAt([12.34, -12.34], %)
|> line([-12.34, 12.34], %) |> yLine(12.34, %)
`) `)
}).toPass({ timeout: 40_000, intervals: [1_000] }) }).toPass({ timeout: 40_000, intervals: [1_000] })
@ -202,35 +202,19 @@ test.describe('Sketch tests', () => {
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) const viewport = { width: 1200, height: 500 }
await page.setViewportSize(viewport)
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
await page.waitForTimeout(100) const center = {
await u.openAndClearDebugPanel() x: viewport.width / 2,
await u.sendCustomCmd({ y: viewport.height / 2,
type: 'modeling_cmd_req', }
cmd_id: uuidv4(), const modelAreaSize = await u.getModelViewAreaSize()
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await u.closeDebugPanel()
// If we have the code pane open, we should see the code. // If we have the code pane open, we should see the code.
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
@ -244,7 +228,7 @@ test.describe('Sketch tests', () => {
await expect(u.codeLocator).not.toBeVisible() await expect(u.codeLocator).not.toBeVisible()
} }
const startPX = [665, 458] const startPX = [center.x + 65, 458]
const dragPX = 30 const dragPX = 30
let prevContent = '' let prevContent = ''
@ -255,7 +239,7 @@ test.describe('Sketch tests', () => {
// Wait for the render. // Wait for the render.
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
// Select the sketch // Select the sketch
await page.mouse.click(700, 370) await page.mouse.click(center.x + 100, 370)
} }
await expect( await expect(
page.getByRole('button', { name: 'Edit Sketch' }) page.getByRole('button', { name: 'Edit Sketch' })
@ -266,24 +250,47 @@ test.describe('Sketch tests', () => {
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
} }
await page.waitForTimeout(1000)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 - modelAreaSize.w },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(1000)
await u.closeDebugPanel()
const step5 = { steps: 5 } const step5 = { steps: 5 }
await expect(page.getByTestId('segment-overlay')).toHaveCount(2) await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfieAt handle test.step('drag startProfileAt handle', async () => {
await page.mouse.move(startPX[0], startPX[1]) await page.mouse.move(startPX[0], startPX[1])
await page.mouse.down() await page.mouse.down()
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5) await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
await page.mouse.up() await page.mouse.up()
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
} }
})
// drag line handle
await page.waitForTimeout(100) await page.waitForTimeout(100)
test.step('drag line handle', async () => {
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]') const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.mouse.move(lineEnd.x - 5, lineEnd.y) await page.mouse.move(lineEnd.x - 5, lineEnd.y)
await page.mouse.down() await page.mouse.down()
@ -294,17 +301,23 @@ test.describe('Sketch tests', () => {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
} }
})
// drag tangentialArcTo handle test.step('drag tangentialArcTo handle', async () => {
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]') const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5) await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
await page.mouse.down() await page.mouse.down()
await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5) await page.mouse.move(
tangentEnd.x + dragPX,
tangentEnd.y - dragPX,
step5
)
await page.mouse.up() await page.mouse.up()
await page.waitForTimeout(100) await page.waitForTimeout(100)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
} }
})
// Open the code pane // Open the code pane
await u.openKclCodePanel() await u.openKclCodePanel()
@ -580,7 +593,7 @@ test.describe('Sketch tests', () => {
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
const startPX = [665, 458] const center = await u.getCenterOfModelViewArea()
const dragPX = 30 const dragPX = 30
@ -596,7 +609,7 @@ test.describe('Sketch tests', () => {
await expect(page.getByTestId('segment-overlay')).toHaveCount(2) await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfieAt handle // drag startProfileAt handle
await page.mouse.move(startPX[0], startPX[1]) await page.mouse.move(startPX[0], startPX[1])
await page.mouse.down() await page.mouse.down()
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5) await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
@ -638,6 +651,7 @@ test.describe('Sketch tests', () => {
}) })
test('Can add multiple sketches', async ({ page }) => { test('Can add multiple sketches', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize) await page.setViewportSize(viewportSize)
@ -645,7 +659,7 @@ test.describe('Sketch tests', () => {
await u.openDebugPanel() await u.openDebugPanel()
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 } const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const { toSU, click00r } = getMovementUtils({ center, page }) const { toSU, toU, click00r } = getMovementUtils({ center, page })
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
@ -661,29 +675,32 @@ test.describe('Sketch tests', () => {
200 200
) )
const center = await u.getCenterOfModelViewArea()
let codeStr = "sketch001 = startSketchOn('XY')" let codeStr = "sketch001 = startSketchOn('XY')"
await page.mouse.click(center.x, viewportSize.height * 0.55) await page.mouse.click(center.x - 50, viewportSize.height * 0.55)
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await u.closeDebugPanel() await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await click00r(0, 0) const { click00r } = await getMovementUtils({ center, page })
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
let coord = await click00r(0, 0)
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(50, 0) await click00r(50, 0)
await page.waitForTimeout(100) await page.waitForTimeout(100)
codeStr += ` |> lineTo(${toSU([50, 0])}, %)` codeStr += ` |> xLine(${toU(50, 0)[0]}, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 50) await click00r(0, 50)
codeStr += ` |> line(${toSU([0, 50])}, %)` codeStr += ` |> yLine(${toU(0, 50)[1]}, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
let clickCoords = await click00r(-50, 0) await click00r(-50, 0)
expect(clickCoords).not.toBeUndefined() codeStr += ` |> xLine(${toU(-50, 0)[0]}, %)`
codeStr += ` |> lineTo(${toSU(clickCoords!)}, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
// exit the sketch, reset relative clicker // exit the sketch, reset relative clicker
@ -699,28 +716,29 @@ test.describe('Sketch tests', () => {
// when exiting the sketch above the camera is still looking down at XY, // when exiting the sketch above the camera is still looking down at XY,
// so selecting the plane again is a bit easier. // so selecting the plane again is a bit easier.
await page.mouse.click(center.x + 200, center.y + 100) await page.mouse.move(center.x - 100, center.y + 50, { steps: 5 })
await page.mouse.click(center.x - 100, center.y + 50)
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
codeStr += "sketch002 = startSketchOn('XY')" codeStr += "sketch002 = startSketchOn('XY')"
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await u.closeDebugPanel() await u.closeDebugPanel()
await click00r(30, 0) coord = await click00r(30, 0)
codeStr += ` |> startProfileAt([2.03, 0], %)` codeStr += ` |> startProfileAt(${coord.kcl}, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
// TODO: I couldn't use `toSU` here because of some rounding error causing // TODO: I couldn't use `toSU` here because of some rounding error causing
// it to be off by 0.01 // it to be off by 0.01
await click00r(30, 0) await click00r(30, 0)
codeStr += ` |> lineTo([4.07, 0], %)` codeStr += ` |> xLine(2.04, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 30) await click00r(0, 30)
codeStr += ` |> line([0, -2.03], %)` codeStr += ` |> yLine(-2.03, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(-30, 0) await click00r(-30, 0)
codeStr += ` |> line([-2.04, 0], %)` codeStr += ` |> xLine(-2.04, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(undefined, undefined) await click00r(undefined, undefined)
@ -744,8 +762,8 @@ test.describe('Sketch tests', () => {
const code = `sketch001 = startSketchOn('-XZ') const code = `sketch001 = startSketchOn('-XZ')
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %) |> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|> line([${roundOff(scale * 139.19)}, 0], %) |> xLine(${roundOff(scale * 139.19)}, %)
|> line([0, -${roundOff(scale * 139.2)}], %) |> yLine(-${roundOff(scale * 139.2)}, %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)` |> close(%)`
@ -764,20 +782,21 @@ test.describe('Sketch tests', () => {
await u.updateCamPosition(camPos) await u.updateCamPosition(camPos)
await u.closeDebugPanel() await u.closeDebugPanel()
const center = await u.getCenterOfModelViewArea()
await page.mouse.move(0, 0) await page.mouse.move(0, 0)
// select a plane // select a plane
await page.mouse.move(700, 200, { steps: 10 }) await page.mouse.move(center.x + 100, 200, { steps: 10 })
await page.mouse.click(700, 200, { delay: 200 }) await page.mouse.click(center.x + 100, 200, { delay: 200 })
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn('-XZ')` `sketch001 = startSketchOn('-XZ')`
) )
let prevContent = await page.locator('.cm-content').innerText() let prevContent = await page.locator('.cm-content').innerText()
const pointA = [700, 200] const pointA = [center.x + 100, 200]
const pointB = [900, 200] const pointB = [center.x + 300, 200]
const pointC = [900, 400] const pointC = [center.x + 300, 400]
// draw three lines // draw three lines
await page.waitForTimeout(500) await page.waitForTimeout(500)
@ -914,7 +933,9 @@ extrude001 = extrude(5, sketch001)
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.click(622, 355) const center = await u.getCenterOfModelViewArea()
await page.mouse.click(center.x + 22, 355)
await page.waitForTimeout(800) await page.waitForTimeout(800)
await page.getByText(`END')`).click() await page.getByText(`END')`).click()

View File

@ -462,7 +462,7 @@ test(
await page.waitForTimeout(100) await page.waitForTimeout(100)
code += ` code += `
|> line([7.25, 0], %)` |> xLine(7.25, %)`
await expect(page.locator('.cm-content')).toHaveText(code) await expect(page.locator('.cm-content')).toHaveText(code)
await page await page
@ -647,7 +647,7 @@ test.describe(
await page.waitForTimeout(100) await page.waitForTimeout(100)
code += ` code += `
|> line([7.25, 0], %)` |> xLine(7.25, %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page await page
@ -752,7 +752,7 @@ test.describe(
await page.waitForTimeout(100) await page.waitForTimeout(100)
code += ` code += `
|> line([184.3, 0], %)` |> xLine(184.3, %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page await page

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

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

After

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

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -141,7 +141,7 @@ test.describe('Test network and connection issues', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`) |> xLine(${commonPoints.num1}, %)`)
// Expect the network to be up // Expect the network to be up
await expect(networkToggle).toContainText('Connected') await expect(networkToggle).toContainText('Connected')
@ -207,7 +207,7 @@ test.describe('Test network and connection issues', () => {
await expect.poll(u.normalisedEditorCode) await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %) |> startProfileAt([12.34, -12.34], %)
|> line([12.34, 0], %) |> xLine(12.34, %)
|> line([-12.34, 12.34], %) |> line([-12.34, 12.34], %)
`) `)
@ -217,9 +217,9 @@ test.describe('Test network and connection issues', () => {
await expect.poll(u.normalisedEditorCode) await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %) |> startProfileAt([12.34, -12.34], %)
|> line([12.34, 0], %) |> xLine(12.34, %)
|> line([-12.34, 12.34], %) |> line([-12.34, 12.34], %)
|> lineTo([0, -12.34], %) |> xLine(-12.34, %)
`) `)

View File

@ -8,6 +8,21 @@ import {
Locator, Locator,
test, test,
} from '@playwright/test' } from '@playwright/test'
import {
OrthographicCamera,
Mesh,
Scene,
Raycaster,
PlaneGeometry,
MeshBasicMaterial,
DoubleSide,
Vector2,
Vector3,
} from 'three'
import {
RAYCASTABLE_PLANE,
INTERSECTION_PLANE_LAYER,
} from 'clientSideScene/constants'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import fsSync from 'fs' import fsSync from 'fs'
@ -257,55 +272,141 @@ export const circleMove = async (
} }
} }
export const getMovementUtils = (opts: any) => { export function rollingRound(n: number, digitsAfterDecimal: number) {
// The way we truncate is kinda odd apparently, so we need this function const s = String(n).split('.')
// "[k]itty[c]ad round"
const kcRound = (n: number) => Math.trunc(n * 100) / 100
// To translate between screen and engine ("[U]nit") coordinates // There are no decimals, just return the number.
// NOTE: these pretty much can't be perfect because of screen scaling. if (s.length === 1) return n
// Handle on a case-by-case.
const toU = (x: number, y: number) => [
kcRound(x * 0.0678),
kcRound(-y * 0.0678), // Y is inverted in our coordinate system
]
// Turn the array into a string with specific formatting // Find the closest 9. We don't care about anything beyond that.
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]` const nineIndex = s[1].indexOf('9')
// Combine because used often const fractStr = nineIndex > 0 ? s[1].slice(0, nineIndex + 1) : s[1]
const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
let fract = Number(fractStr) / 10 ** fractStr.length
for (let i = fractStr.length - 1; i >= 0; i -= 1) {
if (i === digitsAfterDecimal) break
fract = Math.round(fract * 10 ** i) / 10 ** i
}
return (Number(s[0]) + fract).toFixed(digitsAfterDecimal)
}
export const getMovementUtils = async (opts: any) => {
const sceneInfra = await opts.page.evaluate(() => window.sceneInfra)
// Various data for raycasting into the scene to get our XY.
const hundredM = 100_0000
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
const planeMaterial = new MeshBasicMaterial({
color: 0xff0000,
side: DoubleSide,
transparent: true,
opacity: 0.5,
})
const scene = new Scene()
const intersectionPlane = new Mesh(planeGeometry, planeMaterial)
intersectionPlane.userData = { type: RAYCASTABLE_PLANE }
intersectionPlane.name = RAYCASTABLE_PLANE
intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
scene.add(intersectionPlane)
const planeRaycaster = new Raycaster()
planeRaycaster.far = Infinity
planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
const kcRound = (n: number) => Math.round(n * 100) / 100
// Make it easier to click around from center ("click [from] zero zero") // Make it easier to click around from center ("click [from] zero zero")
const click00 = (x: number, y: number) => const click00 = (x: number, y: number) =>
opts.page.mouse.click(opts.center.x + x, opts.center.y + y, { delay: 100 }) opts.page.mouse.click(x, y, { delay: 100 })
// Relative clicker, must keep state // Relative clicker, must keep state
let last = { x: 0, y: 0 } let last = { x: 0, y: 0 }
let lastScreenSpace = { x: 0, y: 0 }
const click00r = async (x?: number, y?: number) => { const click00r = async (x?: number, y?: number) => {
// reset relative coordinates when anything is undefined // reset relative coordinates when anything is undefined
if (x === undefined || y === undefined) { if (x === undefined || y === undefined) {
last.x = 0 last = { x: 0, y: 0 }
last.y = 0 lastScreenSpace = { x: 0, y: 0 }
return return {
nextXY: [0, 0],
kcl: `[0, 0]`,
}
} }
await circleMove( const absX = opts.center.x + x
opts.page, const absY = opts.center.y + y
opts.center.x + last.x + x,
opts.center.y + last.y + y, const nextX = last.x + x
10, const nextY = last.y + y
10
const targetX = opts.center.x + nextX
const targetY = opts.center.y + -nextY
// Use the current camera specification
const camera = await opts.page.evaluate(() => {
window.sceneInfra.camControls.onCameraChange(true)
return window.sceneInfra.camControls.camera
})
const windowWH = await opts.page.evaluate(() => ({
w: window.innerWidth,
h: window.innerHeight,
}))
// I didn't write this math, it's copied from sceneInfra.ts, and I understand
// it's just normalizing the point, but why *-2 ± 1 I have no idea.
const mouseVector = new Vector2(
(targetX / windowWH.w) * 2 - 1,
-(targetY / windowWH.h) * 2 + 1
) )
await click00(last.x + x, last.y + y) planeRaycaster.setFromCamera(mouseVector, camera)
const intersections = planeRaycaster.intersectObjects(scene.children, true)
const planePosition = intersections[0].object.position
const inversePlaneQuaternion = intersections[0].object.quaternion
.clone()
.invert()
let transformedPoint = intersections[0].point.clone()
if (transformedPoint) {
transformedPoint.applyQuaternion(inversePlaneQuaternion)
}
const twoD = new Vector2(
// I think the intersection plane doesn't get scale when nearly everything else does, maybe that should change
transformedPoint.x / sceneInfra._baseUnitMultiplier,
transformedPoint.y / sceneInfra._baseUnitMultiplier
) // z should be 0
const planePositionCorrected = new Vector3(
...planePosition
).applyQuaternion(inversePlaneQuaternion)
twoD.sub(new Vector2(...planePositionCorrected))
await circleMove(opts.page, targetX, targetY, 10, 10)
await click00(targetX, targetY)
last.x += x last.x += x
last.y += y last.y += y
// Returns the new absolute coordinate if you need it. const relativeScreenSpace = {
return [last.x, last.y] x: twoD.x - lastScreenSpace.x,
y: -(twoD.y - lastScreenSpace.y),
} }
return { toSU, click00r } lastScreenSpace.x = kcRound(twoD.x)
lastScreenSpace.y = kcRound(twoD.y)
// Returns the new absolute coordinate and the screen space coordinate if you need it.
return {
nextXY: [last.x, last.y],
kcl: `[${kcRound(relativeScreenSpace.x)}, ${-kcRound(
relativeScreenSpace.y
)}]`,
}
}
return { toSU, toU, click00r }
} }
async function waitForAuthAndLsp(page: Page) { async function waitForAuthAndLsp(page: Page) {
@ -356,6 +457,30 @@ export async function getUtils(page: Page, test_?: typeof test) {
browserType !== 'chromium' ? null : await page.context().newCDPSession(page) browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
const util = { const util = {
async getModelViewAreaSize() {
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
const sidebar = page.getByTestId('modeling-sidebar')
const bb = await sidebar.boundingBox()
return {
w: windowInnerWidth - (bb?.width ?? 0),
h: windowInnerHeight - (bb?.height ?? 0),
}
},
async getCenterOfModelViewArea() {
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
const sidebar = page.getByTestId('modeling-sidebar')
const bb = await sidebar.boundingBox()
const goRightPx = (bb?.width ?? 0) / 2
const borderWidthsCombined = 2
return {
x: Math.round(windowInnerWidth / 2 + goRightPx) - borderWidthsCombined,
y: Math.round(windowInnerHeight / 2),
}
},
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page), waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
waitForPageLoad: () => waitForPageLoad(page), waitForPageLoad: () => waitForPageLoad(page),
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page), waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),

View File

@ -43,10 +43,12 @@ test.describe('Testing constraints', () => {
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500) // wait for animation await page.waitForTimeout(500) // wait for animation
const startXPx = 500 const center = await u.getCenterOfModelViewArea()
const startXPx = center.x - 100
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10) await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.mouse.click(834, 244) await page.mouse.click(center.x + 234, 244)
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await page await page

View File

@ -32,10 +32,17 @@ test.describe('Testing selections', () => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
const xAxisClick = () => const yAxisClick = () =>
page.mouse.click(700, 253).then(() => page.waitForTimeout(100)) test.step('Click on Y axis', async () => {
await page.mouse.move(600, 200, { steps: 5 })
await page.mouse.click(600, 200)
await page.waitForTimeout(100)
})
const xAxisClickAfterExitingSketch = () => const xAxisClickAfterExitingSketch = () =>
page.mouse.click(639, 278).then(() => page.waitForTimeout(100)) test.step(`Click on X axis after exiting sketch, which shifts it at the moment`, async () => {
await page.mouse.click(639, 278)
await page.waitForTimeout(100)
})
const emptySpaceHover = () => const emptySpaceHover = () =>
test.step('Hover over empty space', async () => { test.step('Hover over empty space', async () => {
await page.mouse.move(700, 143, { steps: 5 }) await page.mouse.move(700, 143, { steps: 5 })
@ -80,23 +87,23 @@ test.describe('Testing selections', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`) |> xLine(${commonPoints.num1}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> xLine(${commonPoints.num1}, %)
|> line([0, ${commonPoints.num1 + 0.01}], %)`) |> yLine(${commonPoints.num1 + 0.01}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> xLine(${commonPoints.num1}, %)
|> line([0, ${commonPoints.num1 + 0.01}], %) |> yLine(${commonPoints.num1 + 0.01}, %)
|> lineTo([0, ${commonPoints.num3}], %)`) |> xLine(${commonPoints.num2 * -1}, %)`)
// deselect line tool // deselect line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
@ -121,29 +128,31 @@ test.describe('Testing selections', () => {
// now check clicking works including axis // now check clicking works including axis
// click a segment hold shift and click an axis, see that a relevant constraint is enabled // click a segment hold shift and click an axis, see that a relevant constraint is enabled
await topHorzSegmentClick()
await page.keyboard.down('Shift')
const constrainButton = page.getByRole('button', { const constrainButton = page.getByRole('button', {
name: 'Length: open menu', name: 'Length: open menu',
}) })
const absYButton = page.getByRole('button', { name: 'Absolute Y' }) const absXButton = page.getByRole('button', { name: 'Absolute X' })
await test.step(`Select a segment and an axis, see that a relevant constraint is enabled`, async () => {
await topHorzSegmentClick()
await page.keyboard.down('Shift')
await constrainButton.click() await constrainButton.click()
await expect(absYButton).toBeDisabled() await expect(absXButton).toBeDisabled()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await xAxisClick() await yAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await constrainButton.click() await constrainButton.click()
await absYButton.and(page.locator(':not([disabled])')).waitFor() await absXButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absYButton).not.toBeDisabled() await expect(absXButton).not.toBeDisabled()
})
// clear selection by clicking on nothing
await emptySpaceClick() await emptySpaceClick()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// same selection but click the axis first
await xAxisClick() await test.step(`Same selection but click the axis first`, async () => {
await yAxisClick()
await constrainButton.click() await constrainButton.click()
await expect(absYButton).toBeDisabled() await expect(absXButton).toBeDisabled()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.waitForTimeout(100) await page.waitForTimeout(100)
await topHorzSegmentClick() await topHorzSegmentClick()
@ -151,23 +160,26 @@ test.describe('Testing selections', () => {
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await constrainButton.click() await constrainButton.click()
await expect(absYButton).not.toBeDisabled() await expect(absXButton).not.toBeDisabled()
})
// clear selection by clicking on nothing // clear selection by clicking on nothing
await emptySpaceClick() await emptySpaceClick()
// check the same selection again by putting cursor in code first then selecting axis // check the same selection again by putting cursor in code first then selecting axis
await test.step(`Same selection but code selection then axis`, async () => {
await page await page
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`) .getByText(` |> xLine(${commonPoints.num2 * -1}, %)`)
.click() .click()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await constrainButton.click() await constrainButton.click()
await expect(absYButton).toBeDisabled() await expect(absXButton).toBeDisabled()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await xAxisClick() await yAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await constrainButton.click() await constrainButton.click()
await expect(absYButton).not.toBeDisabled() await expect(absXButton).not.toBeDisabled()
})
// clear selection by clicking on nothing // clear selection by clicking on nothing
await emptySpaceClick() await emptySpaceClick()
@ -182,9 +194,7 @@ test.describe('Testing selections', () => {
process.platform === 'linux' ? 'Control' : 'Meta' process.platform === 'linux' ? 'Control' : 'Meta'
) )
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
.click()
await expect(page.locator('.cm-cursor')).toHaveCount(2) await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(500) await page.waitForTimeout(500)
@ -928,6 +938,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
// test fillet button with the body in the scene // test fillet button with the body in the scene
const codeToAdd = `${await u.codeLocator.allInnerTexts()} const codeToAdd = `${await u.codeLocator.allInnerTexts()}
extrude001 = extrude(10, sketch001)` extrude001 = extrude(10, sketch001)`
await u.codeLocator.clear()
await u.codeLocator.fill(codeToAdd) await u.codeLocator.fill(codeToAdd)
await selectSegment() await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled() await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()

View File

@ -258,7 +258,7 @@ test.describe('Testing settings', () => {
}) })
}) })
test( test.fixme(
`Project settings override user settings on desktop`, `Project settings override user settings on desktop`,
{ tag: ['@electron', '@skipWin'] }, { tag: ['@electron', '@skipWin'] },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
@ -318,7 +318,6 @@ test.describe('Testing settings', () => {
timeout: 5_000, timeout: 5_000,
}) })
.toContain(`themeColor = "${userThemeColor}"`) .toContain(`themeColor = "${userThemeColor}"`)
// Only close the button after we've confirmed
}) })
await test.step('Set project theme color', async () => { await test.step('Set project theme color', async () => {
@ -744,18 +743,19 @@ extrude001 = extrude(5, sketch001)
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// Selectors and constants // Selectors and constants
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' }) const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
const lineToolButton = page.getByTestId('line') const lineToolButton = page.getByTestId('line')
const segmentOverlays = page.getByTestId('segment-overlay') const segmentOverlays = page.getByTestId('segment-overlay')
const sketchOriginLocation = { x: 600, y: 250 } const sketchOriginLocation = await u.getCenterOfModelViewArea()
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215] const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90] const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
await test.step(`Get into sketch mode`, async () => { await test.step(`Get into sketch mode`, async () => {
await u.waitForAuthSkipAppStart() await page.mouse.click(sketchOriginLocation.x, sketchOriginLocation.y)
await page.mouse.click(700, 200)
await expect(editSketchButton).toBeVisible() await expect(editSketchButton).toBeVisible()
await editSketchButton.click() await editSketchButton.click()
@ -766,12 +766,18 @@ extrude001 = extrude(5, sketch001)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
}) })
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
// Our lines are translucent (surprise!), so we need to get on portion
// of the line that is only on the background, and not on top of something
// like the axis lines.
line1.x -= 1
line1.y -= 1
await test.step(`Check the sketch line color before`, async () => { await test.step(`Check the sketch line color before`, async () => {
await expect await expect
.poll(() => .poll(() => u.getGreatestPixDiff(line1, darkThemeSegmentColor))
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor) .toBeLessThanOrEqual(34)
)
.toBeLessThan(15)
}) })
await test.step(`Change theme to light using command palette`, async () => { await test.step(`Change theme to light using command palette`, async () => {
@ -786,10 +792,8 @@ extrude001 = extrude(5, sketch001)
await test.step(`Check the sketch line color after`, async () => { await test.step(`Check the sketch line color after`, async () => {
await expect await expect
.poll(() => .poll(() => u.getGreatestPixDiff(line1, lightThemeSegmentColor))
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor) .toBeLessThanOrEqual(34)
)
.toBeLessThan(15)
}) })
}) })

View File

@ -503,14 +503,16 @@ test('Sketch on face', async ({ page }) => {
let previousCodeContent = await page.locator('.cm-content').innerText() let previousCodeContent = await page.locator('.cm-content').innerText()
await u.openAndClearDebugPanel() const center = await u.getCenterOfModelViewArea()
// This basically waits for sketch mode to be ready.
await u.doAndWaitForCmd( await u.doAndWaitForCmd(
() => page.mouse.click(625, 165), async () => page.mouse.click(center.x, 180),
'default_camera_get_settings', 'default_camera_get_settings',
true true
) )
await page.waitForTimeout(150)
await u.closeDebugPanel() await page.waitForTimeout(300)
const firstClickPosition = [612, 238] const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242] const secondClickPosition = [661, 242]

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.26.3", "version": "0.26.5",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {

View File

@ -1,15 +1,14 @@
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo, useRef } from 'react'
import { useHotKeyListener } from './hooks/useHotKeyListener' import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import { AppHeader } from './components/AppHeader' import { AppHeader } from './components/AppHeader'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useLoaderData, useNavigate } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions' import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
import { codeManager, engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager, sceneInfra } from 'lib/singletons'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
@ -22,6 +21,8 @@ import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu' import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle' import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import EngineStreamContext from 'hooks/useEngineStreamContext'
import { EngineStream } from 'components/EngineStream'
import { maybeWriteToDisk } from 'lib/telemetry' import { maybeWriteToDisk } from 'lib/telemetry'
maybeWriteToDisk() maybeWriteToDisk()
.then(() => {}) .then(() => {})
@ -37,6 +38,13 @@ export function App() {
// the coredump. // the coredump.
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
// Stream related refs and data
const videoRef = useRef<HTMLVideoElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const modelingSidebarRef = useRef<HTMLUListElement>(null)
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
const projectName = project?.name || null const projectName = project?.name || null
const projectPath = project?.path || null const projectPath = project?.path || null
useEffect(() => { useEffect(() => {
@ -57,6 +65,10 @@ export function App() {
app: { onboardingStatus }, app: { onboardingStatus },
} = settings.context } = settings.context
useEffect(() => {
sceneInfra.camControls.modelingSidebarRef = modelingSidebarRef
}, [modelingSidebarRef.current])
useHotkeys('backspace', (e) => { useHotkeys('backspace', (e) => {
e.preventDefault() e.preventDefault()
}) })
@ -84,14 +96,26 @@ export function App() {
enableMenu={true} enableMenu={true}
/> />
<ModalContainer /> <ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} /> <ModelingSidebar paneOpacity={paneOpacity} ref={modelingSidebarRef} />
<Stream /> <EngineStreamContext.Provider
options={{
input: {
videoRef,
canvasRef,
mediaStream: null,
authToken: auth?.context?.token ?? null,
pool,
},
}}
>
<EngineStream />
{/* <CamToggle /> */} {/* <CamToggle /> */}
<LowerRightControls coreDumpManager={coreDumpManager}> <LowerRightControls coreDumpManager={coreDumpManager}>
<UnitsMenu /> <UnitsMenu />
<Gizmo /> <Gizmo />
<CameraProjectionToggle /> <CameraProjectionToggle />
</LowerRightControls> </LowerRightControls>
</EngineStreamContext.Provider>
</div> </div>
) )
} }

View File

@ -22,6 +22,7 @@ import {
} from 'lib/toolbar' } from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { EngineConnectionStateType } from 'lang/std/engineConnection'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -48,7 +49,7 @@ export function Toolbar({
}, [engineCommandManager.artifactGraph, context.selectionRanges]) }, [engineCommandManager.artifactGraph, context.selectionRanges])
const toolbarButtonsRef = useRef<HTMLUListElement>(null) const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const { overallState } = useNetworkContext() const { overallState, immediateState } = useNetworkContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState() const { isStreamReady } = useAppState()
@ -56,6 +57,7 @@ export function Toolbar({
(overallState !== NetworkHealthState.Ok && (overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) || overallState !== NetworkHealthState.Weak) ||
isExecuting || isExecuting ||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished ||
!isStreamReady !isStreamReady
const currentMode = const currentMode =

View File

@ -1,3 +1,5 @@
import { Models } from '@kittycad/lib'
import { MutableRefObject } from 'react'
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls' import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
import { import {
Euler, Euler,
@ -87,6 +89,9 @@ class CameraRateLimiter {
export class CameraControls { export class CameraControls {
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
modelingSidebarRef: MutableRefObject<HTMLUListElement | null> = {
current: null,
}
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient' syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
camera: PerspectiveCamera | OrthographicCamera camera: PerspectiveCamera | OrthographicCamera
target: Vector3 target: Vector3
@ -95,6 +100,13 @@ export class CameraControls {
wasDragging: boolean wasDragging: boolean
mouseDownPosition: Vector2 mouseDownPosition: Vector2
mouseNewPosition: Vector2 mouseNewPosition: Vector2
cameraDragStartXY = new Vector2()
old:
| {
camera: PerspectiveCamera | OrthographicCamera
target: Vector3
}
| undefined
rotationSpeed = 0.3 rotationSpeed = 0.3
enableRotate = true enableRotate = true
enablePan = true enablePan = true
@ -461,6 +473,7 @@ export class CameraControls {
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
const interaction = this.getInteractionType(event) const interaction = this.getInteractionType(event)
if (interaction === 'none') return if (interaction === 'none') return
void this.engineCommandManager.sendSceneCommand({ void this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -909,18 +922,123 @@ export class CameraControls {
up: { x: 0, y: 0, z: 1 }, up: { x: 0, y: 0, z: 1 },
}, },
}) })
await this.engineCommandManager.sendSceneCommand({
await this.centerModelRelativeToPanes({
zoomToFit: true,
resetLastPaneWidth: true,
})
this.cameraDragStartXY = new Vector2()
this.cameraDragStartXY.x = 0
this.cameraDragStartXY.y = 0
}
async restoreCameraPosition(): Promise<void> {
if (!this.old) return
this.camera = this.old.camera.clone()
this.target = this.old.target.clone()
void this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'zoom_to_fit', type: 'default_camera_look_at',
object_ids: [], // leave empty to zoom to all objects ...convertThreeCamValuesToEngineCam({
padding: 0.2, // padding around the objects isPerspective: true,
animated: false, // don't animate the zoom for now position: this.camera.position,
quaternion: this.camera.quaternion,
zoom: this.camera.zoom,
target: this.target,
}),
}, },
}) })
} }
private lastFramePaneWidth: number = 0
async centerModelRelativeToPanes(args?: {
zoomObjectId?: string
zoomToFit?: boolean
resetLastPaneWidth?: boolean
}): Promise<void> {
const panes = this.modelingSidebarRef?.current
if (!panes) return
const panesWidth = panes.offsetWidth + panes.offsetLeft
if (args?.resetLastPaneWidth) {
this.lastFramePaneWidth = 0
}
const goPx =
(panesWidth - this.lastFramePaneWidth) / 2 / window.devicePixelRatio
this.lastFramePaneWidth = panesWidth
// Originally I had tried to use the default_camera_look_at endpoint and
// some quaternion math to move the camera right, but it ended up being
// overly complicated, and I think the threejs scene also doesn't have the
// camera coordinates after a zoom-to-fit... So this is much easier, and
// maps better to screen coordinates.
const requests: Models['ModelingCmdReq_type'][] = [
{
cmd: {
type: 'camera_drag_start',
interaction: 'pan',
window: { x: goPx < 0 ? -goPx : 0, y: 0 },
},
cmd_id: uuidv4(),
},
{
cmd: {
type: 'camera_drag_move',
interaction: 'pan',
window: {
x: goPx < 0 ? 0 : goPx,
y: 0,
},
},
cmd_id: uuidv4(),
},
]
if (args?.zoomToFit) {
requests.unshift({
cmd: {
type: 'zoom_to_fit',
object_ids: args?.zoomObjectId ? [args?.zoomObjectId] : [], // leave empty to zoom to all objects
padding: 0.2, // padding around the objects
},
cmd_id: uuidv4(),
})
}
await this.engineCommandManager
.sendSceneCommand({
type: 'modeling_cmd_batch_req',
batch_id: uuidv4(),
responses: true,
requests,
})
// engineCommandManager can't subscribe to batch responses so we'll send
// this one off by its lonesome after.
.then(() =>
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_end',
interaction: 'pan',
window: {
x: goPx < 0 ? 0 : goPx,
y: 0,
},
},
cmd_id: uuidv4(),
})
)
}
async tweenCameraToQuaternion( async tweenCameraToQuaternion(
targetQuaternion: Quaternion, targetQuaternion: Quaternion,
targetPosition = new Vector3(), targetPosition = new Vector3(),

View File

@ -1,4 +1,11 @@
import { useRef, useEffect, useState, useMemo, Fragment } from 'react' import {
CSSProperties,
useRef,
useEffect,
useState,
useMemo,
Fragment,
} from 'react'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
@ -202,12 +209,20 @@ const Overlay = ({
let xAlignment = overlay.angle < 0 ? '0%' : '-100%' let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%' let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
// It's possible for the pathToNode to request a newer AST node
// than what's available in the AST at the moment of query.
// It eventually settles on being updated.
const _node1 = getNodeFromPath<Node<CallExpression>>( const _node1 = getNodeFromPath<Node<CallExpression>>(
kclManager.ast, kclManager.ast,
overlay.pathToNode, overlay.pathToNode,
'CallExpression' 'CallExpression'
) )
if (err(_node1)) return
// For that reason, to prevent console noise, we do not use err here.
if (_node1 instanceof Error) {
console.warn('ast older than pathToNode, not fatal, eventually settles', '')
return
}
const callExpression = _node1.node const callExpression = _node1.node
const constraints = getConstraintInfo( const constraints = getConstraintInfo(
@ -234,6 +249,13 @@ const Overlay = ({
state.matches({ Sketch: 'Rectangle tool' }) state.matches({ Sketch: 'Rectangle tool' })
) )
// Line labels will cover the constraints overlay if this is not used.
// For each line label, ThreeJS increments each CSS2DObject z-index as they
// are added. I have looked into overriding renderOrder and depthTest and
// while renderOrder is set, ThreeJS still sets z-index on these 2D objects.
// It is easier to set this to a large number, such as a billion.
const zIndex = 1000000000
return ( return (
<div className={`absolute w-0 h-0`}> <div className={`absolute w-0 h-0`}>
<div <div
@ -244,6 +266,7 @@ const Overlay = ({
data-overlay-angle={overlay.angle} data-overlay-angle={overlay.angle}
className="pointer-events-auto absolute w-0 h-0" className="pointer-events-auto absolute w-0 h-0"
style={{ style={{
zIndex,
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`, transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
}} }}
></div> ></div>
@ -252,6 +275,7 @@ const Overlay = ({
data-overlay-toolbar-index={overlayIndex} data-overlay-toolbar-index={overlayIndex}
className={`px-0 pointer-events-auto absolute flex gap-1`} className={`px-0 pointer-events-auto absolute flex gap-1`}
style={{ style={{
zIndex,
transform: `translate3d(calc(${ transform: `translate3d(calc(${
overlay.windowCoords[0] + xOffset overlay.windowCoords[0] + xOffset
}px + ${xAlignment}), calc(${ }px + ${xAlignment}), calc(${
@ -293,6 +317,7 @@ const Overlay = ({
*/} */}
{callExpression?.callee?.name !== 'circle' && ( {callExpression?.callee?.name !== 'circle' && (
<SegmentMenu <SegmentMenu
style={{ zIndex }}
verticalPosition={ verticalPosition={
overlay.windowCoords[1] > window.innerHeight / 2 overlay.windowCoords[1] > window.innerHeight / 2
? 'top' ? 'top'
@ -434,15 +459,17 @@ const SegmentMenu = ({
verticalPosition, verticalPosition,
pathToNode, pathToNode,
stdLibFnName, stdLibFnName,
style,
}: { }: {
verticalPosition: 'top' | 'bottom' verticalPosition: 'top' | 'bottom'
pathToNode: PathToNode pathToNode: PathToNode
stdLibFnName: string stdLibFnName: string
style?: CSSProperties
}) => { }) => {
const { send } = useModelingContext() const { send } = useModelingContext()
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode) const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
return ( return (
<Popover className="relative"> <Popover style={style} className="relative">
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button <Popover.Button
@ -637,10 +664,16 @@ const ConstraintSymbol = ({
kclManager.ast, kclManager.ast,
kclManager.programMemory kclManager.programMemory
) )
if (!transform) return if (!transform) return
const { modifiedAst } = transform const { modifiedAst } = transform
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.updateAst(modifiedAst, true) await kclManager.updateAst(modifiedAst, true)
// Code editor will be updated in the modelingMachine.
const newCode = recast(modifiedAst)
if (err(newCode)) return
await codeManager.updateCodeEditor(newCode)
} catch (e) { } catch (e) {
console.log('error', e) console.log('error', e)
} }

View File

@ -0,0 +1,22 @@
// 63.5 is definitely a bit of a magic number, play with it until it looked right
// if it were 64, that would feel like it's something in the engine where a random
// power of 2 is used, but it's the 0.5 seems to make things look much more correct
export const ZOOM_MAGIC_NUMBER = 63.5
export const INTERSECTION_PLANE_LAYER = 1
export const SKETCH_LAYER = 2
export const RAYCASTABLE_PLANE = 'raycastable-plane'
// redundant types so that it can be changed temporarily but CI will catch the wrong type
export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
export const DEBUG_SHOW_BOTH_SCENES: false = false
export const X_AXIS = 'xAxis'
export const Y_AXIS = 'yAxis'
export const AXIS_GROUP = 'axisGroup'
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
export const ARROWHEAD = 'arrowhead'
export const SEGMENT_LENGTH_LABEL = 'segment-length-label'
export const SEGMENT_LENGTH_LABEL_TEXT = 'segment-length-label-text'
export const SEGMENT_LENGTH_LABEL_OFFSET_PX = 30

View File

@ -2,10 +2,7 @@ import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { import {
GridHelper, GridHelper,
LineBasicMaterial, LineBasicMaterial,
OrthographicCamera,
PerspectiveCamera, PerspectiveCamera,
Group,
Mesh,
Quaternion, Quaternion,
Vector3, Vector3,
} from 'three' } from 'three'
@ -28,15 +25,9 @@ export function createGridHelper({
gridHelper.rotation.x = Math.PI / 2 gridHelper.rotation.x = Math.PI / 2
return gridHelper return gridHelper
} }
const fudgeFactor = 72.66985970437086
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) => // Re-export scale.ts
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight export * from './scale'
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
4000 /
window.innerHeight
export function isQuaternionVertical(q: Quaternion) { export function isQuaternionVertical(q: Quaternion) {
const v = new Vector3(0, 0, 1).applyQuaternion(q) const v = new Vector3(0, 0, 1).applyQuaternion(q)

View File

@ -0,0 +1,17 @@
import { OrthographicCamera, PerspectiveCamera, Group, Mesh } from 'three'
export const fudgeFactor = 72.66985970437086
export const orthoScale = (
cam: OrthographicCamera | PerspectiveCamera,
innerHeight?: number
) => (0.55 * fudgeFactor) / cam.zoom / (innerHeight ?? window.innerHeight)
export const perspScale = (
cam: PerspectiveCamera,
group: Group | Mesh,
innerHeight?: number
) =>
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
4000 /
(innerHeight ?? window.innerHeight)

View File

@ -17,6 +17,7 @@ import {
Vector3, Vector3,
} from 'three' } from 'three'
import { import {
ANGLE_SNAP_THRESHOLD_DEGREES,
ARROWHEAD, ARROWHEAD,
AXIS_GROUP, AXIS_GROUP,
DRAFT_POINT, DRAFT_POINT,
@ -95,6 +96,7 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d' import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
import { SegmentInputs } from 'lang/std/stdTypes' import { SegmentInputs } from 'lang/std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { radToDeg } from 'three/src/math/MathUtils'
type DraftSegment = 'line' | 'tangentialArcTo' type DraftSegment = 'line' | 'tangentialArcTo'
@ -451,6 +453,7 @@ export class SceneEntities {
const { modifiedAst } = addStartProfileAtRes const { modifiedAst } = addStartProfileAtRes
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
this.removeIntersectionPlane() this.removeIntersectionPlane()
this.scene.remove(draftPointGroup) this.scene.remove(draftPointGroup)
@ -683,7 +686,7 @@ export class SceneEntities {
}) })
return nextAst return nextAst
} }
setUpDraftSegment = async ( setupDraftSegment = async (
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
forward: [number, number, number], forward: [number, number, number],
up: [number, number, number], up: [number, number, number],
@ -798,11 +801,24 @@ export class SceneEntities {
(sceneObject) => sceneObject.object.name === X_AXIS (sceneObject) => sceneObject.object.name === X_AXIS
) )
const lastSegment = sketch.paths.slice(-1)[0] const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
const snappedPoint = { const snappedPoint = {
x: intersectsYAxis ? 0 : intersection2d.x, x: intersectsYAxis ? 0 : intersection2d.x,
y: intersectsXAxis ? 0 : intersection2d.y, y: intersectsXAxis ? 0 : intersection2d.y,
} }
// Get the angle between the previous segment (or sketch start)'s end and this one's
const angle = Math.atan2(
snappedPoint.y - lastSegment.to[1],
snappedPoint.x - lastSegment.to[0]
)
const isHorizontal =
radToDeg(Math.abs(angle)) < ANGLE_SNAP_THRESHOLD_DEGREES ||
Math.abs(radToDeg(Math.abs(angle) - Math.PI)) <
ANGLE_SNAP_THRESHOLD_DEGREES
const isVertical =
Math.abs(radToDeg(Math.abs(angle) - Math.PI / 2)) <
ANGLE_SNAP_THRESHOLD_DEGREES
let resolvedFunctionName: ToolTip = 'line' let resolvedFunctionName: ToolTip = 'line'
@ -810,6 +826,12 @@ export class SceneEntities {
// case-based logic for different segment types // case-based logic for different segment types
if (lastSegment.type === 'TangentialArcTo') { if (lastSegment.type === 'TangentialArcTo') {
resolvedFunctionName = 'tangentialArcTo' resolvedFunctionName = 'tangentialArcTo'
} else if (isHorizontal) {
// If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine
resolvedFunctionName = 'xLine'
} else if (isVertical) {
// If the angle between is 90 or 270 degrees (+/- the snapping angle), make the line a yLine
resolvedFunctionName = 'yLine'
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) { } else if (snappedPoint.x === 0 || snappedPoint.y === 0) {
// We consider a point placed on axes or origin to be absolute // We consider a point placed on axes or origin to be absolute
resolvedFunctionName = 'lineTo' resolvedFunctionName = 'lineTo'
@ -835,10 +857,11 @@ export class SceneEntities {
} }
await kclManager.executeAstMock(modifiedAst) await kclManager.executeAstMock(modifiedAst)
if (intersectsProfileStart) { if (intersectsProfileStart) {
sceneInfra.modelingSend({ type: 'CancelSketch' }) sceneInfra.modelingSend({ type: 'CancelSketch' })
} else { } else {
await this.setUpDraftSegment( await this.setupDraftSegment(
sketchPathToNode, sketchPathToNode,
forward, forward,
up, up,
@ -846,6 +869,8 @@ export class SceneEntities {
segmentName segmentName
) )
} }
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
}, },
onMove: (args) => { onMove: (args) => {
this.onDragSegment({ this.onDragSegment({
@ -970,10 +995,14 @@ export class SceneEntities {
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declarations?.[0]?.init
if (sketchInit.type === 'PipeExpression') { if (sketchInit.type !== 'PipeExpression') {
return
}
updateRectangleSketch(sketchInit, x, y, tags[0]) updateRectangleSketch(sketchInit, x, y, tags[0])
let _recastAst = parse(recast(_ast)) const newCode = recast(_ast)
let _recastAst = parse(newCode)
if (trap(_recastAst)) return if (trap(_recastAst)) return
_ast = _recastAst _ast = _recastAst
@ -981,6 +1010,11 @@ export class SceneEntities {
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish rectangle' }) sceneInfra.modelingSend({ type: 'Finish rectangle' })
// lee: I had this at the bottom of the function, but it's
// possible sketchFromKclValue "fails" when sketching on a face,
// and this couldn't wouldn't run.
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: _ast, ast: _ast,
useFakeExecutor: true, useFakeExecutor: true,
@ -1006,7 +1040,6 @@ export class SceneEntities {
sgPaths.forEach((seg, index) => sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch) this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
) )
}
}, },
}) })
} }
@ -1166,13 +1199,17 @@ export class SceneEntities {
if (err(moddedResult)) return if (err(moddedResult)) return
modded = moddedResult.modifiedAst modded = moddedResult.modifiedAst
let _recastAst = parse(recast(modded)) const newCode = recast(modded)
if (err(newCode)) return
let _recastAst = parse(newCode)
if (trap(_recastAst)) return Promise.reject(_recastAst) if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst _ast = _recastAst
// Update the primary AST and unequip the rectangle tool // Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish circle' }) sceneInfra.modelingSend({ type: 'Finish circle' })
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
} }
}, },
}) })
@ -1208,6 +1245,7 @@ export class SceneEntities {
forward, forward,
position, position,
}) })
await codeManager.writeToFile()
} }
}, },
onDrag: async ({ onDrag: async ({

View File

@ -50,6 +50,8 @@ export const RAYCASTABLE_PLANE = 'raycastable-plane'
export const X_AXIS = 'xAxis' export const X_AXIS = 'xAxis'
export const Y_AXIS = 'yAxis' export const Y_AXIS = 'yAxis'
/** If a segment angle is less than this many degrees off a meanginful angle it'll snap to it */
export const ANGLE_SNAP_THRESHOLD_DEGREES = 3
/** the THREEjs representation of the group surrounding a "snapped" point that is not yet placed */ /** the THREEjs representation of the group surrounding a "snapped" point that is not yet placed */
export const DRAFT_POINT_GROUP = 'draft-point-group' export const DRAFT_POINT_GROUP = 'draft-point-group'
/** the THREEjs representation of a "snapped" point that is not yet placed */ /** the THREEjs representation of a "snapped" point that is not yet placed */
@ -289,14 +291,14 @@ export class SceneInfra {
engineCommandManager engineCommandManager
) )
this.camControls.subscribeToCamChange(() => this.onCameraChange()) this.camControls.subscribeToCamChange(() => this.onCameraChange())
this.camControls.camera.layers.enable(SKETCH_LAYER) this.camControls.camera.layers.enable(constants.SKETCH_LAYER)
if (DEBUG_SHOW_INTERSECTION_PLANE) if (constants.DEBUG_SHOW_INTERSECTION_PLANE)
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER) this.camControls.camera.layers.enable(constants.INTERSECTION_PLANE_LAYER)
// RAYCASTERS // RAYCASTERS
this.raycaster.layers.enable(SKETCH_LAYER) this.raycaster.layers.enable(constants.SKETCH_LAYER)
this.raycaster.layers.disable(0) this.raycaster.layers.disable(0)
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER) this.planeRaycaster.layers.enable(constants.INTERSECTION_PLANE_LAYER)
// GRID // GRID
const size = 100 const size = 100
@ -331,7 +333,7 @@ export class SceneInfra {
this.camControls.target this.camControls.target
) )
const axisGroup = this.scene const axisGroup = this.scene
.getObjectByName(AXIS_GROUP) .getObjectByName(constants.AXIS_GROUP)
?.getObjectByName('gridHelper') ?.getObjectByName('gridHelper')
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale) axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
} }
@ -342,7 +344,6 @@ export class SceneInfra {
} }
animate = () => { animate = () => {
requestAnimationFrame(this.animate)
TWEEN.update() // This will update all tweens during the animation loop TWEEN.update() // This will update all tweens during the animation loop
if (!this.isFovAnimationInProgress) { if (!this.isFovAnimationInProgress) {
// console.log('animation frame', this.cameraControls.camera) // console.log('animation frame', this.cameraControls.camera)
@ -350,6 +351,7 @@ export class SceneInfra {
this.renderer.render(this.scene, this.camControls.camera) this.renderer.render(this.scene, this.camControls.camera)
this.labelRenderer.render(this.scene, this.camControls.camera) this.labelRenderer.render(this.scene, this.camControls.camera)
} }
requestAnimationFrame(this.animate)
} }
dispose = () => { dispose = () => {
@ -653,11 +655,11 @@ export class SceneInfra {
} }
updateOtherSelectionColors = (otherSelections: Axis[]) => { updateOtherSelectionColors = (otherSelections: Axis[]) => {
const axisGroup = this.scene.children.find( const axisGroup = this.scene.children.find(
({ userData }) => userData?.type === AXIS_GROUP ({ userData }) => userData?.type === constants.AXIS_GROUP
) )
const axisMap: { [key: string]: Axis } = { const axisMap: { [key: string]: Axis } = {
[X_AXIS]: 'x-axis', [constants.X_AXIS]: 'x-axis',
[Y_AXIS]: 'y-axis', [constants.Y_AXIS]: 'y-axis',
} }
axisGroup?.children.forEach((_mesh) => { axisGroup?.children.forEach((_mesh) => {
const mesh = _mesh as Mesh const mesh = _mesh as Mesh

View File

@ -300,7 +300,7 @@ class StraightSegment implements SegmentUtils {
sceneInfra.updateOverlayDetails({ sceneInfra.updateOverlayDetails({
arrowGroup, arrowGroup,
group, group,
isHandlesVisible, isHandlesVisible: true,
from, from,
to, to,
}) })
@ -476,7 +476,7 @@ class TangentialArcToSegment implements SegmentUtils {
sceneInfra.updateOverlayDetails({ sceneInfra.updateOverlayDetails({
arrowGroup, arrowGroup,
group, group,
isHandlesVisible, isHandlesVisible: true,
from, from,
to, to,
angle, angle,
@ -542,7 +542,7 @@ class CircleSegment implements SegmentUtils {
} }
group.name = CIRCLE_SEGMENT group.name = CIRCLE_SEGMENT
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup) group.add(arcMesh, arrowGroup, circleCenterGroup)
const updateOverlaysCallback = this.update({ const updateOverlaysCallback = this.update({
prevSegment, prevSegment,
input, input,
@ -677,7 +677,7 @@ class CircleSegment implements SegmentUtils {
sceneInfra.updateOverlayDetails({ sceneInfra.updateOverlayDetails({
arrowGroup, arrowGroup,
group, group,
isHandlesVisible, isHandlesVisible: true,
from: from, from: from,
to: [center[0], center[1]], to: [center[0], center[1]],
angle: Math.PI / 4, angle: Math.PI / 4,

View File

@ -145,7 +145,7 @@ export function useCalc({
const _programMem: ProgramMemory = ProgramMemory.empty() const _programMem: ProgramMemory = ProgramMemory.empty()
for (const { key, value } of availableVarInfo.variables) { for (const { key, value } of availableVarInfo.variables) {
const error = _programMem.set(key, { const error = _programMem.set(key, {
type: 'UserVal', type: 'String',
value, value,
__meta: [], __meta: [],
}) })

View File

@ -1,6 +1,8 @@
import { Dialog, Popover, Transition } from '@headlessui/react' import { Dialog, Popover, Transition } from '@headlessui/react'
import { Fragment, useEffect } from 'react' import { Fragment, useEffect } from 'react'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { EngineConnectionStateType } from 'lang/std/engineConnection'
import CommandBarArgument from './CommandBarArgument' import CommandBarArgument from './CommandBarArgument'
import CommandComboBox from '../CommandComboBox' import CommandComboBox from '../CommandComboBox'
import CommandBarReview from './CommandBarReview' import CommandBarReview from './CommandBarReview'
@ -14,6 +16,7 @@ export const COMMAND_PALETTE_HOTKEY = 'mod+k'
export const CommandBar = () => { export const CommandBar = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const { immediateState } = useNetworkContext()
const { const {
context: { selectedCommand, currentArgument, commands }, context: { selectedCommand, currentArgument, commands },
} = commandBarState } = commandBarState
@ -25,6 +28,14 @@ export const CommandBar = () => {
commandBarSend({ type: 'Close' }) commandBarSend({ type: 'Close' })
}, [pathname]) }, [pathname])
useEffect(() => {
if (
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
) {
commandBarSend({ type: 'Close' })
}
}, [immediateState])
// Hook up keyboard shortcuts // Hook up keyboard shortcuts
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => { useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
if (commandBarState.context.commands.length === 0) return if (commandBarState.context.commands.length === 0) return

View File

@ -2,13 +2,20 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { hotkeyDisplay } from 'lib/hotkeyWrapper' import { hotkeyDisplay } from 'lib/hotkeyWrapper'
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar' import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { EngineConnectionStateType } from 'lang/std/engineConnection'
export function CommandBarOpenButton() { export function CommandBarOpenButton() {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { immediateState } = useNetworkContext()
const platform = usePlatform() const platform = usePlatform()
const isDisabled =
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
return ( return (
<button <button
disabled={isDisabled}
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit" className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
onClick={() => commandBarSend({ type: 'Open' })} onClick={() => commandBarSend({ type: 'Open' })}
data-testid="command-bar-open-button" data-testid="command-bar-open-button"

View File

@ -0,0 +1,293 @@
import { MouseEventHandler, useEffect, useRef } from 'react'
import { useAppState } from 'AppState'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useModelingContext } from 'hooks/useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { btnName } from 'lib/cameraControls'
import { trap } from 'lib/trap'
import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager } from 'lib/singletons'
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types'
import useEngineStreamContext, {
EngineStreamState,
EngineStreamTransition,
} from 'hooks/useEngineStreamContext'
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
export const EngineStream = () => {
const { setAppState } = useAppState()
const { overallState } = useNetworkContext()
const { settings } = useSettingsAuthContext()
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const last = useRef<number>(Date.now())
const settingsEngine = {
theme: settings.context.app.theme.current,
enableSSAO: settings.context.app.enableSSAO.current,
highlightEdges: settings.context.modeling.highlightEdges.current,
showScaleGrid: settings.context.modeling.showScaleGrid.current,
cameraProjection: settings.context.modeling.cameraProjection.current,
}
const { state: modelingMachineState, send: modelingMachineActorSend } =
useModelingContext()
const engineStreamActor = useEngineStreamContext.useActorRef()
const engineStreamState = engineStreamActor.getSnapshot()
const streamIdleMode = settings.context.app.streamIdleMode.current
const configure = () => {
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
// It's possible a reconnect happens as we drag the window :')
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream,
})
},
})
}
useEffect(() => {
const play = () => {
engineStreamActor.send({
type: EngineStreamTransition.Play,
})
}
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
play
)
return () => {
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.SceneReady,
play
)
}
}, [])
useEffect(() => {
const video = engineStreamState.context.videoRef?.current
if (!video) return
const canvas = engineStreamState.context.canvasRef?.current
if (!canvas) return
new ResizeObserver(() => {
if (Date.now() - last.current < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE)
return
last.current = Date.now()
if (
Math.abs(video.width - window.innerWidth) > 4 ||
Math.abs(video.height - window.innerHeight) > 4
) {
timeoutStart.current = Date.now()
configure()
}
}).observe(document.body)
}, [engineStreamState.value])
// When the video and canvas element references are set, start the engine.
useEffect(() => {
if (
engineStreamState.context.canvasRef.current &&
engineStreamState.context.videoRef.current
) {
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream,
})
},
})
}
}, [
engineStreamState.context.canvasRef.current,
engineStreamState.context.videoRef.current,
])
// On settings change, reconfigure the engine. When paused this gets really tricky,
// and also requires onMediaStream to be set!
useEffect(() => {
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream,
})
},
})
}, [settings.context])
/**
* Subscribe to execute code when the file changes
* but only if the scene is already ready.
* See onSceneReady for the initial scene setup.
*/
useEffect(() => {
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
console.log('execute on file change')
void kclManager.executeCode(true).catch(trap)
}
}, [file?.path, engineCommandManager.engineConnection])
const IDLE_TIME_MS = Number(streamIdleMode)
// When streamIdleMode is changed, setup or teardown the timeouts
const timeoutStart = useRef<number | null>(null)
useEffect(() => {
timeoutStart.current = streamIdleMode ? Date.now() : null
}, [streamIdleMode])
useEffect(() => {
let frameId: ReturnType<typeof window.requestAnimationFrame> = 0
const frameLoop = () => {
// Do not pause if the user is in the middle of an operation
if (!modelingMachineState.matches('idle')) {
// In fact, stop the timeout, because we don't want to trigger the
// pause when we exit the operation.
timeoutStart.current = null
} else if (timeoutStart.current) {
const elapsed = Date.now() - timeoutStart.current
if (elapsed >= IDLE_TIME_MS) {
timeoutStart.current = null
engineStreamActor.send({ type: EngineStreamTransition.Pause })
}
}
frameId = window.requestAnimationFrame(frameLoop)
}
frameId = window.requestAnimationFrame(frameLoop)
return () => {
window.cancelAnimationFrame(frameId)
}
}, [modelingMachineState])
useEffect(() => {
if (!streamIdleMode) return
const onAnyInput = () => {
// Just in case it happens in the middle of the user turning off
// idle mode.
if (!streamIdleMode) {
timeoutStart.current = null
return
}
if (engineStreamState.value === EngineStreamState.Paused) {
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream,
})
},
})
}
timeoutStart.current = Date.now()
}
// It's possible after a reconnect, the user doesn't move their mouse at
// all, meaning the timer is not reset to run. We need to set it every
// time our effect dependencies change then.
timeoutStart.current = Date.now()
window.document.addEventListener('keydown', onAnyInput)
window.document.addEventListener('keyup', onAnyInput)
window.document.addEventListener('mousemove', onAnyInput)
window.document.addEventListener('mousedown', onAnyInput)
window.document.addEventListener('mouseup', onAnyInput)
window.document.addEventListener('scroll', onAnyInput)
window.document.addEventListener('touchstart', onAnyInput)
window.document.addEventListener('touchstop', onAnyInput)
return () => {
timeoutStart.current = null
window.document.removeEventListener('keydown', onAnyInput)
window.document.removeEventListener('keyup', onAnyInput)
window.document.removeEventListener('mousemove', onAnyInput)
window.document.removeEventListener('mousedown', onAnyInput)
window.document.removeEventListener('mouseup', onAnyInput)
window.document.removeEventListener('scroll', onAnyInput)
window.document.removeEventListener('touchstart', onAnyInput)
window.document.removeEventListener('touchstop', onAnyInput)
}
}, [streamIdleMode, engineStreamState.value])
const isNetworkOkay =
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return
if (!engineStreamState.context.videoRef.current) return
if (modelingMachineState.matches('Sketch')) return
if (modelingMachineState.matches({ idle: 'showPlanes' })) return
if (btnName(e.nativeEvent).left) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendSelectEventToEngine(e, engineStreamState.context.videoRef.current)
}
}
return (
<div
className="absolute inset-0 z-0"
id="stream"
data-testid="stream"
onMouseUp={handleMouseUp}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
>
<video
autoPlay
muted
key={engineStreamActor.id + 'video'}
ref={engineStreamState.context.videoRef}
controls={false}
className="cursor-pointer"
disablePictureInPicture
id="video-stream"
/>
<canvas
key={engineStreamActor.id + 'canvas'}
ref={engineStreamState.context.canvasRef}
className="cursor-pointer"
id="freeze-frame"
>
No canvas support
</canvas>
<ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current}
/>
</div>
)
}

View File

@ -22,6 +22,7 @@ import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project' import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor' import { normalizeLineEndings } from 'lib/codeEditor'
import { reportRejection } from 'lib/trap'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})` return `calc(1rem * ${level + 1})`
@ -196,8 +197,7 @@ const FileTreeItem = ({
return return
} }
// Don't try to read a file that was removed. if (isCurrentFile && eventType === 'change') {
if (isCurrentFile && eventType !== 'unlink') {
let code = await window.electron.readFile(path, { encoding: 'utf-8' }) let code = await window.electron.readFile(path, { encoding: 'utf-8' })
code = normalizeLineEndings(code) code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
@ -242,7 +242,7 @@ const FileTreeItem = ({
// Show the renaming form // Show the renaming form
addCurrentItemToRenaming() addCurrentItemToRenaming()
} else if (e.code === 'Space') { } else if (e.code === 'Space') {
void handleClick() void handleClick().catch(reportRejection)
} }
} }
@ -293,7 +293,7 @@ const FileTreeItem = ({
style={{ paddingInlineStart: getIndentationCSS(level) }} style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => { onClick={(e) => {
e.currentTarget.focus() e.currentTarget.focus()
void handleClick() void handleClick().catch(reportRejection)
}} }}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
> >

View File

@ -1,40 +1,47 @@
import { useEffect, useState } from 'react'
import { useEngineCommands } from './EngineCommands' import { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import useEngineStreamContext, {
EngineStreamState,
} from 'hooks/useEngineStreamContext'
import { CommandLogType } from 'lang/std/engineConnection'
export const ModelStateIndicator = () => { export const ModelStateIndicator = () => {
const [commands] = useEngineCommands() const [commands] = useEngineCommands()
const [isDone, setIsDone] = useState<boolean>(false)
const engineStreamActor = useEngineStreamContext.useActorRef()
const engineStreamState = engineStreamActor.getSnapshot()
const lastCommandType = commands[commands.length - 1]?.type const lastCommandType = commands[commands.length - 1]?.type
useEffect(() => {
if (lastCommandType === CommandLogType.SetDefaultSystemProperties) {
setIsDone(false)
}
if (lastCommandType === CommandLogType.ExecutionDone) {
setIsDone(true)
}
}, [lastCommandType])
let className = 'w-6 h-6 ' let className = 'w-6 h-6 '
let icon = <Spinner className={className} /> let icon = <div className={className}></div>
let dataTestId = 'model-state-indicator' let dataTestId = 'model-state-indicator'
if (lastCommandType === 'receive-reliable') { if (engineStreamState.value === EngineStreamState.Paused) {
className += className += 'text-secondary'
'bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed' icon = <CustomIcon data-testid={dataTestId + '-paused'} name="parallel" />
icon = ( } else if (engineStreamState.value === EngineStreamState.Resuming) {
<CustomIcon className += 'text-secondary'
data-testid={dataTestId + '-receive-reliable'} icon = <CustomIcon data-testid={dataTestId + '-resuming'} name="parallel" />
name="checkmark" } else if (isDone) {
/> className += 'text-secondary'
)
} else if (lastCommandType === 'execution-done') {
className +=
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
icon = ( icon = (
<CustomIcon <CustomIcon
data-testid={dataTestId + '-execution-done'} data-testid={dataTestId + '-execution-done'}
name="checkmark" name="checkmark"
/> />
) )
} else if (lastCommandType === 'export-done') {
className +=
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
icon = (
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
)
} }
return ( return (

View File

@ -20,7 +20,6 @@ import {
modelingMachine, modelingMachine,
modelingMachineDefaultContext, modelingMachineDefaultContext,
} from 'machines/modelingMachine' } from 'machines/modelingMachine'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { import {
isCursorInSketchCommandRange, isCursorInSketchCommandRange,
@ -112,13 +111,8 @@ export const ModelingMachineProvider = ({
auth, auth,
settings: { settings: {
context: { context: {
app: { theme, enableSSAO }, app: { theme },
modeling: { modeling: { defaultUnit, highlightEdges, cameraProjection },
defaultUnit,
cameraProjection,
highlightEdges,
showScaleGrid,
},
}, },
}, },
} = useSettingsAuthContext() } = useSettingsAuthContext()
@ -129,9 +123,6 @@ export const ModelingMachineProvider = ({
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), []) const persistedContext = useMemo(() => getPersistedContext(), [])
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
// Settings machine setup // Settings machine setup
@ -304,6 +295,7 @@ export const ModelingMachineProvider = ({
const dispatchSelection = (selection?: EditorSelection) => { const dispatchSelection = (selection?: EditorSelection) => {
if (!selection) return // TODO less of hack for the below please if (!selection) return // TODO less of hack for the below please
if (!editorManager.editorView) return if (!editorManager.editorView) return
setTimeout(() => { setTimeout(() => {
if (!editorManager.editorView) return if (!editorManager.editorView) return
editorManager.editorView.dispatch({ editorManager.editorView.dispatch({
@ -657,6 +649,9 @@ export const ModelingMachineProvider = ({
engineCommandManager, engineCommandManager,
input.faceId input.faceId
) )
await sceneInfra.camControls.centerModelRelativeToPanes({
resetLastPaneWidth: true,
})
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
return { return {
sketchPathToNode: pathToNewSketchNode, sketchPathToNode: pathToNewSketchNode,
@ -677,6 +672,9 @@ export const ModelingMachineProvider = ({
engineCommandManager, engineCommandManager,
input.planeId input.planeId
) )
await sceneInfra.camControls.centerModelRelativeToPanes({
resetLastPaneWidth: true,
})
return { return {
sketchPathToNode: pathToNode, sketchPathToNode: pathToNode,
@ -699,6 +697,9 @@ export const ModelingMachineProvider = ({
engineCommandManager, engineCommandManager,
info?.sketchDetails?.faceId || '' info?.sketchDetails?.faceId || ''
) )
await sceneInfra.camControls.centerModelRelativeToPanes({
resetLastPaneWidth: true,
})
return { return {
sketchPathToNode: sketchPathToNode || [], sketchPathToNode: sketchPathToNode || [],
zAxis: info.sketchDetails.zAxis || null, zAxis: info.sketchDetails.zAxis || null,
@ -732,6 +733,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -768,6 +774,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -813,6 +824,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -846,6 +862,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -881,6 +902,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -917,6 +943,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -953,6 +984,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -999,6 +1035,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
{ 0: pathToReplacedNode }, { 0: pathToReplacedNode },
selectionRanges, selectionRanges,
@ -1027,21 +1068,6 @@ export const ModelingMachineProvider = ({
} }
) )
useSetupEngineManager(
streamRef,
modelingSend,
modelingState.context,
{
pool: pool,
theme: theme.current,
highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current,
showScaleGrid: showScaleGrid.current,
cameraProjection: cameraProjection.current,
},
token
)
useEffect(() => { useEffect(() => {
kclManager.registerExecuteCallback(() => { kclManager.registerExecuteCallback(() => {
modelingSend({ type: 'Re-execute' }) modelingSend({ type: 'Re-execute' })

View File

@ -89,9 +89,9 @@ export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {} const processedMemory: any = {}
for (const [key, val] of programMemory?.visibleEntries()) { for (const [key, val] of programMemory?.visibleEntries()) {
if ( if (
(val.type === 'UserVal' && val.value.type === 'Sketch') || val.type === 'Sketch' ||
// @ts-ignore // @ts-ignore
(val.type !== 'Function' && val.type !== 'UserVal') val.type !== 'Function'
) { ) {
const sg = sketchFromKclValue(val, key) const sg = sketchFromKclValue(val, key)
if (val.type === 'Solid') { if (val.type === 'Solid') {
@ -110,8 +110,6 @@ export const processMemory = (programMemory: ProgramMemory) => {
processedMemory[key] = `__function(${(val as any)?.expression?.params processedMemory[key] = `__function(${(val as any)?.expression?.params
?.map?.(({ identifier }: any) => identifier?.name || '') ?.map?.(({ identifier }: any) => identifier?.name || '')
.join(', ')})__` .join(', ')})__`
} else {
processedMemory[key] = val.value
} }
} }
return processedMemory return processedMemory

View File

@ -6,6 +6,11 @@ import {
useEffect, useEffect,
useMemo, useMemo,
useContext, useContext,
MutableRefObject,
forwardRef,
// https://stackoverflow.com/a/77055468 Thank you.
useImperativeHandle,
useRef,
} from 'react' } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes' import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
@ -19,9 +24,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider' import { MachineManagerContext } from 'components/MachineManagerProvider'
import { sceneInfra } from 'lib/singletons'
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
ref: MutableRefObject<HTMLDivElement>
} }
interface BadgeInfoComputed { interface BadgeInfoComputed {
@ -33,19 +41,34 @@ function getPlatformString(): 'web' | 'desktop' {
return isDesktop() ? 'desktop' : 'web' return isDesktop() ? 'desktop' : 'web'
} }
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { export const ModelingSidebar = forwardRef<
HTMLUListElement,
ModelingSidebarProps
>(function ModelingSidebar({ paneOpacity }, outerRef) {
const machineManager = useContext(MachineManagerContext) const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext() const kclContext = useKclContext()
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus const onboardingStatus = settings.context.app.onboardingStatus
const { send, context } = useModelingContext() const { send, state, context } = useModelingContext()
const pointerEventsCssClass = const pointerEventsCssClass =
onboardingStatus.current === 'camera' || onboardingStatus.current === 'camera' ||
context.store?.openPanes.length === 0 context.store?.openPanes.length === 0
? 'pointer-events-none ' ? 'pointer-events-none '
: 'pointer-events-auto ' : 'pointer-events-auto '
const showDebugPanel = settings.context.modeling.showDebugPanel const showDebugPanel = settings.context.modeling.showDebugPanel
const innerRef = useRef<HTMLUListElement>(null)
// forwardRef's type causes me to do this type narrowing.
useEffect(() => {
if (typeof outerRef === 'function') {
outerRef(innerRef.current)
} else {
if (outerRef) {
outerRef.current = innerRef.current
}
}
}, [innerRef.current])
const paneCallbackProps = useMemo( const paneCallbackProps = useMemo(
() => ({ () => ({
@ -159,8 +182,37 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
[context.store?.openPanes, send] [context.store?.openPanes, send]
) )
useEffect(() => {
// Don't send camera adjustment commands after 1 pane is open. It
// won't make any difference.
if (context.store?.openPanes.length > 1) return
void sceneInfra.camControls.centerModelRelativeToPanes()
}, [context.store?.openPanes])
// If the panes are resized then center the model also
useEffect(() => {
if (!innerRef.current) return
let last = Date.now()
const observer = new ResizeObserver(() => {
if (Date.now() - last < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE) return
if (!innerRef.current) return
last = Date.now()
void sceneInfra.camControls.centerModelRelativeToPanes()
})
observer.observe(innerRef.current)
return () => {
observer.disconnect()
}
}, [state, innerRef.current])
return ( return (
<Resizable <Resizable
data-testid="modeling-sidebar"
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`} className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
defaultSize={{ defaultSize={{
width: '550px', width: '550px',
@ -192,6 +244,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
> >
<ul <ul
id="pane-buttons-section" id="pane-buttons-section"
data-testid="pane-buttons-section"
className={ className={
'w-fit p-2 flex flex-col gap-2 ' + 'w-fit p-2 flex flex-col gap-2 ' +
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '') (context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
@ -236,6 +289,8 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
</ul> </ul>
<ul <ul
id="pane-section" id="pane-section"
data-testid="pane-section"
ref={innerRef}
className={ className={
'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' + 'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' +
(context.store?.openPanes.length >= 1 ? `w-full` : `hidden`) (context.store?.openPanes.length >= 1 ? `w-full` : `hidden`)
@ -265,7 +320,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
</div> </div>
</Resizable> </Resizable>
) )
} })
interface ModelingPaneButtonProps interface ModelingPaneButtonProps
extends React.HTMLAttributes<HTMLButtonElement> { extends React.HTMLAttributes<HTMLButtonElement> {

View File

@ -1,340 +0,0 @@
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
import Loading from './Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useModelingContext } from 'hooks/useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { btnName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
import { useAppStream } from 'AppState'
import {
EngineCommandManagerEvents,
EngineConnectionStateType,
DisconnectingType,
} from 'lang/std/engineConnection'
import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types'
enum StreamState {
Playing = 'playing',
Paused = 'paused',
Resuming = 'resuming',
Unset = 'unset',
}
export const Stream = () => {
const [isLoading, setIsLoading] = useState(true)
const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext()
const { state, send } = useModelingContext()
const { mediaStream } = useAppStream()
const { overallState, immediateState } = useNetworkContext()
const [streamState, setStreamState] = useState(StreamState.Unset)
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const IDLE = settings.context.app.streamIdleMode.current
const isNetworkOkay =
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
/**
* Execute code and show a "building scene message"
* in Stream.tsx in the meantime.
*
* I would like for this to live somewhere more central,
* but it seems to me that we need the video element ref
* to be able to play the video after the code has been
* executed. If we can find a way to do this from a more
* central place, we can move this code there.
*/
function executeCodeAndPlayStream() {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode(true).then(async () => {
await videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
setStreamState(StreamState.Playing)
})
}
/**
* Subscribe to execute code when the file changes
* but only if the scene is already ready.
* See onSceneReady for the initial scene setup.
*/
useEffect(() => {
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
console.log('execute on file change')
executeCodeAndPlayStream()
}
}, [file?.path, engineCommandManager.engineConnection])
useEffect(() => {
if (
immediateState.type === EngineConnectionStateType.Disconnecting &&
immediateState.value.type === DisconnectingType.Pause
) {
setStreamState(StreamState.Paused)
}
}, [immediateState])
// Linux has a default behavior to paste text on middle mouse up
// This adds a listener to block that pasting if the click target
// is not a text input, so users can move in the 3D scene with
// middle mouse drag with a text input focused without pasting.
useEffect(() => {
const handlePaste = (e: ClipboardEvent) => {
const isHtmlElement = e.target && e.target instanceof HTMLElement
const isEditable =
(isHtmlElement && !('explicitOriginalTarget' in e)) ||
('explicitOriginalTarget' in e &&
((e.explicitOriginalTarget as HTMLElement).contentEditable ===
'true' ||
['INPUT', 'TEXTAREA'].some(
(tagName) =>
tagName === (e.explicitOriginalTarget as HTMLElement).tagName
)))
if (!isEditable) {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
}
}
globalThis?.window?.document?.addEventListener('paste', handlePaste, {
capture: true,
})
const IDLE_TIME_MS = 1000 * 60 * 2
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
const teardown = () => {
// Already paused
if (streamState === StreamState.Paused) return
videoRef.current?.pause()
setStreamState(StreamState.Paused)
sceneInfra.modelingSend({ type: 'Cancel' })
// Give video time to pause
window.requestAnimationFrame(() => {
engineCommandManager.tearDown({ idleMode: true })
})
}
const onVisibilityChange = () => {
if (globalThis.window.document.visibilityState === 'hidden') {
clearTimeout(timeoutIdIdleA)
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
} else if (!engineCommandManager.engineConnection?.isReady()) {
clearTimeout(timeoutIdIdleA)
setStreamState(StreamState.Resuming)
}
}
// Teardown everything if we go hidden or reconnect
if (IDLE) {
globalThis?.window?.document?.addEventListener(
'visibilitychange',
onVisibilityChange
)
}
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
const onAnyInput = () => {
if (streamState === StreamState.Playing) {
// Clear both timers
clearTimeout(timeoutIdIdleA)
clearTimeout(timeoutIdIdleB)
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
if (streamState === StreamState.Paused) {
setStreamState(StreamState.Resuming)
}
}
if (IDLE) {
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
}
if (IDLE) {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
/**
* Add a listener to execute code and play the stream
* on initial stream setup.
*/
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
executeCodeAndPlayStream
)
return () => {
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.SceneReady,
executeCodeAndPlayStream
)
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
capture: true,
})
if (IDLE) {
clearTimeout(timeoutIdIdleA)
clearTimeout(timeoutIdIdleB)
globalThis?.window?.document?.removeEventListener(
'visibilitychange',
onVisibilityChange
)
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'mousemove',
onAnyInput
)
globalThis?.window?.document?.removeEventListener(
'mousedown',
onAnyInput
)
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'touchstart',
onAnyInput
)
}
}
}, [IDLE, streamState])
/**
* Play the vid
*/
useEffect(() => {
if (!kclManager.isExecuting) {
setTimeout(() => {
// execute in the next event loop
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
})
}
}, [kclManager.isExecuting])
useEffect(() => {
if (
typeof window === 'undefined' ||
typeof RTCPeerConnection === 'undefined'
)
return
if (!videoRef.current) return
if (!mediaStream) return
// The browser complains if we try to load a new stream without pausing first.
// Do not immediately play the stream!
try {
videoRef.current.srcObject = mediaStream
videoRef.current.pause()
} catch (e) {
console.warn('Attempted to pause stream while play was still loading', e)
}
send({
type: 'Set context',
data: {
videoElement: videoRef.current,
},
})
setIsLoading(false)
}, [mediaStream])
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
// If we've got no stream or connection, don't do anything
if (!isNetworkOkay) return
if (!videoRef.current) return
// If we're in sketch mode, don't send a engine-side select event
if (state.matches('Sketch')) return
if (state.matches({ idle: 'showPlanes' })) return
// If we're mousing up from a camera drag, don't send a select event
if (sceneInfra.camControls.wasDragging === true) return
if (btnName(e.nativeEvent).left) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendSelectEventToEngine(e, videoRef.current)
}
}
return (
<div
className="absolute inset-0 z-0"
id="stream"
data-testid="stream"
onClick={handleMouseUp}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
>
<video
ref={videoRef}
muted
autoPlay
controls={false}
onPlay={() => setIsLoading(false)}
className="w-full cursor-pointer h-full"
disablePictureInPicture
id="video-stream"
/>
<ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current}
/>
{(streamState === StreamState.Paused ||
streamState === StreamState.Resuming) && (
<div className="text-center absolute inset-0">
<div
className="flex flex-col items-center justify-center h-screen"
data-testid="paused"
>
<div className="border-primary border p-2 rounded-sm">
<svg
width="8"
height="12"
viewBox="0 0 8 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12V0H0V12H2ZM8 12V0H6V12H8Z"
fill="var(--primary)"
/>
</svg>
</div>
<p className="text-base mt-2 text-primary bold">
{streamState === StreamState.Paused && 'Paused'}
{streamState === StreamState.Resuming && 'Resuming'}
</p>
</div>
</div>
)}
{(!isNetworkOkay || isLoading) && (
<div className="text-center absolute inset-0">
<Loading>
{!isNetworkOkay && !isLoading ? (
<span data-testid="loading-stream">Stream disconnected...</span>
) : (
!isLoading && (
<span data-testid="loading-stream">Loading stream...</span>
)
)}
</Loading>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,81 @@
import { editorManager } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
describe('EditorManager Class', () => {
describe('makeUniqueDiagnostics', () => {
it('should filter out duplicated diagnostics', () => {
const duplicatedDiagnostics: Diagnostic[] = [
{
from: 2,
to: 10,
severity: 'hint',
message: 'my cool message',
},
{
from: 2,
to: 10,
severity: 'hint',
message: 'my cool message',
},
{
from: 2,
to: 10,
severity: 'hint',
message: 'my cool message',
},
]
const expected: Diagnostic[] = [
{
from: 2,
to: 10,
severity: 'hint',
message: 'my cool message',
},
]
const actual = editorManager.makeUniqueDiagnostics(duplicatedDiagnostics)
expect(actual).toStrictEqual(expected)
})
it('should filter out duplicated diagnostic and keep some original ones', () => {
const duplicatedDiagnostics: Diagnostic[] = [
{
from: 0,
to: 10,
severity: 'hint',
message: 'my cool message',
},
{
from: 0,
to: 10,
severity: 'hint',
message: 'my cool message',
},
{
from: 88,
to: 99,
severity: 'hint',
message: 'my super cool message',
},
]
const expected: Diagnostic[] = [
{
from: 0,
to: 10,
severity: 'hint',
message: 'my cool message',
},
{
from: 88,
to: 99,
severity: 'hint',
message: 'my super cool message',
},
]
const actual = editorManager.makeUniqueDiagnostics(duplicatedDiagnostics)
expect(actual).toStrictEqual(expected)
})
})
})

View File

@ -24,10 +24,6 @@ export const modelingMachineEvent = modelingMachineAnnotation.of(true)
const setDiagnosticsAnnotation = Annotation.define<boolean>() const setDiagnosticsAnnotation = Annotation.define<boolean>()
export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(true) export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(true)
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
}
export default class EditorManager { export default class EditorManager {
private _editorView: EditorView | null = null private _editorView: EditorView | null = null
private _copilotEnabled: boolean = true private _copilotEnabled: boolean = true
@ -72,9 +68,10 @@ export default class EditorManager {
// we cannot use <>.constructor.name since it will get destroyed // we cannot use <>.constructor.name since it will get destroyed
// when packaging the application. // when packaging the application.
const isTreeHighlightPlugin = const isTreeHighlightPlugin =
e.value.hasOwnProperty('tree') && e?.value &&
e.value.hasOwnProperty('decoratedTo') && e.value?.hasOwnProperty('tree') &&
e.value.hasOwnProperty('decorations') e.value?.hasOwnProperty('decoratedTo') &&
e.value?.hasOwnProperty('decorations')
if (isTreeHighlightPlugin) { if (isTreeHighlightPlugin) {
let originalUpdate = e.value.update let originalUpdate = e.value.update
@ -161,20 +158,29 @@ export default class EditorManager {
} }
} }
/**
* Given an array of Diagnostics remove any duplicates by hashing a key
* in the format of from + ' ' + to + ' ' + message.
*/
makeUniqueDiagnostics(duplicatedDiagnostics: Diagnostic[]): Diagnostic[] {
const uniqueDiagnostics: Diagnostic[] = []
const seenDiagnostic: { [key: string]: boolean } = {}
duplicatedDiagnostics.forEach((diagnostic: Diagnostic) => {
const hash = `${diagnostic.from} ${diagnostic.to} ${diagnostic.message}`
if (!seenDiagnostic[hash]) {
uniqueDiagnostics.push(diagnostic)
seenDiagnostic[hash] = true
}
})
return uniqueDiagnostics
}
setDiagnostics(diagnostics: Diagnostic[]): void { setDiagnostics(diagnostics: Diagnostic[]): void {
if (!this._editorView) return if (!this._editorView) return
// Clear out any existing diagnostics that are the same. // Clear out any existing diagnostics that are the same.
for (const diagnostic of diagnostics) { diagnostics = this.makeUniqueDiagnostics(diagnostics)
for (const otherDiagnostic of diagnostics) {
if (diagnosticIsEqual(diagnostic, otherDiagnostic)) {
diagnostics = diagnostics.filter(
(d) => !diagnosticIsEqual(d, diagnostic)
)
diagnostics.push(diagnostic)
break
}
}
}
this._editorView.dispatch({ this._editorView.dispatch({
effects: [setDiagnosticsEffect.of(diagnostics)], effects: [setDiagnosticsEffect.of(diagnostics)],

View File

@ -0,0 +1,237 @@
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { MutableRefObject } from 'react'
import { setup, assign } from 'xstate'
import { createActorContext } from '@xstate/react'
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
import { trap } from 'lib/trap'
export enum EngineStreamState {
Off = 'off',
On = 'on',
Playing = 'playing',
Paused = 'paused',
Resuming = 'resuming',
}
export enum EngineStreamTransition {
SetMediaStream = 'set-context',
Play = 'play',
Resume = 'resume',
Pause = 'pause',
StartOrReconfigureEngine = 'start-or-reconfigure-engine',
}
export interface EngineStreamContext {
pool: string | null
authToken: string | null
mediaStream: MediaStream | null
videoRef: MutableRefObject<HTMLVideoElement | null>
canvasRef: MutableRefObject<HTMLCanvasElement | null>
}
export function getDimensions(streamWidth: number, streamHeight: number) {
const factorOf = 4
const maxResolution = 2160
const ratio = Math.min(
Math.min(maxResolution / streamWidth, maxResolution / streamHeight),
1.0
)
const quadWidth = Math.round((streamWidth * ratio) / factorOf) * factorOf
const quadHeight = Math.round((streamHeight * ratio) / factorOf) * factorOf
return { width: quadWidth, height: quadHeight }
}
const engineStreamMachine = setup({
types: {
context: {} as EngineStreamContext,
input: {} as EngineStreamContext,
},
actions: {
[EngineStreamTransition.Play]({ context }, params: { zoomToFit: boolean }) {
const canvas = context.canvasRef.current
if (!canvas) return false
const video = context.videoRef.current
if (!video) return false
const mediaStream = context.mediaStream
if (!mediaStream) return false
video.style.display = 'block'
canvas.style.display = 'none'
video.srcObject = mediaStream
void sceneInfra.camControls
.restoreCameraPosition()
.then(() => video.play())
.catch((e) => {
console.warn('Video playing was prevented', e, video)
})
.then(() => kclManager.executeCode(params.zoomToFit))
.catch(trap)
},
[EngineStreamTransition.Pause]({ context }) {
const video = context.videoRef.current
if (!video) return
video.pause()
const canvas = context.canvasRef.current
if (!canvas) return
canvas.width = video.videoWidth
canvas.height = video.videoHeight
canvas.style.width = video.videoWidth + 'px'
canvas.style.height = video.videoHeight + 'px'
canvas.style.display = 'block'
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
// Make sure we're on the next frame for no flickering between canvas
// and the video elements.
window.requestAnimationFrame(() => {
video.style.display = 'none'
// Destroy the media stream only. We will re-establish it. We could
// leave everything at pausing, preventing video decoders from running
// but we can do even better by significantly reducing network
// cards also.
context.mediaStream?.getVideoTracks()[0].stop()
video.srcObject = null
sceneInfra.camControls.old = {
camera: sceneInfra.camControls.camera.clone(),
target: sceneInfra.camControls.target.clone(),
}
engineCommandManager.tearDown({ idleMode: true })
})
},
async [EngineStreamTransition.StartOrReconfigureEngine]({
context,
event,
}) {
if (!context.authToken) return
const video = context.videoRef.current
if (!video) return
const { width, height } = getDimensions(
window.innerWidth,
window.innerHeight
)
video.width = width
video.height = height
const settingsNext = {
// override the pool param (?pool=) to request a specific engine instance
// from a particular pool.
pool: context.pool,
...event.settings,
}
engineCommandManager.settings = settingsNext
engineCommandManager.start({
setMediaStream: event.onMediaStream,
setIsStreamReady: (isStreamReady) =>
event.setAppState({ isStreamReady }),
width,
height,
token: context.authToken,
settings: settingsNext,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
event.modelingMachineActorSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: width,
streamHeight: height,
},
},
})
},
async [EngineStreamTransition.Resume]({ context, event }) {
// engineCommandManager.engineConnection?.reattachMediaStream()
},
},
}).createMachine({
context: (initial) => initial.input,
initial: EngineStreamState.Off,
states: {
[EngineStreamState.Off]: {
on: {
[EngineStreamTransition.StartOrReconfigureEngine]: {
target: EngineStreamState.On,
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
},
},
},
[EngineStreamState.On]: {
on: {
[EngineStreamTransition.SetMediaStream]: {
target: EngineStreamState.On,
actions: [
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
],
},
[EngineStreamTransition.Play]: {
target: EngineStreamState.Playing,
actions: [
{ type: EngineStreamTransition.Play, params: { zoomToFit: true } },
],
},
},
},
[EngineStreamState.Playing]: {
on: {
[EngineStreamTransition.StartOrReconfigureEngine]: {
target: EngineStreamState.Playing,
reenter: true,
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
},
[EngineStreamTransition.Pause]: {
target: EngineStreamState.Paused,
actions: [{ type: EngineStreamTransition.Pause }],
},
},
},
[EngineStreamState.Paused]: {
on: {
[EngineStreamTransition.StartOrReconfigureEngine]: {
target: EngineStreamState.Resuming,
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
},
},
},
[EngineStreamState.Resuming]: {
on: {
[EngineStreamTransition.SetMediaStream]: {
target: EngineStreamState.Resuming,
actions: [
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
],
},
[EngineStreamTransition.Play]: {
target: EngineStreamState.Playing,
actions: [
{ type: EngineStreamTransition.Play, params: { zoomToFit: false } },
],
},
},
},
},
})
export default createActorContext(engineStreamMachine)

View File

@ -2,13 +2,13 @@ import {
SetVarNameModal, SetVarNameModal,
createSetVarNameModal, createSetVarNameModal,
} from 'components/SetVarNameModal' } from 'components/SetVarNameModal'
import { editorManager, kclManager } from 'lib/singletons' import { editorManager, kclManager, codeManager } from 'lib/singletons'
import { reportRejection, trap } from 'lib/trap' import { reportRejection, trap, err } from 'lib/trap'
import { moveValueIntoNewVariable } from 'lang/modifyAst' import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange } from 'lang/wasm' import { PathToNode, SourceRange, recast } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
@ -57,6 +57,11 @@ export function useConvertToVariable(range?: SourceRange) {
) )
await kclManager.updateAst(_modifiedAst, true) await kclManager.updateAst(_modifiedAst, true)
const newCode = recast(_modifiedAst)
if (err(newCode)) return
codeManager.updateCodeEditor(newCode)
return pathToReplacedNode return pathToReplacedNode
} catch (e) { } catch (e) {
console.log('error', e) console.log('error', e)

View File

@ -2,7 +2,7 @@ import { executeAst, lintAst } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { KCLError, kclErrorsToDiagnostics } from './errors' import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager, CommandLogType } from './std/engineConnection'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants' import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
@ -125,7 +125,7 @@ export class KclManager {
if (this.lints.length > 0) { if (this.lints.length > 0) {
diagnostics = diagnostics.concat(this.lints) diagnostics = diagnostics.concat(this.lints)
} }
editorManager.setDiagnostics(diagnostics) editorManager?.setDiagnostics(diagnostics)
} }
addKclErrors(kclErrors: KCLError[]) { addKclErrors(kclErrors: KCLError[]) {
@ -290,15 +290,9 @@ export class KclManager {
) )
} }
await this.engineCommandManager.sendSceneCommand({ await sceneInfra.camControls.centerModelRelativeToPanes({
type: 'modeling_cmd_req', zoomToFit: true,
cmd_id: uuidv4(), zoomObjectId,
cmd: {
type: 'zoom_to_fit',
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
padding: 0.1, // padding around the objects
animated: false, // don't animate the zoom for now
},
}) })
} }
} }
@ -328,7 +322,7 @@ export class KclManager {
this.ast = { ...ast } this.ast = { ...ast }
this._executeCallback() this._executeCallback()
this.engineCommandManager.addCommandLog({ this.engineCommandManager.addCommandLog({
type: 'execution-done', type: CommandLogType.ExecutionDone,
data: null, data: null,
}) })
@ -357,9 +351,6 @@ export class KclManager {
this.clearAst() this.clearAst()
return return
} }
codeManager.updateCodeEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
this._ast = { ...newAst } this._ast = { ...newAst }
const { logs, errors, execState } = await executeAst({ const { logs, errors, execState } = await executeAst({
@ -497,11 +488,6 @@ export class KclManager {
} }
if (execute) { if (execute) {
// Call execute on the set ast.
// Update the code state and editor.
codeManager.updateCodeEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
await this.executeAst({ await this.executeAst({
ast: astWithUpdatedSource, ast: astWithUpdatedSource,
zoomToFit: optionalParams?.zoomToFit, zoomToFit: optionalParams?.zoomToFit,

View File

@ -18,8 +18,7 @@ const mySketch001 = startSketchOn('XY')
// @ts-ignore // @ts-ignore
const sketch001 = execState.memory.get('mySketch001') const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'UserVal', type: 'Sketch',
__meta: [{ sourceRange: [46, 71, 0] }],
value: { value: {
type: 'Sketch', type: 'Sketch',
on: expect.any(Object), on: expect.any(Object),

View File

@ -7,6 +7,8 @@ import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { Annotation, Transaction } from '@codemirror/state' import { Annotation, Transaction } from '@codemirror/state'
import { KeyBinding } from '@codemirror/view' import { KeyBinding } from '@codemirror/view'
import { recast, Program } from 'lang/wasm'
import { err } from 'lib/trap'
const PERSIST_CODE_KEY = 'persistCode' const PERSIST_CODE_KEY = 'persistCode'
@ -147,6 +149,13 @@ export default class CodeManager {
safeLSSetItem(PERSIST_CODE_KEY, this.code) safeLSSetItem(PERSIST_CODE_KEY, this.code)
} }
} }
async updateEditorWithAstAndWriteToFile(ast: Program) {
const newCode = recast(ast)
if (err(newCode)) return
this.updateCodeStateEditor(newCode)
await this.writeToFile()
}
} }
function safeLSGetItem(key: string) { function safeLSGetItem(key: string) {

View File

@ -58,7 +58,13 @@ const newVar = myVar + 1`
` `
const mem = await exe(code) const mem = await exe(code)
// geo is three js buffer geometry and is very bloated to have in tests // geo is three js buffer geometry and is very bloated to have in tests
const minusGeo = mem.get('mySketch')?.value?.paths const sk = mem.get('mySketch')
expect(sk?.type).toEqual('Sketch')
if (sk?.type !== 'Sketch') {
return
}
const minusGeo = sk?.value?.paths
expect(minusGeo).toEqual([ expect(minusGeo).toEqual([
{ {
type: 'ToPoint', type: 'ToPoint',
@ -150,7 +156,7 @@ const newVar = myVar + 1`
].join('\n') ].join('\n')
const mem = await exe(code) const mem = await exe(code)
expect(mem.get('mySk1')).toEqual({ expect(mem.get('mySk1')).toEqual({
type: 'UserVal', type: 'Sketch',
value: { value: {
type: 'Sketch', type: 'Sketch',
on: expect.any(Object), on: expect.any(Object),
@ -215,7 +221,6 @@ const newVar = myVar + 1`
id: expect.any(String), id: expect.any(String),
__meta: [{ sourceRange: [39, 63, 0] }], __meta: [{ sourceRange: [39, 63, 0] }],
}, },
__meta: [{ sourceRange: [39, 63, 0] }],
}) })
}) })
it('execute array expression', async () => { it('execute array expression', async () => {
@ -225,7 +230,7 @@ const newVar = myVar + 1`
const mem = await exe(code) const mem = await exe(code)
// TODO path to node is probably wrong here, zero indexes are not correct // TODO path to node is probably wrong here, zero indexes are not correct
expect(mem.get('three')).toEqual({ expect(mem.get('three')).toEqual({
type: 'UserVal', type: 'Int',
value: 3, value: 3,
__meta: [ __meta: [
{ {
@ -234,8 +239,17 @@ const newVar = myVar + 1`
], ],
}) })
expect(mem.get('yo')).toEqual({ expect(mem.get('yo')).toEqual({
type: 'UserVal', type: 'Array',
value: [1, '2', 3, 9], value: [
{ type: 'Int', value: 1, __meta: [{ sourceRange: [28, 29, 0] }] },
{ type: 'String', value: '2', __meta: [{ sourceRange: [31, 34, 0] }] },
{ type: 'Int', value: 3, __meta: [{ sourceRange: [14, 15, 0] }] },
{
type: 'Number',
value: 9,
__meta: [{ sourceRange: [43, 44, 0] }, { sourceRange: [47, 48, 0] }],
},
],
__meta: [ __meta: [
{ {
sourceRange: [27, 49, 0], sourceRange: [27, 49, 0],
@ -253,8 +267,25 @@ const newVar = myVar + 1`
].join('\n') ].join('\n')
const mem = await exe(code) const mem = await exe(code)
expect(mem.get('yo')).toEqual({ expect(mem.get('yo')).toEqual({
type: 'UserVal', type: 'Object',
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 }, value: {
aStr: {
type: 'String',
value: 'str',
__meta: [{ sourceRange: [34, 39, 0] }],
},
anum: { type: 'Int', value: 2, __meta: [{ sourceRange: [47, 48, 0] }] },
identifier: {
type: 'Int',
value: 3,
__meta: [{ sourceRange: [14, 15, 0] }],
},
binExp: {
type: 'Number',
value: 9,
__meta: [{ sourceRange: [77, 78, 0] }, { sourceRange: [81, 82, 0] }],
},
},
__meta: [ __meta: [
{ {
sourceRange: [27, 83, 0], sourceRange: [27, 83, 0],
@ -268,11 +299,11 @@ const newVar = myVar + 1`
) )
const mem = await exe(code) const mem = await exe(code)
expect(mem.get('myVar')).toEqual({ expect(mem.get('myVar')).toEqual({
type: 'UserVal', type: 'String',
value: '123', value: '123',
__meta: [ __meta: [
{ {
sourceRange: [41, 50, 0], sourceRange: [19, 24, 0],
}, },
], ],
}) })
@ -356,7 +387,26 @@ describe('testing math operators', () => {
it('with unaryExpression in ArrayExpression', async () => { it('with unaryExpression in ArrayExpression', async () => {
const code = 'const myVar = [1,-legLen(5, 4)]' const code = 'const myVar = [1,-legLen(5, 4)]'
const mem = await exe(code) const mem = await exe(code)
expect(mem.get('myVar')?.value).toEqual([1, -3]) expect(mem.get('myVar')?.value).toEqual([
{
__meta: [
{
sourceRange: [15, 16, 0],
},
],
type: 'Int',
value: 1,
},
{
__meta: [
{
sourceRange: [17, 30, 0],
},
],
type: 'Number',
value: -3,
},
])
}) })
it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => { it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => {
const code = [ const code = [

View File

@ -55,18 +55,13 @@ describe('Test KCL Samples from public Github repository', () => {
}) })
// Run through all of the files in the manifest json. This will allow us to be automatically updated // Run through all of the files in the manifest json. This will allow us to be automatically updated
// with the latest changes in github. We won't be hard coding the filenames // with the latest changes in github. We won't be hard coding the filenames
it( files.forEach((file: KclSampleFile) => {
'should run through all the files', it(`should parse ${file.filename} without errors`, async () => {
async () => {
for (let i = 0; i < files.length; i++) {
const file: KclSampleFile = files[i]
const code = await getKclSampleCodeFromGithub(file.filename) const code = await getKclSampleCodeFromGithub(file.filename)
const parsed = parse(code) const parsed = parse(code)
assert(!(parsed instanceof Error)) assert(!(parsed instanceof Error))
} }, 1000)
}, })
files.length * 1000
)
}) })
describe('when performing enginelessExecutor', () => { describe('when performing enginelessExecutor', () => {

View File

@ -35,7 +35,12 @@ import {
ArtifactGraph, ArtifactGraph,
getSweepFromSuspectedPath, getSweepFromSuspectedPath,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { kclManager, engineCommandManager, editorManager } from 'lib/singletons' import {
kclManager,
engineCommandManager,
editorManager,
codeManager,
} from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
// Apply Fillet To Selection // Apply Fillet To Selection
@ -253,6 +258,9 @@ async function updateAstAndFocus(
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode, focusPath: pathToFilletNode,
}) })
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) { if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections) editorManager.selectRange(updatedAst?.selections)
} }

View File

@ -633,7 +633,7 @@ export function expandSweep(
if (err(path)) return path if (err(path)) return path
return { return {
type: 'sweep', type: 'sweep',
subType: 'extrusion', subType: sweep.subType,
surfaces: Array.from(surfs.values()), surfaces: Array.from(surfs.values()),
edges: Array.from(edges.values()), edges: Array.from(edges.values()),
path, path,

View File

@ -406,13 +406,14 @@ class EngineConnection extends EventTarget {
default: default:
if (this.isConnecting()) break if (this.isConnecting()) break
// Means we never could do an initial connection. Reconnect everything. // Means we never could do an initial connection. Reconnect everything.
if (!this.pingPongSpan.ping) this.connect().catch(reportRejection) if (!this.pingPongSpan.ping)
this.connect({ reconnect: false }).catch(reportRejection)
break break
} }
}, pingIntervalMs) }, pingIntervalMs)
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.connect() this.connect({ reconnect: false })
} }
// SHOULD ONLY BE USED FOR VITESTS // SHOULD ONLY BE USED FOR VITESTS
@ -523,7 +524,9 @@ class EngineConnection extends EventTarget {
this.idleMode = opts?.idleMode ?? false this.idleMode = opts?.idleMode ?? false
clearInterval(this.pingIntervalId) clearInterval(this.pingIntervalId)
if (opts?.idleMode) { this.disconnectAll()
if (this.idleMode) {
this.state = { this.state = {
type: EngineConnectionStateType.Disconnecting, type: EngineConnectionStateType.Disconnecting,
value: { value: {
@ -542,8 +545,6 @@ class EngineConnection extends EventTarget {
type: DisconnectingType.Quit, type: DisconnectingType.Quit,
}, },
} }
this.disconnectAll()
} }
/** /**
@ -553,7 +554,7 @@ class EngineConnection extends EventTarget {
* This will attempt the full handshake, and retry if the connection * This will attempt the full handshake, and retry if the connection
* did not establish. * did not establish.
*/ */
connect(reconnecting?: boolean): Promise<void> { connect(args: { reconnect: boolean }): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => {
if (this.isConnecting() || this.isReady()) { if (this.isConnecting() || this.isReady()) {
return return
@ -1165,7 +1166,7 @@ class EngineConnection extends EventTarget {
this.websocket.addEventListener('message', this.onWebSocketMessage) this.websocket.addEventListener('message', this.onWebSocketMessage)
} }
if (reconnecting) { if (args.reconnect) {
createWebSocketConnection() createWebSocketConnection()
} else { } else {
this.onNetworkStatusReady = () => { this.onNetworkStatusReady = () => {
@ -1178,6 +1179,32 @@ class EngineConnection extends EventTarget {
} }
}) })
} }
async reattachMediaStream() {
return this.pc
?.createOffer({ iceRestart: true })
.then((offer: RTCSessionDescriptionInit) => {
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.SetLocalDescription,
},
}
return this.pc?.setLocalDescription(offer).then(() => {
this.send({
type: 'sdp_offer',
offer: offer as Models['RtcSessionDescription_type'],
})
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.OfferedSdp,
},
}
})
})
}
// Do not change this back to an object or any, we should only be sending the // Do not change this back to an object or any, we should only be sending the
// WebSocketRequest type! // WebSocketRequest type!
unreliableSend(message: Models['WebSocketRequest_type']) { unreliableSend(message: Models['WebSocketRequest_type']) {
@ -1229,8 +1256,17 @@ class EngineConnection extends EventTarget {
this.websocket?.readyState === 3 this.websocket?.readyState === 3
if (closedPc && closedUDC && closedWS) { if (closedPc && closedUDC && closedWS) {
if (!this.idleMode) {
// Do not notify the rest of the program that we have cut off anything. // Do not notify the rest of the program that we have cut off anything.
this.state = { type: EngineConnectionStateType.Disconnected } this.state = { type: EngineConnectionStateType.Disconnected }
} else {
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Pause,
},
}
}
} }
} }
} }
@ -1255,27 +1291,40 @@ export interface Subscription<T extends ModelTypes> {
) => void ) => void
} }
export enum CommandLogType {
SendModeling = 'send-modeling',
SendScene = 'send-scene',
ReceiveReliable = 'receive-reliable',
ExecutionDone = 'execution-done',
ExportDone = 'export-done',
SetDefaultSystemProperties = 'set_default_system_properties',
}
export type CommandLog = export type CommandLog =
| { | {
type: 'send-modeling' type: CommandLogType.SendModeling
data: EngineCommand data: EngineCommand
} }
| { | {
type: 'send-scene' type: CommandLogType.SendScene
data: EngineCommand data: EngineCommand
} }
| { | {
type: 'receive-reliable' type: CommandLogType.ReceiveReliable
data: OkWebSocketResponseData data: OkWebSocketResponseData
id: string id: string
cmd_type?: string cmd_type?: string
} }
| { | {
type: 'execution-done' type: CommandLogType.ExecutionDone
data: null data: null
} }
| { | {
type: 'export-done' type: CommandLogType.ExportDone
data: null
}
| {
type: CommandLogType.SetDefaultSystemProperties
data: null data: null
} }
@ -1686,7 +1735,7 @@ export class EngineCommandManager extends EventTarget {
message.request_id message.request_id
) { ) {
this.addCommandLog({ this.addCommandLog({
type: 'receive-reliable', type: CommandLogType.ReceiveReliable,
data: message.resp, data: message.resp,
id: message?.request_id || '', id: message?.request_id || '',
cmd_type: pending?.command?.cmd?.type, cmd_type: pending?.command?.cmd?.type,
@ -1720,7 +1769,7 @@ export class EngineCommandManager extends EventTarget {
if (!command) return if (!command) return
if (command.type === 'modeling_cmd_req') if (command.type === 'modeling_cmd_req')
this.addCommandLog({ this.addCommandLog({
type: 'receive-reliable', type: CommandLogType.ReceiveReliable,
data: { data: {
type: 'modeling', type: 'modeling',
data: { data: {
@ -1762,7 +1811,7 @@ export class EngineCommandManager extends EventTarget {
) )
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineConnection?.connect() this.engineConnection?.connect({ reconnect: false })
} }
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStarted, EngineConnectionEvents.ConnectionStarted,
@ -1824,6 +1873,7 @@ export class EngineCommandManager extends EventTarget {
) )
this.engineConnection?.tearDown(opts) this.engineConnection?.tearDown(opts)
this.engineConnection = undefined
// Our window.tearDown assignment causes this case to happen which is // Our window.tearDown assignment causes this case to happen which is
// only really for tests. // only really for tests.
@ -1831,6 +1881,8 @@ export class EngineCommandManager extends EventTarget {
} else if (this.engineCommandManager?.engineConnection) { } else if (this.engineCommandManager?.engineConnection) {
// @ts-ignore // @ts-ignore
this.engineCommandManager?.engineConnection?.tearDown(opts) this.engineCommandManager?.engineConnection?.tearDown(opts)
// @ts-ignore
this.engineCommandManager.engineConnection = null
} }
} }
async startNewSession() { async startNewSession() {
@ -1929,7 +1981,7 @@ export class EngineCommandManager extends EventTarget {
) { ) {
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy // highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
this.addCommandLog({ this.addCommandLog({
type: 'send-scene', type: CommandLogType.SendScene,
data: command, data: command,
}) })
} }
@ -1988,7 +2040,7 @@ export class EngineCommandManager extends EventTarget {
toastId, toastId,
resolve: (passThrough) => { resolve: (passThrough) => {
this.addCommandLog({ this.addCommandLog({
type: 'export-done', type: CommandLogType.ExportDone,
data: null, data: null,
}) })
resolve(passThrough) resolve(passThrough)

View File

@ -336,7 +336,7 @@ export class ProgramMemory {
*/ */
hasSketchOrSolid(): boolean { hasSketchOrSolid(): boolean {
for (const node of this.visibleEntries().values()) { for (const node of this.visibleEntries().values()) {
if (node.type === 'Solid' || node.value?.type === 'Sketch') { if (node.type === 'Solid' || node.type === 'Sketch') {
return true return true
} }
} }

View File

@ -145,12 +145,6 @@ export const fileLoader: LoaderFunction = async (
? await getProjectInfo(projectPath) ? await getProjectInfo(projectPath)
: null : null
console.log('maybeProjectInfo', {
maybeProjectInfo,
defaultProjectData,
projectPathData,
})
const projectData: IndexLoaderData = { const projectData: IndexLoaderData = {
code, code,
project: maybeProjectInfo ?? defaultProjectData, project: maybeProjectInfo ?? defaultProjectData,

View File

@ -118,6 +118,8 @@ export class Setting<T = unknown> {
} }
} }
const MS_IN_MINUTE = 1000 * 60
export function createSettings() { export function createSettings() {
return { return {
/** Settings that affect the behavior of the entire app, /** Settings that affect the behavior of the entire app,
@ -181,13 +183,58 @@ export function createSettings() {
/** /**
* Stream resource saving behavior toggle * Stream resource saving behavior toggle
*/ */
streamIdleMode: new Setting<boolean>({ streamIdleMode: new Setting<number | undefined>({
defaultValue: false, defaultValue: undefined,
description: 'Toggle stream idling, saving bandwidth and battery', description: 'Toggle stream idling, saving bandwidth and battery',
validate: (v) => typeof v === 'boolean', validate: (v) =>
commandConfig: { v === undefined ||
inputType: 'boolean', (typeof v === 'number' &&
}, v >= 1 * MS_IN_MINUTE &&
v <= 60 * MS_IN_MINUTE),
Component: ({ value, updateValue }) => (
<div className="flex item-center gap-4 px-2 m-0 py-0">
<div className="flex flex-col">
<input
type="checkbox"
checked={value !== undefined}
onChange={(e) =>
updateValue(
!e.currentTarget.checked ? undefined : 5 * MS_IN_MINUTE
)
}
className="block w-4 h-4"
/>
<div></div>
</div>
<div className="flex flex-col grow">
<input
type="range"
onChange={(e) =>
updateValue(Number(e.currentTarget.value) * MS_IN_MINUTE)
}
disabled={value === undefined}
value={
value !== null && value !== undefined
? value / MS_IN_MINUTE
: 5
}
min={1}
max={60}
step={1}
className="block flex-1"
/>
{value !== undefined && value !== null && (
<div>
{value / MS_IN_MINUTE === 60
? '1 hour'
: value / MS_IN_MINUTE === 1
? '1 minute'
: value / MS_IN_MINUTE + ' minutes'}
</div>
)}
</div>
</div>
),
}), }),
onboardingStatus: new Setting<string>({ onboardingStatus: new Setting<string>({
defaultValue: '', defaultValue: '',

View File

@ -24,6 +24,10 @@ import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration
import { BROWSER_PROJECT_NAME } from 'lib/constants' import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { DeepPartial } from 'lib/types' import { DeepPartial } from 'lib/types'
type OmitNull<T> = T extends null ? undefined : T
const toUndefinedIfNull = (a: any): OmitNull<any> =>
a === null ? undefined : a
/** /**
* Convert from a rust settings struct into the JS settings struct. * Convert from a rust settings struct into the JS settings struct.
* We do this because the JS settings type has all the fancy shit * We do this because the JS settings type has all the fancy shit
@ -40,7 +44,9 @@ export function configurationToSettingsPayload(
: undefined, : undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status, onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner, dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode, streamIdleMode: toUndefinedIfNull(
configuration?.settings?.app?.stream_idle_mode
),
projectDirectory: configuration?.settings?.project?.directory, projectDirectory: configuration?.settings?.project?.directory,
enableSSAO: configuration?.settings?.modeling?.enable_ssao, enableSSAO: configuration?.settings?.modeling?.enable_ssao,
}, },
@ -79,7 +85,9 @@ export function projectConfigurationToSettingsPayload(
: undefined, : undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status, onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner, dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode, streamIdleMode: toUndefinedIfNull(
configuration?.settings?.app?.stream_idle_mode
),
enableSSAO: configuration?.settings?.modeling?.enable_ssao, enableSSAO: configuration?.settings?.modeling?.enable_ssao,
}, },
modeling: { modeling: {

View File

@ -10,8 +10,14 @@ export const codeManager = new CodeManager()
export const engineCommandManager = new EngineCommandManager() export const engineCommandManager = new EngineCommandManager()
// Accessible for tests mostly declare global {
// @ts-ignore interface Window {
tearDown: typeof engineCommandManager.tearDown
sceneInfra: typeof sceneInfra
}
}
// Accessible for tests
window.tearDown = engineCommandManager.tearDown window.tearDown = engineCommandManager.tearDown
// This needs to be after codeManager is created. // This needs to be after codeManager is created.
@ -21,7 +27,9 @@ engineCommandManager.kclManager = kclManager
engineCommandManager.getAstCb = () => kclManager.ast engineCommandManager.getAstCb = () => kclManager.ast
export const sceneInfra = new SceneInfra(engineCommandManager) export const sceneInfra = new SceneInfra(engineCommandManager)
engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange
// Accessible for tests
window.sceneInfra = sceneInfra
export const sceneEntitiesManager = new SceneEntities(engineCommandManager) export const sceneEntitiesManager = new SceneEntities(engineCommandManager)

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

@ -0,0 +1,3 @@
// 0.25s is the average visual reaction time for humans so we'll go a bit less
// so those exception people don't see.
export const REASONABLE_TIME_TO_REFRESH_STREAM_SIZE = 100

View File

@ -91,7 +91,7 @@ export function useCalculateKclExpression({
const _programMem: ProgramMemory = ProgramMemory.empty() const _programMem: ProgramMemory = ProgramMemory.empty()
for (const { key, value } of availableVarInfo.variables) { for (const { key, value } of availableVarInfo.variables) {
const error = _programMem.set(key, { const error = _programMem.set(key, {
type: 'UserVal', type: 'String',
value, value,
__meta: [], __meta: [],
}) })
@ -115,6 +115,7 @@ export function useCalculateKclExpression({
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
} }
if (!value) return
execAstAndSetResult().catch(() => { execAstAndSetResult().catch(() => {
setCalcResult('NAN') setCalcResult('NAN')
setValueNode(null) setValueNode(null)

View File

@ -18,6 +18,7 @@ import {
sceneEntitiesManager, sceneEntitiesManager,
engineCommandManager, engineCommandManager,
editorManager, editorManager,
codeManager,
} from 'lib/singletons' } from 'lib/singletons'
import { import {
horzVertInfo, horzVertInfo,
@ -531,8 +532,10 @@ export const modelingMachine = setup({
} }
} }
), ),
// eslint-disable-next-line @typescript-eslint/no-misused-promises 'hide default planes': () => {
'hide default planes': () => kclManager.hidePlanes(), // eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.hidePlanes()
},
'reset sketch metadata': assign({ 'reset sketch metadata': assign({
sketchDetails: null, sketchDetails: null,
sketchEnginePathId: '', sketchEnginePathId: '',
@ -595,7 +598,6 @@ export const modelingMachine = setup({
if (trap(extrudeSketchRes)) return if (trap(extrudeSketchRes)) return
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
store.videoElement?.pause()
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToExtrudeArg], focusPath: [pathToExtrudeArg],
zoomToFit: true, zoomToFit: true,
@ -604,11 +606,9 @@ export const modelingMachine = setup({
type: 'path', type: 'path',
}, },
}) })
if (!engineCommandManager.engineConnection?.idleMode) {
store.videoElement?.play().catch((e) => { await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
console.warn('Video playing was prevented', e)
})
}
if (updatedAst?.selections) { if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections) editorManager.selectRange(updatedAst?.selections)
} }
@ -642,7 +642,6 @@ export const modelingMachine = setup({
if (trap(revolveSketchRes)) return if (trap(revolveSketchRes)) return
const { modifiedAst, pathToRevolveArg } = revolveSketchRes const { modifiedAst, pathToRevolveArg } = revolveSketchRes
store.videoElement?.pause()
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToRevolveArg], focusPath: [pathToRevolveArg],
zoomToFit: true, zoomToFit: true,
@ -651,11 +650,9 @@ export const modelingMachine = setup({
type: 'path', type: 'path',
}, },
}) })
if (!engineCommandManager.engineConnection?.idleMode) {
store.videoElement?.play().catch((e) => { await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
console.warn('Video playing was prevented', e)
})
}
if (updatedAst?.selections) { if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections) editorManager.selectRange(updatedAst?.selections)
} }
@ -685,6 +682,7 @@ export const modelingMachine = setup({
} }
await kclManager.updateAst(modifiedAst, true) await kclManager.updateAst(modifiedAst, true)
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
})().catch(reportRejection) })().catch(reportRejection)
}, },
'AST fillet': ({ event }) => { 'AST fillet': ({ event }) => {
@ -702,6 +700,9 @@ export const modelingMachine = setup({
radius radius
) )
if (err(applyFilletToSelectionResult)) return applyFilletToSelectionResult if (err(applyFilletToSelectionResult)) return applyFilletToSelectionResult
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
}, },
'set selection filter to curves only': () => { 'set selection filter to curves only': () => {
;(async () => { ;(async () => {
@ -758,25 +759,35 @@ export const modelingMachine = setup({
'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(), 'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
'set up draft line': ({ context: { sketchDetails } }) => { 'set up draft line': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setUpDraftSegment( sceneEntitiesManager
.setupDraftSegment(
sketchDetails.sketchPathToNode, sketchDetails.sketchPathToNode,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
sketchDetails.origin, sketchDetails.origin,
'line' 'line'
) )
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'set up draft arc': ({ context: { sketchDetails } }) => { 'set up draft arc': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setUpDraftSegment( sceneEntitiesManager
.setupDraftSegment(
sketchDetails.sketchPathToNode, sketchDetails.sketchPathToNode,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
sketchDetails.origin, sketchDetails.origin,
'tangentialArcTo' 'tangentialArcTo'
) )
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'listen for rectangle origin': ({ context: { sketchDetails } }) => { 'listen for rectangle origin': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
@ -834,31 +845,43 @@ export const modelingMachine = setup({
'set up draft rectangle': ({ context: { sketchDetails }, event }) => { 'set up draft rectangle': ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Add rectangle origin') return if (event.type !== 'Add rectangle origin') return
if (!sketchDetails || !event.data) return if (!sketchDetails || !event.data) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setupDraftRectangle( sceneEntitiesManager
.setupDraftRectangle(
sketchDetails.sketchPathToNode, sketchDetails.sketchPathToNode,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
sketchDetails.origin, sketchDetails.origin,
event.data event.data
) )
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'set up draft circle': ({ context: { sketchDetails }, event }) => { 'set up draft circle': ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Add circle origin') return if (event.type !== 'Add circle origin') return
if (!sketchDetails || !event.data) return if (!sketchDetails || !event.data) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setupDraftCircle( sceneEntitiesManager
.setupDraftCircle(
sketchDetails.sketchPathToNode, sketchDetails.sketchPathToNode,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
sketchDetails.origin, sketchDetails.origin,
event.data event.data
) )
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'set up draft line without teardown': ({ context: { sketchDetails } }) => { 'set up draft line without teardown': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setUpDraftSegment( sceneEntitiesManager
.setupDraftSegment(
sketchDetails.sketchPathToNode, sketchDetails.sketchPathToNode,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -866,6 +889,9 @@ export const modelingMachine = setup({
'line', 'line',
false false
) )
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'show default planes': () => { 'show default planes': () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
@ -882,12 +908,17 @@ export const modelingMachine = setup({
'add axis n grid': ({ context: { sketchDetails } }) => { 'add axis n grid': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
if (localStorage.getItem('disableAxis')) return if (localStorage.getItem('disableAxis')) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.createSketchAxis( sceneEntitiesManager.createSketchAxis(
sketchDetails.sketchPathToNode || [], sketchDetails.sketchPathToNode || [],
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
sketchDetails.origin sketchDetails.origin
) )
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
}, },
'reset client scene mouse handlers': () => { 'reset client scene mouse handlers': () => {
// when not in sketch mode we don't need any mouse listeners // when not in sketch mode we don't need any mouse listeners
@ -916,10 +947,13 @@ export const modelingMachine = setup({
'Delete segment': ({ context: { sketchDetails }, event }) => { 'Delete segment': ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Delete segment') return if (event.type !== 'Delete segment') return
if (!sketchDetails || !event.data) return if (!sketchDetails || !event.data) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
deleteSegment({ deleteSegment({
pathToNode: event.data, pathToNode: event.data,
sketchDetails, sketchDetails,
}).then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
}) })
}, },
'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(), 'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(),
@ -984,6 +1018,9 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: updateSelections( selection: updateSelections(
@ -1018,6 +1055,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: updateSelections( selection: updateSelections(
@ -1052,6 +1090,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: updateSelections( selection: updateSelections(
@ -1084,6 +1123,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1117,6 +1157,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1150,6 +1191,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1183,6 +1225,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1220,6 +1263,8 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1252,6 +1297,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1556,7 +1602,7 @@ export const modelingMachine = setup({
}, },
}, },
entry: 'setup client side sketch segments', entry: ['setup client side sketch segments'],
}, },
'Await horizontal distance info': { 'Await horizontal distance info': {
@ -1801,7 +1847,7 @@ export const modelingMachine = setup({
onError: 'SketchIdle', onError: 'SketchIdle',
onDone: { onDone: {
target: 'SketchIdle', target: 'SketchIdle',
actions: ['Set selection'], actions: 'Set selection',
}, },
}, },
}, },

View File

@ -27,7 +27,7 @@ pub use crate::ast::types::{
use crate::{ use crate::{
docs::StdLibFn, docs::StdLibFn,
errors::KclError, errors::KclError,
executor::{ExecState, ExecutorContext, KclValue, Metadata, SourceRange, TagIdentifier, UserVal}, executor::{ExecState, ExecutorContext, KclValue, Metadata, SourceRange, TagIdentifier},
parser::PIPE_OPERATOR, parser::PIPE_OPERATOR,
std::kcl_stdlib::KclStdLibFn, std::kcl_stdlib::KclStdLibFn,
}; };
@ -59,6 +59,14 @@ pub struct Node<T> {
pub module_id: ModuleId, pub module_id: ModuleId,
} }
impl<T> Node<T> {
pub fn metadata(&self) -> Metadata {
Metadata {
source_range: SourceRange([self.start, self.end, self.module_id.0 as usize]),
}
}
}
impl<T: JsonSchema> schemars::JsonSchema for Node<T> { impl<T: JsonSchema> schemars::JsonSchema for Node<T> {
fn schema_name() -> String { fn schema_name() -> String {
T::schema_name() T::schema_name()
@ -1708,34 +1716,26 @@ impl Literal {
impl From<Node<Literal>> for KclValue { impl From<Node<Literal>> for KclValue {
fn from(literal: Node<Literal>) -> Self { fn from(literal: Node<Literal>) -> Self {
KclValue::UserVal(UserVal { let meta = vec![literal.metadata()];
value: JValue::from(literal.value.clone()), match literal.inner.value {
meta: vec![Metadata { LiteralValue::IInteger(value) => KclValue::Int { value, meta },
source_range: literal.into(), LiteralValue::Fractional(value) => KclValue::Number { value, meta },
}], LiteralValue::String(value) => KclValue::String { value, meta },
}) LiteralValue::Bool(value) => KclValue::Bool { value, meta },
}
} }
} }
impl From<&Node<Literal>> for KclValue { impl From<&Node<Literal>> for KclValue {
fn from(literal: &Node<Literal>) -> Self { fn from(literal: &Node<Literal>) -> Self {
KclValue::UserVal(UserVal { Self::from(literal.to_owned())
value: JValue::from(literal.value.clone()),
meta: vec![Metadata {
source_range: literal.into(),
}],
})
} }
} }
impl From<&BoxNode<Literal>> for KclValue { impl From<&BoxNode<Literal>> for KclValue {
fn from(literal: &BoxNode<Literal>) -> Self { fn from(literal: &BoxNode<Literal>) -> Self {
KclValue::UserVal(UserVal { let b: &Node<Literal> = literal;
value: JValue::from(literal.value.clone()), Self::from(b)
meta: vec![Metadata {
source_range: literal.into(),
}],
})
} }
} }
@ -3010,17 +3010,6 @@ impl ConstraintLevels {
} }
} }
pub(crate) fn human_friendly_type(j: &JValue) -> &'static str {
match j {
JValue::Null => "null",
JValue::Bool(_) => "boolean (true/false value)",
JValue::Number(_) => "number",
JValue::String(_) => "string (text)",
JValue::Array(_) => "array (list)",
JValue::Object(_) => "object",
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View File

@ -1,18 +1,21 @@
use std::collections::HashMap;
use super::{ use super::{
human_friendly_type, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Expr,
CallExpression, Expr, IfExpression, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, IfExpression, KclNone, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, ObjectExpression,
ObjectExpression, TagDeclarator, UnaryExpression, UnaryOperator, TagDeclarator, UnaryExpression, UnaryOperator,
}; };
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ executor::{
BodyType, ExecState, ExecutorContext, KclValue, Metadata, Sketch, SourceRange, StatementKind, TagEngineInfo, BodyType, ExecState, ExecutorContext, KclValue, Metadata, SourceRange, StatementKind, TagEngineInfo,
TagIdentifier, UserVal, TagIdentifier,
}, },
std::FunctionKind, std::FunctionKind,
}; };
use async_recursion::async_recursion; use async_recursion::async_recursion;
use serde_json::Value as JValue;
const FLOAT_TO_INT_MAX_DELTA: f64 = 0.01;
impl BinaryPart { impl BinaryPart {
#[async_recursion] #[async_recursion]
@ -42,28 +45,21 @@ impl Node<MemberExpression> {
} }
}; };
let array_json = array.get_json_value()?; let KclValue::Array { value: array, meta: _ } = array else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression array is not an array: {:?}", array),
source_ranges: vec![self.clone().into()],
}));
};
if let serde_json::Value::Array(array) = array_json {
if let Some(value) = array.get(index) { if let Some(value) = array.get(index) {
Ok(KclValue::UserVal(UserVal { Ok(value.to_owned())
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else { } else {
Err(KclError::UndefinedValue(KclErrorDetails { Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("index {} not found in array", index), message: format!("index {} not found in array", index),
source_ranges: vec![self.clone().into()], source_ranges: vec![self.clone().into()],
})) }))
} }
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression array is not an array: {:?}", array),
source_ranges: vec![self.clone().into()],
}))
}
} }
pub fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> { pub fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
@ -77,18 +73,11 @@ impl Node<MemberExpression> {
} }
}; };
let object_json = object.get_json_value()?;
// Check the property and object match -- e.g. ints for arrays, strs for objects. // Check the property and object match -- e.g. ints for arrays, strs for objects.
match (object_json, property) { match (object, property) {
(JValue::Object(map), Property::String(property)) => { (KclValue::Object { value: map, meta: _ }, Property::String(property)) => {
if let Some(value) = map.get(&property) { if let Some(value) = map.get(&property) {
Ok(KclValue::UserVal(UserVal { Ok(value.to_owned())
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else { } else {
Err(KclError::UndefinedValue(KclErrorDetails { Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("Property '{property}' not found in object"), message: format!("Property '{property}' not found in object"),
@ -96,22 +85,20 @@ impl Node<MemberExpression> {
})) }))
} }
} }
(JValue::Object(_), p) => Err(KclError::Semantic(KclErrorDetails { (KclValue::Object { .. }, p) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!(
"Only strings can be used as the property of an object, but you're using a {}", "Only strings can be used as the property of an object, but you're using {article} {t}",
p.type_name()
), ),
source_ranges: vec![self.clone().into()], source_ranges: vec![self.clone().into()],
})),
(JValue::Array(arr), Property::Number(index)) => {
let value_of_arr: Option<&JValue> = arr.get(index);
if let Some(value) = value_of_arr {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
})) }))
}
(KclValue::Array { value: arr, meta: _ }, Property::Number(index)) => {
let value_of_arr = arr.get(index);
if let Some(value) = value_of_arr {
Ok(value.to_owned())
} else { } else {
Err(KclError::UndefinedValue(KclErrorDetails { Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("The array doesn't have any item at index {index}"), message: format!("The array doesn't have any item at index {index}"),
@ -119,17 +106,36 @@ impl Node<MemberExpression> {
})) }))
} }
} }
(JValue::Array(_), p) => Err(KclError::Semantic(KclErrorDetails { (KclValue::Array { .. }, p) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!(
"Only integers >= 0 can be used as the index of an array, but you're using a {}", "Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",
p.type_name()
), ),
source_ranges: vec![self.clone().into()], source_ranges: vec![self.clone().into()],
})), }))
}
(KclValue::Solid(solid), Property::String(prop)) if prop == "sketch" => Ok(KclValue::Sketch {
value: Box::new(solid.sketch),
}),
(KclValue::Sketch { value: sk }, Property::String(prop)) if prop == "tags" => Ok(KclValue::Object {
meta: vec![Metadata {
source_range: SourceRange::from(self.clone()),
}],
value: sk
.tags
.iter()
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
.collect(),
}),
(being_indexed, _) => { (being_indexed, _) => {
let t = human_friendly_type(&being_indexed); let t = being_indexed.human_friendly_type();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails { Err(KclError::Semantic(KclErrorDetails {
message: format!("Only arrays and objects can be indexed, but you're trying to index a {t}"), message: format!(
"Only arrays and objects can be indexed, but you're trying to index {article} {t}"
),
source_ranges: vec![self.clone().into()], source_ranges: vec![self.clone().into()],
})) }))
} }
@ -140,81 +146,134 @@ impl Node<MemberExpression> {
impl Node<BinaryExpression> { impl Node<BinaryExpression> {
#[async_recursion] #[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let left_json_value = self.left.get_result(exec_state, ctx).await?.get_json_value()?; let left_value = self.left.get_result(exec_state, ctx).await?;
let right_json_value = self.right.get_result(exec_state, ctx).await?.get_json_value()?; let right_value = self.right.get_result(exec_state, ctx).await?;
let mut meta = left_value.metadata();
meta.extend(right_value.metadata());
// First check if we are doing string concatenation. // First check if we are doing string concatenation.
if self.operator == BinaryOperator::Add { if self.operator == BinaryOperator::Add {
if let (Some(left), Some(right)) = ( if let (KclValue::String { value: left, meta: _ }, KclValue::String { value: right, meta: _ }) =
parse_json_value_as_string(&left_json_value), (&left_value, &right_value)
parse_json_value_as_string(&right_json_value), {
) { return Ok(KclValue::String {
let value = serde_json::Value::String(format!("{}{}", left, right)); value: format!("{}{}", left, right),
return Ok(KclValue::UserVal(UserVal { meta,
value, });
meta: vec![Metadata {
source_range: self.into(),
}],
}));
} }
} }
let left = parse_json_number_as_f64(&left_json_value, self.left.clone().into())?; let left = parse_number_as_f64(&left_value, self.left.clone().into())?;
let right = parse_json_number_as_f64(&right_json_value, self.right.clone().into())?; let right = parse_number_as_f64(&right_value, self.right.clone().into())?;
let value: serde_json::Value = match self.operator { let value = match self.operator {
BinaryOperator::Add => (left + right).into(), BinaryOperator::Add => KclValue::Number {
BinaryOperator::Sub => (left - right).into(), value: left + right,
BinaryOperator::Mul => (left * right).into(), meta,
BinaryOperator::Div => (left / right).into(), },
BinaryOperator::Mod => (left % right).into(), BinaryOperator::Sub => KclValue::Number {
BinaryOperator::Pow => (left.powf(right)).into(), value: left - right,
BinaryOperator::Eq => (left == right).into(), meta,
BinaryOperator::Neq => (left != right).into(), },
BinaryOperator::Gt => (left > right).into(), BinaryOperator::Mul => KclValue::Number {
BinaryOperator::Gte => (left >= right).into(), value: left * right,
BinaryOperator::Lt => (left < right).into(), meta,
BinaryOperator::Lte => (left <= right).into(), },
BinaryOperator::Div => KclValue::Number {
value: left / right,
meta,
},
BinaryOperator::Mod => KclValue::Number {
value: left % right,
meta,
},
BinaryOperator::Pow => KclValue::Number {
value: left.powf(right),
meta,
},
BinaryOperator::Neq => KclValue::Bool {
value: left != right,
meta,
},
BinaryOperator::Gt => KclValue::Bool {
value: left > right,
meta,
},
BinaryOperator::Gte => KclValue::Bool {
value: left >= right,
meta,
},
BinaryOperator::Lt => KclValue::Bool {
value: left < right,
meta,
},
BinaryOperator::Lte => KclValue::Bool {
value: left <= right,
meta,
},
BinaryOperator::Eq => KclValue::Bool {
value: left == right,
meta,
},
}; };
Ok(KclValue::UserVal(UserVal { Ok(value)
value,
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} }
} }
impl Node<UnaryExpression> { impl Node<UnaryExpression> {
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
if self.operator == UnaryOperator::Not { if self.operator == UnaryOperator::Not {
let value = self.argument.get_result(exec_state, ctx).await?.get_json_value()?; let value = self.argument.get_result(exec_state, ctx).await?;
let Some(bool_value) = json_as_bool(&value) else { let KclValue::Bool {
value: bool_value,
meta: _,
} = value
else {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!("Cannot apply unary operator ! to non-boolean value: {}", value), message: format!(
"Cannot apply unary operator ! to non-boolean value: {}",
value.human_friendly_type()
),
source_ranges: vec![self.into()], source_ranges: vec![self.into()],
})); }));
}; };
let negated = !bool_value; let meta = vec![Metadata {
return Ok(KclValue::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }];
})); let negated = KclValue::Bool {
value: !bool_value,
meta,
};
return Ok(negated);
} }
let num = parse_json_number_as_f64( let value = &self.argument.get_result(exec_state, ctx).await?;
&self.argument.get_result(exec_state, ctx).await?.get_json_value()?, match value {
self.into(), KclValue::Number { value, meta: _ } => {
)?; let meta = vec![Metadata {
Ok(KclValue::UserVal(UserVal {
value: (-(num)).into(),
meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }];
})) Ok(KclValue::Number { value: -value, meta })
}
KclValue::Int { value, meta: _ } => {
let meta = vec![Metadata {
source_range: self.into(),
}];
Ok(KclValue::Number {
value: (-value) as f64,
meta,
})
}
_ => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"You can only negate numbers, but this is a {}",
value.human_friendly_type()
),
source_ranges: vec![self.into()],
})),
}
} }
} }
@ -325,13 +384,10 @@ impl Node<CallExpression> {
// TODO: This could probably be done in a better way, but as of now this was my only idea // TODO: This could probably be done in a better way, but as of now this was my only idea
// and it works. // and it works.
match result { match result {
KclValue::UserVal(ref mut uval) => { KclValue::Sketch { value: ref mut sketch } => {
uval.mutate(|sketch: &mut Sketch| {
for (_, tag) in sketch.tags.iter() { for (_, tag) in sketch.tags.iter() {
exec_state.memory.update_tag(&tag.value, tag.clone())?; exec_state.memory.update_tag(&tag.value, tag.clone())?;
} }
Ok::<_, KclError>(())
})?;
} }
KclValue::Solid(ref mut solid) => { KclValue::Solid(ref mut solid) => {
for value in &solid.value { for value in &solid.value {
@ -425,10 +481,10 @@ impl Node<CallExpression> {
} else { } else {
fn_memory.add( fn_memory.add(
&param.identifier.name, &param.identifier.name,
KclValue::UserVal(UserVal { KclValue::KclNone {
value: serde_json::value::Value::Null, value: KclNone::new(),
meta: Default::default(), meta: vec![self.into()],
}), },
param.identifier.clone().into(), param.identifier.clone().into(),
)?; )?;
} }
@ -531,15 +587,13 @@ impl Node<ArrayExpression> {
.execute_expr(element, exec_state, &metadata, StatementKind::Expression) .execute_expr(element, exec_state, &metadata, StatementKind::Expression)
.await?; .await?;
results.push(value.get_json_value()?); results.push(value);
} }
Ok(KclValue::UserVal(UserVal { Ok(KclValue::Array {
value: results.into(), value: results,
meta: vec![Metadata { meta: vec![self.into()],
source_range: self.into(), })
}],
}))
} }
} }
@ -549,15 +603,19 @@ impl Node<ArrayRangeExpression> {
let metadata = Metadata::from(&self.start_element); let metadata = Metadata::from(&self.start_element);
let start = ctx let start = ctx
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression) .execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
.await? .await?;
.get_json_value()?; let start = start.as_int().ok_or(KclError::Semantic(KclErrorDetails {
let start = parse_json_number_as_i64(&start, (&self.start_element).into())?; source_ranges: vec![self.into()],
message: format!("Expected int but found {}", start.human_friendly_type()),
}))?;
let metadata = Metadata::from(&self.end_element); let metadata = Metadata::from(&self.end_element);
let end = ctx let end = ctx
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression) .execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
.await? .await?;
.get_json_value()?; let end = end.as_int().ok_or(KclError::Semantic(KclErrorDetails {
let end = parse_json_number_as_i64(&end, (&self.end_element).into())?; source_ranges: vec![self.into()],
message: format!("Expected int but found {}", end.human_friendly_type()),
}))?;
if end < start { if end < start {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
@ -567,91 +625,73 @@ impl Node<ArrayRangeExpression> {
} }
let range: Vec<_> = if self.end_inclusive { let range: Vec<_> = if self.end_inclusive {
(start..=end).map(JValue::from).collect() (start..=end).collect()
} else { } else {
(start..end).map(JValue::from).collect() (start..end).collect()
}; };
Ok(KclValue::UserVal(UserVal { let meta = vec![Metadata {
value: range.into(),
meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }];
})) Ok(KclValue::Array {
value: range
.into_iter()
.map(|num| KclValue::Int {
value: num,
meta: meta.clone(),
})
.collect(),
meta,
})
} }
} }
impl Node<ObjectExpression> { impl Node<ObjectExpression> {
#[async_recursion] #[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let mut object = serde_json::Map::new(); let mut object = HashMap::with_capacity(self.properties.len());
for property in &self.properties { for property in &self.properties {
let metadata = Metadata::from(&property.value); let metadata = Metadata::from(&property.value);
let result = ctx let result = ctx
.execute_expr(&property.value, exec_state, &metadata, StatementKind::Expression) .execute_expr(&property.value, exec_state, &metadata, StatementKind::Expression)
.await?; .await?;
object.insert(property.key.name.clone(), result.get_json_value()?); object.insert(property.key.name.clone(), result);
} }
Ok(KclValue::UserVal(UserVal { Ok(KclValue::Object {
value: object.into(), value: object,
meta: vec![Metadata { meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }],
})) })
} }
} }
fn parse_json_number_as_i64(j: &serde_json::Value, source_range: SourceRange) -> Result<i64, KclError> { fn article_for(s: &str) -> &'static str {
if let serde_json::Value::Number(n) = &j { if s.starts_with(['a', 'e', 'i', 'o', 'u']) {
n.as_i64().ok_or_else(|| { "an"
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
})
})
} else { } else {
Err(KclError::Syntax(KclErrorDetails { "a"
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
}))
} }
} }
pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> { pub fn parse_number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<f64, KclError> {
if let serde_json::Value::Number(n) = &j { if let KclValue::Number { value: n, .. } = &v {
n.as_f64().ok_or_else(|| { Ok(*n)
KclError::Syntax(KclErrorDetails { } else if let KclValue::Int { value: n, .. } = &v {
source_ranges: vec![source_range], Ok(*n as f64)
message: format!("Invalid number: {}", j),
})
})
} else { } else {
Err(KclError::Syntax(KclErrorDetails { let actual_type = v.human_friendly_type();
source_ranges: vec![source_range], let article = if actual_type.starts_with(['a', 'e', 'i', 'o', 'u']) {
message: format!("Invalid number: {}", j), "an"
}))
}
}
pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
if let serde_json::Value::String(n) = &j {
Some(n.clone())
} else { } else {
None "a"
} };
} Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![source_range],
/// JSON value as bool. If it isn't a bool, returns None. message: format!("Expected a number, but found {article} {actual_type}",),
pub fn json_as_bool(j: &serde_json::Value) -> Option<bool> { }))
match j {
JValue::Null => None,
JValue::Bool(b) => Some(*b),
JValue::Number(_) => None,
JValue::String(_) => None,
JValue::Array(_) => None,
JValue::Object(_) => None,
} }
} }
@ -724,15 +764,7 @@ impl Property {
} else { } else {
// Actually evaluate memory to compute the property. // Actually evaluate memory to compute the property.
let prop = exec_state.memory.get(name, property_src)?; let prop = exec_state.memory.get(name, property_src)?;
let KclValue::UserVal(prop) = prop else { jvalue_to_prop(prop, property_sr, name)
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
),
}));
};
jvalue_to_prop(&prop.value, property_sr, name)
} }
} }
LiteralIdentifier::Literal(literal) => { LiteralIdentifier::Literal(literal) => {
@ -759,35 +791,37 @@ impl Property {
} }
} }
fn jvalue_to_prop(value: &JValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> { fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
let make_err = |message: String| { let make_err = |message: String| {
Err::<Property, _>(KclError::Semantic(KclErrorDetails { Err::<Property, _>(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr, source_ranges: property_sr,
message, message,
})) }))
}; };
const MUST_BE_POSINT: &str = "indices must be whole positive numbers";
const TRY_INT: &str = "try using the int() function to make this a whole number";
match value { match value {
JValue::Number(ref num) => { KclValue::Int { value:num, meta: _ } => {
let maybe_uint = num.as_u64().and_then(|x| usize::try_from(x).ok()); let maybe_int: Result<usize, _> = (*num).try_into();
if let Some(uint) = maybe_uint { if let Ok(uint) = maybe_int {
Ok(Property::Number(uint)) Ok(Property::Number(uint))
} else if let Some(iint) = num.as_i64() { }
make_err(format!("'{iint}' is not a valid index, {MUST_BE_POSINT}")) else {
} else if let Some(fnum) = num.as_f64() { make_err(format!("'{num}' is negative, so you can't index an array with it"))
if fnum < 0.0 { }
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}")) }
} else if fnum.fract() == 0.0 { KclValue::Number{value: num, meta:_} => {
make_err(format!("'{fnum:.1}' is stored as a fractional number but indices must be whole numbers, {TRY_INT}")) let num = *num;
if num < 0.0 {
return make_err(format!("'{num}' is negative, so you can't index an array with it"))
}
let nearest_int = num.round();
let delta = num-nearest_int;
if delta < FLOAT_TO_INT_MAX_DELTA {
Ok(Property::Number(nearest_int as usize))
} else { } else {
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}, {TRY_INT}")) make_err(format!("'{num}' is not an integer, so you can't index an array with it"))
}
} else {
make_err(format!("'{num}' is not a valid index, {MUST_BE_POSINT}"))
} }
} }
JValue::String(ref x) => Ok(Property::String(x.to_owned())), KclValue::String{value: x, meta:_} => Ok(Property::String(x.to_owned())),
_ => { _ => {
make_err(format!("{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array")) make_err(format!("{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array"))
} }

View File

@ -4,10 +4,7 @@ use databake::*;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{ast::types::ConstraintLevel, executor::KclValue};
ast::types::ConstraintLevel,
executor::{KclValue, UserVal},
};
use super::Node; use super::Node;
@ -16,7 +13,7 @@ const KCL_NONE_ID: &str = "KCL_NONE_ID";
/// KCL value for an optional parameter which was not given an argument. /// KCL value for an optional parameter which was not given an argument.
/// (remember, parameters are in the function declaration, /// (remember, parameters are in the function declaration,
/// arguments are in the function call/application). /// arguments are in the function call/application).
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake, Default)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake, Default, Copy)]
#[databake(path = kcl_lib::ast::types)] #[databake(path = kcl_lib::ast::types)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -58,19 +55,12 @@ where
} }
} }
impl From<&KclNone> for UserVal {
fn from(none: &KclNone) -> Self {
UserVal {
value: serde_json::to_value(none).expect("can always serialize a None"),
meta: Default::default(),
}
}
}
impl From<&KclNone> for KclValue { impl From<&KclNone> for KclValue {
fn from(none: &KclNone) -> Self { fn from(none: &KclNone) -> Self {
let val = UserVal::from(none); KclValue::KclNone {
KclValue::UserVal(val) value: *none,
meta: Default::default(),
}
} }
} }

View File

@ -790,6 +790,7 @@ fn test_generate_stdlib_json_schema() {
// If this test fails and you've modified the AST or something else which affects the json repr // If this test fails and you've modified the AST or something else which affects the json repr
// of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new // of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new
// test data, then check `/docs/kcl/std.json` to ensure the changes are expected. // test data, then check `/docs/kcl/std.json` to ensure the changes are expected.
// Alternatively, run `just redo-kcl-stdlib-docs` (make sure to have just installed).
let stdlib = StdLib::new(); let stdlib = StdLib::new();
let combined = stdlib.combined(); let combined = stdlib.combined();

View File

@ -19,7 +19,6 @@ use kittycad_modeling_cmds::length_unit::LengthUnit;
use parse_display::{Display, FromStr}; use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange}; use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
type Point2D = kcmc::shared::Point2d<f64>; type Point2D = kcmc::shared::Point2d<f64>;
@ -27,8 +26,8 @@ type Point3D = kcmc::shared::Point3d<f64>;
use crate::{ use crate::{
ast::types::{ ast::types::{
human_friendly_type, BodyItem, Expr, FunctionExpression, ItemVisibility, KclNone, ModuleId, Node, NodeRef, BodyItem, Expr, FunctionExpression, ItemVisibility, KclNone, ModuleId, Node, NodeRef, Program, TagDeclarator,
Program, TagDeclarator, TagNode, TagNode,
}, },
engine::{EngineManager, ExecutionKind}, engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
@ -201,33 +200,18 @@ impl Environment {
Self { Self {
// Prelude // Prelude
bindings: HashMap::from([ bindings: HashMap::from([
( ("ZERO".to_string(), KclValue::from_number(0.0, Default::default())),
"ZERO".to_string(),
KclValue::UserVal(UserVal {
value: serde_json::Value::Number(serde_json::value::Number::from(0)),
meta: Default::default(),
}),
),
( (
"QUARTER_TURN".to_string(), "QUARTER_TURN".to_string(),
KclValue::UserVal(UserVal { KclValue::from_number(90.0, Default::default()),
value: serde_json::Value::Number(serde_json::value::Number::from(90)),
meta: Default::default(),
}),
), ),
( (
"HALF_TURN".to_string(), "HALF_TURN".to_string(),
KclValue::UserVal(UserVal { KclValue::from_number(180.0, Default::default()),
value: serde_json::Value::Number(serde_json::value::Number::from(180)),
meta: Default::default(),
}),
), ),
( (
"THREE_QUARTER_TURN".to_string(), "THREE_QUARTER_TURN".to_string(),
KclValue::UserVal(UserVal { KclValue::from_number(270.0, Default::default()),
value: serde_json::Value::Number(serde_json::value::Number::from(270)),
meta: Default::default(),
}),
), ),
]), ]),
parent: None, parent: None,
@ -264,22 +248,15 @@ impl Environment {
} }
for (_, val) in self.bindings.iter_mut() { for (_, val) in self.bindings.iter_mut() {
let KclValue::UserVal(v) = val else { continue }; let KclValue::Sketch { value } = val else { continue };
let meta = v.meta.clone(); let mut sketch = value.to_owned();
let maybe_sg: Result<Sketch, _> = serde_json::from_value(v.value.clone());
let Ok(mut sketch) = maybe_sg else {
continue;
};
if sketch.original_id == sg.original_id { if sketch.original_id == sg.original_id {
for tag in sg.tags.iter() { for tag in sg.tags.iter() {
sketch.tags.insert(tag.0.clone(), tag.1.clone()); sketch.tags.insert(tag.0.clone(), tag.1.clone());
} }
} }
*val = KclValue::UserVal(UserVal { *val = KclValue::Sketch { value: sketch };
meta,
value: serde_json::to_value(sketch).expect("can always turn Sketch into JSON"),
});
} }
} }
} }
@ -360,12 +337,52 @@ impl IdGenerator {
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum KclValue { pub enum KclValue {
UserVal(UserVal), Uuid {
value: ::uuid::Uuid,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
Bool {
value: bool,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
Number {
value: f64,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
Int {
value: i64,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
String {
value: String,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
Array {
value: Vec<KclValue>,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
Object {
value: HashMap<String, KclValue>,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
TagIdentifier(Box<TagIdentifier>), TagIdentifier(Box<TagIdentifier>),
TagDeclarator(crate::ast::types::BoxNode<TagDeclarator>), TagDeclarator(crate::ast::types::BoxNode<TagDeclarator>),
Plane(Box<Plane>), Plane(Box<Plane>),
Face(Box<Face>), Face(Box<Face>),
Sketch {
value: Box<Sketch>,
},
Sketches {
value: Vec<Box<Sketch>>,
},
Solid(Box<Solid>), Solid(Box<Solid>),
Solids { Solids {
value: Vec<Box<Solid>>, value: Vec<Box<Solid>>,
@ -380,31 +397,55 @@ pub enum KclValue {
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
meta: Vec<Metadata>, meta: Vec<Metadata>,
}, },
KclNone {
value: KclNone,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
} }
impl KclValue { impl KclValue {
pub(crate) fn new_user_val<T: Serialize>(meta: Vec<Metadata>, val: T) -> Self { pub(crate) fn metadata(&self) -> Vec<Metadata> {
Self::UserVal(UserVal::new(meta, val)) match self {
KclValue::Uuid { value: _, meta } => meta.clone(),
KclValue::Bool { value: _, meta } => meta.clone(),
KclValue::Number { value: _, meta } => meta.clone(),
KclValue::Int { value: _, meta } => meta.clone(),
KclValue::String { value: _, meta } => meta.clone(),
KclValue::Array { value: _, meta } => meta.clone(),
KclValue::Object { value: _, meta } => meta.clone(),
KclValue::TagIdentifier(x) => x.meta.clone(),
KclValue::TagDeclarator(x) => vec![x.metadata()],
KclValue::Plane(x) => x.meta.clone(),
KclValue::Face(x) => x.meta.clone(),
KclValue::Sketch { value } => value.meta.clone(),
KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Solid(x) => x.meta.clone(),
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::ImportedGeometry(x) => x.meta.clone(),
KclValue::Function { meta, .. } => meta.clone(),
KclValue::KclNone { meta, .. } => meta.clone(),
}
} }
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> { pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
match self { match self {
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())), KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())), KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
KclValue::UserVal(value) => { KclValue::Array { value, .. } => {
let value = value.value.clone(); let solids: Vec<_> = value
match value { .iter()
JValue::Null | JValue::Bool(_) | JValue::Number(_) | JValue::String(_) => Err(anyhow::anyhow!( .enumerate()
"Failed to deserialize solid set from JSON {}", .map(|(i, v)| {
human_friendly_type(&value) v.as_solid().map(|v| v.to_owned()).map(Box::new).ok_or_else(|| {
)), anyhow::anyhow!(
JValue::Array(_) => serde_json::from_value::<Vec<Box<Solid>>>(value) "expected this array to only contain solids, but element {i} was actually {}",
.map(SolidSet::from) v.human_friendly_type()
.map_err(|e| anyhow::anyhow!("Failed to deserialize array of solids from JSON: {}", e)), )
JValue::Object(_) => serde_json::from_value::<Box<Solid>>(value) })
.map(SolidSet::from) })
.map_err(|e| anyhow::anyhow!("Failed to deserialize solid from JSON: {}", e)), .collect::<Result<_, _>>()?;
} Ok(SolidSet::Solids(solids))
} }
_ => anyhow::bail!("Not a solid or solids: {:?}", self), _ => anyhow::bail!("Not a solid or solids: {:?}", self),
} }
@ -414,43 +455,44 @@ impl KclValue {
/// on for program logic. /// on for program logic.
pub(crate) fn human_friendly_type(&self) -> &'static str { pub(crate) fn human_friendly_type(&self) -> &'static str {
match self { match self {
KclValue::UserVal(u) => human_friendly_type(&u.value), KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator", KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier", KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid(_) => "Solid", KclValue::Solid(_) => "Solid",
KclValue::Solids { .. } => "Solids", KclValue::Solids { .. } => "Solids",
KclValue::Sketch { .. } => "Sketch",
KclValue::Sketches { .. } => "Sketches",
KclValue::ImportedGeometry(_) => "ImportedGeometry", KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function", KclValue::Function { .. } => "Function",
KclValue::Plane(_) => "Plane", KclValue::Plane(_) => "Plane",
KclValue::Face(_) => "Face", KclValue::Face(_) => "Face",
KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { .. } => "number",
KclValue::Int { .. } => "integer",
KclValue::String { .. } => "string (text)",
KclValue::Array { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::KclNone { .. } => "None",
} }
} }
pub(crate) fn is_function(&self) -> bool { pub(crate) fn is_function(&self) -> bool {
match self { matches!(self, KclValue::Function { .. })
KclValue::UserVal(..)
| KclValue::TagIdentifier(..)
| KclValue::TagDeclarator(..)
| KclValue::Plane(..)
| KclValue::Face(..)
| KclValue::Solid(..)
| KclValue::Solids { .. }
| KclValue::ImportedGeometry(..) => false,
KclValue::Function { .. } => true,
}
} }
} }
impl From<SketchSet> for KclValue { impl From<SketchSet> for KclValue {
fn from(sg: SketchSet) -> Self { fn from(sg: SketchSet) -> Self {
KclValue::UserVal(UserVal::new(sg.meta(), sg)) match sg {
SketchSet::Sketch(value) => KclValue::Sketch { value },
SketchSet::Sketches(value) => KclValue::Sketches { value },
}
} }
} }
impl From<Vec<Box<Sketch>>> for KclValue { impl From<Vec<Box<Sketch>>> for KclValue {
fn from(sg: Vec<Box<Sketch>>) -> Self { fn from(sg: Vec<Box<Sketch>>) -> Self {
let meta = sg.iter().flat_map(|sg| sg.meta.clone()).collect(); KclValue::Sketches { value: sg }
KclValue::UserVal(UserVal::new(meta, sg))
} }
} }
@ -815,52 +857,6 @@ pub enum PlaneType {
Custom, Custom,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct UserVal {
#[ts(type = "any")]
pub value: serde_json::Value,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
impl UserVal {
pub fn new<T: serde::Serialize>(meta: Vec<Metadata>, val: T) -> Self {
Self {
meta,
value: serde_json::to_value(val).expect("all KCL values should be compatible with JSON"),
}
}
/// If the UserVal matches the type `T`, return it.
pub fn get<T: serde::de::DeserializeOwned>(&self) -> Option<(T, Vec<Metadata>)> {
let meta = self.meta.clone();
// TODO: This clone might cause performance problems, it'll happen a lot.
let res: Result<T, _> = serde_json::from_value(self.value.clone());
if let Ok(t) = res {
Some((t, meta))
} else {
None
}
}
/// If the UserVal matches the type `T`, then mutate it via the given closure.
/// If the closure returns Err, the mutation won't be applied.
pub fn mutate<T, F, E>(&mut self, mutate: F) -> Result<(), E>
where
T: serde::de::DeserializeOwned + Serialize,
F: FnOnce(&mut T) -> Result<(), E>,
{
let Some((mut val, meta)) = self.get::<T>() else {
return Ok(());
};
mutate(&mut val)?;
*self = Self::new(meta, val);
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
@ -922,108 +918,177 @@ pub type MemoryFunction =
impl From<KclValue> for Vec<SourceRange> { impl From<KclValue> for Vec<SourceRange> {
fn from(item: KclValue) -> Self { fn from(item: KclValue) -> Self {
match item { match item {
KclValue::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(), KclValue::TagDeclarator(t) => vec![SourceRange([t.start, t.end, t.module_id.0 as usize])],
KclValue::TagDeclarator(t) => vec![(&t).into()], KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::TagIdentifier(t) => t.meta.iter().map(|m| m.source_range).collect(), KclValue::Solid(e) => to_vec_sr(&e.meta),
KclValue::Solid(e) => e.meta.iter().map(|m| m.source_range).collect(), KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Solids { value } => value KclValue::Sketch { value } => to_vec_sr(&value.meta),
.iter() KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range)) KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
.collect(), KclValue::Function { meta, .. } => to_vec_sr(&meta),
KclValue::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(), KclValue::Plane(p) => to_vec_sr(&p.meta),
KclValue::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(), KclValue::Face(f) => to_vec_sr(&f.meta),
KclValue::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(), KclValue::Bool { meta, .. } => to_vec_sr(&meta),
KclValue::Face(f) => f.meta.iter().map(|m| m.source_range).collect(), KclValue::Number { meta, .. } => to_vec_sr(&meta),
KclValue::Int { meta, .. } => to_vec_sr(&meta),
KclValue::String { meta, .. } => to_vec_sr(&meta),
KclValue::Array { meta, .. } => to_vec_sr(&meta),
KclValue::Object { meta, .. } => to_vec_sr(&meta),
KclValue::Uuid { meta, .. } => to_vec_sr(&meta),
KclValue::KclNone { meta, .. } => to_vec_sr(&meta),
} }
} }
} }
fn to_vec_sr(meta: &[Metadata]) -> Vec<SourceRange> {
meta.iter().map(|m| m.source_range).collect()
}
impl From<&KclValue> for Vec<SourceRange> { impl From<&KclValue> for Vec<SourceRange> {
fn from(item: &KclValue) -> Self { fn from(item: &KclValue) -> Self {
match item { match item {
KclValue::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(), KclValue::TagDeclarator(t) => vec![SourceRange([t.start, t.end, t.module_id.0 as usize])],
KclValue::TagDeclarator(ref t) => vec![t.into()], KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::TagIdentifier(t) => t.meta.iter().map(|m| m.source_range).collect(), KclValue::Solid(e) => to_vec_sr(&e.meta),
KclValue::Solid(e) => e.meta.iter().map(|m| m.source_range).collect(), KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Solids { value } => value KclValue::Sketch { value } => to_vec_sr(&value.meta),
.iter() KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range)) KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
.collect(), KclValue::Function { meta, .. } => to_vec_sr(meta),
KclValue::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(), KclValue::Plane(p) => to_vec_sr(&p.meta),
KclValue::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(), KclValue::Face(f) => to_vec_sr(&f.meta),
KclValue::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(), KclValue::Bool { meta, .. } => to_vec_sr(meta),
KclValue::Face(f) => f.meta.iter().map(|m| m.source_range).collect(), KclValue::Number { meta, .. } => to_vec_sr(meta),
KclValue::Int { meta, .. } => to_vec_sr(meta),
KclValue::String { meta, .. } => to_vec_sr(meta),
KclValue::Uuid { meta, .. } => to_vec_sr(meta),
KclValue::Array { meta, .. } => to_vec_sr(meta),
KclValue::Object { meta, .. } => to_vec_sr(meta),
KclValue::KclNone { meta, .. } => to_vec_sr(meta),
} }
} }
} }
impl KclValue { impl KclValue {
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> { /// Put the number into a KCL value.
if let KclValue::UserVal(user_val) = self { pub fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
Ok(user_val.value.clone()) Self::Number { value: f, meta }
} else { }
serde_json::to_value(self).map_err(|err| {
KclError::Semantic(KclErrorDetails { /// Put the point into a KCL value.
message: format!("Cannot convert memory item to json value: {:?}", err), pub fn from_point2d(p: [f64; 2], meta: Vec<Metadata>) -> Self {
source_ranges: self.clone().into(), Self::Array {
}) value: vec![
}) Self::Number {
value: p[0],
meta: meta.clone(),
},
Self::Number {
value: p[1],
meta: meta.clone(),
},
],
meta,
} }
} }
/// Get a JSON value and deserialize it into some concrete type. pub(crate) fn as_usize(&self) -> Option<usize> {
pub fn get_json<T: serde::de::DeserializeOwned>(&self) -> Result<T, KclError> { match self {
let json = self.get_json_value()?; KclValue::Int { value, .. } => Some(*value as usize),
_ => None,
serde_json::from_value(json).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to deserialize struct from JSON: {}", e),
source_ranges: self.clone().into(),
})
})
}
/// Get a JSON value and deserialize it into some concrete type.
/// If it's a KCL None, return None. Otherwise return Some.
pub fn get_json_opt<T: serde::de::DeserializeOwned>(&self) -> Result<Option<T>, KclError> {
let json = self.get_json_value()?;
if let JValue::Object(ref o) = json {
if let Some(JValue::String(s)) = o.get("type") {
if s == "KclNone" {
return Ok(None);
}
} }
} }
serde_json::from_value(json) pub fn as_int(&self) -> Option<i64> {
.map_err(|e| { if let KclValue::Int { value, meta: _ } = &self {
KclError::Type(KclErrorDetails { Some(*value)
message: format!("Failed to deserialize struct from JSON: {}", e),
source_ranges: self.clone().into(),
})
})
.map(Some)
}
pub fn as_user_val(&self) -> Option<&UserVal> {
if let KclValue::UserVal(x) = self {
Some(x)
} else { } else {
None None
} }
} }
/// If this value is of type u32, return it. pub fn as_object(&self) -> Option<&HashMap<String, KclValue>> {
if let KclValue::Object { value, meta: _ } = &self {
Some(value)
} else {
None
}
}
pub fn as_str(&self) -> Option<&str> {
if let KclValue::String { value, meta: _ } = &self {
Some(value)
} else {
None
}
}
pub fn as_array(&self) -> Option<&[KclValue]> {
if let KclValue::Array { value, meta: _ } = &self {
Some(value)
} else {
None
}
}
pub fn as_point2d(&self) -> Option<[f64; 2]> {
let arr = self.as_array()?;
if arr.len() != 2 {
return None;
}
let x = arr[0].as_f64()?;
let y = arr[1].as_f64()?;
Some([x, y])
}
pub fn as_uuid(&self) -> Option<uuid::Uuid> {
if let KclValue::Uuid { value, meta: _ } = &self {
Some(*value)
} else {
None
}
}
pub fn as_solid(&self) -> Option<&Solid> {
if let KclValue::Solid(value) = &self {
Some(value)
} else {
None
}
}
pub fn as_f64(&self) -> Option<f64> {
if let KclValue::Number { value, meta: _ } = &self {
Some(*value)
} else if let KclValue::Int { value, meta: _ } = &self {
Some(*value as f64)
} else {
None
}
}
pub fn as_bool(&self) -> Option<bool> {
if let KclValue::Bool { value, meta: _ } = &self {
Some(*value)
} else {
None
}
}
/// If this value fits in a u32, return it.
pub fn get_u32(&self, source_ranges: Vec<SourceRange>) -> Result<u32, KclError> { pub fn get_u32(&self, source_ranges: Vec<SourceRange>) -> Result<u32, KclError> {
let err = KclError::Semantic(KclErrorDetails { let u = self.as_int().and_then(|n| u64::try_from(n).ok()).ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: "Expected an integer >= 0".to_owned(), message: "Expected an integer >= 0".to_owned(),
source_ranges: source_ranges.clone(),
})
})?;
u32::try_from(u).map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: "Number was too big".to_owned(),
source_ranges, source_ranges,
}); })
self.as_user_val() })
.and_then(|uv| uv.value.as_number())
.and_then(|n| n.as_u64())
.and_then(|n| u32::try_from(n).ok())
.ok_or(err)
} }
/// If this value is of type function, return it. /// If this value is of type function, return it.
@ -1048,16 +1113,6 @@ impl KclValue {
pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> { pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
match self { match self {
KclValue::TagIdentifier(t) => Ok(*t.clone()), KclValue::TagIdentifier(t) => Ok(*t.clone()),
KclValue::UserVal(_) => {
if let Some(identifier) = self.get_json_opt::<TagIdentifier>()? {
Ok(identifier)
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("Not a tag identifier: {:?}", self),
source_ranges: self.clone().into(),
}))
}
}
_ => Err(KclError::Semantic(KclErrorDetails { _ => Err(KclError::Semantic(KclErrorDetails {
message: format!("Not a tag identifier: {:?}", self), message: format!("Not a tag identifier: {:?}", self),
source_ranges: self.clone().into(), source_ranges: self.clone().into(),
@ -1089,19 +1144,13 @@ impl KclValue {
/// If this KCL value is a bool, retrieve it. /// If this KCL value is a bool, retrieve it.
pub fn get_bool(&self) -> Result<bool, KclError> { pub fn get_bool(&self) -> Result<bool, KclError> {
let Self::UserVal(uv) = self else { let Self::Bool { value: b, .. } = self else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
source_ranges: self.into(), source_ranges: self.into(),
message: format!("Expected bool, found {}", self.human_friendly_type()), message: format!("Expected bool, found {}", self.human_friendly_type()),
})); }));
}; };
let JValue::Bool(b) = uv.value else { Ok(*b)
return Err(KclError::Type(KclErrorDetails {
source_ranges: self.into(),
message: format!("Expected bool, found {}", human_friendly_type(&uv.value)),
}));
};
Ok(b)
} }
/// If this memory item is a function, call it with the given arguments, return its val as Ok. /// If this memory item is a function, call it with the given arguments, return its val as Ok.
@ -1555,7 +1604,7 @@ impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
} }
/// Metadata. /// Metadata.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Metadata { pub struct Metadata {
@ -1563,6 +1612,12 @@ pub struct Metadata {
pub source_range: SourceRange, pub source_range: SourceRange,
} }
impl From<Metadata> for Vec<SourceRange> {
fn from(meta: Metadata) -> Self {
vec![meta.source_range]
}
}
impl From<SourceRange> for Metadata { impl From<SourceRange> for Metadata {
fn from(source_range: SourceRange) -> Self { fn from(source_range: SourceRange) -> Self {
Self { source_range } Self { source_range }
@ -2655,74 +2710,8 @@ mod tests {
} }
/// Convenience function to get a JSON value from memory and unwrap. /// Convenience function to get a JSON value from memory and unwrap.
fn mem_get_json(memory: &ProgramMemory, name: &str) -> serde_json::Value { fn mem_get_json(memory: &ProgramMemory, name: &str) -> KclValue {
memory memory.get(name, SourceRange::default()).unwrap().to_owned()
.get(name, SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_assign_two_variables() {
let ast = r#"const myVar = 5
const newVar = myVar + 1"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(6.0),
memory
.get("newVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_angled_line_that_intersects() {
let ast_fn = |offset: &str| -> String {
format!(
r#"const part001 = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> lineTo([2, 2], %, $yo)
|> lineTo([3, 1], %)
|> angledLineThatIntersects({{
angle: 180,
intersectTag: yo,
offset: {},
}}, %, $yo2)
const intersect = segEndX(yo2)"#,
offset
)
};
let memory = parse_execute(&ast_fn("-1")).await.unwrap();
assert_eq!(
serde_json::json!(1.0 + 2.0f64.sqrt()),
memory
.get("intersect", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
let memory = parse_execute(&ast_fn("0")).await.unwrap();
assert_eq!(
serde_json::json!(1.0000000000000002),
memory
.get("intersect", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -3120,200 +3109,41 @@ let shape = layer() |> patternTransform(10, transform, %)
); );
} }
#[tokio::test(flavor = "multi_thread")] // ADAM: Move some of these into simulation tests.
async fn test_execute_function_with_parameter_redefined_outside() {
let ast = r#"
fn myIdentity = (x) => {
return x
}
const x = 33
const two = myIdentity(2)"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(2),
memory
.get("two", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(33),
memory
.get("x", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_referencing_variable_in_parent_scope() {
let ast = r#"
const x = 22
const y = 3
fn add = (x) => {
return x + y
}
const answer = add(2)"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5.0),
memory
.get("answer", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(22),
memory
.get("x", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_redefining_variable_in_parent_scope() {
let ast = r#"
const x = 1
fn foo = () => {
const x = 2
return x
}
const answer = foo()"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(2),
memory
.get("answer", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(1),
memory
.get("x", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_pattern_transform_function_redefining_variable_in_parent_scope() {
let ast = r#"
const scale = 100
fn transform = (replicaId) => {
// Redefine same variable as in parent scope.
const scale = 2
return {
translate: [0, 0, replicaId * 10],
scale: [scale, 1, 0],
}
}
fn layer = () => {
return startSketchOn("XY")
|> circle({ center: [0, 0], radius: 1 }, %, $tag1)
|> extrude(10, %)
}
// The 10 layers are replicas of each other, with a transform applied to each.
let shape = layer() |> patternTransform(10, transform, %)"#;
let memory = parse_execute(ast).await.unwrap();
// TODO: Assert that scale 2 was used.
assert_eq!(
serde_json::json!(100),
memory
.get("scale", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() { async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#; let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(5.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
serde_json::json!(5.0),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute() { async fn test_math_execute() {
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#; let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(7.4, mem_get_json(&memory, "myVar").as_f64().unwrap());
serde_json::json!(7.4),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_start_negative() { async fn test_math_execute_start_negative() {
let ast = r#"const myVar = -5 + 6"#; let ast = r#"const myVar = -5 + 6"#;
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(1.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
serde_json::json!(1.0),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_pi() { async fn test_math_execute_with_pi() {
let ast = r#"const myVar = pi() * 2"#; let ast = r#"const myVar = pi() * 2"#;
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(std::f64::consts::TAU, mem_get_json(&memory, "myVar").as_f64().unwrap());
serde_json::json!(std::f64::consts::TAU),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_define_decimal_without_leading_zero() { async fn test_math_define_decimal_without_leading_zero() {
let ast = r#"let thing = .4 + 7"#; let ast = r#"let thing = .4 + 7"#;
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(7.4, mem_get_json(&memory, "thing").as_f64().unwrap());
serde_json::json!(7.4),
memory
.get("thing", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -3353,10 +3183,10 @@ fn check = (x) => {
check(false) check(false)
"#; "#;
let mem = parse_execute(ast).await.unwrap(); let mem = parse_execute(ast).await.unwrap();
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "notTrue")); assert_eq!(false, mem_get_json(&mem, "notTrue").as_bool().unwrap());
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "notFalse")); assert_eq!(true, mem_get_json(&mem, "notFalse").as_bool().unwrap());
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "c")); assert_eq!(true, mem_get_json(&mem, "c").as_bool().unwrap());
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "d")); assert_eq!(false, mem_get_json(&mem, "d").as_bool().unwrap());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -3369,7 +3199,7 @@ let notNull = !myNull
assert_eq!( assert_eq!(
parse_execute(code1).await.unwrap_err().downcast::<KclError>().unwrap(), parse_execute(code1).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: null".to_owned(), message: "Cannot apply unary operator ! to non-boolean value: number".to_owned(),
source_ranges: vec![SourceRange([56, 63, 0])], source_ranges: vec![SourceRange([56, 63, 0])],
}) })
); );
@ -3378,7 +3208,7 @@ let notNull = !myNull
assert_eq!( assert_eq!(
parse_execute(code2).await.unwrap_err().downcast::<KclError>().unwrap(), parse_execute(code2).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: 0".to_owned(), message: "Cannot apply unary operator ! to non-boolean value: integer".to_owned(),
source_ranges: vec![SourceRange([14, 16, 0])], source_ranges: vec![SourceRange([14, 16, 0])],
}) })
); );
@ -3389,7 +3219,7 @@ let notEmptyString = !""
assert_eq!( assert_eq!(
parse_execute(code3).await.unwrap_err().downcast::<KclError>().unwrap(), parse_execute(code3).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: \"\"".to_owned(), message: "Cannot apply unary operator ! to non-boolean value: string (text)".to_owned(),
source_ranges: vec![SourceRange([22, 25, 0])], source_ranges: vec![SourceRange([22, 25, 0])],
}) })
); );
@ -3401,7 +3231,7 @@ let notMember = !obj.a
assert_eq!( assert_eq!(
parse_execute(code4).await.unwrap_err().downcast::<KclError>().unwrap(), parse_execute(code4).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: 1".to_owned(), message: "Cannot apply unary operator ! to non-boolean value: integer".to_owned(),
source_ranges: vec![SourceRange([36, 42, 0])], source_ranges: vec![SourceRange([36, 42, 0])],
}) })
); );
@ -3412,7 +3242,7 @@ let notArray = !a";
assert_eq!( assert_eq!(
parse_execute(code5).await.unwrap_err().downcast::<KclError>().unwrap(), parse_execute(code5).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: []".to_owned(), message: "Cannot apply unary operator ! to non-boolean value: array (list)".to_owned(),
source_ranges: vec![SourceRange([27, 29, 0])], source_ranges: vec![SourceRange([27, 29, 0])],
}) })
); );
@ -3423,7 +3253,7 @@ let notObject = !x";
assert_eq!( assert_eq!(
parse_execute(code6).await.unwrap_err().downcast::<KclError>().unwrap(), parse_execute(code6).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: {}".to_owned(), message: "Cannot apply unary operator ! to non-boolean value: object".to_owned(),
source_ranges: vec![SourceRange([28, 30, 0])], source_ranges: vec![SourceRange([28, 30, 0])],
}) })
); );
@ -3451,7 +3281,7 @@ let notTagDeclarator = !myTagDeclarator";
assert!( assert!(
tag_declarator_err tag_declarator_err
.message() .message()
.starts_with("Cannot apply unary operator ! to non-boolean value: {\"type\":\"TagDeclarator\","), .starts_with("Cannot apply unary operator ! to non-boolean value: TagDeclarator"),
"Actual error: {:?}", "Actual error: {:?}",
tag_declarator_err tag_declarator_err
); );
@ -3465,7 +3295,7 @@ let notTagIdentifier = !myTag";
assert!( assert!(
tag_identifier_err tag_identifier_err
.message() .message()
.starts_with("Cannot apply unary operator ! to non-boolean value: {\"type\":\"TagIdentifier\","), .starts_with("Cannot apply unary operator ! to non-boolean value: TagIdentifier"),
"Actual error: {:?}", "Actual error: {:?}",
tag_identifier_err tag_identifier_err
); );
@ -3603,10 +3433,10 @@ let w = f() + f()
fn test_assign_args_to_params() { fn test_assign_args_to_params() {
// Set up a little framework for this test. // Set up a little framework for this test.
fn mem(number: usize) -> KclValue { fn mem(number: usize) -> KclValue {
KclValue::UserVal(UserVal { KclValue::Int {
value: number.into(), value: number as i64,
meta: Default::default(), meta: Default::default(),
}) }
} }
fn ident(s: &'static str) -> Node<Identifier> { fn ident(s: &'static str) -> Node<Identifier> {
Node::no_src(Identifier { Node::no_src(Identifier {

View File

@ -5,7 +5,7 @@ pub mod project;
use anyhow::Result; use anyhow::Result;
use parse_display::{Display, FromStr}; use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserializer, Deserialize, Serialize};
use validator::{Validate, ValidateRange}; use validator::{Validate, ValidateRange};
const DEFAULT_THEME_COLOR: f64 = 264.5; const DEFAULT_THEME_COLOR: f64 = 264.5;
@ -119,12 +119,34 @@ pub struct AppSettings {
/// This setting only applies to the web app. And is temporary until we have Linux support. /// This setting only applies to the web app. And is temporary until we have Linux support.
#[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")] #[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
pub dismiss_web_banner: bool, pub dismiss_web_banner: bool,
/// When the user is idle, and this is true, the stream will be torn down. /// When the user is idle, teardown the stream after some time.
#[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")] #[serde(default, deserialize_with = "deserialize_stream_idle_mode", alias = "streamIdleMode", skip_serializing_if = "is_default")]
stream_idle_mode: bool, stream_idle_mode: Option<u32>,
}
fn deserialize_stream_idle_mode<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StreamIdleModeValue {
String(String),
Boolean(bool),
}
const DEFAULT_TIMEOUT: u32 = 1000 * 60 * 5;
Ok(match StreamIdleModeValue::deserialize(deserializer) {
Ok(StreamIdleModeValue::String(value)) => Some(value.parse::<u32>().unwrap_or(DEFAULT_TIMEOUT)),
// The old type of this value. I'm willing to say no one used it but
// we can never guarantee it.
Ok(StreamIdleModeValue::Boolean(true)) => Some(DEFAULT_TIMEOUT),
Ok(StreamIdleModeValue::Boolean(false)) => None,
_ => None
})
} }
// TODO: When we remove backwards compatibility with the old settings file, we can remove this.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)] #[ts(export)]
#[serde(untagged)] #[serde(untagged)]
@ -582,7 +604,7 @@ textWrapping = true
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::In, base_unit: UnitLength::In,
@ -643,7 +665,7 @@ includeSettings = false
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Yd, base_unit: UnitLength::Yd,
@ -709,7 +731,7 @@ defaultProjectName = "projects-$nnn"
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Yd, base_unit: UnitLength::Yd,
@ -787,7 +809,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Mm, base_unit: UnitLength::Mm,

View File

@ -123,7 +123,7 @@ includeSettings = false
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Yd, base_unit: UnitLength::Yd,

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +1,25 @@
use derive_docs::stdlib; use derive_docs::stdlib;
use serde_json::Value as JValue;
use super::{args::FromArgs, Args, FnAsArg}; use super::{args::FromArgs, Args, FnAsArg};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ExecState, KclValue, SourceRange, UserVal}, executor::{ExecState, KclValue, SourceRange},
function_param::FunctionParam, function_param::FunctionParam,
}; };
/// Apply a function to each element of an array. /// Apply a function to each element of an array.
pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (array, f): (Vec<JValue>, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?; let (array, f): (Vec<KclValue>, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
let array: Vec<KclValue> = array let meta = vec![args.source_range.into()];
.into_iter()
.map(|jval| {
KclValue::UserVal(UserVal {
value: jval,
meta: vec![args.source_range.into()],
})
})
.collect();
let map_fn = FunctionParam { let map_fn = FunctionParam {
inner: f.func, inner: f.func,
fn_expr: f.expr, fn_expr: f.expr,
meta: vec![args.source_range.into()], meta: meta.clone(),
ctx: args.ctx.clone(), ctx: args.ctx.clone(),
memory: *f.memory, memory: *f.memory,
}; };
let new_array = inner_map(array, map_fn, exec_state, &args).await?; let new_array = inner_map(array, map_fn, exec_state, &args).await?;
let unwrapped = new_array Ok(KclValue::Array { value: new_array, meta })
.clone()
.into_iter()
.map(|k| match k {
KclValue::UserVal(user_val) => Ok(user_val.value),
_ => Err(()),
})
.collect::<Result<Vec<_>, _>>();
if let Ok(unwrapped) = unwrapped {
let uv = UserVal::new(vec![args.source_range.into()], unwrapped);
return Ok(KclValue::UserVal(uv));
}
let uv = UserVal::new(vec![args.source_range.into()], new_array);
Ok(KclValue::UserVal(uv))
} }
/// Apply a function to every element of a list. /// Apply a function to every element of a list.
@ -110,16 +88,7 @@ async fn call_map_closure<'a>(
/// For each item in an array, update a value. /// For each item in an array, update a value.
pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (array, start, f): (Vec<JValue>, KclValue, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?; let (array, start, f): (Vec<KclValue>, KclValue, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
let array: Vec<KclValue> = array
.into_iter()
.map(|jval| {
KclValue::UserVal(UserVal {
value: jval,
meta: vec![args.source_range.into()],
})
})
.collect();
let reduce_fn = FunctionParam { let reduce_fn = FunctionParam {
inner: f.func, inner: f.func,
fn_expr: f.expr, fn_expr: f.expr,
@ -206,50 +175,26 @@ async fn call_reduce_closure<'a>(
#[stdlib { #[stdlib {
name = "push", name = "push",
}] }]
async fn inner_push(array: Vec<KclValue>, elem: KclValue, args: &Args) -> Result<KclValue, KclError> { async fn inner_push(mut array: Vec<KclValue>, elem: KclValue, args: &Args) -> Result<KclValue, KclError> {
// Unwrap the KclValues to JValues for manipulation // Unwrap the KclValues to JValues for manipulation
let mut unwrapped_array = array array.push(elem);
.into_iter() Ok(KclValue::Array {
.map(|k| match k { value: array,
KclValue::UserVal(user_val) => Ok(user_val.value), meta: vec![args.source_range.into()],
_ => Err(KclError::Semantic(KclErrorDetails {
message: "Expected a UserVal in array".to_string(),
source_ranges: vec![args.source_range],
})),
}) })
.collect::<Result<Vec<_>, _>>()?;
// Unwrap the element
let unwrapped_elem = match elem {
KclValue::UserVal(user_val) => user_val.value,
_ => {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected a UserVal as element".to_string(),
source_ranges: vec![args.source_range],
}));
}
};
// Append the element to the array
unwrapped_array.push(unwrapped_elem);
// Wrap the new array into a UserVal with the source range metadata
let uv = UserVal::new(vec![args.source_range.into()], unwrapped_array);
// Return the new array wrapped as a KclValue::UserVal
Ok(KclValue::UserVal(uv))
} }
pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
// Extract the array and the element from the arguments // Extract the array and the element from the arguments
let (array_jvalues, elem): (Vec<JValue>, KclValue) = FromArgs::from_args(&args, 0)?; let (val, elem): (KclValue, KclValue) = FromArgs::from_args(&args, 0)?;
// Convert the array of JValue into Vec<KclValue> let meta = vec![args.source_range];
let array: Vec<KclValue> = array_jvalues let KclValue::Array { value: array, meta: _ } = val else {
.into_iter() let actual_type = val.human_friendly_type();
.map(|jval| KclValue::UserVal(UserVal::new(vec![args.source_range.into()], jval))) return Err(KclError::Semantic(KclErrorDetails {
.collect(); source_ranges: meta,
message: format!("You can't push to a value of type {actual_type}, only an array"),
// Call the inner_push function }));
};
inner_push(array, elem, &args).await inner_push(array, elem, &args).await
} }

View File

@ -24,7 +24,7 @@ async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError
pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, description): (bool, String) = args.get_data()?; let (data, description): (bool, String) = args.get_data()?;
inner_assert(data, &description, &args).await?; inner_assert(data, &description, &args).await?;
Ok(args.make_null_user_val()) Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check a value at runtime, and raise an error if the argument provided /// Check a value at runtime, and raise an error if the argument provided
@ -44,7 +44,7 @@ async fn inner_assert(data: bool, message: &str, args: &Args) -> Result<(), KclE
pub async fn assert_lt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_lt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?; let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_lt(left, right, &description, &args).await?; inner_assert_lt(left, right, &description, &args).await?;
Ok(args.make_null_user_val()) Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value is less than to another at runtime, /// Check that a numerical value is less than to another at runtime,
@ -63,7 +63,7 @@ async fn inner_assert_lt(left: f64, right: f64, message: &str, args: &Args) -> R
pub async fn assert_gt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_gt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?; let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_gt(left, right, &description, &args).await?; inner_assert_gt(left, right, &description, &args).await?;
Ok(args.make_null_user_val()) Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value equals another at runtime, /// Check that a numerical value equals another at runtime,
@ -96,7 +96,7 @@ async fn inner_assert_equal(left: f64, right: f64, epsilon: f64, message: &str,
pub async fn assert_equal(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_equal(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, epsilon, description): (f64, f64, f64, String) = args.get_data()?; let (left, right, epsilon, description): (f64, f64, f64, String) = args.get_data()?;
inner_assert_equal(left, right, epsilon, &description, &args).await?; inner_assert_equal(left, right, epsilon, &description, &args).await?;
Ok(args.make_null_user_val()) Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value is greater than another at runtime, /// Check that a numerical value is greater than another at runtime,
@ -115,7 +115,7 @@ async fn inner_assert_gt(left: f64, right: f64, message: &str, args: &Args) -> R
pub async fn assert_lte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_lte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?; let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_lte(left, right, &description, &args).await?; inner_assert_lte(left, right, &description, &args).await?;
Ok(args.make_null_user_val()) Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value is less than or equal to another at runtime, /// Check that a numerical value is less than or equal to another at runtime,
@ -135,7 +135,7 @@ async fn inner_assert_lte(left: f64, right: f64, message: &str, args: &Args) ->
pub async fn assert_gte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_gte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?; let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_gte(left, right, &description, &args).await?; inner_assert_gte(left, right, &description, &args).await?;
Ok(args.make_null_user_val()) Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value is greater than or equal to another at runtime, /// Check that a numerical value is greater than or equal to another at runtime,

View File

@ -233,7 +233,7 @@ pub(crate) async fn do_post_extrude(
tag: path.get_base().tag.clone(), tag: path.get_base().tag.clone(),
geo_meta: GeoMeta { geo_meta: GeoMeta {
id: path.get_base().geo_meta.id, id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(), metadata: path.get_base().geo_meta.metadata,
}, },
}); });
Some(extrude_surface) Some(extrude_surface)
@ -244,7 +244,7 @@ pub(crate) async fn do_post_extrude(
tag: path.get_base().tag.clone(), tag: path.get_base().tag.clone(),
geo_meta: GeoMeta { geo_meta: GeoMeta {
id: path.get_base().geo_meta.id, id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(), metadata: path.get_base().geo_meta.metadata,
}, },
}); });
Some(extrude_surface) Some(extrude_surface)
@ -259,7 +259,7 @@ pub(crate) async fn do_post_extrude(
tag: path.get_base().tag.clone(), tag: path.get_base().tag.clone(),
geo_meta: GeoMeta { geo_meta: GeoMeta {
id: path.get_base().geo_meta.id, id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(), metadata: path.get_base().geo_meta.metadata,
}, },
}); });
Some(extrude_surface) Some(extrude_surface)

View File

@ -14,7 +14,7 @@ use uuid::Uuid;
use crate::{ use crate::{
ast::types::TagNode, ast::types::TagNode,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier, UserVal}, executor::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier},
settings::types::UnitLength, settings::types::UnitLength,
std::Args, std::Args,
}; };
@ -186,15 +186,10 @@ pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result
let tag: TagIdentifier = args.get_data()?; let tag: TagIdentifier = args.get_data()?;
let edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?; let edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?;
Ok(KclValue::UserVal(UserVal { Ok(KclValue::Uuid {
value: serde_json::to_value(edge).map_err(|e| { value: edge,
KclError::Type(KclErrorDetails {
message: format!("Failed to convert Uuid to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
meta: vec![args.source_range.into()], meta: vec![args.source_range.into()],
})) })
} }
/// Get the opposite edge to the edge given. /// Get the opposite edge to the edge given.
@ -264,15 +259,10 @@ pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> R
let tag: TagIdentifier = args.get_data()?; let tag: TagIdentifier = args.get_data()?;
let edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?; let edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?;
Ok(KclValue::UserVal(UserVal { Ok(KclValue::Uuid {
value: serde_json::to_value(edge).map_err(|e| { value: edge,
KclError::Type(KclErrorDetails {
message: format!("Failed to convert Uuid to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
meta: vec![args.source_range.into()], meta: vec![args.source_range.into()],
})) })
} }
/// Get the next adjacent edge to the edge given. /// Get the next adjacent edge to the edge given.
@ -354,15 +344,10 @@ pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args)
let tag: TagIdentifier = args.get_data()?; let tag: TagIdentifier = args.get_data()?;
let edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?; let edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?;
Ok(KclValue::UserVal(UserVal { Ok(KclValue::Uuid {
value: serde_json::to_value(edge).map_err(|e| { value: edge,
KclError::Type(KclErrorDetails {
message: format!("Failed to convert Uuid to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
meta: vec![args.source_range.into()], meta: vec![args.source_range.into()],
})) })
} }
/// Get the previous adjacent edge to the edge given. /// Get the previous adjacent edge to the edge given.

View File

@ -40,7 +40,7 @@ pub async fn cos(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_cos(num)?; let result = inner_cos(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the cosine of a number (in radians). /// Compute the cosine of a number (in radians).
@ -70,7 +70,7 @@ pub async fn sin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_sin(num)?; let result = inner_sin(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the sine of a number (in radians). /// Compute the sine of a number (in radians).
@ -100,7 +100,7 @@ pub async fn tan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_tan(num)?; let result = inner_tan(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the tangent of a number (in radians). /// Compute the tangent of a number (in radians).
@ -129,7 +129,7 @@ fn inner_tan(num: f64) -> Result<f64, KclError> {
pub async fn pi(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn pi(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_pi()?; let result = inner_pi()?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Return the value of `pi`. Archimedes constant (π). /// Return the value of `pi`. Archimedes constant (π).
@ -155,7 +155,7 @@ pub async fn sqrt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_sqrt(num)?; let result = inner_sqrt(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the square root of a number. /// Compute the square root of a number.
@ -185,7 +185,7 @@ pub async fn abs(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_abs(num)?; let result = inner_abs(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the absolute value of a number. /// Compute the absolute value of a number.
@ -222,7 +222,7 @@ pub async fn floor(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_floor(num)?; let result = inner_floor(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the largest integer less than or equal to a number. /// Compute the largest integer less than or equal to a number.
@ -250,7 +250,7 @@ pub async fn ceil(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_ceil(num)?; let result = inner_ceil(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the smallest integer greater than or equal to a number. /// Compute the smallest integer greater than or equal to a number.
@ -278,7 +278,7 @@ pub async fn min(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let nums = args.get_number_array()?; let nums = args.get_number_array()?;
let result = inner_min(nums); let result = inner_min(nums);
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the minimum of the given arguments. /// Compute the minimum of the given arguments.
@ -315,7 +315,7 @@ pub async fn max(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let nums = args.get_number_array()?; let nums = args.get_number_array()?;
let result = inner_max(nums); let result = inner_max(nums);
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the maximum of the given arguments. /// Compute the maximum of the given arguments.
@ -366,7 +366,7 @@ pub async fn pow(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let result = inner_pow(nums[0], nums[1])?; let result = inner_pow(nums[0], nums[1])?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the number to a power. /// Compute the number to a power.
@ -396,7 +396,7 @@ pub async fn acos(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_acos(num)?; let result = inner_acos(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the arccosine of a number (in radians). /// Compute the arccosine of a number (in radians).
@ -427,7 +427,7 @@ pub async fn asin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_asin(num)?; let result = inner_asin(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the arcsine of a number (in radians). /// Compute the arcsine of a number (in radians).
@ -457,7 +457,7 @@ pub async fn atan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_atan(num)?; let result = inner_atan(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the arctangent of a number (in radians). /// Compute the arctangent of a number (in radians).
@ -504,7 +504,7 @@ pub async fn log(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
} }
let result = inner_log(nums[0], nums[1])?; let result = inner_log(nums[0], nums[1])?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the logarithm of the number with respect to an arbitrary base. /// Compute the logarithm of the number with respect to an arbitrary base.
@ -536,7 +536,7 @@ pub async fn log2(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_log2(num)?; let result = inner_log2(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the base 2 logarithm of the number. /// Compute the base 2 logarithm of the number.
@ -564,7 +564,7 @@ pub async fn log10(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_log10(num)?; let result = inner_log10(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the base 10 logarithm of the number. /// Compute the base 10 logarithm of the number.
@ -592,7 +592,7 @@ pub async fn ln(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_ln(num)?; let result = inner_ln(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the natural logarithm of the number. /// Compute the natural logarithm of the number.
@ -619,7 +619,7 @@ fn inner_ln(num: f64) -> Result<f64, KclError> {
pub async fn e(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn e(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_e()?; let result = inner_e()?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Return the value of Eulers number `e`. /// Return the value of Eulers number `e`.
@ -648,7 +648,7 @@ fn inner_e() -> Result<f64, KclError> {
pub async fn tau(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn tau(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_tau()?; let result = inner_tau()?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Return the value of `tau`. The full circle constant (τ). Equal to 2π. /// Return the value of `tau`. The full circle constant (τ). Equal to 2π.
@ -678,7 +678,7 @@ pub async fn to_radians(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_to_radians(num)?; let result = inner_to_radians(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Converts a number from degrees to radians. /// Converts a number from degrees to radians.
@ -708,7 +708,7 @@ pub async fn to_degrees(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_to_degrees(num)?; let result = inner_to_degrees(num)?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Converts a number from radians to degrees. /// Converts a number from radians to degrees.

View File

@ -244,7 +244,7 @@ pub enum FunctionKind {
pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg) = args.get_hypotenuse_leg()?; let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
let result = inner_leg_length(hypotenuse, leg); let result = inner_leg_length(hypotenuse, leg);
args.make_user_val_from_f64(result) Ok(KclValue::from_number(result, vec![args.into()]))
} }
/// Compute the length of the given leg. /// Compute the length of the given leg.
@ -264,7 +264,7 @@ fn inner_leg_length(hypotenuse: f64, leg: f64) -> f64 {
pub async fn leg_angle_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn leg_angle_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg) = args.get_hypotenuse_leg()?; let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
let result = inner_leg_angle_x(hypotenuse, leg); let result = inner_leg_angle_x(hypotenuse, leg);
args.make_user_val_from_f64(result) Ok(KclValue::from_number(result, vec![args.into()]))
} }
/// Compute the angle of the given leg for x. /// Compute the angle of the given leg for x.
@ -284,7 +284,7 @@ fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
pub async fn leg_angle_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn leg_angle_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg) = args.get_hypotenuse_leg()?; let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
let result = inner_leg_angle_y(hypotenuse, leg); let result = inner_leg_angle_y(hypotenuse, leg);
args.make_user_val_from_f64(result) Ok(KclValue::from_number(result, vec![args.into()]))
} }
/// Compute the angle of the given leg for y. /// Compute the angle of the given leg for y.

View File

@ -14,13 +14,10 @@ use kittycad_modeling_cmds::{
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ executor::{ExecState, Geometries, Geometry, KclValue, Point3d, Sketch, SketchSet, Solid, SolidSet, SourceRange},
ExecState, Geometries, Geometry, KclValue, Point3d, Sketch, SketchSet, Solid, SolidSet, SourceRange, UserVal,
},
function_param::FunctionParam, function_param::FunctionParam,
std::{types::Uint, Args}, std::{types::Uint, Args},
}; };
@ -361,10 +358,10 @@ async fn make_transform<'a>(
exec_state: &mut ExecState, exec_state: &mut ExecState,
) -> Result<Transform, KclError> { ) -> Result<Transform, KclError> {
// Call the transform fn for this repetition. // Call the transform fn for this repetition.
let repetition_num = KclValue::UserVal(UserVal { let repetition_num = KclValue::Int {
value: JValue::Number(i.into()), value: i.into(),
meta: vec![source_range.into()], meta: vec![source_range.into()],
}); };
let transform_fn_args = vec![repetition_num]; let transform_fn_args = vec![repetition_num];
let transform_fn_return = transform_function.call(exec_state, transform_fn_args).await?; let transform_fn_return = transform_function.call(exec_state, transform_fn_args).await?;
@ -376,7 +373,7 @@ async fn make_transform<'a>(
source_ranges: source_ranges.clone(), source_ranges: source_ranges.clone(),
}) })
})?; })?;
let KclValue::UserVal(transform) = transform_fn_return else { let KclValue::Object { value: transform, meta } = transform_fn_return else {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: "Transform function must return a transform object".to_string(), message: "Transform function must return a transform object".to_string(),
source_ranges: source_ranges.clone(), source_ranges: source_ranges.clone(),
@ -384,9 +381,9 @@ async fn make_transform<'a>(
}; };
// Apply defaults to the transform. // Apply defaults to the transform.
let replicate = match transform.value.get("replicate") { let replicate = match transform.get("replicate") {
Some(JValue::Bool(true)) => true, Some(KclValue::Bool { value: true, .. }) => true,
Some(JValue::Bool(false)) => false, Some(KclValue::Bool { value: false, .. }) => false,
Some(_) => { Some(_) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: "The 'replicate' key must be a bool".to_string(), message: "The 'replicate' key must be a bool".to_string(),
@ -395,38 +392,43 @@ async fn make_transform<'a>(
} }
None => true, None => true,
}; };
let scale = match transform.value.get("scale") { let scale = match transform.get("scale") {
Some(x) => array_to_point3d(x, source_ranges.clone())?, Some(x) => array_to_point3d(x, source_ranges.clone())?,
None => Point3d { x: 1.0, y: 1.0, z: 1.0 }, None => Point3d { x: 1.0, y: 1.0, z: 1.0 },
}; };
let translate = match transform.value.get("translate") { let translate = match transform.get("translate") {
Some(x) => array_to_point3d(x, source_ranges.clone())?, Some(x) => array_to_point3d(x, source_ranges.clone())?,
None => Point3d { x: 0.0, y: 0.0, z: 0.0 }, None => Point3d { x: 0.0, y: 0.0, z: 0.0 },
}; };
let mut rotation = Rotation::default(); let mut rotation = Rotation::default();
if let Some(rot) = transform.value.get("rotation") { if let Some(rot) = transform.get("rotation") {
let KclValue::Object { value: rot, meta: _ } = rot else {
return Err(KclError::Semantic(KclErrorDetails {
message: "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')"
.to_string(),
source_ranges: source_ranges.clone(),
}));
};
if let Some(axis) = rot.get("axis") { if let Some(axis) = rot.get("axis") {
rotation.axis = array_to_point3d(axis, source_ranges.clone())?.into(); rotation.axis = array_to_point3d(axis, source_ranges.clone())?.into();
} }
if let Some(angle) = rot.get("angle") { if let Some(angle) = rot.get("angle") {
match angle { match angle {
JValue::Number(number) => { KclValue::Number { value: number, meta: _ } => {
if let Some(number) = number.as_f64() { rotation.angle = Angle::from_degrees(*number);
rotation.angle = Angle::from_degrees(number);
}
} }
_ => { _ => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: "The 'rotation.angle' key must be a number (of degrees)".to_string(), message: "The 'rotation.angle' key must be a number (of degrees)".to_string(),
source_ranges: source_ranges.clone(), source_ranges: meta.iter().map(|m| m.source_range).collect(),
})); }));
} }
} }
} }
if let Some(origin) = rot.get("origin") { if let Some(origin) = rot.get("origin") {
rotation.origin = match origin { rotation.origin = match origin {
JValue::String(s) if s == "local" => OriginType::Local, KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
JValue::String(s) if s == "global" => OriginType::Global, KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
other => { other => {
let origin = array_to_point3d(other, source_ranges.clone())?.into(); let origin = array_to_point3d(other, source_ranges.clone())?.into();
OriginType::Custom { origin } OriginType::Custom { origin }
@ -443,8 +445,8 @@ async fn make_transform<'a>(
Ok(t) Ok(t)
} }
fn array_to_point3d(json: &JValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> { fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
let JValue::Array(arr) = json else { let KclValue::Array { value: arr, meta } = val else {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: "Expected an array of 3 numbers (i.e. a 3D point)".to_string(), message: "Expected an array of 3 numbers (i.e. a 3D point)".to_string(),
source_ranges, source_ranges,
@ -457,17 +459,21 @@ fn array_to_point3d(json: &JValue, source_ranges: Vec<SourceRange>) -> Result<Po
source_ranges, source_ranges,
})); }));
}; };
// Gets an f64 from a JSON value, returns Option. // Gets an f64 from a KCL value.
let f = |j: &JValue| j.as_number().and_then(|num| num.as_f64()).map(|x| x.to_owned()); let f = |k: &KclValue, component: char| {
let err = |component| { use super::args::FromKclValue;
KclError::Semantic(KclErrorDetails { if let Some(value) = f64::from_mem_item(k) {
Ok(value)
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("{component} component of this point was not a number"), message: format!("{component} component of this point was not a number"),
source_ranges: source_ranges.clone(), source_ranges: meta.iter().map(|m| m.source_range).collect(),
}) }))
}
}; };
let x = f(&arr[0]).ok_or_else(|| err("X"))?; let x = f(&arr[0], 'x')?;
let y = f(&arr[1]).ok_or_else(|| err("Y"))?; let y = f(&arr[1], 'y')?;
let z = f(&arr[2]).ok_or_else(|| err("Z"))?; let z = f(&arr[2], 'z')?;
Ok(Point3d { x, y, z }) Ok(Point3d { x, y, z })
} }
@ -477,8 +483,22 @@ mod tests {
#[test] #[test]
fn test_array_to_point3d() { fn test_array_to_point3d() {
let input = serde_json::json! { let input = KclValue::Array {
[1.1, 2.2, 3.3] value: vec![
KclValue::Number {
value: 1.1,
meta: Default::default(),
},
KclValue::Number {
value: 2.2,
meta: Default::default(),
},
KclValue::Number {
value: 3.3,
meta: Default::default(),
},
],
meta: Default::default(),
}; };
let expected = Point3d { x: 1.1, y: 2.2, z: 3.3 }; let expected = Point3d { x: 1.1, y: 2.2, z: 3.3 };
let actual = array_to_point3d(&input, Vec::new()); let actual = array_to_point3d(&input, Vec::new());

View File

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
errors::KclError, errors::KclError,
executor::{ExecState, KclValue, Metadata, Plane, UserVal}, executor::{ExecState, KclValue, Plane},
std::{sketch::PlaneData, Args}, std::{sketch::PlaneData, Args},
}; };
@ -50,15 +50,9 @@ impl From<StandardPlane> for PlaneData {
/// Offset a plane by a distance along its normal. /// Offset a plane by a distance along its normal.
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?; let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
let plane_data = inner_offset_plane(std_plane, offset, exec_state).await?;
let plane = inner_offset_plane(std_plane, offset, exec_state).await?; let plane = Plane::from_plane_data(plane_data, exec_state);
Ok(KclValue::Plane(Box::new(plane)))
Ok(KclValue::UserVal(UserVal::new(
vec![Metadata {
source_range: args.source_range,
}],
plane,
)))
} }
/// Offset a plane by a distance along its normal. /// Offset a plane by a distance along its normal.
@ -129,6 +123,20 @@ pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclV
/// ///
/// loft([squareSketch, circleSketch]) /// loft([squareSketch, circleSketch])
/// ``` /// ```
/// ```no_run
/// // A circle on the XY plane
/// startSketchOn("XY")
/// |> startProfileAt([0, 0], %)
/// |> circle({radius: 10, center: [0, 0]}, %)
///
/// // Triangle on the plane 4 units above
/// startSketchOn(offsetPlane("XY", 4))
/// |> startProfileAt([0, 0], %)
/// |> line([10, 0], %)
/// |> line([0, 10], %)
/// |> close(%)
/// ```
#[stdlib { #[stdlib {
name = "offsetPlane", name = "offsetPlane",
}] }]

View File

@ -60,7 +60,7 @@ pub async fn segment_end_x(exec_state: &mut ExecState, args: Args) -> Result<Kcl
let tag: TagIdentifier = args.get_data()?; let tag: TagIdentifier = args.get_data()?;
let result = inner_segment_end_x(&tag, exec_state, args.clone())?; let result = inner_segment_end_x(&tag, exec_state, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the ending point of the provided line segment along the 'x' axis. /// Compute the ending point of the provided line segment along the 'x' axis.
@ -96,7 +96,7 @@ pub async fn segment_end_y(exec_state: &mut ExecState, args: Args) -> Result<Kcl
let tag: TagIdentifier = args.get_data()?; let tag: TagIdentifier = args.get_data()?;
let result = inner_segment_end_y(&tag, exec_state, args.clone())?; let result = inner_segment_end_y(&tag, exec_state, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the ending point of the provided line segment along the 'y' axis. /// Compute the ending point of the provided line segment along the 'y' axis.
@ -179,7 +179,7 @@ pub async fn segment_start_x(exec_state: &mut ExecState, args: Args) -> Result<K
let tag: TagIdentifier = args.get_data()?; let tag: TagIdentifier = args.get_data()?;
let result = inner_segment_start_x(&tag, exec_state, args.clone())?; let result = inner_segment_start_x(&tag, exec_state, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the starting point of the provided line segment along the 'x' axis. /// Compute the starting point of the provided line segment along the 'x' axis.
@ -215,7 +215,7 @@ pub async fn segment_start_y(exec_state: &mut ExecState, args: Args) -> Result<K
let tag: TagIdentifier = args.get_data()?; let tag: TagIdentifier = args.get_data()?;
let result = inner_segment_start_y(&tag, exec_state, args.clone())?; let result = inner_segment_start_y(&tag, exec_state, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the starting point of the provided line segment along the 'y' axis. /// Compute the starting point of the provided line segment along the 'y' axis.
@ -251,7 +251,7 @@ pub async fn last_segment_x(_exec_state: &mut ExecState, args: Args) -> Result<K
let sketch = args.get_sketch()?; let sketch = args.get_sketch()?;
let result = inner_last_segment_x(sketch, args.clone())?; let result = inner_last_segment_x(sketch, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Extract the 'x' axis value of the last line segment in the provided 2-d /// Extract the 'x' axis value of the last line segment in the provided 2-d
@ -291,7 +291,7 @@ pub async fn last_segment_y(_exec_state: &mut ExecState, args: Args) -> Result<K
let sketch = args.get_sketch()?; let sketch = args.get_sketch()?;
let result = inner_last_segment_y(sketch, args.clone())?; let result = inner_last_segment_y(sketch, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Extract the 'y' axis value of the last line segment in the provided 2-d /// Extract the 'y' axis value of the last line segment in the provided 2-d
@ -330,7 +330,7 @@ fn inner_last_segment_y(sketch: Sketch, args: Args) -> Result<f64, KclError> {
pub async fn segment_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn segment_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_data()?; let tag: TagIdentifier = args.get_data()?;
let result = inner_segment_length(&tag, exec_state, args.clone())?; let result = inner_segment_length(&tag, exec_state, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the length of the provided line segment. /// Compute the length of the provided line segment.
@ -376,7 +376,7 @@ pub async fn segment_angle(exec_state: &mut ExecState, args: Args) -> Result<Kcl
let tag: TagIdentifier = args.get_data()?; let tag: TagIdentifier = args.get_data()?;
let result = inner_segment_angle(&tag, exec_state, args.clone())?; let result = inner_segment_angle(&tag, exec_state, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the angle (in degrees) of the provided line segment. /// Compute the angle (in degrees) of the provided line segment.
@ -415,10 +415,10 @@ fn inner_segment_angle(tag: &TagIdentifier, exec_state: &mut ExecState, args: Ar
pub async fn angle_to_match_length_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn angle_to_match_length_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (tag, to, sketch) = args.get_tag_to_number_sketch()?; let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
let result = inner_angle_to_match_length_x(&tag, to, sketch, exec_state, args.clone())?; let result = inner_angle_to_match_length_x(&tag, to, sketch, exec_state, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Compute the angle (in degrees) in o /// Returns the angle to match the given length for x.
/// ///
/// ```no_run /// ```no_run
/// const sketch001 = startSketchOn('XZ') /// const sketch001 = startSketchOn('XZ')
@ -478,7 +478,7 @@ fn inner_angle_to_match_length_x(
pub async fn angle_to_match_length_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn angle_to_match_length_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (tag, to, sketch) = args.get_tag_to_number_sketch()?; let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
let result = inner_angle_to_match_length_y(&tag, to, sketch, exec_state, args.clone())?; let result = inner_angle_to_match_length_y(&tag, to, sketch, exec_state, args.clone())?;
args.make_user_val_from_f64(result) Ok(args.make_user_val_from_f64(result))
} }
/// Returns the angle to match the given length for y. /// Returns the angle to match the given length for y.

View File

@ -48,7 +48,9 @@ pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
args.get_circle_args()?; args.get_circle_args()?;
let sketch = inner_circle(data, sketch_surface_or_group, tag, exec_state, args).await?; let sketch = inner_circle(data, sketch_surface_or_group, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(sketch.meta.clone(), sketch)) Ok(KclValue::Sketch {
value: Box::new(sketch),
})
} }
/// Construct a 2-dimensional circle, of the specified radius, centered at /// Construct a 2-dimensional circle, of the specified radius, centered at
@ -166,7 +168,7 @@ pub struct PolygonData {
pub center: [f64; 2], pub center: [f64; 2],
/// The type of the polygon (inscribed or circumscribed) /// The type of the polygon (inscribed or circumscribed)
#[serde(skip)] #[serde(skip)]
polygon_type: PolygonType, pub polygon_type: PolygonType,
/// Whether the polygon is inscribed (true) or circumscribed (false) about a circle with the specified radius /// Whether the polygon is inscribed (true) or circumscribed (false) about a circle with the specified radius
#[serde(default = "default_inscribed")] #[serde(default = "default_inscribed")]
pub inscribed: bool, pub inscribed: bool,
@ -182,7 +184,9 @@ pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
args.get_polygon_args()?; args.get_polygon_args()?;
let sketch = inner_polygon(data, sketch_surface_or_group, tag, exec_state, args).await?; let sketch = inner_polygon(data, sketch_surface_or_group, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(sketch.meta.clone(), sketch)) Ok(KclValue::Sketch {
value: Box::new(sketch),
})
} }
/// Create a regular polygon with the specified number of sides that is either inscribed or circumscribed around a circle of the specified radius. /// Create a regular polygon with the specified number of sides that is either inscribed or circumscribed around a circle of the specified radius.

View File

@ -17,7 +17,7 @@ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ executor::{
BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch, SketchSet, SketchSurface, BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch, SketchSet, SketchSurface,
Solid, TagEngineInfo, TagIdentifier, UserVal, Solid, TagEngineInfo, TagIdentifier,
}, },
std::{ std::{
utils::{ utils::{
@ -97,7 +97,9 @@ pub async fn line_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
let (to, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (to, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_line_to(to, sketch, tag, exec_state, args).await?; let new_sketch = inner_line_to(to, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a line from the current origin to some absolute (x, y) point. /// Draw a line from the current origin to some absolute (x, y) point.
@ -164,7 +166,9 @@ pub async fn x_line_to(exec_state: &mut ExecState, args: Args) -> Result<KclValu
let (to, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (to, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_x_line_to(to, sketch, tag, exec_state, args).await?; let new_sketch = inner_x_line_to(to, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a line parallel to the X axis, that ends at the given X. /// Draw a line parallel to the X axis, that ends at the given X.
@ -212,7 +216,9 @@ pub async fn y_line_to(exec_state: &mut ExecState, args: Args) -> Result<KclValu
let (to, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (to, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_y_line_to(to, sketch, tag, exec_state, args).await?; let new_sketch = inner_y_line_to(to, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a line parallel to the Y axis, that ends at the given Y. /// Draw a line parallel to the Y axis, that ends at the given Y.
@ -252,7 +258,9 @@ pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let (delta, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (delta, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_line(delta, sketch, tag, exec_state, args).await?; let new_sketch = inner_line(delta, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a line relative to the current origin to a specified (x, y) away /// Draw a line relative to the current origin to a specified (x, y) away
@ -333,7 +341,9 @@ pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
let (length, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (length, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_x_line(length, sketch, tag, exec_state, args).await?; let new_sketch = inner_x_line(length, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a line relative to the current origin to a specified distance away /// Draw a line relative to the current origin to a specified distance away
@ -376,7 +386,9 @@ pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
let (length, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (length, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_y_line(length, sketch, tag, exec_state, args).await?; let new_sketch = inner_y_line(length, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a line relative to the current origin to a specified distance away /// Draw a line relative to the current origin to a specified distance away
@ -430,7 +442,9 @@ pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclVa
let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_angled_line(data, sketch, tag, exec_state, args).await?; let new_sketch = inner_angled_line(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a line segment relative to the current origin using the polar /// Draw a line segment relative to the current origin using the polar
@ -515,7 +529,9 @@ pub async fn angled_line_of_x_length(exec_state: &mut ExecState, args: Args) ->
let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_angled_line_of_x_length(data, sketch, tag, exec_state, args).await?; let new_sketch = inner_angled_line_of_x_length(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Create a line segment from the current 2-dimensional sketch origin /// Create a line segment from the current 2-dimensional sketch origin
@ -573,9 +589,9 @@ async fn inner_angled_line_of_x_length(
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AngledLineToData { pub struct AngledLineToData {
/// The angle of the line. /// The angle of the line.
angle: f64, pub angle: f64,
/// The point to draw to. /// The point to draw to.
to: f64, pub to: f64,
} }
/// Draw an angled line to a given x coordinate. /// Draw an angled line to a given x coordinate.
@ -583,7 +599,9 @@ pub async fn angled_line_to_x(exec_state: &mut ExecState, args: Args) -> Result<
let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_angled_line_to_x(data, sketch, tag, exec_state, args).await?; let new_sketch = inner_angled_line_to_x(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Create a line segment from the current 2-dimensional sketch origin /// Create a line segment from the current 2-dimensional sketch origin
@ -641,7 +659,9 @@ pub async fn angled_line_of_y_length(exec_state: &mut ExecState, args: Args) ->
let new_sketch = inner_angled_line_of_y_length(data, sketch, tag, exec_state, args).await?; let new_sketch = inner_angled_line_of_y_length(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Create a line segment from the current 2-dimensional sketch origin /// Create a line segment from the current 2-dimensional sketch origin
@ -700,7 +720,9 @@ pub async fn angled_line_to_y(exec_state: &mut ExecState, args: Args) -> Result<
let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_angled_line_to_y(data, sketch, tag, exec_state, args).await?; let new_sketch = inner_angled_line_to_y(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Create a line segment from the current 2-dimensional sketch origin /// Create a line segment from the current 2-dimensional sketch origin
@ -771,7 +793,9 @@ pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args)
let (data, sketch, tag): (AngledLineThatIntersectsData, Sketch, Option<TagNode>) = let (data, sketch, tag): (AngledLineThatIntersectsData, Sketch, Option<TagNode>) =
args.get_data_and_sketch_and_tag()?; args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_angled_line_that_intersects(data, sketch, tag, exec_state, args).await?; let new_sketch = inner_angled_line_that_intersects(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw an angled line from the current origin, constructing a line segment /// Draw an angled line from the current origin, constructing a line segment
@ -828,7 +852,9 @@ pub async fn start_sketch_at(exec_state: &mut ExecState, args: Args) -> Result<K
let data: [f64; 2] = args.get_data()?; let data: [f64; 2] = args.get_data()?;
let sketch = inner_start_sketch_at(data, exec_state, args).await?; let sketch = inner_start_sketch_at(data, exec_state, args).await?;
Ok(KclValue::new_user_val(sketch.meta.clone(), sketch)) Ok(KclValue::Sketch {
value: Box::new(sketch),
})
} }
/// Start a new 2-dimensional sketch at a given point on the 'XY' plane. /// Start a new 2-dimensional sketch at a given point on the 'XY' plane.
@ -1135,7 +1161,9 @@ pub async fn start_profile_at(exec_state: &mut ExecState, args: Args) -> Result<
args.get_data_and_sketch_surface()?; args.get_data_and_sketch_surface()?;
let sketch = inner_start_profile_at(start, sketch_surface, tag, exec_state, args).await?; let sketch = inner_start_profile_at(start, sketch_surface, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(sketch.meta.clone(), sketch)) Ok(KclValue::Sketch {
value: Box::new(sketch),
})
} }
/// Start a new profile at a given point. /// Start a new profile at a given point.
@ -1262,7 +1290,7 @@ pub(crate) async fn inner_start_profile_at(
pub async fn profile_start_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn profile_start_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch()?; let sketch: Sketch = args.get_sketch()?;
let x = inner_profile_start_x(sketch)?; let x = inner_profile_start_x(sketch)?;
args.make_user_val_from_f64(x) Ok(args.make_user_val_from_f64(x))
} }
/// Extract the provided 2-dimensional sketch's profile's origin's 'x' /// Extract the provided 2-dimensional sketch's profile's origin's 'x'
@ -1286,7 +1314,7 @@ pub(crate) fn inner_profile_start_x(sketch: Sketch) -> Result<f64, KclError> {
pub async fn profile_start_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn profile_start_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch()?; let sketch: Sketch = args.get_sketch()?;
let x = inner_profile_start_y(sketch)?; let x = inner_profile_start_y(sketch)?;
args.make_user_val_from_f64(x) Ok(args.make_user_val_from_f64(x))
} }
/// Extract the provided 2-dimensional sketch's profile's origin's 'y' /// Extract the provided 2-dimensional sketch's profile's origin's 'y'
@ -1309,15 +1337,7 @@ pub(crate) fn inner_profile_start_y(sketch: Sketch) -> Result<f64, KclError> {
pub async fn profile_start(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn profile_start(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch()?; let sketch: Sketch = args.get_sketch()?;
let point = inner_profile_start(sketch)?; let point = inner_profile_start(sketch)?;
Ok(KclValue::UserVal(UserVal { Ok(KclValue::from_point2d(point, args.into()))
value: serde_json::to_value(point).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert point to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
meta: Default::default(),
}))
} }
/// Extract the provided 2-dimensional sketch's profile's origin /// Extract the provided 2-dimensional sketch's profile's origin
@ -1345,7 +1365,9 @@ pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let new_sketch = inner_close(sketch, tag, exec_state, args).await?; let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Construct a line segment from the current origin back to the profile's /// Construct a line segment from the current origin back to the profile's
@ -1452,7 +1474,9 @@ pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
let (data, sketch, tag): (ArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (data, sketch, tag): (ArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_arc(data, sketch, tag, exec_state, args).await?; let new_sketch = inner_arc(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a curved line segment along an imaginary circle. /// Draw a curved line segment along an imaginary circle.
@ -1573,7 +1597,9 @@ pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<Kc
let (data, sketch, tag): (TangentialArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (data, sketch, tag): (TangentialArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_tangential_arc(data, sketch, tag, exec_state, args).await?; let new_sketch = inner_tangential_arc(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a curved line segment along part of an imaginary circle. /// Draw a curved line segment along part of an imaginary circle.
@ -1701,7 +1727,9 @@ pub async fn tangential_arc_to(exec_state: &mut ExecState, args: Args) -> Result
let (to, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = super::args::FromArgs::from_args(&args, 0)?; let (to, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = super::args::FromArgs::from_args(&args, 0)?;
let new_sketch = inner_tangential_arc_to(to, sketch, tag, exec_state, args).await?; let new_sketch = inner_tangential_arc_to(to, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a tangential arc to point some distance away.. /// Draw a tangential arc to point some distance away..
@ -1709,7 +1737,9 @@ pub async fn tangential_arc_to_relative(exec_state: &mut ExecState, args: Args)
let (delta, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = super::args::FromArgs::from_args(&args, 0)?; let (delta, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = super::args::FromArgs::from_args(&args, 0)?;
let new_sketch = inner_tangential_arc_to_relative(delta, sketch, tag, exec_state, args).await?; let new_sketch = inner_tangential_arc_to_relative(delta, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Starting at the current sketch's origin, draw a curved line segment along /// Starting at the current sketch's origin, draw a curved line segment along
@ -1873,11 +1903,11 @@ async fn inner_tangential_arc_to_relative(
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BezierData { pub struct BezierData {
/// The to point. /// The to point.
to: [f64; 2], pub to: [f64; 2],
/// The first control point. /// The first control point.
control1: [f64; 2], pub control1: [f64; 2],
/// The second control point. /// The second control point.
control2: [f64; 2], pub control2: [f64; 2],
} }
/// Draw a bezier curve. /// Draw a bezier curve.
@ -1885,7 +1915,9 @@ pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclV
let (data, sketch, tag): (BezierData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?; let (data, sketch, tag): (BezierData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_bezier_curve(data, sketch, tag, exec_state, args).await?; let new_sketch = inner_bezier_curve(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Draw a smooth, continuous, curved line segment from the current origin to /// Draw a smooth, continuous, curved line segment from the current origin to
@ -1965,7 +1997,9 @@ pub async fn hole(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let (hole_sketch, sketch): (SketchSet, Sketch) = args.get_sketches()?; let (hole_sketch, sketch): (SketchSet, Sketch) = args.get_sketches()?;
let new_sketch = inner_hole(hole_sketch, sketch, exec_state, args).await?; let new_sketch = inner_hole(hole_sketch, sketch, exec_state, args).await?;
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
} }
/// Use a 2-dimensional sketch to cut a hole in another 2-dimensional sketch. /// Use a 2-dimensional sketch to cut a hole in another 2-dimensional sketch.

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