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:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
@ -15,6 +17,8 @@ on:
paths:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'

View File

@ -19,7 +19,7 @@ $(XSTATE_TYPEGENS): $(TS_SRC)
yarn xstate typegen 'src/**/*.ts?(x)'
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
yarn build:wasm-dev
yarn build:wasm
node_modules: package.json yarn.lock
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:
```bash
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
```

View File

@ -1,10 +1,10 @@
---
title: "angleToMatchLengthX"
excerpt: "Compute the angle (in degrees) in o"
excerpt: "Returns the angle to match the given length for x."
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 |
|----------|------|-------------|----------|
| `type` |enum: `UserVal`| | No |
| `value` |``| | No |
| `type` |enum: `Uuid`| | 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 |
@ -111,6 +213,38 @@ A face.
| `__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.
@ -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')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
|> xLine(${commonPoints.num1}, %)`)
}
await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)`)
} else {
await page.waitForTimeout(500)
}
@ -84,9 +84,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 + 0.01}], %)
|> lineTo([0, ${commonPoints.num3}], %)`)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(${commonPoints.num2 * -1}, %)`)
}
// deselect line tool
@ -142,9 +142,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
await u.openKclCodePanel()
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %, $seg01)
|> line([0, ${commonPoints.num1 + 0.01}], %)
|> angledLine([180, segLen(seg01)], %)`)
|> xLine(${commonPoints.num1}, %, $seg01)
|> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(-segLen(seg01), %)`)
}
test.describe('Basic sketch', () => {

View File

@ -694,6 +694,9 @@ test.describe('Editor tests', () => {
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([3.14, 12], %)
|> 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 }) => {

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,
editor,
toolbar,
@ -505,7 +505,7 @@ test(`Verify axis and origin snapping`, async ({
const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
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]}], %)`,
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
}

View File

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

View File

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

View File

@ -8,6 +8,21 @@ import {
Locator,
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 fsp from 'fs/promises'
import fsSync from 'fs'
@ -257,55 +272,141 @@ export const circleMove = async (
}
}
export const getMovementUtils = (opts: any) => {
// The way we truncate is kinda odd apparently, so we need this function
// "[k]itty[c]ad round"
const kcRound = (n: number) => Math.trunc(n * 100) / 100
export function rollingRound(n: number, digitsAfterDecimal: number) {
const s = String(n).split('.')
// To translate between screen and engine ("[U]nit") coordinates
// NOTE: these pretty much can't be perfect because of screen scaling.
// 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
]
// There are no decimals, just return the number.
if (s.length === 1) return n
// Turn the array into a string with specific formatting
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
// Find the closest 9. We don't care about anything beyond that.
const nineIndex = s[1].indexOf('9')
// Combine because used often
const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
const fractStr = nineIndex > 0 ? s[1].slice(0, nineIndex + 1) : s[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")
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
let last = { x: 0, y: 0 }
let lastScreenSpace = { x: 0, y: 0 }
const click00r = async (x?: number, y?: number) => {
// reset relative coordinates when anything is undefined
if (x === undefined || y === undefined) {
last.x = 0
last.y = 0
return
last = { x: 0, y: 0 }
lastScreenSpace = { x: 0, y: 0 }
return {
nextXY: [0, 0],
kcl: `[0, 0]`,
}
}
await circleMove(
opts.page,
opts.center.x + last.x + x,
opts.center.y + last.y + y,
10,
10
const absX = opts.center.x + x
const absY = opts.center.y + y
const nextX = last.x + x
const nextY = last.y + y
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.y += y
// Returns the new absolute coordinate if you need it.
return [last.x, last.y]
const relativeScreenSpace = {
x: twoD.x - lastScreenSpace.x,
y: -(twoD.y - lastScreenSpace.y),
}
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, click00r }
return { toSU, toU, click00r }
}
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)
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),
waitForPageLoad: () => waitForPageLoad(page),
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),

View File

@ -43,10 +43,12 @@ test.describe('Testing constraints', () => {
await page.getByRole('button', { name: 'Edit Sketch' }).click()
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.keyboard.down('Shift')
await page.mouse.click(834, 244)
await page.mouse.click(center.x + 234, 244)
await page.keyboard.up('Shift')
await page

View File

@ -32,10 +32,17 @@ test.describe('Testing selections', () => {
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
const xAxisClick = () =>
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
const yAxisClick = () =>
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 = () =>
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 = () =>
test.step('Hover over empty space', async () => {
await page.mouse.move(700, 143, { steps: 5 })
@ -80,23 +87,23 @@ test.describe('Testing selections', () => {
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
|> xLine(${commonPoints.num1}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 + 0.01}], %)
|> lineTo([0, ${commonPoints.num3}], %)`)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(${commonPoints.num2 * -1}, %)`)
// deselect line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
@ -121,53 +128,58 @@ test.describe('Testing selections', () => {
// now check clicking works including axis
// 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', {
name: 'Length: open menu',
})
const absYButton = page.getByRole('button', { name: 'Absolute Y' })
await constrainButton.click()
await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100)
await xAxisClick()
await page.keyboard.up('Shift')
await constrainButton.click()
await absYButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absYButton).not.toBeDisabled()
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 expect(absXButton).toBeDisabled()
await page.waitForTimeout(100)
await yAxisClick()
await page.keyboard.up('Shift')
await constrainButton.click()
await absXButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absXButton).not.toBeDisabled()
})
// clear selection by clicking on nothing
await emptySpaceClick()
await page.waitForTimeout(100)
// same selection but click the axis first
await xAxisClick()
await constrainButton.click()
await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await topHorzSegmentClick()
await page.waitForTimeout(100)
await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absYButton).not.toBeDisabled()
await test.step(`Same selection but click the axis first`, async () => {
await yAxisClick()
await constrainButton.click()
await expect(absXButton).toBeDisabled()
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await topHorzSegmentClick()
await page.waitForTimeout(100)
await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absXButton).not.toBeDisabled()
})
// clear selection by clicking on nothing
await emptySpaceClick()
// check the same selection again by putting cursor in code first then selecting axis
await page
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
.click()
await page.keyboard.down('Shift')
await constrainButton.click()
await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100)
await xAxisClick()
await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absYButton).not.toBeDisabled()
await test.step(`Same selection but code selection then axis`, async () => {
await page
.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`)
.click()
await page.keyboard.down('Shift')
await constrainButton.click()
await expect(absXButton).toBeDisabled()
await page.waitForTimeout(100)
await yAxisClick()
await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absXButton).not.toBeDisabled()
})
// clear selection by clicking on nothing
await emptySpaceClick()
@ -182,9 +194,7 @@ test.describe('Testing selections', () => {
process.platform === 'linux' ? 'Control' : 'Meta'
)
await page.waitForTimeout(100)
await page
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
.click()
await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(500)
@ -928,6 +938,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
// test fillet button with the body in the scene
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
extrude001 = extrude(10, sketch001)`
await u.codeLocator.clear()
await u.codeLocator.fill(codeToAdd)
await selectSegment()
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`,
{ tag: ['@electron', '@skipWin'] },
async ({ browser: _ }, testInfo) => {
@ -318,7 +318,6 @@ test.describe('Testing settings', () => {
timeout: 5_000,
})
.toContain(`themeColor = "${userThemeColor}"`)
// Only close the button after we've confirmed
})
await test.step('Set project theme color', async () => {
@ -744,18 +743,19 @@ extrude001 = extrude(5, sketch001)
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// Selectors and constants
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
const lineToolButton = page.getByTestId('line')
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 lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
await test.step(`Get into sketch mode`, async () => {
await u.waitForAuthSkipAppStart()
await page.mouse.click(700, 200)
await page.mouse.click(sketchOriginLocation.x, sketchOriginLocation.y)
await expect(editSketchButton).toBeVisible()
await editSketchButton.click()
@ -766,12 +766,18 @@ extrude001 = extrude(5, sketch001)
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 expect
.poll(() =>
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
)
.toBeLessThan(15)
.poll(() => u.getGreatestPixDiff(line1, darkThemeSegmentColor))
.toBeLessThanOrEqual(34)
})
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 expect
.poll(() =>
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
)
.toBeLessThan(15)
.poll(() => u.getGreatestPixDiff(line1, lightThemeSegmentColor))
.toBeLessThanOrEqual(34)
})
})

View File

@ -503,14 +503,16 @@ test('Sketch on face', async ({ page }) => {
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(
() => page.mouse.click(625, 165),
async () => page.mouse.click(center.x, 180),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
await u.closeDebugPanel()
await page.waitForTimeout(300)
const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242]

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import { Models } from '@kittycad/lib'
import { MutableRefObject } from 'react'
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
import {
Euler,
@ -87,6 +89,9 @@ class CameraRateLimiter {
export class CameraControls {
engineCommandManager: EngineCommandManager
modelingSidebarRef: MutableRefObject<HTMLUListElement | null> = {
current: null,
}
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
camera: PerspectiveCamera | OrthographicCamera
target: Vector3
@ -95,6 +100,13 @@ export class CameraControls {
wasDragging: boolean
mouseDownPosition: Vector2
mouseNewPosition: Vector2
cameraDragStartXY = new Vector2()
old:
| {
camera: PerspectiveCamera | OrthographicCamera
target: Vector3
}
| undefined
rotationSpeed = 0.3
enableRotate = true
enablePan = true
@ -461,6 +473,7 @@ export class CameraControls {
if (this.syncDirection === 'engineToClient') {
const interaction = this.getInteractionType(event)
if (interaction === 'none') return
void this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
@ -909,18 +922,123 @@ export class CameraControls {
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',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
object_ids: [], // leave empty to zoom to all objects
padding: 0.2, // padding around the objects
animated: false, // don't animate the zoom for now
type: 'default_camera_look_at',
...convertThreeCamValuesToEngineCam({
isPerspective: true,
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(
targetQuaternion: Quaternion,
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 { cameraMouseDragGuards } from 'lib/cameraControls'
@ -202,12 +209,20 @@ const Overlay = ({
let xAlignment = overlay.angle < 0 ? '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>>(
kclManager.ast,
overlay.pathToNode,
'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 constraints = getConstraintInfo(
@ -234,6 +249,13 @@ const Overlay = ({
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 (
<div className={`absolute w-0 h-0`}>
<div
@ -244,6 +266,7 @@ const Overlay = ({
data-overlay-angle={overlay.angle}
className="pointer-events-auto absolute w-0 h-0"
style={{
zIndex,
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
}}
></div>
@ -252,6 +275,7 @@ const Overlay = ({
data-overlay-toolbar-index={overlayIndex}
className={`px-0 pointer-events-auto absolute flex gap-1`}
style={{
zIndex,
transform: `translate3d(calc(${
overlay.windowCoords[0] + xOffset
}px + ${xAlignment}), calc(${
@ -293,6 +317,7 @@ const Overlay = ({
*/}
{callExpression?.callee?.name !== 'circle' && (
<SegmentMenu
style={{ zIndex }}
verticalPosition={
overlay.windowCoords[1] > window.innerHeight / 2
? 'top'
@ -434,15 +459,17 @@ const SegmentMenu = ({
verticalPosition,
pathToNode,
stdLibFnName,
style,
}: {
verticalPosition: 'top' | 'bottom'
pathToNode: PathToNode
stdLibFnName: string
style?: CSSProperties
}) => {
const { send } = useModelingContext()
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
return (
<Popover className="relative">
<Popover style={style} className="relative">
{({ open }) => (
<>
<Popover.Button
@ -637,10 +664,16 @@ const ConstraintSymbol = ({
kclManager.ast,
kclManager.programMemory
)
if (!transform) return
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) {
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 {
GridHelper,
LineBasicMaterial,
OrthographicCamera,
PerspectiveCamera,
Group,
Mesh,
Quaternion,
Vector3,
} from 'three'
@ -28,15 +25,9 @@ export function createGridHelper({
gridHelper.rotation.x = Math.PI / 2
return gridHelper
}
const fudgeFactor = 72.66985970437086
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
4000 /
window.innerHeight
// Re-export scale.ts
export * from './scale'
export function isQuaternionVertical(q: Quaternion) {
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,
} from 'three'
import {
ANGLE_SNAP_THRESHOLD_DEGREES,
ARROWHEAD,
AXIS_GROUP,
DRAFT_POINT,
@ -95,6 +96,7 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
import { SegmentInputs } from 'lang/std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { radToDeg } from 'three/src/math/MathUtils'
type DraftSegment = 'line' | 'tangentialArcTo'
@ -451,6 +453,7 @@ export class SceneEntities {
const { modifiedAst } = addStartProfileAtRes
await kclManager.updateAst(modifiedAst, false)
this.removeIntersectionPlane()
this.scene.remove(draftPointGroup)
@ -683,7 +686,7 @@ export class SceneEntities {
})
return nextAst
}
setUpDraftSegment = async (
setupDraftSegment = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
@ -798,11 +801,24 @@ export class SceneEntities {
(sceneObject) => sceneObject.object.name === X_AXIS
)
const lastSegment = sketch.paths.slice(-1)[0]
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
const snappedPoint = {
x: intersectsYAxis ? 0 : intersection2d.x,
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'
@ -810,6 +826,12 @@ export class SceneEntities {
// case-based logic for different segment types
if (lastSegment.type === '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) {
// We consider a point placed on axes or origin to be absolute
resolvedFunctionName = 'lineTo'
@ -835,10 +857,11 @@ export class SceneEntities {
}
await kclManager.executeAstMock(modifiedAst)
if (intersectsProfileStart) {
sceneInfra.modelingSend({ type: 'CancelSketch' })
} else {
await this.setUpDraftSegment(
await this.setupDraftSegment(
sketchPathToNode,
forward,
up,
@ -846,6 +869,8 @@ export class SceneEntities {
segmentName
)
}
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
},
onMove: (args) => {
this.onDragSegment({
@ -970,43 +995,51 @@ export class SceneEntities {
if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init
if (sketchInit.type === 'PipeExpression') {
updateRectangleSketch(sketchInit, x, y, tags[0])
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return
_ast = _recastAst
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish rectangle' })
const { execState } = await executeAst({
ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
// Update the rest of the segments of the THREEjs scene
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
if (sketchInit.type !== 'PipeExpression') {
return
}
updateRectangleSketch(sketchInit, x, y, tags[0])
const newCode = recast(_ast)
let _recastAst = parse(newCode)
if (trap(_recastAst)) return
_ast = _recastAst
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
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({
ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
// Update the rest of the segments of the THREEjs scene
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
},
})
}
@ -1166,13 +1199,17 @@ export class SceneEntities {
if (err(moddedResult)) return
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)
_ast = _recastAst
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish circle' })
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
}
},
})
@ -1208,6 +1245,7 @@ export class SceneEntities {
forward,
position,
})
await codeManager.writeToFile()
}
},
onDrag: async ({

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,20 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import usePlatform from 'hooks/usePlatform'
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { EngineConnectionStateType } from 'lang/std/engineConnection'
export function CommandBarOpenButton() {
const { commandBarSend } = useCommandsContext()
const { immediateState } = useNetworkContext()
const platform = usePlatform()
const isDisabled =
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
return (
<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"
onClick={() => commandBarSend({ type: 'Open' })}
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 { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor'
import { reportRejection } from 'lib/trap'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
@ -196,8 +197,7 @@ const FileTreeItem = ({
return
}
// Don't try to read a file that was removed.
if (isCurrentFile && eventType !== 'unlink') {
if (isCurrentFile && eventType === 'change') {
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code)
@ -242,7 +242,7 @@ const FileTreeItem = ({
// Show the renaming form
addCurrentItemToRenaming()
} else if (e.code === 'Space') {
void handleClick()
void handleClick().catch(reportRejection)
}
}
@ -293,7 +293,7 @@ const FileTreeItem = ({
style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => {
e.currentTarget.focus()
void handleClick()
void handleClick().catch(reportRejection)
}}
onKeyUp={handleKeyUp}
>

View File

@ -1,40 +1,47 @@
import { useEffect, useState } from 'react'
import { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon'
import useEngineStreamContext, {
EngineStreamState,
} from 'hooks/useEngineStreamContext'
import { CommandLogType } from 'lang/std/engineConnection'
export const ModelStateIndicator = () => {
const [commands] = useEngineCommands()
const [isDone, setIsDone] = useState<boolean>(false)
const engineStreamActor = useEngineStreamContext.useActorRef()
const engineStreamState = engineStreamActor.getSnapshot()
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 icon = <Spinner className={className} />
let icon = <div className={className}></div>
let dataTestId = 'model-state-indicator'
if (lastCommandType === 'receive-reliable') {
className +=
'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 + '-receive-reliable'}
name="checkmark"
/>
)
} 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'
if (engineStreamState.value === EngineStreamState.Paused) {
className += 'text-secondary'
icon = <CustomIcon data-testid={dataTestId + '-paused'} name="parallel" />
} else if (engineStreamState.value === EngineStreamState.Resuming) {
className += 'text-secondary'
icon = <CustomIcon data-testid={dataTestId + '-resuming'} name="parallel" />
} else if (isDone) {
className += 'text-secondary'
icon = (
<CustomIcon
data-testid={dataTestId + '-execution-done'}
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 (

View File

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

View File

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

View File

@ -6,6 +6,11 @@ import {
useEffect,
useMemo,
useContext,
MutableRefObject,
forwardRef,
// https://stackoverflow.com/a/77055468 Thank you.
useImperativeHandle,
useRef,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
@ -19,9 +24,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { sceneInfra } from 'lib/singletons'
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
ref: MutableRefObject<HTMLDivElement>
}
interface BadgeInfoComputed {
@ -33,19 +41,34 @@ function getPlatformString(): 'web' | 'desktop' {
return isDesktop() ? 'desktop' : 'web'
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
export const ModelingSidebar = forwardRef<
HTMLUListElement,
ModelingSidebarProps
>(function ModelingSidebar({ paneOpacity }, outerRef) {
const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext()
const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus
const { send, context } = useModelingContext()
const { send, state, context } = useModelingContext()
const pointerEventsCssClass =
onboardingStatus.current === 'camera' ||
context.store?.openPanes.length === 0
? 'pointer-events-none '
: 'pointer-events-auto '
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(
() => ({
@ -159,8 +182,37 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
[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 (
<Resizable
data-testid="modeling-sidebar"
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
defaultSize={{
width: '550px',
@ -192,6 +244,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
>
<ul
id="pane-buttons-section"
data-testid="pane-buttons-section"
className={
'w-fit p-2 flex flex-col gap-2 ' +
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
@ -236,6 +289,8 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
</ul>
<ul
id="pane-section"
data-testid="pane-section"
ref={innerRef}
className={
'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' +
(context.store?.openPanes.length >= 1 ? `w-full` : `hidden`)
@ -265,7 +320,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
</div>
</Resizable>
)
}
})
interface ModelingPaneButtonProps
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>()
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 {
private _editorView: EditorView | null = null
private _copilotEnabled: boolean = true
@ -72,9 +68,10 @@ export default class EditorManager {
// we cannot use <>.constructor.name since it will get destroyed
// when packaging the application.
const isTreeHighlightPlugin =
e.value.hasOwnProperty('tree') &&
e.value.hasOwnProperty('decoratedTo') &&
e.value.hasOwnProperty('decorations')
e?.value &&
e.value?.hasOwnProperty('tree') &&
e.value?.hasOwnProperty('decoratedTo') &&
e.value?.hasOwnProperty('decorations')
if (isTreeHighlightPlugin) {
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 {
if (!this._editorView) return
// Clear out any existing diagnostics that are the same.
for (const diagnostic of diagnostics) {
for (const otherDiagnostic of diagnostics) {
if (diagnosticIsEqual(diagnostic, otherDiagnostic)) {
diagnostics = diagnostics.filter(
(d) => !diagnosticIsEqual(d, diagnostic)
)
diagnostics.push(diagnostic)
break
}
}
}
diagnostics = this.makeUniqueDiagnostics(diagnostics)
this._editorView.dispatch({
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,
createSetVarNameModal,
} from 'components/SetVarNameModal'
import { editorManager, kclManager } from 'lib/singletons'
import { reportRejection, trap } from 'lib/trap'
import { editorManager, kclManager, codeManager } from 'lib/singletons'
import { reportRejection, trap, err } from 'lib/trap'
import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react'
import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange } from 'lang/wasm'
import { PathToNode, SourceRange, recast } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider'
import { toSync } from 'lib/utils'
@ -57,6 +57,11 @@ export function useConvertToVariable(range?: SourceRange) {
)
await kclManager.updateAst(_modifiedAst, true)
const newCode = recast(_modifiedAst)
if (err(newCode)) return
codeManager.updateCodeEditor(newCode)
return pathToReplacedNode
} catch (e) {
console.log('error', e)

View File

@ -2,7 +2,7 @@ import { executeAst, lintAst } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection'
import { EngineCommandManager, CommandLogType } from './std/engineConnection'
import { err } from 'lib/trap'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
@ -125,7 +125,7 @@ export class KclManager {
if (this.lints.length > 0) {
diagnostics = diagnostics.concat(this.lints)
}
editorManager.setDiagnostics(diagnostics)
editorManager?.setDiagnostics(diagnostics)
}
addKclErrors(kclErrors: KCLError[]) {
@ -290,15 +290,9 @@ export class KclManager {
)
}
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
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
},
await sceneInfra.camControls.centerModelRelativeToPanes({
zoomToFit: true,
zoomObjectId,
})
}
}
@ -328,7 +322,7 @@ export class KclManager {
this.ast = { ...ast }
this._executeCallback()
this.engineCommandManager.addCommandLog({
type: 'execution-done',
type: CommandLogType.ExecutionDone,
data: null,
})
@ -357,9 +351,6 @@ export class KclManager {
this.clearAst()
return
}
codeManager.updateCodeEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
this._ast = { ...newAst }
const { logs, errors, execState } = await executeAst({
@ -497,11 +488,6 @@ export class KclManager {
}
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({
ast: astWithUpdatedSource,
zoomToFit: optionalParams?.zoomToFit,

View File

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

View File

@ -7,6 +7,8 @@ import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons'
import { Annotation, Transaction } from '@codemirror/state'
import { KeyBinding } from '@codemirror/view'
import { recast, Program } from 'lang/wasm'
import { err } from 'lib/trap'
const PERSIST_CODE_KEY = 'persistCode'
@ -147,6 +149,13 @@ export default class CodeManager {
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) {

View File

@ -58,7 +58,13 @@ const newVar = myVar + 1`
`
const mem = await exe(code)
// 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([
{
type: 'ToPoint',
@ -150,7 +156,7 @@ const newVar = myVar + 1`
].join('\n')
const mem = await exe(code)
expect(mem.get('mySk1')).toEqual({
type: 'UserVal',
type: 'Sketch',
value: {
type: 'Sketch',
on: expect.any(Object),
@ -215,7 +221,6 @@ const newVar = myVar + 1`
id: expect.any(String),
__meta: [{ sourceRange: [39, 63, 0] }],
},
__meta: [{ sourceRange: [39, 63, 0] }],
})
})
it('execute array expression', async () => {
@ -225,7 +230,7 @@ const newVar = myVar + 1`
const mem = await exe(code)
// TODO path to node is probably wrong here, zero indexes are not correct
expect(mem.get('three')).toEqual({
type: 'UserVal',
type: 'Int',
value: 3,
__meta: [
{
@ -234,8 +239,17 @@ const newVar = myVar + 1`
],
})
expect(mem.get('yo')).toEqual({
type: 'UserVal',
value: [1, '2', 3, 9],
type: 'Array',
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: [
{
sourceRange: [27, 49, 0],
@ -253,8 +267,25 @@ const newVar = myVar + 1`
].join('\n')
const mem = await exe(code)
expect(mem.get('yo')).toEqual({
type: 'UserVal',
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
type: 'Object',
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: [
{
sourceRange: [27, 83, 0],
@ -268,11 +299,11 @@ const newVar = myVar + 1`
)
const mem = await exe(code)
expect(mem.get('myVar')).toEqual({
type: 'UserVal',
type: 'String',
value: '123',
__meta: [
{
sourceRange: [41, 50, 0],
sourceRange: [19, 24, 0],
},
],
})
@ -356,7 +387,26 @@ describe('testing math operators', () => {
it('with unaryExpression in ArrayExpression', async () => {
const code = 'const myVar = [1,-legLen(5, 4)]'
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 () => {
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
// with the latest changes in github. We won't be hard coding the filenames
it(
'should run through all the files',
async () => {
for (let i = 0; i < files.length; i++) {
const file: KclSampleFile = files[i]
const code = await getKclSampleCodeFromGithub(file.filename)
const parsed = parse(code)
assert(!(parsed instanceof Error))
}
},
files.length * 1000
)
files.forEach((file: KclSampleFile) => {
it(`should parse ${file.filename} without errors`, async () => {
const code = await getKclSampleCodeFromGithub(file.filename)
const parsed = parse(code)
assert(!(parsed instanceof Error))
}, 1000)
})
})
describe('when performing enginelessExecutor', () => {

View File

@ -35,7 +35,12 @@ import {
ArtifactGraph,
getSweepFromSuspectedPath,
} 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'
// Apply Fillet To Selection
@ -253,6 +258,9 @@ async function updateAstAndFocus(
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode,
})
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}

View File

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

View File

@ -406,13 +406,14 @@ class EngineConnection extends EventTarget {
default:
if (this.isConnecting()) break
// 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
}
}, pingIntervalMs)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.connect()
this.connect({ reconnect: false })
}
// SHOULD ONLY BE USED FOR VITESTS
@ -523,7 +524,9 @@ class EngineConnection extends EventTarget {
this.idleMode = opts?.idleMode ?? false
clearInterval(this.pingIntervalId)
if (opts?.idleMode) {
this.disconnectAll()
if (this.idleMode) {
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
@ -542,8 +545,6 @@ class EngineConnection extends EventTarget {
type: DisconnectingType.Quit,
},
}
this.disconnectAll()
}
/**
@ -553,7 +554,7 @@ class EngineConnection extends EventTarget {
* This will attempt the full handshake, and retry if the connection
* did not establish.
*/
connect(reconnecting?: boolean): Promise<void> {
connect(args: { reconnect: boolean }): Promise<void> {
return new Promise((resolve) => {
if (this.isConnecting() || this.isReady()) {
return
@ -1165,7 +1166,7 @@ class EngineConnection extends EventTarget {
this.websocket.addEventListener('message', this.onWebSocketMessage)
}
if (reconnecting) {
if (args.reconnect) {
createWebSocketConnection()
} else {
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
// WebSocketRequest type!
unreliableSend(message: Models['WebSocketRequest_type']) {
@ -1229,8 +1256,17 @@ class EngineConnection extends EventTarget {
this.websocket?.readyState === 3
if (closedPc && closedUDC && closedWS) {
// Do not notify the rest of the program that we have cut off anything.
this.state = { type: EngineConnectionStateType.Disconnected }
if (!this.idleMode) {
// Do not notify the rest of the program that we have cut off anything.
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
}
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 =
| {
type: 'send-modeling'
type: CommandLogType.SendModeling
data: EngineCommand
}
| {
type: 'send-scene'
type: CommandLogType.SendScene
data: EngineCommand
}
| {
type: 'receive-reliable'
type: CommandLogType.ReceiveReliable
data: OkWebSocketResponseData
id: string
cmd_type?: string
}
| {
type: 'execution-done'
type: CommandLogType.ExecutionDone
data: null
}
| {
type: 'export-done'
type: CommandLogType.ExportDone
data: null
}
| {
type: CommandLogType.SetDefaultSystemProperties
data: null
}
@ -1686,7 +1735,7 @@ export class EngineCommandManager extends EventTarget {
message.request_id
) {
this.addCommandLog({
type: 'receive-reliable',
type: CommandLogType.ReceiveReliable,
data: message.resp,
id: message?.request_id || '',
cmd_type: pending?.command?.cmd?.type,
@ -1720,7 +1769,7 @@ export class EngineCommandManager extends EventTarget {
if (!command) return
if (command.type === 'modeling_cmd_req')
this.addCommandLog({
type: 'receive-reliable',
type: CommandLogType.ReceiveReliable,
data: {
type: 'modeling',
data: {
@ -1762,7 +1811,7 @@ export class EngineCommandManager extends EventTarget {
)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineConnection?.connect()
this.engineConnection?.connect({ reconnect: false })
}
this.engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStarted,
@ -1824,6 +1873,7 @@ export class EngineCommandManager extends EventTarget {
)
this.engineConnection?.tearDown(opts)
this.engineConnection = undefined
// Our window.tearDown assignment causes this case to happen which is
// only really for tests.
@ -1831,6 +1881,8 @@ export class EngineCommandManager extends EventTarget {
} else if (this.engineCommandManager?.engineConnection) {
// @ts-ignore
this.engineCommandManager?.engineConnection?.tearDown(opts)
// @ts-ignore
this.engineCommandManager.engineConnection = null
}
}
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
this.addCommandLog({
type: 'send-scene',
type: CommandLogType.SendScene,
data: command,
})
}
@ -1988,7 +2040,7 @@ export class EngineCommandManager extends EventTarget {
toastId,
resolve: (passThrough) => {
this.addCommandLog({
type: 'export-done',
type: CommandLogType.ExportDone,
data: null,
})
resolve(passThrough)

View File

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

View File

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

View File

@ -118,6 +118,8 @@ export class Setting<T = unknown> {
}
}
const MS_IN_MINUTE = 1000 * 60
export function createSettings() {
return {
/** Settings that affect the behavior of the entire app,
@ -181,13 +183,58 @@ export function createSettings() {
/**
* Stream resource saving behavior toggle
*/
streamIdleMode: new Setting<boolean>({
defaultValue: false,
streamIdleMode: new Setting<number | undefined>({
defaultValue: undefined,
description: 'Toggle stream idling, saving bandwidth and battery',
validate: (v) => typeof v === 'boolean',
commandConfig: {
inputType: 'boolean',
},
validate: (v) =>
v === undefined ||
(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>({
defaultValue: '',

View File

@ -24,6 +24,10 @@ import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration
import { BROWSER_PROJECT_NAME } from 'lib/constants'
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.
* We do this because the JS settings type has all the fancy shit
@ -40,7 +44,9 @@ export function configurationToSettingsPayload(
: undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status,
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,
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
},
@ -79,7 +85,9 @@ export function projectConfigurationToSettingsPayload(
: undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status,
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,
},
modeling: {

View File

@ -10,8 +10,14 @@ export const codeManager = new CodeManager()
export const engineCommandManager = new EngineCommandManager()
// Accessible for tests mostly
// @ts-ignore
declare global {
interface Window {
tearDown: typeof engineCommandManager.tearDown
sceneInfra: typeof sceneInfra
}
}
// Accessible for tests
window.tearDown = engineCommandManager.tearDown
// This needs to be after codeManager is created.
@ -21,7 +27,9 @@ engineCommandManager.kclManager = kclManager
engineCommandManager.getAstCb = () => kclManager.ast
export const sceneInfra = new SceneInfra(engineCommandManager)
engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange
// Accessible for tests
window.sceneInfra = sceneInfra
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()
for (const { key, value } of availableVarInfo.variables) {
const error = _programMem.set(key, {
type: 'UserVal',
type: 'String',
value,
__meta: [],
})
@ -115,6 +115,7 @@ export function useCalculateKclExpression({
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
}
if (!value) return
execAstAndSetResult().catch(() => {
setCalcResult('NAN')
setValueNode(null)

View File

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

View File

@ -27,7 +27,7 @@ pub use crate::ast::types::{
use crate::{
docs::StdLibFn,
errors::KclError,
executor::{ExecState, ExecutorContext, KclValue, Metadata, SourceRange, TagIdentifier, UserVal},
executor::{ExecState, ExecutorContext, KclValue, Metadata, SourceRange, TagIdentifier},
parser::PIPE_OPERATOR,
std::kcl_stdlib::KclStdLibFn,
};
@ -59,6 +59,14 @@ pub struct Node<T> {
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> {
fn schema_name() -> String {
T::schema_name()
@ -1708,34 +1716,26 @@ impl Literal {
impl From<Node<Literal>> for KclValue {
fn from(literal: Node<Literal>) -> Self {
KclValue::UserVal(UserVal {
value: JValue::from(literal.value.clone()),
meta: vec![Metadata {
source_range: literal.into(),
}],
})
let meta = vec![literal.metadata()];
match literal.inner.value {
LiteralValue::IInteger(value) => KclValue::Int { value, meta },
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 {
fn from(literal: &Node<Literal>) -> Self {
KclValue::UserVal(UserVal {
value: JValue::from(literal.value.clone()),
meta: vec![Metadata {
source_range: literal.into(),
}],
})
Self::from(literal.to_owned())
}
}
impl From<&BoxNode<Literal>> for KclValue {
fn from(literal: &BoxNode<Literal>) -> Self {
KclValue::UserVal(UserVal {
value: JValue::from(literal.value.clone()),
meta: vec![Metadata {
source_range: literal.into(),
}],
})
let b: &Node<Literal> = literal;
Self::from(b)
}
}
@ -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)]
mod tests {
use pretty_assertions::assert_eq;

View File

@ -1,18 +1,21 @@
use std::collections::HashMap;
use super::{
human_friendly_type, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart,
CallExpression, Expr, IfExpression, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node,
ObjectExpression, TagDeclarator, UnaryExpression, UnaryOperator,
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Expr,
IfExpression, KclNone, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, ObjectExpression,
TagDeclarator, UnaryExpression, UnaryOperator,
};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{
BodyType, ExecState, ExecutorContext, KclValue, Metadata, Sketch, SourceRange, StatementKind, TagEngineInfo,
TagIdentifier, UserVal,
BodyType, ExecState, ExecutorContext, KclValue, Metadata, SourceRange, StatementKind, TagEngineInfo,
TagIdentifier,
},
std::FunctionKind,
};
use async_recursion::async_recursion;
use serde_json::Value as JValue;
const FLOAT_TO_INT_MAX_DELTA: f64 = 0.01;
impl BinaryPart {
#[async_recursion]
@ -42,26 +45,19 @@ impl Node<MemberExpression> {
}
};
let array_json = array.get_json_value()?;
if let serde_json::Value::Array(array) = array_json {
if let Some(value) = array.get(index) {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("index {} not found in array", index),
source_ranges: vec![self.clone().into()],
}))
}
} else {
Err(KclError::Semantic(KclErrorDetails {
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 Some(value) = array.get(index) {
Ok(value.to_owned())
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("index {} not found in array", index),
source_ranges: vec![self.clone().into()],
}))
}
}
@ -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.
match (object_json, property) {
(JValue::Object(map), Property::String(property)) => {
match (object, property) {
(KclValue::Object { value: map, meta: _ }, Property::String(property)) => {
if let Some(value) = map.get(&property) {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
Ok(value.to_owned())
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("Property '{property}' not found in object"),
@ -96,22 +85,20 @@ impl Node<MemberExpression> {
}))
}
}
(JValue::Object(_), p) => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only strings can be used as the property of an object, but you're using a {}",
p.type_name()
),
source_ranges: vec![self.clone().into()],
})),
(JValue::Array(arr), Property::Number(index)) => {
let value_of_arr: Option<&JValue> = arr.get(index);
(KclValue::Object { .. }, p) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only strings can be used as the property of an object, but you're using {article} {t}",
),
source_ranges: vec![self.clone().into()],
}))
}
(KclValue::Array { value: arr, meta: _ }, Property::Number(index)) => {
let value_of_arr = arr.get(index);
if let Some(value) = value_of_arr {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
Ok(value.to_owned())
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
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 {
message: format!(
"Only integers >= 0 can be used as the index of an array, but you're using a {}",
p.type_name()
),
source_ranges: vec![self.clone().into()],
})),
(being_indexed, _) => {
let t = human_friendly_type(&being_indexed);
(KclValue::Array { .. }, p) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
message: format!("Only arrays and objects can be indexed, but you're trying to index a {t}"),
message: format!(
"Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",
),
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, _) => {
let t = being_indexed.human_friendly_type();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only arrays and objects can be indexed, but you're trying to index {article} {t}"
),
source_ranges: vec![self.clone().into()],
}))
}
@ -140,81 +146,134 @@ impl Node<MemberExpression> {
impl Node<BinaryExpression> {
#[async_recursion]
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 right_json_value = self.right.get_result(exec_state, ctx).await?.get_json_value()?;
let left_value = self.left.get_result(exec_state, ctx).await?;
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.
if self.operator == BinaryOperator::Add {
if let (Some(left), Some(right)) = (
parse_json_value_as_string(&left_json_value),
parse_json_value_as_string(&right_json_value),
) {
let value = serde_json::Value::String(format!("{}{}", left, right));
return Ok(KclValue::UserVal(UserVal {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
}));
if let (KclValue::String { value: left, meta: _ }, KclValue::String { value: right, meta: _ }) =
(&left_value, &right_value)
{
return Ok(KclValue::String {
value: format!("{}{}", left, right),
meta,
});
}
}
let left = parse_json_number_as_f64(&left_json_value, self.left.clone().into())?;
let right = parse_json_number_as_f64(&right_json_value, self.right.clone().into())?;
let left = parse_number_as_f64(&left_value, self.left.clone().into())?;
let right = parse_number_as_f64(&right_value, self.right.clone().into())?;
let value: serde_json::Value = match self.operator {
BinaryOperator::Add => (left + right).into(),
BinaryOperator::Sub => (left - right).into(),
BinaryOperator::Mul => (left * right).into(),
BinaryOperator::Div => (left / right).into(),
BinaryOperator::Mod => (left % right).into(),
BinaryOperator::Pow => (left.powf(right)).into(),
BinaryOperator::Eq => (left == right).into(),
BinaryOperator::Neq => (left != right).into(),
BinaryOperator::Gt => (left > right).into(),
BinaryOperator::Gte => (left >= right).into(),
BinaryOperator::Lt => (left < right).into(),
BinaryOperator::Lte => (left <= right).into(),
let value = match self.operator {
BinaryOperator::Add => KclValue::Number {
value: left + right,
meta,
},
BinaryOperator::Sub => KclValue::Number {
value: left - right,
meta,
},
BinaryOperator::Mul => KclValue::Number {
value: left * right,
meta,
},
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 {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
}))
Ok(value)
}
}
impl Node<UnaryExpression> {
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
if self.operator == UnaryOperator::Not {
let value = self.argument.get_result(exec_state, ctx).await?.get_json_value()?;
let Some(bool_value) = json_as_bool(&value) else {
let value = self.argument.get_result(exec_state, ctx).await?;
let KclValue::Bool {
value: bool_value,
meta: _,
} = value
else {
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()],
}));
};
let negated = !bool_value;
return Ok(KclValue::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
let meta = vec![Metadata {
source_range: self.into(),
}];
let negated = KclValue::Bool {
value: !bool_value,
meta,
};
return Ok(negated);
}
let num = parse_json_number_as_f64(
&self.argument.get_result(exec_state, ctx).await?.get_json_value()?,
self.into(),
)?;
Ok(KclValue::UserVal(UserVal {
value: (-(num)).into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
let value = &self.argument.get_result(exec_state, ctx).await?;
match value {
KclValue::Number { value, meta: _ } => {
let meta = vec![Metadata {
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
// and it works.
match result {
KclValue::UserVal(ref mut uval) => {
uval.mutate(|sketch: &mut Sketch| {
for (_, tag) in sketch.tags.iter() {
exec_state.memory.update_tag(&tag.value, tag.clone())?;
}
Ok::<_, KclError>(())
})?;
KclValue::Sketch { value: ref mut sketch } => {
for (_, tag) in sketch.tags.iter() {
exec_state.memory.update_tag(&tag.value, tag.clone())?;
}
}
KclValue::Solid(ref mut solid) => {
for value in &solid.value {
@ -425,10 +481,10 @@ impl Node<CallExpression> {
} else {
fn_memory.add(
&param.identifier.name,
KclValue::UserVal(UserVal {
value: serde_json::value::Value::Null,
meta: Default::default(),
}),
KclValue::KclNone {
value: KclNone::new(),
meta: vec![self.into()],
},
param.identifier.clone().into(),
)?;
}
@ -531,15 +587,13 @@ impl Node<ArrayExpression> {
.execute_expr(element, exec_state, &metadata, StatementKind::Expression)
.await?;
results.push(value.get_json_value()?);
results.push(value);
}
Ok(KclValue::UserVal(UserVal {
value: results.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
Ok(KclValue::Array {
value: results,
meta: vec![self.into()],
})
}
}
@ -549,15 +603,19 @@ impl Node<ArrayRangeExpression> {
let metadata = Metadata::from(&self.start_element);
let start = ctx
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let start = parse_json_number_as_i64(&start, (&self.start_element).into())?;
.await?;
let start = start.as_int().ok_or(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Expected int but found {}", start.human_friendly_type()),
}))?;
let metadata = Metadata::from(&self.end_element);
let end = ctx
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let end = parse_json_number_as_i64(&end, (&self.end_element).into())?;
.await?;
let end = end.as_int().ok_or(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Expected int but found {}", end.human_friendly_type()),
}))?;
if end < start {
return Err(KclError::Semantic(KclErrorDetails {
@ -567,94 +625,76 @@ impl Node<ArrayRangeExpression> {
}
let range: Vec<_> = if self.end_inclusive {
(start..=end).map(JValue::from).collect()
(start..=end).collect()
} else {
(start..end).map(JValue::from).collect()
(start..end).collect()
};
Ok(KclValue::UserVal(UserVal {
value: range.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
let meta = vec![Metadata {
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> {
#[async_recursion]
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 {
let metadata = Metadata::from(&property.value);
let result = ctx
.execute_expr(&property.value, exec_state, &metadata, StatementKind::Expression)
.await?;
object.insert(property.key.name.clone(), result.get_json_value()?);
object.insert(property.key.name.clone(), result);
}
Ok(KclValue::UserVal(UserVal {
value: object.into(),
Ok(KclValue::Object {
value: object,
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
fn parse_json_number_as_i64(j: &serde_json::Value, source_range: SourceRange) -> Result<i64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_i64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
})
})
}
}
fn article_for(s: &str) -> &'static str {
if s.starts_with(['a', 'e', 'i', 'o', 'u']) {
"an"
} else {
Err(KclError::Syntax(KclErrorDetails {
"a"
}
}
pub fn parse_number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<f64, KclError> {
if let KclValue::Number { value: n, .. } = &v {
Ok(*n)
} else if let KclValue::Int { value: n, .. } = &v {
Ok(*n as f64)
} else {
let actual_type = v.human_friendly_type();
let article = if actual_type.starts_with(['a', 'e', 'i', 'o', 'u']) {
"an"
} else {
"a"
};
Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
message: format!("Expected a number, but found {article} {actual_type}",),
}))
}
}
pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_f64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid number: {}", j),
})
})
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid number: {}", j),
}))
}
}
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 {
None
}
}
/// JSON value as bool. If it isn't a bool, returns None.
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,
}
}
impl Node<IfExpression> {
#[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
@ -724,15 +764,7 @@ impl Property {
} else {
// Actually evaluate memory to compute the property.
let prop = exec_state.memory.get(name, property_src)?;
let KclValue::UserVal(prop) = prop else {
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)
jvalue_to_prop(prop, property_sr, name)
}
}
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| {
Err::<Property, _>(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
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 {
JValue::Number(ref num) => {
let maybe_uint = num.as_u64().and_then(|x| usize::try_from(x).ok());
if let Some(uint) = maybe_uint {
KclValue::Int { value:num, meta: _ } => {
let maybe_int: Result<usize, _> = (*num).try_into();
if let Ok(uint) = maybe_int {
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 if let Some(fnum) = num.as_f64() {
if fnum < 0.0 {
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}"))
} else if fnum.fract() == 0.0 {
make_err(format!("'{fnum:.1}' is stored as a fractional number but indices must be whole numbers, {TRY_INT}"))
} else {
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}, {TRY_INT}"))
}
} else {
make_err(format!("'{num}' is not a valid index, {MUST_BE_POSINT}"))
}
else {
make_err(format!("'{num}' is negative, so you can't index an array with it"))
}
}
JValue::String(ref x) => Ok(Property::String(x.to_owned())),
KclValue::Number{value: num, meta:_} => {
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 {
make_err(format!("'{num}' is not an integer, so you can't index an array with it"))
}
}
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"))
}

View File

@ -4,10 +4,7 @@ use databake::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
ast::types::ConstraintLevel,
executor::{KclValue, UserVal},
};
use crate::{ast::types::ConstraintLevel, executor::KclValue};
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.
/// (remember, parameters are in the function declaration,
/// 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)]
#[ts(export)]
#[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 {
fn from(none: &KclNone) -> Self {
let val = UserVal::from(none);
KclValue::UserVal(val)
KclValue::KclNone {
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
// 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.
// Alternatively, run `just redo-kcl-stdlib-docs` (make sure to have just installed).
let stdlib = StdLib::new();
let combined = stdlib.combined();

View File

@ -19,7 +19,6 @@ use kittycad_modeling_cmds::length_unit::LengthUnit;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
type Point2D = kcmc::shared::Point2d<f64>;
@ -27,8 +26,8 @@ type Point3D = kcmc::shared::Point3d<f64>;
use crate::{
ast::types::{
human_friendly_type, BodyItem, Expr, FunctionExpression, ItemVisibility, KclNone, ModuleId, Node, NodeRef,
Program, TagDeclarator, TagNode,
BodyItem, Expr, FunctionExpression, ItemVisibility, KclNone, ModuleId, Node, NodeRef, Program, TagDeclarator,
TagNode,
},
engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails},
@ -201,33 +200,18 @@ impl Environment {
Self {
// Prelude
bindings: HashMap::from([
(
"ZERO".to_string(),
KclValue::UserVal(UserVal {
value: serde_json::Value::Number(serde_json::value::Number::from(0)),
meta: Default::default(),
}),
),
("ZERO".to_string(), KclValue::from_number(0.0, Default::default())),
(
"QUARTER_TURN".to_string(),
KclValue::UserVal(UserVal {
value: serde_json::Value::Number(serde_json::value::Number::from(90)),
meta: Default::default(),
}),
KclValue::from_number(90.0, Default::default()),
),
(
"HALF_TURN".to_string(),
KclValue::UserVal(UserVal {
value: serde_json::Value::Number(serde_json::value::Number::from(180)),
meta: Default::default(),
}),
KclValue::from_number(180.0, Default::default()),
),
(
"THREE_QUARTER_TURN".to_string(),
KclValue::UserVal(UserVal {
value: serde_json::Value::Number(serde_json::value::Number::from(270)),
meta: Default::default(),
}),
KclValue::from_number(270.0, Default::default()),
),
]),
parent: None,
@ -264,22 +248,15 @@ impl Environment {
}
for (_, val) in self.bindings.iter_mut() {
let KclValue::UserVal(v) = val else { continue };
let meta = v.meta.clone();
let maybe_sg: Result<Sketch, _> = serde_json::from_value(v.value.clone());
let Ok(mut sketch) = maybe_sg else {
continue;
};
let KclValue::Sketch { value } = val else { continue };
let mut sketch = value.to_owned();
if sketch.original_id == sg.original_id {
for tag in sg.tags.iter() {
sketch.tags.insert(tag.0.clone(), tag.1.clone());
}
}
*val = KclValue::UserVal(UserVal {
meta,
value: serde_json::to_value(sketch).expect("can always turn Sketch into JSON"),
});
*val = KclValue::Sketch { value: sketch };
}
}
}
@ -360,12 +337,52 @@ impl IdGenerator {
#[ts(export)]
#[serde(tag = "type")]
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>),
TagDeclarator(crate::ast::types::BoxNode<TagDeclarator>),
Plane(Box<Plane>),
Face(Box<Face>),
Sketch {
value: Box<Sketch>,
},
Sketches {
value: Vec<Box<Sketch>>,
},
Solid(Box<Solid>),
Solids {
value: Vec<Box<Solid>>,
@ -380,31 +397,55 @@ pub enum KclValue {
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
KclNone {
value: KclNone,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
}
impl KclValue {
pub(crate) fn new_user_val<T: Serialize>(meta: Vec<Metadata>, val: T) -> Self {
Self::UserVal(UserVal::new(meta, val))
pub(crate) fn metadata(&self) -> Vec<Metadata> {
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> {
match self {
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
KclValue::UserVal(value) => {
let value = value.value.clone();
match value {
JValue::Null | JValue::Bool(_) | JValue::Number(_) | JValue::String(_) => Err(anyhow::anyhow!(
"Failed to deserialize solid set from JSON {}",
human_friendly_type(&value)
)),
JValue::Array(_) => serde_json::from_value::<Vec<Box<Solid>>>(value)
.map(SolidSet::from)
.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)),
}
KclValue::Array { value, .. } => {
let solids: Vec<_> = value
.iter()
.enumerate()
.map(|(i, v)| {
v.as_solid().map(|v| v.to_owned()).map(Box::new).ok_or_else(|| {
anyhow::anyhow!(
"expected this array to only contain solids, but element {i} was actually {}",
v.human_friendly_type()
)
})
})
.collect::<Result<_, _>>()?;
Ok(SolidSet::Solids(solids))
}
_ => anyhow::bail!("Not a solid or solids: {:?}", self),
}
@ -414,43 +455,44 @@ impl KclValue {
/// on for program logic.
pub(crate) fn human_friendly_type(&self) -> &'static str {
match self {
KclValue::UserVal(u) => human_friendly_type(&u.value),
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid(_) => "Solid",
KclValue::Solids { .. } => "Solids",
KclValue::Sketch { .. } => "Sketch",
KclValue::Sketches { .. } => "Sketches",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane(_) => "Plane",
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 {
match self {
KclValue::UserVal(..)
| KclValue::TagIdentifier(..)
| KclValue::TagDeclarator(..)
| KclValue::Plane(..)
| KclValue::Face(..)
| KclValue::Solid(..)
| KclValue::Solids { .. }
| KclValue::ImportedGeometry(..) => false,
KclValue::Function { .. } => true,
}
matches!(self, KclValue::Function { .. })
}
}
impl From<SketchSet> for KclValue {
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 {
fn from(sg: Vec<Box<Sketch>>) -> Self {
let meta = sg.iter().flat_map(|sg| sg.meta.clone()).collect();
KclValue::UserVal(UserVal::new(meta, sg))
KclValue::Sketches { value: sg }
}
}
@ -815,52 +857,6 @@ pub enum PlaneType {
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)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
@ -922,108 +918,177 @@ pub type MemoryFunction =
impl From<KclValue> for Vec<SourceRange> {
fn from(item: KclValue) -> Self {
match item {
KclValue::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
KclValue::TagDeclarator(t) => vec![(&t).into()],
KclValue::TagIdentifier(t) => t.meta.iter().map(|m| m.source_range).collect(),
KclValue::Solid(e) => e.meta.iter().map(|m| m.source_range).collect(),
KclValue::Solids { value } => value
.iter()
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range))
.collect(),
KclValue::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(),
KclValue::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
KclValue::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(),
KclValue::Face(f) => f.meta.iter().map(|m| m.source_range).collect(),
KclValue::TagDeclarator(t) => vec![SourceRange([t.start, t.end, t.module_id.0 as usize])],
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::Solid(e) => to_vec_sr(&e.meta),
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Sketch { value } => to_vec_sr(&value.meta),
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
KclValue::Function { meta, .. } => to_vec_sr(&meta),
KclValue::Plane(p) => to_vec_sr(&p.meta),
KclValue::Face(f) => to_vec_sr(&f.meta),
KclValue::Bool { meta, .. } => to_vec_sr(&meta),
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> {
fn from(item: &KclValue) -> Self {
match item {
KclValue::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
KclValue::TagDeclarator(ref t) => vec![t.into()],
KclValue::TagIdentifier(t) => t.meta.iter().map(|m| m.source_range).collect(),
KclValue::Solid(e) => e.meta.iter().map(|m| m.source_range).collect(),
KclValue::Solids { value } => value
.iter()
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range))
.collect(),
KclValue::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(),
KclValue::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
KclValue::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(),
KclValue::Face(f) => f.meta.iter().map(|m| m.source_range).collect(),
KclValue::TagDeclarator(t) => vec![SourceRange([t.start, t.end, t.module_id.0 as usize])],
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::Solid(e) => to_vec_sr(&e.meta),
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Sketch { value } => to_vec_sr(&value.meta),
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
KclValue::Function { meta, .. } => to_vec_sr(meta),
KclValue::Plane(p) => to_vec_sr(&p.meta),
KclValue::Face(f) => to_vec_sr(&f.meta),
KclValue::Bool { meta, .. } => to_vec_sr(meta),
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 {
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
if let KclValue::UserVal(user_val) = self {
Ok(user_val.value.clone())
} else {
serde_json::to_value(self).map_err(|err| {
KclError::Semantic(KclErrorDetails {
message: format!("Cannot convert memory item to json value: {:?}", err),
source_ranges: self.clone().into(),
})
})
/// Put the number into a KCL value.
pub fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
Self::Number { value: f, meta }
}
/// Put the point into a KCL value.
pub fn from_point2d(p: [f64; 2], meta: Vec<Metadata>) -> Self {
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 fn get_json<T: serde::de::DeserializeOwned>(&self) -> Result<T, KclError> {
let json = self.get_json_value()?;
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);
}
}
pub(crate) fn as_usize(&self) -> Option<usize> {
match self {
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(),
})
})
.map(Some)
}
pub fn as_user_val(&self) -> Option<&UserVal> {
if let KclValue::UserVal(x) = self {
Some(x)
pub fn as_int(&self) -> Option<i64> {
if let KclValue::Int { value, meta: _ } = &self {
Some(*value)
} else {
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> {
let err = KclError::Semantic(KclErrorDetails {
message: "Expected an integer >= 0".to_owned(),
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)
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(),
source_ranges: source_ranges.clone(),
})
})?;
u32::try_from(u).map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: "Number was too big".to_owned(),
source_ranges,
})
})
}
/// If this value is of type function, return it.
@ -1048,16 +1113,6 @@ impl KclValue {
pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
match self {
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 {
message: format!("Not a tag identifier: {:?}", self),
source_ranges: self.clone().into(),
@ -1089,19 +1144,13 @@ impl KclValue {
/// If this KCL value is a bool, retrieve it.
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 {
source_ranges: self.into(),
message: format!("Expected bool, found {}", self.human_friendly_type()),
}));
};
let JValue::Bool(b) = uv.value else {
return Err(KclError::Type(KclErrorDetails {
source_ranges: self.into(),
message: format!("Expected bool, found {}", human_friendly_type(&uv.value)),
}));
};
Ok(b)
Ok(*b)
}
/// 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.
#[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)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
@ -1563,6 +1612,12 @@ pub struct Metadata {
pub source_range: SourceRange,
}
impl From<Metadata> for Vec<SourceRange> {
fn from(meta: Metadata) -> Self {
vec![meta.source_range]
}
}
impl From<SourceRange> for Metadata {
fn from(source_range: SourceRange) -> Self {
Self { source_range }
@ -2655,74 +2710,8 @@ mod tests {
}
/// Convenience function to get a JSON value from memory and unwrap.
fn mem_get_json(memory: &ProgramMemory, name: &str) -> serde_json::Value {
memory
.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()
);
fn mem_get_json(memory: &ProgramMemory, name: &str) -> KclValue {
memory.get(name, SourceRange::default()).unwrap().to_owned()
}
#[tokio::test(flavor = "multi_thread")]
@ -3120,200 +3109,41 @@ let shape = layer() |> patternTransform(10, transform, %)
);
}
#[tokio::test(flavor = "multi_thread")]
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()
);
}
// ADAM: Move some of these into simulation tests.
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5.0),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(5.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute() {
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(7.4),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(7.4, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_start_negative() {
let ast = r#"const myVar = -5 + 6"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(1.0),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(1.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_pi() {
let ast = r#"const myVar = pi() * 2"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(std::f64::consts::TAU),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(std::f64::consts::TAU, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_define_decimal_without_leading_zero() {
let ast = r#"let thing = .4 + 7"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(7.4),
memory
.get("thing", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(7.4, mem_get_json(&memory, "thing").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
@ -3353,10 +3183,10 @@ fn check = (x) => {
check(false)
"#;
let mem = parse_execute(ast).await.unwrap();
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "notTrue"));
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "notFalse"));
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "c"));
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "d"));
assert_eq!(false, mem_get_json(&mem, "notTrue").as_bool().unwrap());
assert_eq!(true, mem_get_json(&mem, "notFalse").as_bool().unwrap());
assert_eq!(true, mem_get_json(&mem, "c").as_bool().unwrap());
assert_eq!(false, mem_get_json(&mem, "d").as_bool().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
@ -3369,7 +3199,7 @@ let notNull = !myNull
assert_eq!(
parse_execute(code1).await.unwrap_err().downcast::<KclError>().unwrap(),
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])],
})
);
@ -3378,7 +3208,7 @@ let notNull = !myNull
assert_eq!(
parse_execute(code2).await.unwrap_err().downcast::<KclError>().unwrap(),
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])],
})
);
@ -3389,7 +3219,7 @@ let notEmptyString = !""
assert_eq!(
parse_execute(code3).await.unwrap_err().downcast::<KclError>().unwrap(),
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])],
})
);
@ -3401,7 +3231,7 @@ let notMember = !obj.a
assert_eq!(
parse_execute(code4).await.unwrap_err().downcast::<KclError>().unwrap(),
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])],
})
);
@ -3412,7 +3242,7 @@ let notArray = !a";
assert_eq!(
parse_execute(code5).await.unwrap_err().downcast::<KclError>().unwrap(),
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])],
})
);
@ -3423,7 +3253,7 @@ let notObject = !x";
assert_eq!(
parse_execute(code6).await.unwrap_err().downcast::<KclError>().unwrap(),
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])],
})
);
@ -3451,7 +3281,7 @@ let notTagDeclarator = !myTagDeclarator";
assert!(
tag_declarator_err
.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: {:?}",
tag_declarator_err
);
@ -3465,7 +3295,7 @@ let notTagIdentifier = !myTag";
assert!(
tag_identifier_err
.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: {:?}",
tag_identifier_err
);
@ -3603,10 +3433,10 @@ let w = f() + f()
fn test_assign_args_to_params() {
// Set up a little framework for this test.
fn mem(number: usize) -> KclValue {
KclValue::UserVal(UserVal {
value: number.into(),
KclValue::Int {
value: number as i64,
meta: Default::default(),
})
}
}
fn ident(s: &'static str) -> Node<Identifier> {
Node::no_src(Identifier {

View File

@ -5,7 +5,7 @@ pub mod project;
use anyhow::Result;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde::{Deserializer, Deserialize, Serialize};
use validator::{Validate, ValidateRange};
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.
#[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
pub dismiss_web_banner: bool,
/// When the user is idle, and this is true, the stream will be torn down.
#[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")]
stream_idle_mode: bool,
/// When the user is idle, teardown the stream after some time.
#[serde(default, deserialize_with = "deserialize_stream_idle_mode", alias = "streamIdleMode", skip_serializing_if = "is_default")]
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)]
#[ts(export)]
#[serde(untagged)]
@ -582,7 +604,7 @@ textWrapping = true
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
stream_idle_mode: None,
},
modeling: ModelingSettings {
base_unit: UnitLength::In,
@ -643,7 +665,7 @@ includeSettings = false
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
stream_idle_mode: None,
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
@ -709,7 +731,7 @@ defaultProjectName = "projects-$nnn"
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
stream_idle_mode: None,
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
@ -787,7 +809,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
stream_idle_mode: None,
},
modeling: ModelingSettings {
base_unit: UnitLength::Mm,

View File

@ -123,7 +123,7 @@ includeSettings = false
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
stream_idle_mode: None,
},
modeling: ModelingSettings {
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 serde_json::Value as JValue;
use super::{args::FromArgs, Args, FnAsArg};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExecState, KclValue, SourceRange, UserVal},
executor::{ExecState, KclValue, SourceRange},
function_param::FunctionParam,
};
/// Apply a function to each element of an array.
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: Vec<KclValue> = array
.into_iter()
.map(|jval| {
KclValue::UserVal(UserVal {
value: jval,
meta: vec![args.source_range.into()],
})
})
.collect();
let (array, f): (Vec<KclValue>, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
let meta = vec![args.source_range.into()];
let map_fn = FunctionParam {
inner: f.func,
fn_expr: f.expr,
meta: vec![args.source_range.into()],
meta: meta.clone(),
ctx: args.ctx.clone(),
memory: *f.memory,
};
let new_array = inner_map(array, map_fn, exec_state, &args).await?;
let unwrapped = new_array
.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))
Ok(KclValue::Array { value: new_array, meta })
}
/// 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.
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: Vec<KclValue> = array
.into_iter()
.map(|jval| {
KclValue::UserVal(UserVal {
value: jval,
meta: vec![args.source_range.into()],
})
})
.collect();
let (array, start, f): (Vec<KclValue>, KclValue, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
let reduce_fn = FunctionParam {
inner: f.func,
fn_expr: f.expr,
@ -206,50 +175,26 @@ async fn call_reduce_closure<'a>(
#[stdlib {
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
let mut unwrapped_array = array
.into_iter()
.map(|k| match k {
KclValue::UserVal(user_val) => Ok(user_val.value),
_ => 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))
array.push(elem);
Ok(KclValue::Array {
value: array,
meta: vec![args.source_range.into()],
})
}
pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
// 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 array: Vec<KclValue> = array_jvalues
.into_iter()
.map(|jval| KclValue::UserVal(UserVal::new(vec![args.source_range.into()], jval)))
.collect();
// Call the inner_push function
let meta = vec![args.source_range];
let KclValue::Array { value: array, meta: _ } = val else {
let actual_type = val.human_friendly_type();
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: meta,
message: format!("You can't push to a value of type {actual_type}, only an array"),
}));
};
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> {
let (data, description): (bool, String) = args.get_data()?;
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
@ -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> {
let (left, right, description): (f64, f64, String) = args.get_data()?;
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,
@ -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> {
let (left, right, description): (f64, f64, String) = args.get_data()?;
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,
@ -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> {
let (left, right, epsilon, description): (f64, f64, f64, String) = args.get_data()?;
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,
@ -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> {
let (left, right, description): (f64, f64, String) = args.get_data()?;
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,
@ -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> {
let (left, right, description): (f64, f64, String) = args.get_data()?;
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,

View File

@ -233,7 +233,7 @@ pub(crate) async fn do_post_extrude(
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
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)
@ -244,7 +244,7 @@ pub(crate) async fn do_post_extrude(
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
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)
@ -259,7 +259,7 @@ pub(crate) async fn do_post_extrude(
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
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)

View File

@ -14,7 +14,7 @@ use uuid::Uuid;
use crate::{
ast::types::TagNode,
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,
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 edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?;
Ok(KclValue::UserVal(UserVal {
value: serde_json::to_value(edge).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert Uuid to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
Ok(KclValue::Uuid {
value: edge,
meta: vec![args.source_range.into()],
}))
})
}
/// 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 edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?;
Ok(KclValue::UserVal(UserVal {
value: serde_json::to_value(edge).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert Uuid to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
Ok(KclValue::Uuid {
value: edge,
meta: vec![args.source_range.into()],
}))
})
}
/// 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 edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?;
Ok(KclValue::UserVal(UserVal {
value: serde_json::to_value(edge).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert Uuid to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
Ok(KclValue::Uuid {
value: edge,
meta: vec![args.source_range.into()],
}))
})
}
/// 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 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).
@ -70,7 +70,7 @@ pub async fn sin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?;
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).
@ -100,7 +100,7 @@ pub async fn tan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?;
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).
@ -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> {
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 (π).
@ -155,7 +155,7 @@ pub async fn sqrt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?;
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.
@ -185,7 +185,7 @@ pub async fn abs(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?;
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.
@ -222,7 +222,7 @@ pub async fn floor(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
let num = args.get_number()?;
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.
@ -250,7 +250,7 @@ pub async fn ceil(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?;
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.
@ -278,7 +278,7 @@ pub async fn min(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let nums = args.get_number_array()?;
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.
@ -315,7 +315,7 @@ pub async fn max(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let nums = args.get_number_array()?;
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.
@ -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])?;
args.make_user_val_from_f64(result)
Ok(args.make_user_val_from_f64(result))
}
/// 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 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).
@ -427,7 +427,7 @@ pub async fn asin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?;
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).
@ -457,7 +457,7 @@ pub async fn atan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?;
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).
@ -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])?;
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.
@ -536,7 +536,7 @@ pub async fn log2(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let num = args.get_number()?;
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.
@ -564,7 +564,7 @@ pub async fn log10(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
let num = args.get_number()?;
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.
@ -592,7 +592,7 @@ pub async fn ln(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
let num = args.get_number()?;
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.
@ -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> {
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`.
@ -648,7 +648,7 @@ fn inner_e() -> Result<f64, KclError> {
pub async fn tau(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
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π.
@ -678,7 +678,7 @@ pub async fn to_radians(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
let num = args.get_number()?;
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.
@ -708,7 +708,7 @@ pub async fn to_degrees(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
let num = args.get_number()?;
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.

View File

@ -244,7 +244,7 @@ pub enum FunctionKind {
pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg) = args.get_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.
@ -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> {
let (hypotenuse, leg) = args.get_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.
@ -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> {
let (hypotenuse, leg) = args.get_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.

View File

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

View File

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
executor::{ExecState, KclValue, Metadata, Plane, UserVal},
executor::{ExecState, KclValue, Plane},
std::{sketch::PlaneData, Args},
};
@ -50,15 +50,9 @@ impl From<StandardPlane> for PlaneData {
/// Offset a plane by a distance along its normal.
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 plane = inner_offset_plane(std_plane, offset, exec_state).await?;
Ok(KclValue::UserVal(UserVal::new(
vec![Metadata {
source_range: args.source_range,
}],
plane,
)))
let plane_data = 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)))
}
/// 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])
/// ```
/// ```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 {
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 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.
@ -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 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.
@ -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 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.
@ -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 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.
@ -251,7 +251,7 @@ pub async fn last_segment_x(_exec_state: &mut ExecState, args: Args) -> Result<K
let sketch = args.get_sketch()?;
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
@ -291,7 +291,7 @@ pub async fn last_segment_y(_exec_state: &mut ExecState, args: Args) -> Result<K
let sketch = args.get_sketch()?;
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
@ -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> {
let tag: TagIdentifier = args.get_data()?;
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.
@ -376,7 +376,7 @@ pub async fn segment_angle(exec_state: &mut ExecState, args: Args) -> Result<Kcl
let tag: TagIdentifier = args.get_data()?;
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.
@ -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> {
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())?;
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
/// 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> {
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())?;
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.

View File

@ -48,7 +48,9 @@ pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
args.get_circle_args()?;
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
@ -166,7 +168,7 @@ pub struct PolygonData {
pub center: [f64; 2],
/// The type of the polygon (inscribed or circumscribed)
#[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
#[serde(default = "default_inscribed")]
pub inscribed: bool,
@ -182,7 +184,9 @@ pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
args.get_polygon_args()?;
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.

View File

@ -17,7 +17,7 @@ use crate::{
errors::{KclError, KclErrorDetails},
executor::{
BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch, SketchSet, SketchSurface,
Solid, TagEngineInfo, TagIdentifier, UserVal,
Solid, TagEngineInfo, TagIdentifier,
},
std::{
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 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.
@ -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 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.
@ -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 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.
@ -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 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
@ -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 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
@ -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 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
@ -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 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
@ -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 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
@ -573,9 +589,9 @@ async fn inner_angled_line_of_x_length(
#[serde(rename_all = "camelCase")]
pub struct AngledLineToData {
/// The angle of the line.
angle: f64,
pub angle: f64,
/// The point to draw to.
to: f64,
pub to: f64,
}
/// 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 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
@ -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?;
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
@ -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 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
@ -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>) =
args.get_data_and_sketch_and_tag()?;
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
@ -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 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.
@ -1135,7 +1161,9 @@ pub async fn start_profile_at(exec_state: &mut ExecState, args: Args) -> Result<
args.get_data_and_sketch_surface()?;
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.
@ -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> {
let sketch: Sketch = args.get_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'
@ -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> {
let sketch: Sketch = args.get_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'
@ -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> {
let sketch: Sketch = args.get_sketch()?;
let point = inner_profile_start(sketch)?;
Ok(KclValue::UserVal(UserVal {
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(),
}))
Ok(KclValue::from_point2d(point, args.into()))
}
/// 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?;
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
@ -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 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.
@ -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 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.
@ -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 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..
@ -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 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
@ -1873,11 +1903,11 @@ async fn inner_tangential_arc_to_relative(
#[serde(rename_all = "camelCase")]
pub struct BezierData {
/// The to point.
to: [f64; 2],
pub to: [f64; 2],
/// The first control point.
control1: [f64; 2],
pub control1: [f64; 2],
/// The second control point.
control2: [f64; 2],
pub control2: [f64; 2],
}
/// 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 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
@ -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 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.

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