Compare commits

..

108 Commits

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

Changes:

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

* Fix math error with horizontal/vertical determination

* Move snap constant to where other sketch constants are

* fmt

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

* A couple other little PW test issues

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

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

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

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

* Fix remaining tests that need updating

* Make `yarn lint` happy

---------

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

* Update docs

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

* update doc regen instructions

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

* Re-run CI after snapshots

---------

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

* Fix failing test

* tweak to leave function in place

---------

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

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

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

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

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

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

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

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

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

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

* Bump uuid from 9.0.1 to 11.0.2 (#4393)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* File tree acts as VS Code's file tree

* Adjust test for new expectations

* Remove screenshot

* Actually remove this screenshot

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

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

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

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

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

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

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

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

---------

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

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

* File tree acts as VS Code's file tree

* Adjust test for new expectations

* Remove screenshot

* Actually remove this screenshot

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

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

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

* fix: fixed logic for selection

* fix: always show the current file

---------

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

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

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

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

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

* Change Dependabot PR frequency to weekly (#4424)

* Update insta snapshots (#4423)

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

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

* Add ts_rs feature to work with indexmap

* Add feature for schemars to work with indexmap

* Add module ID to intern module paths

* Update code to use new source range with three fields

* Update generated files

* Update docs

* Fix wasm

* Fix TS code to use new SourceRange

* Fix TS tests to use new SourceRange and moduleId

* Fix formatting

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

* Fix to reuse module IDs

* Fix to disallow empty path for import

* Revert unneeded Self change

* Rename field to be clearer

* Fix parser tests

* Update snapshots

* Change to not serialize module_id of 0

* Update snapshots after adding default module_id

* Move module_id functions to separate module

* Fix tests for console errors

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

Just like in AST nodes.

Also I think "is_top_level" communicates intention better than is_default

---------

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

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

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

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

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

* Cut release v0.26.3 (#4427)

* Cut release v0.26.3

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

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

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

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

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

* Nadro/3799/perf (#4145)

* chore: building out perf testing

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

* feat: adding invocation count table

* fix: markOnce iunstead

* fix: typescript additions

* fix: adding more types

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

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

* fix: deleting unused imports

* fix: deleting some unused files

* fix: auto cleanup

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

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

* fix: moving command init

* fix: removing debugging marks

* fix: adding some comments

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

* chore: adding will pages load within the router config

* chore: implementing marks for routes

* fix: auto fixes and checkers

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

* fix: auto fixes, removing unused code

* chore: timing for syntax highlighting and auto fixes

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

* fix: fixing typescript checks

* chore: adding mag bar chart icon for telemetry

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

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

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

* chore: swapped telemetry icon for stopwatch

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

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

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

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

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

* chore: writing telemetry to disk

* fix: auto fixers

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

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

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

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

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

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

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

* chore: writing raw marks to disk as well

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

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

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

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

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

* fix: cleaned up the testing names

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

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

* Fix fmt and codespell

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

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

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

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

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

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

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

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

* fix: moving this route loader data stuff

* chore: adding comment

* fix: fmt

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

* empty :(

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

* empty :(

---------

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

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

---------

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

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

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

This reverts commit ad1cd56891.

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

* fix: implemented all scenarios for inc/dec formatting

* fix: deleting unused code

* fix: clearer name

* fix: adding commments

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

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

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

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

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

* fix: does this trigger the CI?

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

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

---------

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

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

* feat: adding invocation count table

* fix: markOnce iunstead

* fix: typescript additions

* fix: adding more types

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

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

* fix: deleting unused imports

* fix: deleting some unused files

* fix: auto cleanup

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

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

* fix: moving command init

* fix: removing debugging marks

* fix: adding some comments

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

* chore: adding will pages load within the router config

* chore: implementing marks for routes

* fix: auto fixes and checkers

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

* fix: auto fixes, removing unused code

* chore: timing for syntax highlighting and auto fixes

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

* fix: fixing typescript checks

* chore: adding mag bar chart icon for telemetry

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

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

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

* chore: swapped telemetry icon for stopwatch

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

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

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

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

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

* chore: writing telemetry to disk

* fix: auto fixers

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

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

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

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

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

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

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

* chore: writing raw marks to disk as well

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

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

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

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

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

* fix: cleaned up the testing names

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

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

* Fix fmt and codespell

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

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

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

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

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

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

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

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

* fix: moving this route loader data stuff

* chore: adding comment

* fix: fmt

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

* empty :(

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

* empty :(

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:39:42 -05:00
2c9eb7f7c0 Cut release v0.26.3 (#4427)
* Cut release v0.26.3

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:39:42 -05:00
91049204c5 Fix KCL source ranges to know which source file they point to (#4418)
* Add ts_rs feature to work with indexmap

* Add feature for schemars to work with indexmap

* Add module ID to intern module paths

* Update code to use new source range with three fields

* Update generated files

* Update docs

* Fix wasm

* Fix TS code to use new SourceRange

* Fix TS tests to use new SourceRange and moduleId

* Fix formatting

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

* Fix to reuse module IDs

* Fix to disallow empty path for import

* Revert unneeded Self change

* Rename field to be clearer

* Fix parser tests

* Update snapshots

* Change to not serialize module_id of 0

* Update snapshots after adding default module_id

* Move module_id functions to separate module

* Fix tests for console errors

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

Just like in AST nodes.

Also I think "is_top_level" communicates intention better than is_default

---------

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

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

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

* Adjust test for new expectations

* Remove screenshot

* Actually remove this screenshot

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

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

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

* fix: fixed logic for selection

* fix: always show the current file

---------

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

* Adjust test for new expectations

* Remove screenshot

* Actually remove this screenshot

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

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

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

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

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

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

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

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

---------

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:50 -05:00
efe207f4d2 Bump thiserror from 1.0.65 to 2.0.0 in /src/wasm-lib (#4397)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.65 to 2.0.0.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.65...2.0.0)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:50 -05:00
2bbf7fad67 Bump insta from 1.41.0 to 1.41.1 in /src/wasm-lib (#4400)
Bumps [insta](https://github.com/mitsuhiko/insta) from 1.41.0 to 1.41.1.
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.41.0...1.41.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:50 -05:00
98549945a4 Bump validator from 0.18.1 to 0.19.0 in /src/wasm-lib (#4396)
Bumps [validator](https://github.com/Keats/validator) from 0.18.1 to 0.19.0.
- [Changelog](https://github.com/Keats/validator/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Keats/validator/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:50 -05:00
315fdc3060 Bump uuid from 9.0.1 to 11.0.2 (#4393)
Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.1 to 11.0.2.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.1...v11.0.2)

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

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

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

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

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

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

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

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

---------

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

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
8a36a4c205 Bump kittycad-modeling-cmds from 0.2.71 to 0.2.72 in /src/wasm-lib (#4356)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.71 to 0.2.72.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.71...kittycad-modeling-cmds-0.2.72)

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:38:49 -05:00
f29f2557de implement a simple startSketchOn / offsetPlane lint rule (#4384) 2025-02-05 11:38:49 -05:00
5f0ffb56c4 Bump @electron-forge/maker-wix from 7.4.0 to 7.5.0 (#4033)
Bumps [@electron-forge/maker-wix](https://github.com/electron/forge) from 7.4.0 to 7.5.0.
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/compare/v7.4.0...v7.5.0)

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

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

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

* fix: fmt, lint, and tsc

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

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

* fix: trying to fix the exclude logic

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

* fix: bump CI

* fix:typo

* fix: had conflicting filters ope, now fixed

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
a8b3ec660d Bump electron-updater from 6.3.0 to 6.3.9 (#4093)
Bumps [electron-updater](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-updater) from 6.3.0 to 6.3.9.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-updater@6.3.9/packages/electron-updater)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
3747c6ff0e Bump reqwest from 0.12.8 to 0.12.9 in /src/wasm-lib (#4346)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.8 to 0.12.9.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.8...v0.12.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
bc1bc817ba Bump @codemirror/language from 6.10.2 to 6.10.3 (#4357)
Bumps [@codemirror/language](https://github.com/codemirror/language) from 6.10.2 to 6.10.3.
- [Changelog](https://github.com/codemirror/language/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/language/compare/6.10.2...6.10.3)

---
updated-dependencies:
- dependency-name: "@codemirror/language"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
b415e88746 Bump google-github-actions/auth from 2.1.6 to 2.1.7 (#4363)
Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 2.1.6 to 2.1.7.
- [Release notes](https://github.com/google-github-actions/auth/releases)
- [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/auth/compare/v2.1.6...v2.1.7)

---
updated-dependencies:
- dependency-name: google-github-actions/auth
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
9173e368a2 Bump google-github-actions/setup-gcloud from 2.1.0 to 2.1.2 (#4365)
Bumps [google-github-actions/setup-gcloud](https://github.com/google-github-actions/setup-gcloud) from 2.1.0 to 2.1.2.
- [Release notes](https://github.com/google-github-actions/setup-gcloud/releases)
- [Changelog](https://github.com/google-github-actions/setup-gcloud/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/setup-gcloud/compare/v2.1.0...v2.1.2)

---
updated-dependencies:
- dependency-name: google-github-actions/setup-gcloud
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
25928813e3 Back to regular updater-test URL (#4332)
* Back to regular updater-test URL

* Test: build release

* Revert "Test: build release"

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
0e82fbf7b0 Bump anyhow from 1.0.91 to 1.0.92 in /src/wasm-lib (#4378)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.91 to 1.0.92.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.91...1.0.92)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:38:49 -05:00
f5975bbd61 Move tests from no_visuals into simulation tests (#4367)
Now you can properly inspect program memory for the no_visuals tests, instead of relying on a lot of KCL asserts.
2025-02-05 11:38:49 -05:00
765e587f6b Deflake project settings override on desktop (#4370) 2025-02-05 11:38:49 -05:00
6ccd5e22b8 KCL: More simulation tests (#4359)
Demonstrate simulation tests where we don't care about visuals, e.g. the double-map test. 

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

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

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

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

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

* Slightly better quaternion rotation

* Actually snap new profiles to the X and Y axis

* Add snapping behavior while dragging

* Fix flickering on non-XY planes

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

* Add new test to verify snapping behavior

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

* All feedback except absolute lines

* Use `lineTo` for lines that have snapped

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

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

* Re-run CI

---------

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

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

* Fix formatting

* Fix yarn build:wasm

* Fix ts_rs bindings

* Fix tsc errors

* Fix wasm TS types

* Add minimal failing test

* Rename field to avoid name collisions

* Remove node wrapper around NonCodeMeta

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

* Rename Node to BoxNode

* Fix lints

* Fix lint by boxing literals

* Rename UnboxedNode to Node

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

* Update docs

* Update snapshots

* initial trait

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

* update docs

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

* updates

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

* gross hack for TagNode

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

* extend gross hack

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

* fix EnvRef bullshit

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

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

* Fix test errors after merging main

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

* Confirm

* Change to use simpler map_err

* Add comment

---------

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:37:31 -05:00
e8b5618b34 Bump handlebars from 6.1.0 to 6.2.0 in /src/wasm-lib (#4330)
Bumps [handlebars](https://github.com/sunng87/handlebars-rust) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/sunng87/handlebars-rust/releases)
- [Changelog](https://github.com/sunng87/handlebars-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sunng87/handlebars-rust/compare/v6.1.0...v6.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-02-05 11:37:31 -05:00
799b2d77b4 fix auth test in engine (#4354)
* fix auth test in engine

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

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

* emoty

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

---------

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

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

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 11:37:12 -05:00
83214a88a3 Tests: remove all timeouts and pasting into editor from file name collision PW test (#4352)
remove all timeouts and pasting into editor from file name collision PW test
2025-02-05 11:37:12 -05:00
dbab7876de Add lsystem.kcl to tests (#4146)
* Add lsystem.kcl to tests

* Reduce iterations

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

* Fix yarn test:rust to lint all targets

* Remove redundant options

* Change cargo --all to --workspace

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

View File

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

View File

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

View File

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

View File

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

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

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

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
interface.d.ts vendored
View File

@ -78,6 +78,7 @@ export interface IElectronAPI {
) => Electron.IpcRenderer
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
appRestart: () => void
getArgvParsed: () => any
}
declare global {

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": {
@ -65,7 +65,8 @@
"vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2",
"xstate": "^5.17.4"
"xstate": "^5.17.4",
"yargs": "^17.7.2"
},
"scripts": {
"start": "vite",

View File

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

View File

@ -8,6 +8,7 @@ import {
} from 'react-router-dom'
import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings'
import { Telemetry } from './routes/Telemetry'
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
import SignIn from './routes/SignIn'
import { Auth } from './Auth'
@ -28,6 +29,7 @@ import {
homeLoader,
onboardingRedirectLoader,
settingsLoader,
telemetryLoader,
} from 'lib/routeLoaders'
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
import SettingsAuthProvider from 'components/SettingsAuthProvider'
@ -43,6 +45,7 @@ import { coreDump } from 'lang/wasm'
import { useMemo } from 'react'
import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -56,19 +59,21 @@ const router = createRouter([
* inefficient re-renders, use the react profiler to see. */
element: (
<CommandBarProvider>
<SettingsAuthProvider>
<LspProvider>
<ProjectsContextProvider>
<KclContextProvider>
<AppStateProvider>
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
</AppStateProvider>
</KclContextProvider>
</ProjectsContextProvider>
</LspProvider>
</SettingsAuthProvider>
<RouteProvider>
<SettingsAuthProvider>
<LspProvider>
<ProjectsContextProvider>
<KclContextProvider>
<AppStateProvider>
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
</AppStateProvider>
</KclContextProvider>
</ProjectsContextProvider>
</LspProvider>
</SettingsAuthProvider>
</RouteProvider>
</CommandBarProvider>
),
errorElement: <ErrorPage />,
@ -124,6 +129,16 @@ const router = createRouter([
},
],
},
{
id: PATHS.FILE + 'TELEMETRY',
loader: telemetryLoader,
children: [
{
path: makeUrlPathRelative(PATHS.TELEMETRY),
element: <Telemetry />,
},
],
},
],
},
{
@ -149,6 +164,11 @@ const router = createRouter([
loader: settingsLoader,
element: <Settings />,
},
{
path: makeUrlPathRelative(PATHS.TELEMETRY),
loader: telemetryLoader,
element: <Telemetry />,
},
],
},
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

12
src/commandLineArgs.ts Normal file
View File

@ -0,0 +1,12 @@
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
const argv = yargs(hideBin(process.argv))
.option('telemetry', {
alias: 't',
type: 'boolean',
description: 'Writes startup telemetry to file on disk.',
})
.parse()
export default argv

View File

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

View File

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

View File

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

View File

@ -1161,6 +1161,29 @@ const CustomIconMap = {
/>
</svg>
),
stopwatch: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.95705 5.99046C7.05643 6.44935 6.33654 7.19809 5.91336 8.11602C5.49019 9.03396 5.38838 10.0676 5.62434 11.0505C5.8603 12.0334 6.42029 12.9081 7.21408 13.5339C8.00787 14.1597 8.98922 14.5 10 14.5C11.0108 14.5 11.9921 14.1597 12.7859 13.5339C13.5797 12.9082 14.1397 12.0334 14.3757 11.0505C14.6116 10.0676 14.5098 9.03396 14.0866 8.11603C13.6635 7.19809 12.9436 6.44935 12.043 5.99046L12.497 5.09946C13.5977 5.66032 14.4776 6.57544 14.9948 7.69737C15.512 8.81929 15.6364 10.0827 15.348 11.2839C15.0596 12.4852 14.3752 13.5544 13.405 14.3192C12.4348 15.0841 11.2354 15.5 10 15.5C8.7646 15.5 7.56517 15.0841 6.59499 14.3192C5.6248 13.5544 4.94037 12.4852 4.65197 11.2839C4.36357 10.0827 4.488 8.81929 5.00522 7.69736C5.52243 6.57544 6.40231 5.66032 7.50306 5.09946L7.95705 5.99046Z"
fill="currentColor"
/>
<path d="M10 5.5V4M10 4H8M10 4H12" stroke="currentColor" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.8536 7.85356L10.3536 10.3536C10.1583 10.5488 9.84171 10.5488 9.64645 10.3536C9.45118 10.1583 9.45118 9.84172 9.64645 9.64645L12.1464 7.14645L12.8536 7.85356Z"
fill="currentColor"
/>
</svg>
),
} as const
export type CustomIconName = keyof typeof CustomIconMap

View File

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

View File

@ -29,6 +29,7 @@ import {
KclSamplesManifestItem,
} from 'lib/getKclSamplesManifest'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { markOnce } from 'lib/performance'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -54,6 +55,7 @@ export const FileMachineProvider = ({
)
useEffect(() => {
markOnce('code/didLoadFile')
async function fetchKclSamples() {
setKclSamples(await getKclSamplesManifest())
}

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

@ -96,6 +96,23 @@ export function LowerRightControls({
Report a bug
</Tooltip>
</a>
<Link
to={
location.pathname.includes(PATHS.FILE)
? filePath + PATHS.TELEMETRY + '?tab=project'
: PATHS.HOME + PATHS.TELEMETRY
}
data-testid="telemetry-link"
>
<CustomIcon
name="stopwatch"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<span className="sr-only">Telemetry</span>
<Tooltip position="top" contentClassName="text-xs">
Telemetry
</Tooltip>
</Link>
<Link
to={
location.pathname.includes(PATHS.FILE)

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { Themes, getSystemTheme } from 'lib/theme'
import { useMemo, useRef } from 'react'
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
import { lineHighlightField } from 'editor/highlightextension'
import { roundOff } from 'lib/utils'
import { onMouseDragMakeANewNumber, onMouseDragRegex } from 'lib/utils'
import {
lineNumbers,
rectangularSelection,
@ -129,7 +129,9 @@ export const KclEditorPane = () => {
closeBrackets(),
highlightActiveLine(),
highlightSelectionMatches(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
syntaxHighlighting(defaultHighlightStyle, {
fallback: true,
}),
rectangularSelection(),
dropCursor(),
interact({
@ -137,29 +139,12 @@ export const KclEditorPane = () => {
// a rule for a number dragger
{
// the regexp matching the value
regexp: /-?\b\d+\.?\d*\b/g,
regexp: onMouseDragRegex,
// set cursor to "ew-resize" on hover
cursor: 'ew-resize',
// change number value based on mouse X movement on drag
onDrag: (text, setText, e) => {
const multiplier =
e.shiftKey && e.metaKey
? 0.01
: e.metaKey
? 0.1
: e.shiftKey
? 10
: 1
const delta = e.movementX * multiplier
const newVal = roundOff(
Number(text) + delta,
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
)
if (isNaN(newVal)) return
setText(newVal.toString())
onMouseDragMakeANewNumber(text, setText, e)
},
},
],

View File

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

View File

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

View File

@ -0,0 +1,33 @@
import { useEffect, useState, createContext, ReactNode } from 'react'
import { useNavigation, useLocation } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { markOnce } from 'lib/performance'
export const RouteProviderContext = createContext({})
export function RouteProvider({ children }: { children: ReactNode }) {
const [first, setFirstState] = useState(true)
const navigation = useNavigation()
const location = useLocation()
useEffect(() => {
// On initialization, the react-router-dom does not send a 'loading' state event.
// it sends an idle event first.
const pathname = first ? location.pathname : navigation.location?.pathname
const isHome = pathname === PATHS.HOME
const isFile =
pathname?.includes(PATHS.FILE) &&
pathname?.substring(pathname?.length - 4) === '.kcl'
if (isHome) {
markOnce('code/willLoadHome')
} else if (isFile) {
markOnce('code/willLoadFile')
}
setFirstState(false)
}, [navigation])
return (
<RouteProviderContext.Provider value={{}}>
{children}
</RouteProviderContext.Provider>
)
}

View File

@ -1,7 +1,7 @@
import { trap } from 'lib/trap'
import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useState } from 'react'
@ -42,6 +42,7 @@ import { getAppSettingsFilePath } from 'lib/desktop'
import { isDesktop } from 'lib/isDesktop'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { codeManager } from 'lib/singletons'
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -288,6 +289,44 @@ export const SettingsAuthProviderBase = ({
settingsWithCommandConfigs,
])
// Due to the route provider, i've moved this to the SettingsAuthProvider instead of CommandBarProvider
// This will register the commands to route to Telemetry, Home, and Settings.
useEffect(() => {
const filePath =
PATHS.FILE +
'/' +
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
createRouteCommands(navigate, location, filePath)
commandBarSend({
type: 'Remove commands',
data: {
commands: [
RouteTelemetryCommand,
RouteHomeCommand,
RouteSettingsCommand,
],
},
})
if (location.pathname === PATHS.HOME) {
commandBarSend({
type: 'Add commands',
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
})
} else if (location.pathname.includes(PATHS.FILE)) {
commandBarSend({
type: 'Add commands',
data: {
commands: [
RouteTelemetryCommand,
RouteSettingsCommand,
RouteHomeCommand,
],
},
})
}
}, [location])
// Listen for changes to the system theme and update the app theme accordingly
// This is only done if the theme setting is set to 'system'.
// It can't be done in XState (in an invoked callback, for example)

View File

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

View File

@ -0,0 +1,72 @@
import { getMarks } from 'lib/performance'
import {
printDeltaTotal,
printInvocationCount,
printMarkDownTable,
printRawMarks,
} from 'lib/telemetry'
export function TelemetryExplorer() {
const marks = getMarks()
const markdownTable = printMarkDownTable(marks)
const rawMarks = printRawMarks(marks)
const deltaTotalTable = printDeltaTotal(marks)
const invocationCount = printInvocationCount(marks)
// TODO data-telemetry-type
// TODO data-telemetry-name
return (
<div>
<h1 className="pb-4">Marks</h1>
<div className="max-w-xl max-h-64 overflow-auto select-all">
{marks.map((mark, index) => {
return (
<pre className="text-xs" key={index}>
<code key={index}>{JSON.stringify(mark, null, 2)}</code>
</pre>
)
})}
</div>
<h1 className="pb-4">Startup Performance</h1>
<div className="max-w-xl max-h-64 overflow-auto select-all">
{markdownTable.map((line, index) => {
return (
<pre className="text-xs" key={index}>
<code key={index}>{line}</code>
</pre>
)
})}
</div>
<h1 className="pb-4">Delta and Totals</h1>
<div className="max-w-xl max-h-64 overflow-auto select-all">
{deltaTotalTable.map((line, index) => {
return (
<pre className="text-xs" key={index}>
<code key={index}>{line}</code>
</pre>
)
})}
</div>
<h1 className="pb-4">Raw Marks</h1>
<div className="max-w-xl max-h-64 overflow-auto select-all">
{rawMarks.map((line, index) => {
return (
<pre className="text-xs" key={index}>
<code key={index}>{line}</code>
</pre>
)
})}
</div>
<h1 className="pb-4">Invocation Count</h1>
<div className="max-w-xl max-h-64 overflow-auto select-all">
{invocationCount.map((line, index) => {
return (
<pre className="text-xs" key={index}>
<code key={index}>{line}</code>
</pre>
)
})}
</div>
</div>
)
}

View File

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

View File

@ -1,4 +1,5 @@
import { EditorView, ViewUpdate } from '@codemirror/view'
import { syntaxTree } from '@codemirror/language'
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
import { engineCommandManager } from 'lib/singletons'
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
@ -12,6 +13,7 @@ import {
setDiagnosticsEffect,
} from '@codemirror/lint'
import { StateFrom } from 'xstate'
import { markOnce } from 'lib/performance'
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
@ -22,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
@ -59,6 +57,49 @@ export default class EditorManager {
setEditorView(editorView: EditorView) {
this._editorView = editorView
this.overrideTreeHighlighterUpdateForPerformanceTracking()
}
overrideTreeHighlighterUpdateForPerformanceTracking() {
// @ts-ignore
this._editorView?.plugins.forEach((e) => {
let sawATreeDiff = false
// we cannot use <>.constructor.name since it will get destroyed
// when packaging the application.
const isTreeHighlightPlugin =
e?.value &&
e.value?.hasOwnProperty('tree') &&
e.value?.hasOwnProperty('decoratedTo') &&
e.value?.hasOwnProperty('decorations')
if (isTreeHighlightPlugin) {
let originalUpdate = e.value.update
// @ts-ignore
function performanceTrackingUpdate(args) {
/**
* TreeHighlighter.update will be called multiple times on start up.
* We do not want to track the highlight performance of an empty update.
* mark the syntax highlight one time when the new tree comes in with the
* initial code
*/
const treeIsDifferent =
// @ts-ignore
!sawATreeDiff && this.tree !== syntaxTree(args.state)
if (treeIsDifferent && !sawATreeDiff) {
markOnce('code/willSyntaxHighlight')
}
// Call the original function
// @ts-ignore
originalUpdate.apply(this, [args])
if (treeIsDifferent && !sawATreeDiff) {
markOnce('code/didSyntaxHighlight')
sawATreeDiff = true
}
}
e.value.update = performanceTrackingUpdate
}
})
}
get editorView(): EditorView | null {
@ -117,20 +158,29 @@ export default class EditorManager {
}
}
/**
* Given an array of Diagnostics remove any duplicates by hashing a key
* in the format of from + ' ' + to + ' ' + message.
*/
makeUniqueDiagnostics(duplicatedDiagnostics: Diagnostic[]): Diagnostic[] {
const uniqueDiagnostics: Diagnostic[] = []
const seenDiagnostic: { [key: string]: boolean } = {}
duplicatedDiagnostics.forEach((diagnostic: Diagnostic) => {
const hash = `${diagnostic.from} ${diagnostic.to} ${diagnostic.message}`
if (!seenDiagnostic[hash]) {
uniqueDiagnostics.push(diagnostic)
seenDiagnostic[hash] = true
}
})
return uniqueDiagnostics
}
setDiagnostics(diagnostics: Diagnostic[]): void {
if (!this._editorView) return
// Clear out any existing diagnostics that are the same.
for (const diagnostic of diagnostics) {
for (const otherDiagnostic of diagnostics) {
if (diagnosticIsEqual(diagnostic, otherDiagnostic)) {
diagnostics = diagnostics.filter(
(d) => !diagnosticIsEqual(d, diagnostic)
)
diagnostics.push(diagnostic)
break
}
}
}
diagnostics = this.makeUniqueDiagnostics(diagnostics)
this._editorView.dispatch({
effects: [setDiagnosticsEffect.of(diagnostics)],

View File

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

View File

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

View File

@ -8,8 +8,10 @@ import ModalContainer from 'react-modal-promise'
import { isDesktop } from 'lib/isDesktop'
import { AppStreamProvider } from 'AppState'
import { ToastUpdate } from 'components/ToastUpdate'
import { markOnce } from 'lib/performance'
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
markOnce('code/willAuth')
// uncomment for xstate inspector
// import { DEV } from 'env'
// import { inspect } from '@xstate/inspect'

View File

@ -2,7 +2,7 @@ import { executeAst, lintAst } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection'
import { EngineCommandManager, CommandLogType } from './std/engineConnection'
import { err } from 'lib/trap'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
@ -21,6 +21,7 @@ import {
import { getNodeFromPath } from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
import { markOnce } from 'lib/performance'
import { Node } from 'wasm-lib/kcl/bindings/Node'
interface ExecuteArgs {
@ -124,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[]) {
@ -257,6 +258,7 @@ export class KclManager {
}
const ast = args.ast || this.ast
markOnce('code/startExecuteAst')
const currentExecutionId = args.executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)
@ -288,15 +290,9 @@ export class KclManager {
)
}
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
padding: 0.1, // padding around the objects
animated: false, // don't animate the zoom for now
},
await sceneInfra.camControls.centerModelRelativeToPanes({
zoomToFit: true,
zoomObjectId,
})
}
}
@ -326,11 +322,12 @@ export class KclManager {
this.ast = { ...ast }
this._executeCallback()
this.engineCommandManager.addCommandLog({
type: 'execution-done',
type: CommandLogType.ExecutionDone,
data: null,
})
this._cancelTokens.delete(currentExecutionId)
markOnce('code/endExecuteAst')
}
// NOTE: this always updates the code state and editor.
// DO NOT CALL THIS from codemirror ever.
@ -354,9 +351,6 @@ export class KclManager {
this.clearAst()
return
}
codeManager.updateCodeEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
this._ast = { ...newAst }
const { logs, errors, execState } = await executeAst({
@ -494,11 +488,6 @@ export class KclManager {
}
if (execute) {
// Call execute on the set ast.
// Update the code state and editor.
codeManager.updateCodeEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
await this.executeAst({
ast: astWithUpdatedSource,
zoomToFit: optionalParams?.zoomToFit,

View File

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

View File

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

View File

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

View File

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

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