Compare commits

...

20 Commits

Author SHA1 Message Date
739a5c5132 named export files
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-11-18 11:58:22 -08:00
97b9529c81 doc: Add more docs and links for KCL import (#4488)
* Add links in docs to KCL module documentation

* Add import statement test in import() stdlib doc comments

* Update doc test outputs

* Update doc outputs
2024-11-18 18:47:24 +00:00
8e5fc02941 Revert electron-updater to 6.3.0 (Linux AppImage updater fix) (#4501)
* WIP: Fix linux updater 'mv' error

First commit to test the updater-test builds

* Downgrade electron-updater to 6.3.0

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

* Trigger CI

* Pin to 6.3.0

* Clean up

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-18 18:31:45 +00:00
909690f3c7 Better reduce docs (#4493)
Based on conversation with Josh and Jordan earlier this week.
2024-11-18 10:34:57 -06:00
14ba66378d Nadro/adhoc/center rectangle (#4480)
* fix: big commit, doing this to save work then do a PR cleanup; center rectangle

* fix: making a center function for each scenario

* fix: reverting the update rectangle code since I have a center rectangle one

* fix: does not allow seletcing circle or rectangle tool while selecting a face

* chore: adding comment to better read the HTML

* fix: cleaning up for PR

* fix: pushing broken code for someone to checkout

* fix: fixed the typescript issues, removed the as keyword for my center rectangle expressions

* fix: removing comment

* fix: removed as for type narrowing checks
2024-11-18 09:04:09 -06:00
b2e895e508 Fix finding path to node for import and if-else (#4483) 2024-11-17 18:03:21 -05:00
05f4f34269 Do not write to file or update code editor a ridiculous amount of times and update them both at the most appropriate moments. (#4479)
* Reapply "Deflake project settings override on desktop (#4370)" (#4450)

This reverts commit b11040c23c.

* Refactor writeToFile and updateCodeEditor to happen at appropriate times

* Turn error into warning about out of date AST.

* Rename setUp to setup

* ONLY reload current file on changes.

* If value is falsey then don't try to executeAst

* Fix up code based selections after constraints

* Correct any last missing code mods

* Update src/clientSideScene/ClientSideSceneComp.tsx

Remove eslint rule no-floating-promises

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

* Fixups

* Fix FileTree failing

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-11-16 21:49:44 +00:00
fbb7b08b62 Refactor: Clean up FromKclValue code (#4489)
* Rename ::from_mem_item to ::from_kcl_val

The old name is from a dark time lost to human memory,
when we had an enum called MemoryItem.

* Simplify the FromKclValue macros

There's only really need for one macro, not three.
2024-11-16 12:19:00 -05:00
16c7a2457a Add docs for modules and import statements (#4435) 2024-11-15 11:36:38 -05:00
c429bc6ed7 chore: implemented a O(n) unique and added unit tests (#4429) 2024-11-15 09:09:16 -06:00
a0493cb332 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!`
2024-11-14 17:27:19 -06:00
b798f7da03 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>
2024-11-13 09:41:27 -05:00
c5d051855f 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>
2024-11-13 09:04:35 -05:00
0b24216dc5 Cut release v0.26.5 (#4477) 2024-11-12 22:05:56 -05:00
2d3841bf61 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>
2024-11-13 12:01:47 +11:00
51a71a180e 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>
2024-11-12 15:49:49 -05:00
1ec25dfe96 Remove references to wasm-dev build (#4449) 2024-11-11 14:58:44 -05:00
8de29dd461 Cut release v0.26.4 (#4452)
* Cut release v0.26.4

* Turn of setting persistence test with `fixme`
2024-11-08 23:49:34 +00:00
b11040c23c 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
2024-11-08 16:16:46 -06:00
2bc4f076cb Fix to run cargo tests when generated files change (#4430) 2024-11-08 15:30:37 -05:00
106 changed files with 9804 additions and 2763 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.

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,7 @@ layout: manual
## Table of Contents
* [Types](kcl/types)
* [Modules](kcl/modules)
* [Known Issues](kcl/KNOWN-ISSUES)
* [`abs`](kcl/abs)
* [`acos`](kcl/acos)

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 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] })
@ -645,7 +645,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' })
@ -674,16 +674,15 @@ test.describe('Sketch tests', () => {
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
@ -712,15 +711,15 @@ test.describe('Sketch tests', () => {
// 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 +743,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(%)`

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: 47 KiB

After

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

@ -305,7 +305,7 @@ export const getMovementUtils = (opts: any) => {
return [last.x, last.y]
}
return { toSU, click00r }
return { toSU, toU, click00r }
}
async function waitForAuthAndLsp(page: 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 () => {

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": {
@ -40,7 +40,7 @@
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.3.9",
"electron-updater": "6.3.0",
"fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0",

View File

@ -141,6 +141,7 @@ export function Toolbar({
>
{/* A menu item will either be a vertical line break, a button with a dropdown, or a single button */}
{currentModeItems.map((maybeIconConfig, i) => {
// Vertical Line Break
if (maybeIconConfig === 'break') {
return (
<div
@ -149,6 +150,7 @@ export function Toolbar({
/>
)
} else if (Array.isArray(maybeIconConfig)) {
// A button with a dropdown
return (
<ActionButtonDropdown
Element="button"
@ -215,6 +217,7 @@ export function Toolbar({
}
const itemConfig = maybeIconConfig
// A single button
return (
<div className="relative" key={itemConfig.id}>
<ActionButton

View File

@ -202,12 +202,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(
@ -637,10 +645,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

@ -17,6 +17,7 @@ import {
Vector3,
} from 'three'
import {
ANGLE_SNAP_THRESHOLD_DEGREES,
ARROWHEAD,
AXIS_GROUP,
DRAFT_POINT,
@ -88,6 +89,7 @@ import { EngineCommandManager } from 'lang/std/engineConnection'
import {
getRectangleCallExpressions,
updateRectangleSketch,
updateCenterRectangleSketch,
} from 'lib/rectangleTool'
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
import { err, reportRejection, trap } from 'lib/trap'
@ -95,6 +97,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 +454,7 @@ export class SceneEntities {
const { modifiedAst } = addStartProfileAtRes
await kclManager.updateAst(modifiedAst, false)
this.removeIntersectionPlane()
this.scene.remove(draftPointGroup)
@ -683,7 +687,7 @@ export class SceneEntities {
})
return nextAst
}
setUpDraftSegment = async (
setupDraftSegment = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
@ -798,11 +802,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 +827,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 +858,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 +870,8 @@ export class SceneEntities {
segmentName
)
}
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
},
onMove: (args) => {
this.onDragSegment({
@ -970,8 +996,179 @@ export class SceneEntities {
if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init
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)
)
},
})
}
setupDraftCenterRectangle = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchOrigin: [number, number, number],
rectangleOrigin: [x: number, y: number]
) => {
let _ast = structuredClone(kclManager.ast)
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
// startSketchOn already exists
const variableDeclarationName =
_node1.node?.declarations?.[0]?.id?.name || ''
const startSketchOn = _node1.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'),
findUniqueName(_ast, 'rectangleSegmentB'),
findUniqueName(_ast, 'rectangleSegmentC'),
]
startSketchOn[0].init = createPipeExpression([
startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags),
])
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
forward,
up,
position: sketchOrigin,
maybeModdedAst: _ast,
draftExpressionsIndices: { start: 0, end: 3 },
})
sceneInfra.setCallbacks({
onMove: async (args) => {
// Update the width and height of the draft rectangle
const pathToNodeTwo = structuredClone(sketchPathToNode)
pathToNodeTwo[1][0] = 0
const _node = getNodeFromPath<VariableDeclaration>(
truncatedAst,
pathToNodeTwo || [],
'VariableDeclaration'
)
if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
if (sketchInit.type === 'PipeExpression') {
updateRectangleSketch(sketchInit, x, y, tags[0])
updateCenterRectangleSketch(
sketchInit,
x,
y,
tags[0],
rectangleOrigin[0],
rectangleOrigin[1]
)
}
const { execState } = await executeAst({
ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
},
onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return
const x = roundOff((cornerPoint.x || 0) - rectangleOrigin[0])
const y = roundOff((cornerPoint.y || 0) - rectangleOrigin[1])
const _node = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init
if (sketchInit.type === 'PipeExpression') {
updateCenterRectangleSketch(
sketchInit,
x,
y,
tags[0],
rectangleOrigin[0],
rectangleOrigin[1]
)
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return
@ -979,7 +1176,7 @@ export class SceneEntities {
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish rectangle' })
sceneInfra.modelingSend({ type: 'Finish center rectangle' })
const { execState } = await executeAst({
ast: _ast,
@ -1166,13 +1363,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 +1409,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 */

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

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

@ -304,6 +304,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({
@ -481,8 +482,9 @@ export const ModelingMachineProvider = ({
// Set the export intent.
engineCommandManager.exportInfo = {
intent: ExportIntent.Save,
// This never gets used its only for make.
name: '',
// If we have a name for the file, like if we are in the desktop
// app, we can use it for the name of the resulting export.
name: file?.name || '',
}
const format = {
@ -732,6 +734,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 +775,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 +825,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 +863,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 +903,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 +944,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 +985,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 +1036,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin
)
if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections(
{ 0: pathToReplacedNode },
selectionRanges,

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

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

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

@ -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[]) {
@ -357,9 +357,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 +494,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

@ -8,6 +8,7 @@ import {
VariableDeclarator,
Expr,
Literal,
LiteralValue,
PipeSubstitution,
Identifier,
ArrayExpression,
@ -573,7 +574,7 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
}
export function createLiteral(value: string | number): Node<Literal> {
export function createLiteral(value: LiteralValue): Node<Literal> {
return {
type: 'Literal',
start: 0,

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

@ -1,4 +1,4 @@
import { parse, recast, initPromise, PathToNode } from './wasm'
import { parse, recast, initPromise, PathToNode, Identifier } from './wasm'
import {
findAllPreviousVariables,
isNodeSafeToReplace,
@ -10,6 +10,7 @@ import {
hasSketchPipeBeenExtruded,
doesSceneHaveSweepableSketch,
traverse,
getNodeFromPath,
} from './queryAst'
import { enginelessExecutor } from '../lib/testHelpers'
import {
@ -266,6 +267,86 @@ describe('testing getNodePathFromSourceRange', () => {
])
expect(selectWholeThing).toEqual(expected)
})
it('finds the node in if-else condition', () => {
const code = `y = 0
x = if x > y {
x + 1
} else {
y
}`
const searchLn = `x > y`
const sourceIndex = code.indexOf(searchLn)
const ast = parse(code)
if (err(ast)) throw ast
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
expect(result).toEqual([
['body', ''],
[1, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', ''],
['cond', 'IfExpression'],
['left', 'BinaryExpression'],
])
const _node = getNodeFromPath<Identifier>(ast, result)
if (err(_node)) throw _node
expect(_node.node.type).toEqual('Identifier')
expect(_node.node.name).toEqual('x')
})
it('finds the node in if-else then', () => {
const code = `y = 0
x = if x > y {
x + 1
} else {
y
}`
const searchLn = `x + 1`
const sourceIndex = code.indexOf(searchLn)
const ast = parse(code)
if (err(ast)) throw ast
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
expect(result).toEqual([
['body', ''],
[1, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', ''],
['then_val', 'IfExpression'],
['body', 'IfExpression'],
[0, 'index'],
['expression', 'ExpressionStatement'],
['left', 'BinaryExpression'],
])
const _node = getNodeFromPath<Identifier>(ast, result)
if (err(_node)) throw _node
expect(_node.node.type).toEqual('Identifier')
expect(_node.node.name).toEqual('x')
})
it('finds the node in import statement item', () => {
const code = `import foo, bar as baz from 'thing.kcl'`
const searchLn = `bar`
const sourceIndex = code.indexOf(searchLn)
const ast = parse(code)
if (err(ast)) throw ast
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
expect(result).toEqual([
['body', ''],
[0, 'index'],
['items', 'ImportStatement'],
[1, 'index'],
['name', 'ImportItem'],
])
const _node = getNodeFromPath<Identifier>(ast, result)
if (err(_node)) throw _node
expect(_node.node.type).toEqual('Identifier')
expect(_node.node.name).toEqual('bar')
})
})
describe('testing doesPipeHave', () => {

View File

@ -317,6 +317,62 @@ function moreNodePathFromSourceRange(
}
if (_node.type === 'PipeSubstitution' && isInRange) return path
if (_node.type === 'IfExpression' && isInRange) {
const { cond, then_val, else_ifs, final_else } = _node
if (cond.start <= start && cond.end >= end) {
path.push(['cond', 'IfExpression'])
return moreNodePathFromSourceRange(cond, sourceRange, path)
}
if (then_val.start <= start && then_val.end >= end) {
path.push(['then_val', 'IfExpression'])
path.push(['body', 'IfExpression'])
return getNodePathFromSourceRange(then_val, sourceRange, path)
}
for (let i = 0; i < else_ifs.length; i++) {
const else_if = else_ifs[i]
if (else_if.start <= start && else_if.end >= end) {
path.push(['else_ifs', 'IfExpression'])
path.push([i, 'index'])
const { cond, then_val } = else_if
if (cond.start <= start && cond.end >= end) {
path.push(['cond', 'IfExpression'])
return moreNodePathFromSourceRange(cond, sourceRange, path)
}
path.push(['then_val', 'IfExpression'])
path.push(['body', 'IfExpression'])
return getNodePathFromSourceRange(then_val, sourceRange, path)
}
}
if (final_else.start <= start && final_else.end >= end) {
path.push(['final_else', 'IfExpression'])
path.push(['body', 'IfExpression'])
return getNodePathFromSourceRange(final_else, sourceRange, path)
}
return path
}
if (_node.type === 'ImportStatement' && isInRange) {
const { items } = _node
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.start <= start && item.end >= end) {
path.push(['items', 'ImportStatement'])
path.push([i, 'index'])
if (item.name.start <= start && item.name.end >= end) {
path.push(['name', 'ImportItem'])
return path
}
if (item.alias && item.alias.start <= start && item.alias.end >= end) {
path.push(['alias', 'ImportItem'])
return path
}
return path
}
}
return path
}
console.error('not implemented: ' + node.type)
return path

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

@ -1631,7 +1631,11 @@ export class EngineCommandManager extends EventTarget {
switch (this.exportInfo.intent) {
case ExportIntent.Save: {
exportSave(event.data, this.pendingExport.toastId).then(() => {
exportSave(
event.data,
this.exportInfo.name,
this.pendingExport.toastId
).then(() => {
this.pendingExport?.resolve(null)
}, this.pendingExport?.reject)
break

View File

@ -1,5 +1,12 @@
import { Selections } from 'lib/selections'
import { Program, PathToNode } from './wasm'
import {
Program,
PathToNode,
CallExpression,
Literal,
ArrayExpression,
BinaryExpression,
} from './wasm'
import { getNodeFromPath } from './queryAst'
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils'
@ -84,3 +91,19 @@ export function isCursorInSketchCommandRange(
([, artifact]) => artifact.type === 'path'
)?.[0] || false
}
export function isCallExpression(e: any): e is CallExpression {
return e && e.type === 'CallExpression'
}
export function isArrayExpression(e: any): e is ArrayExpression {
return e && e.type === 'ArrayExpression'
}
export function isLiteral(e: any): e is Literal {
return e && e.type === 'Literal'
}
export function isBinaryExpression(e: any): e is BinaryExpression {
return e && e.type === 'BinaryExpression'
}

View File

@ -62,6 +62,7 @@ export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression'
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type SyntaxType =
@ -81,6 +82,7 @@ export type SyntaxType =
| 'PipeExpression'
| 'PipeSubstitution'
| 'Literal'
| 'LiteralValue'
| 'NonCodeNode'
| 'UnaryExpression'
@ -336,7 +338,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

@ -6,6 +6,7 @@ import JSZip from 'jszip'
import ModelingAppFile from './modelingAppFile'
import toast from 'react-hot-toast'
import { EXPORT_TOAST_MESSAGES } from './constants'
import { string } from 'yargs'
const save_ = async (file: ModelingAppFile, toastId: string) => {
try {
@ -68,21 +69,33 @@ const save_ = async (file: ModelingAppFile, toastId: string) => {
}
// Saves files locally from an export call.
export async function exportSave(data: ArrayBuffer, toastId: string) {
export async function exportSave(
data: ArrayBuffer,
filename: string,
toastId: string
) {
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
let uintArray = new Uint8Array(data)
let files: ModelingAppFile[] = deserialize_files(uintArray)
if (filename === '') {
filename = 'output'
}
if (files.length > 1) {
let zip = new JSZip()
for (const file of files) {
zip.file(file.name, new Uint8Array(file.contents), { binary: true })
}
return zip.generateAsync({ type: 'array' }).then((contents) => {
return save_({ name: 'output.zip', contents }, toastId)
return save_({ name: filename + '.zip', contents }, toastId)
})
} else {
return save_(files[0], toastId)
const file = files[0]
return save_(
{ name: file.name.replace('output', filename), contents: file.contents },
toastId
)
}
}

View File

@ -9,8 +9,16 @@ import {
createUnaryExpression,
} from 'lang/modifyAst'
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm'
import { roundOff } from 'lib/utils'
import {
isCallExpression,
isArrayExpression,
isLiteral,
isBinaryExpression,
} from 'lang/util'
/**
* It does not create the startSketchOn and it does not create the startProfileAt.
* Returns AST expressions for this KCL code:
* const yo = startSketchOn('XY')
* |> startProfileAt([0, 0], %)
@ -92,3 +100,69 @@ export function updateRectangleSketch(
createLiteral(Math.abs(y)), // This will be the height of the rectangle
])
}
/**
* Mutates the pipeExpression to update the center rectangle sketch
* @param pipeExpression
* @param x
* @param y
* @param tag
*/
export function updateCenterRectangleSketch(
pipeExpression: PipeExpression,
deltaX: number,
deltaY: number,
tag: string,
originX: number,
originY: number
) {
let startX = originX - Math.abs(deltaX)
let startY = originY - Math.abs(deltaY)
// pipeExpression.body[1] is startProfileAt
let callExpression = pipeExpression.body[1]
if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) {
callExpression.arguments[0] = createArrayExpression([
createLiteral(roundOff(startX)),
createLiteral(roundOff(startY)),
])
}
}
const twoX = deltaX * 2
const twoY = deltaY * 2
callExpression = pipeExpression.body[2]
if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) {
const literal = arrayExpression.elements[0]
if (isLiteral(literal)) {
callExpression.arguments[0] = createArrayExpression([
createLiteral(literal.value),
createLiteral(Math.abs(twoX)),
])
}
}
}
callExpression = pipeExpression.body[3]
if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) {
const binaryExpression = arrayExpression.elements[0]
if (isBinaryExpression(binaryExpression)) {
callExpression.arguments[0] = createArrayExpression([
createBinaryExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
binaryExpression.operator,
createLiteral(90),
]), // 90 offset from the previous line
createLiteral(Math.abs(twoY)), // This will be the height of the rectangle
])
}
}
}
}

View File

@ -407,8 +407,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available',
title: 'Center circle',
disabled: (state) =>
!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Circle tool' }),
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Circle tool' })),
isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
hotkey: (state) =>
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
@ -448,8 +449,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'rectangle',
status: 'available',
disabled: (state) =>
!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Rectangle tool' }),
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Rectangle tool' })),
title: 'Corner rectangle',
hotkey: (state) =>
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
@ -459,13 +461,33 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'center-rectangle',
onClick: () => console.error('Center rectangle not yet implemented'),
icon: 'rectangle',
status: 'unavailable',
onClick: ({ modelingState, modelingSend }) =>
modelingSend({
type: 'change tool',
data: {
tool: !modelingState.matches({
Sketch: 'Center Rectangle tool',
})
? 'center rectangle'
: 'none',
},
}),
icon: 'arc',
status: 'available',
disabled: (state) =>
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Center Rectangle tool' })),
title: 'Center rectangle',
showTitle: false,
hotkey: (state) =>
state.matches({ Sketch: 'Center Rectangle tool' })
? ['Esc', 'C']
: 'C',
description: 'Start drawing a rectangle from its center',
links: [],
isActive: (state) => {
return state.matches({ Sketch: 'Center Rectangle tool' })
},
},
],
{

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,
@ -183,6 +184,7 @@ export type SketchTool =
| 'line'
| 'tangentialArc'
| 'rectangle'
| 'center rectangle'
| 'circle'
| 'none'
@ -237,6 +239,10 @@ export type ModelingMachineEvent =
type: 'Add rectangle origin'
data: [x: number, y: number]
}
| {
type: 'Add center rectangle origin'
data: [x: number, y: number]
}
| {
type: 'Add circle origin'
data: [x: number, y: number]
@ -277,6 +283,7 @@ export type ModelingMachineEvent =
}
}
| { type: 'Finish rectangle' }
| { type: 'Finish center rectangle' }
| { type: 'Finish circle' }
| { type: 'Artifact graph populated' }
| { type: 'Artifact graph emptied' }
@ -505,6 +512,9 @@ export const modelingMachine = setup({
'next is rectangle': ({ context: { sketchDetails, currentTool } }) =>
currentTool === 'rectangle' &&
canRectangleOrCircleTool({ sketchDetails }),
'next is center rectangle': ({ context: { sketchDetails, currentTool } }) =>
currentTool === 'center rectangle' &&
canRectangleOrCircleTool({ sketchDetails }),
'next is circle': ({ context: { sketchDetails, currentTool } }) =>
currentTool === 'circle' && canRectangleOrCircleTool({ sketchDetails }),
'next is line': ({ context }) => context.currentTool === 'line',
@ -531,8 +541,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 +607,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 +615,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 +651,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 +659,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 +691,7 @@ export const modelingMachine = setup({
}
await kclManager.updateAst(modifiedAst, true)
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
})().catch(reportRejection)
},
'AST fillet': ({ event }) => {
@ -702,6 +709,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 +768,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
@ -795,6 +815,26 @@ export const modelingMachine = setup({
},
})
},
'listen for center rectangle origin': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return
// setupNoPointsListener has the code for startProfileAt onClick
sceneEntitiesManager.setupNoPointsListener({
sketchDetails,
afterClick: (args) => {
const twoD = args.intersectionPoint?.twoD
if (twoD) {
sceneInfra.modelingSend({
type: 'Add center rectangle origin',
data: [twoD.x, twoD.y],
})
} else {
console.error('No intersection point found')
}
},
})
},
'listen for circle origin': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return
sceneEntitiesManager.createIntersectionPlane()
@ -834,8 +874,28 @@ 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(
sceneEntitiesManager
.setupDraftRectangle(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
event.data
)
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
},
'set up draft center rectangle': ({
context: { sketchDetails },
event,
}) => {
if (event.type !== 'Add center rectangle origin') return
if (!sketchDetails || !event.data) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setupDraftCenterRectangle(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -846,26 +906,36 @@ export const modelingMachine = setup({
'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 +952,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 +991,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 +1062,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 +1099,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 +1134,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 +1167,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 +1201,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 +1235,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 +1269,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 +1307,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 +1341,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 +1646,7 @@ export const modelingMachine = setup({
},
},
entry: 'setup client side sketch segments',
entry: ['setup client side sketch segments'],
},
'Await horizontal distance info': {
@ -1776,6 +1866,40 @@ export const modelingMachine = setup({
},
},
'Center Rectangle tool': {
entry: ['listen for center rectangle origin'],
states: {
'Awaiting corner': {
on: {
'Finish center rectangle': 'Finished Center Rectangle',
},
},
'Awaiting origin': {
on: {
'Add center rectangle origin': {
target: 'Awaiting corner',
// TODO
actions: 'set up draft center rectangle',
},
},
},
'Finished Center Rectangle': {
always: '#Modeling.Sketch.SketchIdle',
},
},
initial: 'Awaiting origin',
on: {
'change tool': {
target: 'Change Tool',
},
},
},
'clean slate': {
always: 'SketchIdle',
},
@ -1801,7 +1925,7 @@ export const modelingMachine = setup({
onError: 'SketchIdle',
onDone: {
target: 'SketchIdle',
actions: ['Set selection'],
actions: 'Set selection',
},
},
},
@ -1969,6 +2093,10 @@ export const modelingMachine = setup({
target: 'Circle tool',
guard: 'next is circle',
},
{
target: 'Center Rectangle tool',
guard: 'next is center rectangle',
},
],
entry: 'assign tool in context',

View File

@ -0,0 +1,20 @@
// This file is used by the import docs.
export fn width = () => {
return 10
}
export fn height = () => {
return 10
}
export fn buildSketch = (plane, offset) => {
w = width()
h = height()
return startSketchOn(plane)
|> startProfileAt(offset, %)
|> line([w, 0], %)
|> line([0, h], %)
|> line([-w, 0], %)
|> close(%)
}

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

@ -7,6 +7,7 @@ layout: manual
## Table of Contents
* [Types](kcl/types)
* [Modules](kcl/modules)
* [Known Issues](kcl/KNOWN-ISSUES)
{{#each functions}}
* [`{{name}}`](kcl/{{name}})

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 {

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,
@ -133,26 +102,79 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// Take a starting value. Then, for each element of an array, calculate the next value,
/// using the previous value and the element.
/// ```no_run
/// fn decagon = (radius) => {
/// let step = (1/10) * tau()
/// let sketch001 = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
/// return reduce([1..10], sketch001, (i, sg) => {
/// let x = cos(step * i) * radius
/// let y = sin(step * i) * radius
/// return lineTo([x, y], sg)
/// })
/// }
/// decagon(5.0) |> close(%)
/// // This function adds two numbers.
/// fn add = (a, b) => { return a + b }
///
/// // This function adds an array of numbers.
/// // It uses the `reduce` function, to call the `add` function on every
/// // element of the `array` parameter. The starting value is 0.
/// fn sum = (array) => { return reduce(array, 0, add) }
///
/// /*
/// The above is basically like this pseudo-code:
/// fn sum(array):
/// let sumSoFar = 0
/// for i in array:
/// sumSoFar = add(sumSoFar, i)
/// return sumSoFar
/// */
///
/// // We use `assertEqual` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
/// ```
/// ```no_run
/// // This example works just like the previous example above, but it uses
/// // an anonymous `add` function as its parameter, instead of declaring a
/// // named function outside.
/// array = [1, 2, 3]
/// sum = reduce(array, 0, (i, result_so_far) => { return i + result_so_far })
///
/// // We use `assertEqual` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
/// ```
/// ```no_run
/// fn add = (a, b) => { return a + b }
/// fn sum = (array) => { return reduce(array, 0, add) }
/// assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
/// // Declare a function that sketches a decagon.
/// fn decagon = (radius) => {
/// // Each side of the decagon is turned this many degrees from the previous angle.
/// stepAngle = (1/10) * tau()
///
/// // Start the decagon sketch at this point.
/// startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
///
/// // Use a `reduce` to draw the remaining decagon sides.
/// // For each number in the array 1..10, run the given function,
/// // which takes a partially-sketched decagon and adds one more edge to it.
/// fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) => {
/// // Draw one edge of the decagon.
/// let x = cos(stepAngle * i) * radius
/// let y = sin(stepAngle * i) * radius
/// return lineTo([x, y], partialDecagon)
/// })
///
/// return fullDecagon
///
/// }
///
/// /*
/// The `decagon` above is basically like this pseudo-code:
/// fn decagon(radius):
/// let stepAngle = (1/10) * tau()
/// let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
///
/// // Here's the reduce part.
/// let partialDecagon = startOfDecagonSketch
/// for i in [1..10]:
/// let x = cos(stepAngle * i) * radius
/// let y = sin(stepAngle * i) * radius
/// partialDecagon = lineTo([x, y], partialDecagon)
/// fullDecagon = partialDecagon // it's now full
/// return fullDecagon
/// */
///
/// // Use the `decagon` function declared above, to sketch a decagon with radius 5.
/// decagon(5.0) |> close(%)
/// ```
#[stdlib {
name = "reduce",
@ -206,50 +228,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

@ -144,6 +144,9 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// Note: The import command currently only works when using the native
/// Modeling App.
///
/// For importing KCL functions using the `import` statement, see the docs on
/// [KCL modules](/docs/kcl/modules).
///
/// ```no_run
/// const model = import("tests/inputs/cube.obj")
/// ```
@ -163,6 +166,15 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// ```no_run
/// const model = import("tests/inputs/cube.step")
/// ```
///
/// ```no_run
/// import height, buildSketch from 'common.kcl'
///
/// plane = 'XZ'
/// margin = 2
/// s1 = buildSketch(plane, [0, 0])
/// s2 = buildSketch(plane, [0, height() + margin])
/// ```
#[stdlib {
name = "import",
tags = [],

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_kcl_val(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.

View File

@ -14,7 +14,7 @@ use crate::{
pub async fn mm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_mm(&args)?;
args.make_user_val_from_f64(result)
Ok(args.make_user_val_from_f64(result))
}
/// Millimeters conversion factor for current projects units.
@ -55,7 +55,7 @@ fn inner_mm(args: &Args) -> Result<f64, KclError> {
pub async fn inch(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_inch(&args)?;
args.make_user_val_from_f64(result)
Ok(args.make_user_val_from_f64(result))
}
/// Inches conversion factor for current projects units.
@ -96,7 +96,7 @@ fn inner_inch(args: &Args) -> Result<f64, KclError> {
pub async fn ft(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_ft(&args)?;
args.make_user_val_from_f64(result)
Ok(args.make_user_val_from_f64(result))
}
/// Feet conversion factor for current projects units.
@ -138,7 +138,7 @@ fn inner_ft(args: &Args) -> Result<f64, KclError> {
pub async fn m(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_m(&args)?;
args.make_user_val_from_f64(result)
Ok(args.make_user_val_from_f64(result))
}
/// Meters conversion factor for current projects units.
@ -180,7 +180,7 @@ fn inner_m(args: &Args) -> Result<f64, KclError> {
pub async fn cm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_cm(&args)?;
args.make_user_val_from_f64(result)
Ok(args.make_user_val_from_f64(result))
}
/// Centimeters conversion factor for current projects units.
@ -222,7 +222,7 @@ fn inner_cm(args: &Args) -> Result<f64, KclError> {
pub async fn yd(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let result = inner_yd(&args)?;
args.make_user_val_from_f64(result)
Ok(args.make_user_val_from_f64(result))
}
/// Yards conversion factor for current projects units.

File diff suppressed because it is too large Load Diff

View File

@ -8,36 +8,67 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
},
"arr": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
1,
2,
3
{
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
7,
8,
0
]
}
]
},
{
"type": "Int",
"value": 2,
"__meta": [
{
"sourceRange": [
10,
11,
0
]
}
]
},
{
"type": "Int",
"value": 3,
"__meta": [
{
"sourceRange": [
13,
14,
0
]
}
]
}
],
"__meta": [
{
@ -50,13 +81,60 @@ snapshot_kind: text
]
},
"new_arr1": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
1,
2,
3,
4
{
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
7,
8,
0
]
}
]
},
{
"type": "Int",
"value": 2,
"__meta": [
{
"sourceRange": [
10,
11,
0
]
}
]
},
{
"type": "Int",
"value": 3,
"__meta": [
{
"sourceRange": [
13,
14,
0
]
}
]
},
{
"type": "Int",
"value": 4,
"__meta": [
{
"sourceRange": [
37,
38,
0
]
}
]
}
],
"__meta": [
{
@ -69,14 +147,73 @@ snapshot_kind: text
]
},
"new_arr2": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
1,
2,
3,
4,
5
{
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
7,
8,
0
]
}
]
},
{
"type": "Int",
"value": 2,
"__meta": [
{
"sourceRange": [
10,
11,
0
]
}
]
},
{
"type": "Int",
"value": 3,
"__meta": [
{
"sourceRange": [
13,
14,
0
]
}
]
},
{
"type": "Int",
"value": 4,
"__meta": [
{
"sourceRange": [
37,
38,
0
]
}
]
},
{
"type": "Int",
"value": 5,
"__meta": [
{
"sourceRange": [
66,
67,
0
]
}
]
}
],
"__meta": [
{

View File

@ -8,32 +8,27 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
},
"five": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 5,
"__meta": [
{
@ -46,8 +41,7 @@ snapshot_kind: text
]
},
"four": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 4,
"__meta": [
{
@ -60,14 +54,73 @@ snapshot_kind: text
]
},
"r1": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
0,
1,
2,
3,
4
{
"type": "Int",
"value": 0,
"__meta": [
{
"sourceRange": [
5,
11,
0
]
}
]
},
{
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
5,
11,
0
]
}
]
},
{
"type": "Int",
"value": 2,
"__meta": [
{
"sourceRange": [
5,
11,
0
]
}
]
},
{
"type": "Int",
"value": 3,
"__meta": [
{
"sourceRange": [
5,
11,
0
]
}
]
},
{
"type": "Int",
"value": 4,
"__meta": [
{
"sourceRange": [
5,
11,
0
]
}
]
}
],
"__meta": [
{
@ -80,14 +133,73 @@ snapshot_kind: text
]
},
"r2": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
0,
1,
2,
3,
4
{
"type": "Int",
"value": 0,
"__meta": [
{
"sourceRange": [
95,
107,
0
]
}
]
},
{
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
95,
107,
0
]
}
]
},
{
"type": "Int",
"value": 2,
"__meta": [
{
"sourceRange": [
95,
107,
0
]
}
]
},
{
"type": "Int",
"value": 3,
"__meta": [
{
"sourceRange": [
95,
107,
0
]
}
]
},
{
"type": "Int",
"value": 4,
"__meta": [
{
"sourceRange": [
95,
107,
0
]
}
]
}
],
"__meta": [
{
@ -100,15 +212,86 @@ snapshot_kind: text
]
},
"r3": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
0,
1,
2,
3,
4,
5
{
"type": "Int",
"value": 0,
"__meta": [
{
"sourceRange": [
194,
206,
0
]
}
]
},
{
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
194,
206,
0
]
}
]
},
{
"type": "Int",
"value": 2,
"__meta": [
{
"sourceRange": [
194,
206,
0
]
}
]
},
{
"type": "Int",
"value": 3,
"__meta": [
{
"sourceRange": [
194,
206,
0
]
}
]
},
{
"type": "Int",
"value": 4,
"__meta": [
{
"sourceRange": [
194,
206,
0
]
}
]
},
{
"type": "Int",
"value": 5,
"__meta": [
{
"sourceRange": [
194,
206,
0
]
}
]
}
],
"__meta": [
{
@ -121,13 +304,60 @@ snapshot_kind: text
]
},
"r4": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
1,
2,
3,
4
{
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
341,
373,
0
]
}
]
},
{
"type": "Int",
"value": 2,
"__meta": [
{
"sourceRange": [
341,
373,
0
]
}
]
},
{
"type": "Int",
"value": 3,
"__meta": [
{
"sourceRange": [
341,
373,
0
]
}
]
},
{
"type": "Int",
"value": 4,
"__meta": [
{
"sourceRange": [
341,
373,
0
]
}
]
}
],
"__meta": [
{
@ -140,8 +370,7 @@ snapshot_kind: text
]
},
"zero": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 0,
"__meta": [
{

View File

@ -8,44 +8,171 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
},
"xs": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
-5,
-4,
-3,
-2,
-1,
0,
1,
2,
3,
4,
5
{
"type": "Int",
"value": -5,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": -4,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": -3,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": -2,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": -1,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": 0,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": 2,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": 3,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": 4,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
},
{
"type": "Int",
"value": 5,
"__meta": [
{
"sourceRange": [
5,
19,
0
]
}
]
}
],
"__meta": [
{

View File

@ -8,27 +8,23 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
}
},

View File

@ -8,27 +8,23 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
},
"cube": {
@ -717,27 +713,23 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
}
},

View File

@ -8,27 +8,23 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
},
"increment": {
@ -89,27 +85,23 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
}
},
@ -130,12 +122,47 @@ snapshot_kind: text
]
},
"xs": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
0,
1,
2
{
"type": "Int",
"value": 0,
"__meta": [
{
"sourceRange": [
47,
53,
0
]
}
]
},
{
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
47,
53,
0
]
}
]
},
{
"type": "Int",
"value": 2,
"__meta": [
{
"sourceRange": [
47,
53,
0
]
}
]
}
],
"__meta": [
{
@ -148,12 +175,89 @@ snapshot_kind: text
]
},
"ys": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
2.0,
3.0,
4.0
{
"type": "Number",
"value": 2.0,
"__meta": [
{
"sourceRange": [
47,
53,
0
]
},
{
"sourceRange": [
37,
38,
0
]
},
{
"sourceRange": [
37,
38,
0
]
}
]
},
{
"type": "Number",
"value": 3.0,
"__meta": [
{
"sourceRange": [
47,
53,
0
]
},
{
"sourceRange": [
37,
38,
0
]
},
{
"sourceRange": [
37,
38,
0
]
}
]
},
{
"type": "Number",
"value": 4.0,
"__meta": [
{
"sourceRange": [
47,
53,
0
]
},
{
"sourceRange": [
37,
38,
0
]
},
{
"sourceRange": [
37,
38,
0
]
}
]
}
],
"__meta": [
{

View File

@ -8,27 +8,23 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
},
"part001": {

View File

@ -8,32 +8,27 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
},
"a": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 3,
"__meta": [
{
@ -46,8 +41,7 @@ snapshot_kind: text
]
},
"b": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 4,
"__meta": [
{
@ -60,8 +54,7 @@ snapshot_kind: text
]
},
"c": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 5,
"__meta": [
{

View File

@ -8,36 +8,67 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
},
"array": {
"type": "UserVal",
"type": "UserVal",
"type": "Array",
"value": [
90,
91,
92
{
"type": "Int",
"value": 90,
"__meta": [
{
"sourceRange": [
44,
46,
0
]
}
]
},
{
"type": "Int",
"value": 91,
"__meta": [
{
"sourceRange": [
48,
50,
0
]
}
]
},
{
"type": "Int",
"value": 92,
"__meta": [
{
"sourceRange": [
52,
54,
0
]
}
]
}
],
"__meta": [
{
@ -50,8 +81,7 @@ snapshot_kind: text
]
},
"i": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 1,
"__meta": [
{
@ -64,28 +94,26 @@ snapshot_kind: text
]
},
"result0": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 91,
"__meta": [
{
"sourceRange": [
93,
101,
48,
50,
0
]
}
]
},
"result1": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 91,
"__meta": [
{
"sourceRange": [
277,
285,
48,
50,
0
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -8,35 +8,54 @@ snapshot_kind: text
{
"bindings": {
"HALF_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 180,
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 90,
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "UserVal",
"type": "UserVal",
"value": 270,
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "UserVal",
"type": "UserVal",
"value": 0,
"type": "Number",
"value": 0.0,
"__meta": []
},
"obj": {
"type": "UserVal",
"type": "UserVal",
"type": "Object",
"value": {
"bar": 0,
"foo": 1
"bar": {
"type": "Int",
"value": 0,
"__meta": [
{
"sourceRange": [
71,
72,
0
]
}
]
},
"foo": {
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
63,
64,
0
]
}
]
}
},
"__meta": [
{
@ -49,12 +68,47 @@ snapshot_kind: text
]
},
"obj2": {
"type": "UserVal",
"type": "UserVal",
"type": "Object",
"value": {
"inner": {
"bar": 0,
"foo": 1
"type": "Object",
"value": {
"bar": {
"type": "Int",
"value": 0,
"__meta": [
{
"sourceRange": [
71,
72,
0
]
}
]
},
"foo": {
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
63,
64,
0
]
}
]
}
},
"__meta": [
{
"sourceRange": [
56,
74,
0
]
}
]
}
},
"__meta": [
@ -68,64 +122,59 @@ snapshot_kind: text
]
},
"one_a": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
122,
132,
63,
64,
0
]
}
]
},
"one_b": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
356,
362,
63,
64,
0
]
}
]
},
"one_c": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
553,
570,
63,
64,
0
]
}
]
},
"one_d": {
"type": "UserVal",
"type": "UserVal",
"type": "Int",
"value": 1,
"__meta": [
{
"sourceRange": [
757,
770,
63,
64,
0
]
}
]
},
"p": {
"type": "UserVal",
"type": "UserVal",
"type": "String",
"value": "foo",
"__meta": [
{

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