Compare commits

...

56 Commits

Author SHA1 Message Date
239ab6850e Cut release v0.26.2 (#4322)
* Cut release v0.26.2

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-26 03:06:01 -04:00
4a7dd6e650 update env vars to match other repos, make dry (#4321)
* update env vars to match other repos, make dry

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

* bump

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-10-26 03:06:32 +00:00
af2609e678 Fix NetworkMachineIndicator and machines dynamically showing in CommandBar (#4311) 2024-10-25 16:28:10 -07:00
30909dedda Bump kittycad from 0.3.23 to 0.3.25 in /src/wasm-lib (#4316)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.3.23 to 0.3.25.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.3.23...v0.3.25)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 22:58:59 +00:00
39d76ed54f Bugfix: arc paths were stored as straight line paths (#4310)
Problem 1 of https://github.com/KittyCAD/modeling-app/issues/4297
2024-10-25 22:49:30 +00:00
4925251c29 Make application aware it saved the buffer and not something else (#4314)
Make application aware it saved the buffer and something else
2024-10-25 18:07:50 -04:00
9772869545 Add a radius length indicator to the circle sketch tool (#4304)
* Add a radius length indicator to the circle sketch tool

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

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

This reverts commit 15b078f641.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-10-25 17:42:27 -04:00
a7e830cd02 Update machine-api spec (#4305)
YOYO NEW API SPEC!

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-25 20:53:08 +00:00
ca102116b6 Bump kittycad-modeling-cmds from 0.2.70 to 0.2.71 in /src/wasm-lib (#4302)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.70 to 0.2.71.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.70...kittycad-modeling-cmds-0.2.71)

---
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>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2024-10-25 20:52:22 +00:00
c2fba89e77 Bump regex from 1.11.0 to 1.11.1 in /src/wasm-lib (#4301)
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.0...1.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 13:50:21 -07:00
7e31678ba2 Chore: Don't let draft lines receive mouseEnter/Leave events, or create invalid overlays (#4306) 2024-10-25 15:34:53 -04:00
1140ced121 Tags should refer to full paths, not just base paths. (#4299)
See "Problem 2" in https://github.com/KittyCAD/modeling-app/issues/4297

This is a pure refactor, it should not change any behaviour at all.
It adds more information into the tag system, but nothing reads that
extra information yet. It will be used to address problem 3 of the above
issue.
2024-10-25 10:27:40 -04:00
32b7ddaa7c Update circular pattern snapshots (#4303)
Engine changed (Serena fixed a bug with circular patterns), so snapshots need to be updated.
2024-10-25 08:28:46 -04:00
2525f99515 Move more KCL tests into files (#4260) 2024-10-25 01:52:43 +00:00
4b8ce34b31 Fix release notes (#4295) 2024-10-24 17:07:12 -04:00
6617f72373 Fix docs for how to patch kcmc (#4294) 2024-10-24 20:14:38 +00:00
e9033e1754 Cut release v0.26.1 (#4289) 2024-10-24 14:08:29 -04:00
9b697e30cf Revert "Separate debug/release electron-builder to help mac job (#4270)" (#4293)
This reverts commit 19d01c563e.
2024-10-24 13:07:43 -04:00
a70facdab4 Disable rotate on start new sketch (#4287)
disable rotate on start new sketch

Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-10-24 10:18:41 -04:00
4083f9f3dd Fix release notes in last_download.json (#4247)
* Fix release notes in last_download.json
Fixes #4246

* Fix job output

* Clean up, ready for merge
2024-10-24 09:47:44 -04:00
7ead2bb875 Stop propagation of camera clicks after drags (#4257)
* Update CameraControls.ts

* fix static analyzer error

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

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

This reverts commit 0b63016217.

* Don't perform sketch click operations if a camera movement interaction also matches

* Don't `stopPropogation`, make selection listener early return if `wasDragging`
+ consolidate `wasDragging` set statements, add comments

* Codespell

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-10-24 09:26:33 -04:00
19d01c563e Separate debug/release electron-builder to help mac job (#4270)
* Separate debug/release electron-builder to help mac job
Will attempt to fix #4199

* Test BUILD_RELEASE: true

* Revert "Test BUILD_RELEASE: true"

This reverts commit f2c0c24432.
2024-10-24 08:10:16 -04:00
dfe7cfc91c Bump syn from 2.0.82 to 2.0.85 in /src/wasm-lib (#4283)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.82 to 2.0.85.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.82...2.0.85)

---
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>
2024-10-24 05:52:18 +00:00
01443e445d Bump kittycad-modeling-cmds from 0.2.68 to 0.2.70 in /src/wasm-lib (#4284)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.68 to 0.2.70.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.68...kittycad-modeling-cmds-0.2.70)

---
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>
2024-10-24 03:53:07 +00:00
e16eb49f51 Bump serde from 1.0.210 to 1.0.213 in /src/wasm-lib (#4285)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.210 to 1.0.213.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.210...v1.0.213)

---
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>
2024-10-24 03:50:38 +00:00
5d5138e8e6 Bump syn from 2.0.79 to 2.0.82 in /src/wasm-lib (#4252)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.79 to 2.0.82.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.79...2.0.82)

---
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: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-23 23:48:18 +00:00
e1d6e29523 Update machine-api spec (#4241)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-23 23:41:48 +00:00
49657ad2e5 Bump @types/node from 22.5.0 to 22.7.8 (#4258)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.0 to 22.7.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  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>
2024-10-23 15:12:40 -07:00
b40d353994 Bump thiserror from 1.0.64 to 1.0.65 in /src/wasm-lib (#4268)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.64 to 1.0.65.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.64...1.0.65)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 21:47:08 +00:00
62ffa53add Bump proc-macro2 from 1.0.88 to 1.0.89 in /src/wasm-lib (#4266)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.88 to 1.0.89.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.88...1.0.89)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 21:46:23 +00:00
64dce4d8b1 Bump anyhow from 1.0.89 to 1.0.91 in /src/wasm-lib (#4269)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.89 to 1.0.91.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.89...1.0.91)

---
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>
2024-10-23 21:41:00 +00:00
02588b2672 Rename Sketch.value to Sketch.paths (#4272)
'value' is not a useful name
2024-10-23 17:42:54 +00:00
3d1ac2ac0b Fix engine connection break when starting onboarding from a fresh install (#4263)
* Make electron test setting overrides not entirely replace default settings

* Add failing test

* Fix test by checking for healthy engine connection before executing demo code

* Fix one electron test that assumed all settings got wiped if you override any.

* 🤷🏻‍♂️ an engine-side camera position in one of the E2E tests changed by 0.01 randomly
2024-10-22 20:22:52 -04:00
ff5ce29fd7 Delete ..env.development.local.swp (#4259) 2024-10-22 18:33:28 -04:00
4bd7e02271 Support negative start and end in ranges (#4249)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-10-22 14:28:48 +13:00
26042790b6 Remove setInterval implementations from camera controls (#4255)
* remove setInterval implementations from camera controls

* fmt
2024-10-21 16:46:47 -07:00
af74f3bb05 Upgrade to rust toolchain 1.82.0 (#4245)
* Upgrade to rust toolchain 1.82.0

* Fix lint about variant being too large

* Fix lint about Err variant being too large
2024-10-21 16:35:04 -05:00
0bdedf5854 fix job name for printers (#4234)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
2024-10-21 21:27:32 +00:00
d2c6b5cf3a Buffer file writes, because writing to file after every keypress has bad consequences (#4242)
* Buffer file writes, because writing to file after every keypress does bad things

* fix: kevin -- added timeouts for the time being since the workflow for saving data to disk is now changed

---------

Co-authored-by: Kevin Nadro <kevin@zoo.dev>
2024-10-21 15:07:20 -05:00
c42967d0e7 Fix auto changelog in make-release.sh (#4197) 2024-10-18 11:57:21 -07:00
cb8fc33adb Change CUT_RELEASE_PR to BUILD_RELEASE for out-yml (#4214)
Change CUT_RELEASE_PR to BUILD_RELEASE
2024-10-18 12:46:12 -04:00
2dc8b429ff Cut release v0.26.0 (#4196) 2024-10-18 09:17:13 -07:00
19ffa220e8 Fix reading files from WebAssembly (#4183) 2024-10-18 14:43:01 +00:00
5332ddd88e Add more Machine API capabilities (#4203) 2024-10-18 10:25:54 -04:00
11d9a2ee00 Fix test settings to actually get used (#4191)
* Fix test settings to actually get used

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

* Export file size changed out from under us again, relax this test to just be above a reasonable size

* Missed on updated export expectation

* Wrong check, should just be greater than

* Fix E2E test, remove console log

* fmt

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

* Sketchy rectangle commit fix

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

* Re-run CI

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

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

This reverts commit 2ace7a3b0e.

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

* Bump timeouts for snapshots

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

* Re-run CI

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-18 08:31:00 +00:00
bfebc41a5c Franknoirot/adhoc/revert-dedupe-commits (#4213)
* Revert "KCL: Fix duplicate 'type' key"

This reverts commit f650281855.

* Revert "Remove duplicate "type" field in the snapshots (#4211)"

This reverts commit 824b4c823e.
2024-10-18 02:16:44 -04:00
824b4c823e Remove duplicate "type" field in the snapshots (#4211)
* Remove duplicate "type" field in the snapshots

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

* Confirm

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-10-18 01:32:03 -04:00
785002fa4e Live reload on file tree changes and project settings changes (#4142)
* Reload FileTree and File when changed externally

* Added tests

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

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

* New machine-api types

* empty

* Update .codespellrc

* Update .codespellrc

---------

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

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

* Confirm

---------

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

* New machine-api types

* empt

---------

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

* fies

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

* add status

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

* pass disabled

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

* disabled

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

* add nozzle diameter

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

* ypdates

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

* update types

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

* update types

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

* update types

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
2024-10-17 15:30:46 -07:00
190 changed files with 30919 additions and 14112 deletions

View File

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

View File

@ -15,6 +15,7 @@ on:
env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@ -25,7 +26,6 @@ jobs:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
notes: ${{ steps.export_version.outputs.notes }}
steps:
- uses: actions/checkout@v4
@ -55,8 +55,6 @@ jobs:
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
- name: Generate release notes
env:
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
run: |
echo "$NOTES" > release-notes.md
cat release-notes.md
@ -84,9 +82,6 @@ jobs:
path: |
electron-builder.yml
- id: export_notes
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
@ -211,7 +206,7 @@ jobs:
out/*-x86_64-linux.*
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
if: ${{ env.BUILD_RELEASE == 'true' }}
with:
name: out-yml
path: |
@ -262,7 +257,6 @@ jobs:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ needs.prepare-files.outputs.notes }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}

View File

@ -62,7 +62,7 @@ jobs:
shell: bash
run: |-
cd "${{ matrix.dir }}"
cargo llvm-cov nextest --all --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
cargo llvm-cov nextest --workspace --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000

File diff suppressed because it is too large Load Diff

View File

@ -162,6 +162,28 @@ A base path.
----
A circular arc, not necessarily tangential to the current point.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Arc`| | No |
| `center` |`[number, number]`| Center of the circle that this arc is drawn on. | No |
| `radius` |`number`| Radius of the circle that this arc is drawn on. | No |
| `from` |`[number, number]`| The from point. | No |
| `to` |`[number, number]`| The to point. | No |
| `tag` |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No |
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
----

View File

@ -16,8 +16,8 @@ A sketch is a collection of paths.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes. | No |
| `value` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes). | No |
| `paths` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |

View File

@ -25,8 +25,8 @@ A sketch is a collection of paths.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `sketch`| | No |
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes. | No |
| `value` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes). | No |
| `paths` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |

View File

@ -18,7 +18,7 @@ Engine information for a tag.
|----------|------|-------------|----------|
| `id` |`string`| The id of the tagged object. | No |
| `sketch` |`string`| The sketch the tag is on. | No |
| `path` |[`BasePath`](/docs/kcl/types/BasePath)| The path the tag is on. | No |
| `path` |[`Path`](/docs/kcl/types/Path)| The path the tag is on. | No |
| `surface` |[`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface)| The surface information for the tag. | No |

View File

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

View File

@ -104,7 +104,7 @@ test(
},
{ timeout: 15_000 }
)
.toBe(431341)
.toBeGreaterThan(300_000)
// clean up output.gltf
await fsp.rm('output.gltf')
@ -179,7 +179,7 @@ test(
},
{ timeout: 15_000 }
)
.toBe(102040)
.toBeGreaterThan(100_000)
// clean up output.gltf
await fsp.rm('output.gltf')

View File

@ -1,6 +1,16 @@
import { test, expect } from '@playwright/test'
import fsp from 'fs/promises'
import { uuidv4 } from 'lib/utils'
import { getUtils, setup, tearDown } from './test-utils'
import {
darkModeBgColor,
darkModePlaneColorXZ,
executorInputPath,
getUtils,
setup,
setupElectron,
tearDown,
} from './test-utils'
import { join } from 'path'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
@ -974,4 +984,84 @@ test.describe('Editor tests', () => {
|> close(%)
|> extrude(5, %)`)
})
test(
`Can use the import stdlib function on a local OBJ file`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'cube')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.obj'),
join(bracketDir, 'cube.obj')
)
await fsp.writeFile(join(bracketDir, 'main.kcl'), '')
},
})
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
// Locators and constants
const u = await getUtils(page)
const projectLink = page.getByRole('link', { name: 'cube' })
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
const locationToHavColor = async (
position: { x: number; y: number },
color: [number, number, number]
) => {
return u.getGreatestPixDiff(position, color)
}
const notTheOrigin = {
x: viewportSize.width * 0.55,
y: viewportSize.height * 0.3,
}
const origin = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const errorIndicators = page.locator('.cm-lint-marker-error')
await test.step(`Open the empty file, see the default planes`, async () => {
await projectLink.click()
await u.waitForPageLoad()
await expect
.poll(
async () => locationToHavColor(notTheOrigin, darkModePlaneColorXZ),
{
timeout: 5000,
message: 'XZ plane color is visible',
}
)
.toBeLessThan(15)
})
await test.step(`Write the import function line`, async () => {
await u.codeLocator.fill(`import('cube.obj')`)
await page.waitForTimeout(800)
})
await test.step(`Reset the camera before checking`, async () => {
await u.doAndWaitForCmd(async () => {
await gizmo.click({ button: 'right' })
await resetCameraButton.click()
}, 'zoom_to_fit')
})
await test.step(`Verify that we see the imported geometry and no errors`, async () => {
await expect(errorIndicators).toHaveCount(0)
await expect
.poll(async () => locationToHavColor(origin, darkModePlaneColorXZ), {
timeout: 3000,
message: 'Plane color should not be visible',
})
.toBeGreaterThan(15)
await expect
.poll(async () => locationToHavColor(origin, darkModeBgColor), {
timeout: 3000,
message: 'Background color should not be visible',
})
.toBeGreaterThan(15)
})
await electronApp.close()
}
)
})

View File

@ -136,6 +136,9 @@ test.describe('when using the file tree to', () => {
)
await pasteCodeInEditor(kclCube)
// TODO: We have a timeout of 1s between edits to write to disk. If you reload the page too quickly it won't write to disk.
await tronApp.page.waitForTimeout(2000)
await renameFile(fromFile, toFile)
await tronApp.page.reload()
@ -222,9 +225,11 @@ test.describe('when using the file tree to', () => {
)
await pasteCodeInEditor(kclCube)
// TODO: We have a timeout of 1s between edits to write to disk. If you reload the page too quickly it won't write to disk.
await tronApp.page.waitForTimeout(2000)
const kcl1 = 'main.kcl'
const kcl2 = '2.kcl'
await createNewFileAndSelect(kcl2)
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
@ -232,6 +237,9 @@ test.describe('when using the file tree to', () => {
)
await pasteCodeInEditor(kclCylinder)
// TODO: We have a timeout of 1s between edits to write to disk. If you reload the page too quickly it won't write to disk.
await tronApp.page.waitForTimeout(2000)
await renameFile(kcl2, kcl1)
await test.step(`Postcondition: ${kcl1} still has the original content`, async () => {
@ -960,4 +968,171 @@ _test.describe('Deleting items from the file pane', () => {
'TODO - delete folder we are in, with no main.kcl',
async () => {}
)
// Copied from tests above.
_test(
`external deletion of project navigates back home`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
// Point of divergence. Delete the project folder and see if it goes back
// to the home view.
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(join(projectsDirName, TEST_PROJECT_NAME), {
recursive: true,
force: true,
})
}
)
await _test.step('Check the app is back on the home view', async () => {
const projectsDirLink = page.getByText('Loaded from')
await _expect(projectsDirLink).toBeVisible()
})
await electronApp.close()
}
)
// Similar to the above
_test(
`external deletion of file in sub-directory updates the file tree and recreates it on code editor typing`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(
join(
projectsDirName,
TEST_PROJECT_NAME,
'folderToDelete',
'someFileWithin.kcl'
)
)
}
)
await _test.step('Check the file is gone in the file tree', async () => {
await _expect(
page.getByTestId('file-pane-scroll-container')
).not.toContainText('someFileWithin.kcl')
})
await _test.step(
'Check the file is back in the file tree after typing in code editor',
async () => {
await u.pasteCodeInEditor('hello = 1')
await _expect(
page.getByTestId('file-pane-scroll-container')
).toContainText('someFileWithin.kcl')
}
)
await electronApp.close()
}
)
})

View File

@ -55,6 +55,53 @@ test.describe('Onboarding tests', () => {
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
})
test(
'Desktop: fresh onboarding executes and loads',
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
})
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
// Locators and constants
const newProjectButton = page.getByRole('button', { name: 'New project' })
const projectLink = page.getByTestId('project-link')
await test.step(`Create a project and open to the onboarding`, async () => {
await newProjectButton.click()
await projectLink.click()
await test.step(`Ensure the engine connection works by testing the sketch button`, async () => {
await u.waitForPageLoad()
})
})
await test.step(`Ensure we see the onboarding stuff`, async () => {
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
})
await electronApp.close()
}
)
test('Code resets after confirmation', async ({ page }) => {
const initialCode = `sketch001 = startSketchOn('XZ')`

View File

@ -255,7 +255,7 @@ test.describe('Can export from electron app', () => {
},
{ timeout: 15_000 }
)
.toBe(431341)
.toBeGreaterThan(300_000)
// clean up output.gltf
await fsp.rm('output.gltf')
@ -851,7 +851,7 @@ test(
}
)
test(
test.fixme(
'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
@ -861,6 +861,12 @@ test(
page.on('console', console.log)
// Locators and constants
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
const pointOnModel = { x: 660, y: 250 }
const expectedStartCamZPosition = 15633.47
// expect to see text "No Projects found"
await expect(page.getByText('No Projects found')).toBeVisible()
@ -873,16 +879,7 @@ test(
await page.getByText('project-000').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await u.waitForPageLoad()
await page.locator('.cm-content').fill(`sketch001 = startSketchOn('XZ')
|> startProfileAt([-87.4, 282.92], %)
@ -892,8 +889,28 @@ test(
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(200, sketch001)`)
await page.waitForTimeout(800)
const pointOnModel = { x: 660, y: 250 }
async function getCameraZValue() {
return page
.getByTestId('cam-z-position')
.inputValue()
.then((value) => parseFloat(value))
}
await test.step(`Reset camera`, async () => {
await u.openDebugPanel()
await u.clearCommandLogs()
await u.doAndWaitForCmd(async () => {
await gizmo.click({ button: 'right' })
await resetCameraButton.click()
}, 'zoom_to_fit')
await expect
.poll(getCameraZValue, {
message: 'Camera Z should be at expected position after reset',
})
.toEqual(expectedStartCamZPosition)
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
@ -901,7 +918,7 @@ extrude001 = extrude(200, sketch001)`)
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000,
})
.toBeLessThan(15)
.toBeLessThan(30)
await expect(async () => {
await page.mouse.move(0, 0, { steps: 5 })

View File

@ -471,7 +471,7 @@ test(
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await page.waitForTimeout(300)
await page.waitForTimeout(1000)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
@ -528,6 +528,7 @@ test(
// Draw the rectangle
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 30)
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 10, { steps: 5 })
await page.waitForTimeout(800)
// Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({
@ -895,7 +896,7 @@ test(
// Wait for the second extrusion to appear
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
await page.waitForTimeout(2000)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
@ -939,7 +940,7 @@ test(
// Wait for the second extrusion to appear
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
await page.waitForTimeout(2000)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

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

After

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

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

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

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -47,6 +47,14 @@ export const commonPoints = {
num2: 14.44,
}
/** A semi-reliable color to check the default XZ plane on
* in dark mode in the default camera position
*/
export const darkModePlaneColorXZ: [number, number, number] = [50, 50, 99]
/** A semi-reliable color to check the default dark mode bg color against */
export const darkModeBgColor: [number, number, number] = [27, 27, 27]
export const editorSelector = '[role="textbox"][data-language="kcl"]'
type PaneId = 'variables' | 'code' | 'files' | 'logs'
@ -463,6 +471,9 @@ export async function getUtils(page: Page, test_?: typeof test) {
return test_?.step(
`Create and select project with text "${hasText}"`,
async () => {
// Without this, we get unreliable project creation. It's probably
// due to a race between the FS being read and clicking doing something.
await page.waitForTimeout(100)
await page.getByTestId('home-new-file').click()
const projectLinksPost = page.getByTestId('project-link')
await projectLinksPost.filter({ hasText }).click()
@ -492,6 +503,11 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFile: async (name: string) => {
return test?.step(`Create a file named ${name}`, async () => {
// If the application is in the middle of connecting a stream
// then creating a new file won't work in the end.
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
@ -872,10 +888,20 @@ export async function setupElectron({
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify(
appSettings
? { settings: appSettings }
: {
...TEST_SETTINGS,
? {
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
...appSettings.app,
},
},
}
: {
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,

View File

@ -292,7 +292,7 @@ test.describe(`Testing gizmo, fixture-based`, () => {
await test.step(`Verify the camera moved`, async () => {
await scene.expectState({
camera: {
position: [0, -23865.37, 11073.54],
position: [0, -23865.37, 11073.53],
target: [0, 0, 0],
},
})

View File

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

9
interface.d.ts vendored
View File

@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
import fsSync from 'node:fs'
import path from 'path'
import { dialog, shell } from 'electron'
import { MachinesListing } from 'lib/machineManager'
import { MachinesListing } from 'components/MachineManagerProvider'
type EnvFn = (value?: string) => string
@ -20,10 +20,11 @@ export interface IElectronAPI {
version: typeof process.env.version
watchFileOn: (
path: string,
key: string,
callback: (eventType: string, path: string) => void
) => void
watchFileOff: (path: string) => void
readFile: (path: string) => ReturnType<fs.readFile>
readFile: typeof fs.readFile
watchFileOff: (path: string, key: string) => void
writeFile: (
path: string,
data: string | Uint8Array
@ -67,7 +68,7 @@ export interface IElectronAPI {
}
}
kittycad: (access: string, args: any) => any
listMachines: () => Promise<MachinesListing>
listMachines: (machineApiIp: string) => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null>
onUpdateDownloadStart: (
callback: (value: { version: string }) => void

View File

@ -70,7 +70,7 @@ echo ""
echo "Suggested changelog:"
echo "\`\`\`"
echo "## What's Changed"
git log $(git describe --tags --abbrev=0)..HEAD --oneline --pretty=format:%s | grep -v Bump | grep -v 'Cut release v' | awk '{print "* "toupper(substr($0,0,1))substr($0,2)}'
git log $(git describe --tags --match="v[0-9]*" --abbrev=0)..HEAD --oneline --pretty=format:%s | grep -v Bump | grep -v 'Cut release v' | awk '{print "* "toupper(substr($0,0,1))substr($0,2)}'
echo ""
echo "**Full Changelog**: https://github.com/KittyCAD/modeling-app/compare/${latest_tag}...${new_version}"
echo "\`\`\`"

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.25.6",
"version": "0.26.2",
"private": true,
"productName": "Zoo Modeling App",
"author": {
@ -161,7 +161,7 @@
"@types/isomorphic-fetch": "^0.0.39",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.6",
"@types/node": "^22.5.0",
"@types/node": "^22.7.8",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4",

View File

@ -21,6 +21,7 @@ import { WasmErrBanner } from 'components/WasmErrBanner'
import { CommandBar } from 'components/CommandBar/CommandBar'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import FileMachineProvider from 'components/FileMachineProvider'
import { MachineManagerProvider } from 'components/MachineManagerProvider'
import { PATHS } from 'lib/paths'
import {
fileLoader,
@ -49,6 +50,7 @@ const router = createRouter([
{
loader: settingsLoader,
id: PATHS.INDEX,
// TODO: Re-evaluate if this is true
/* Make sure auth is the outermost provider or else we will have
* inefficient re-renders, use the react profiler to see. */
element: (
@ -57,7 +59,9 @@ const router = createRouter([
<LspProvider>
<KclContextProvider>
<AppStateProvider>
<Outlet />
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
</AppStateProvider>
</KclContextProvider>
</LspProvider>

View File

@ -64,6 +64,27 @@ export type ReactCameraProperties =
const lastCmdDelay = 50
class CameraRateLimiter {
lastSend?: Date = undefined
rateLimitMs: number = 16 //60 FPS
send = (f: () => void) => {
let now = new Date()
if (
this.lastSend === undefined ||
now.getTime() - this.lastSend.getTime() > this.rateLimitMs
) {
f()
this.lastSend = now
}
}
reset = () => {
this.lastSend = undefined
}
}
export class CameraControls {
engineCommandManager: EngineCommandManager
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
@ -71,15 +92,15 @@ export class CameraControls {
target: Vector3
domElement: HTMLCanvasElement
isDragging: boolean
wasDragging: boolean
mouseDownPosition: Vector2
mouseNewPosition: Vector2
rotationSpeed = 0.3
enableRotate = true
enablePan = true
enableZoom = true
zoomDataFromLastFrame?: number = undefined
// holds coordinates, and interaction
moveDataFromLastFrame?: [number, number, string] = undefined
moveSender: CameraRateLimiter = new CameraRateLimiter()
zoomSender: CameraRateLimiter = new CameraRateLimiter()
lastPerspectiveFov: number = 45
pendingZoom: number | null = null
pendingRotation: Vector2 | null = null
@ -171,6 +192,36 @@ export class CameraControls {
}
}
doMove = (interaction: any, coordinates: any) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction: interaction,
window: {
x: coordinates[0],
y: coordinates[1],
},
},
cmd_id: uuidv4(),
})
}
doZoom = (zoom: number) => {
this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: (-1 * zoom) / window.devicePixelRatio,
},
cmd_id: uuidv4(),
})
this.handleEnd()
}
constructor(
isOrtho = false,
domElement: HTMLCanvasElement,
@ -183,6 +234,7 @@ export class CameraControls {
this.target = new Vector3()
this.domElement = domElement
this.isDragging = false
this.wasDragging = false
this.mouseDownPosition = new Vector2()
this.mouseNewPosition = new Vector2()
@ -258,49 +310,6 @@ export class CameraControls {
this.onCameraChange()
}
// Our stream is never more than 60fps.
// We can get away with capping our "virtual fps" to 60 then.
const FPS_VIRTUAL = 60
const doZoom = () => {
if (this.zoomDataFromLastFrame !== undefined) {
this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude:
(-1 * this.zoomDataFromLastFrame) / window.devicePixelRatio,
},
cmd_id: uuidv4(),
})
this.handleEnd()
}
this.zoomDataFromLastFrame = undefined
}
setInterval(doZoom, 1000 / FPS_VIRTUAL)
const doMove = () => {
if (this.moveDataFromLastFrame !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction: this.moveDataFromLastFrame[2] as any,
window: {
x: this.moveDataFromLastFrame[0],
y: this.moveDataFromLastFrame[1],
},
},
cmd_id: uuidv4(),
})
}
this.moveDataFromLastFrame = undefined
}
setInterval(doMove, 1000 / FPS_VIRTUAL)
setTimeout(() => {
this.engineCommandManager.subscribeTo({
event: 'camera_drag_end',
@ -356,6 +365,8 @@ export class CameraControls {
onMouseDown = (event: PointerEvent) => {
this.domElement.setPointerCapture(event.pointerId)
this.isDragging = true
// Reset the wasDragging flag to false when starting a new drag
this.wasDragging = false
this.mouseDownPosition.set(event.clientX, event.clientY)
let interaction = this.getInteractionType(event)
if (interaction === 'none') return
@ -385,11 +396,18 @@ export class CameraControls {
const interaction = this.getInteractionType(event)
if (interaction === 'none') return
// If there's a valid interaction and the mouse is moving,
// our past (and current) interaction was a drag.
this.wasDragging = true
if (this.syncDirection === 'engineToClient') {
this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction]
this.moveSender.send(() => {
this.doMove(interaction, [event.clientX, event.clientY])
})
return
}
// else "clientToEngine" (Sketch Mode) or forceUpdate
// Implement camera movement logic here based on deltaMove
// For example, for rotating the camera around the target:
if (interaction === 'rotate') {
@ -418,6 +436,9 @@ export class CameraControls {
* under the cursor. This recently moved from being handled in App.tsx.
* This might not be the right spot, but it is more consolidated.
*/
// Clear any previous drag state
this.wasDragging = false
if (this.syncDirection === 'engineToClient') {
const newCmdId = uuidv4()
@ -459,7 +480,9 @@ export class CameraControls {
if (this.syncDirection === 'engineToClient') {
if (interaction === 'zoom') {
this.zoomDataFromLastFrame = event.deltaY
this.zoomSender.send(() => {
this.doZoom(event.deltaY)
})
} else {
// This case will get handled when we add pan and rotate using Apple trackpad.
console.error(

View File

@ -338,6 +338,11 @@ export class SceneEntities {
sceneInfra.setCallbacks({
onClick: async (args) => {
if (!args) return
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return
@ -407,7 +412,7 @@ export class SceneEntities {
if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject('sketch not found')
if (!isArray(sketch?.value))
if (!isArray(sketch?.paths))
return {
truncatedAst,
programMemoryOverride,
@ -435,7 +440,7 @@ export class SceneEntities {
maybeModdedAst,
sketch.start.__geoMeta.sourceRange
)
if (sketch?.value?.[0]?.type !== 'Circle') {
if (sketch?.paths?.[0]?.type !== 'Circle') {
const _profileStart = createProfileStartHandle({
from: sketch.start.from,
id: sketch.start.__geoMeta.id,
@ -451,16 +456,16 @@ export class SceneEntities {
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
}
const callbacks: (() => SegmentOverlayPayload | null)[] = []
sketch.value.forEach((segment, index) => {
sketch.paths.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
segment.__geoMeta.sourceRange
)
if (
draftExpressionsIndices &&
(sketch.value[index - 1] || sketch.start)
(sketch.paths[index - 1] || sketch.start)
) {
const previousSegment = sketch.value[index - 1] || sketch.start
const previousSegment = sketch.paths[index - 1] || sketch.start
const previousSegmentPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
previousSegment.__geoMeta.sourceRange
@ -511,7 +516,7 @@ export class SceneEntities {
to: segment.to,
}
const result = initSegment({
prevSegment: sketch.value[index - 1],
prevSegment: sketch.paths[index - 1],
callExpName,
input,
id: segment.__geoMeta.id,
@ -610,9 +615,9 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sg)) return Promise.reject(sg)
const lastSeg = sg?.value?.slice(-1)[0] || sg.start
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
const index = sg.paths.length // because we've added a new segment that's not in the memory yet, no need for `-1`
const mod = addNewSketchLn({
node: _ast,
programMemory: kclManager.programMemory,
@ -645,7 +650,13 @@ export class SceneEntities {
sceneInfra.setCallbacks({
onClick: async (args) => {
if (!args) return
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
let intersection2d = intersectionPoint?.twoD
const profileStart = args.intersects
@ -654,7 +665,7 @@ export class SceneEntities {
let modifiedAst
if (profileStart) {
const lastSegment = sketch.value.slice(-1)[0]
const lastSegment = sketch.paths.slice(-1)[0]
modifiedAst = addCallExpressionsToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
@ -686,7 +697,7 @@ export class SceneEntities {
})
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
} else if (intersection2d) {
const lastSegment = sketch.value.slice(-1)[0]
const lastSegment = sketch.paths.slice(-1)[0]
const tmp = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
@ -735,7 +746,6 @@ export class SceneEntities {
},
})
},
...this.mouseEnterLeaveCallbacks(),
})
}
setupDraftRectangle = async (
@ -817,7 +827,7 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.value
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
@ -826,6 +836,11 @@ export class SceneEntities {
)
},
onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return
@ -868,7 +883,7 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.value
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
@ -985,7 +1000,7 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.value
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
@ -994,6 +1009,11 @@ export class SceneEntities {
)
},
onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return
@ -1105,7 +1125,7 @@ export class SceneEntities {
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
if (addingNewSegmentStatus === 'nothing') {
const prevSegment = sketch.value[pipeIndex - 2]
const prevSegment = sketch.paths[pipeIndex - 2]
const mod = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
@ -1157,6 +1177,11 @@ export class SceneEntities {
},
onMove: () => {},
onClick: (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args?.mouseEvent.which !== 1) return
if (!args || !args.selected) {
sceneInfra.modelingSend({
@ -1345,7 +1370,7 @@ export class SceneEntities {
}
if (!sketch) return
const sgPaths = sketch.value
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(
@ -1393,7 +1418,7 @@ export class SceneEntities {
modifiedAst,
segment.__geoMeta.sourceRange
)
const sgPaths = sketch.value
const sgPaths = sketch.paths
const originalPathToNodeStr = JSON.stringify(segPathToNode)
segPathToNode[1][0] = varDecIndex
const pathToNodeStr = JSON.stringify(segPathToNode)
@ -1701,7 +1726,7 @@ function prepareTruncatedMemoryAndAst(
variableDeclarationName
)
if (err(sg)) return sg
const lastSeg = sg?.value.slice(-1)[0]
const lastSeg = sg?.paths.slice(-1)[0]
if (draftSegment) {
// truncatedAst needs to setup with another segment at the end
let newSegment

View File

@ -213,7 +213,7 @@ export class SceneInfra {
to: Coords2d
angle?: number
}): SegmentOverlayPayload | null {
if (group.userData.pathToNode && arrowGroup) {
if (!group.userData.draft && group.userData.pathToNode && arrowGroup) {
const vector = new Vector3(0, 0, 0)
// Get the position of the object3D in world space

View File

@ -58,7 +58,7 @@ import { err } from 'lib/trap'
interface CreateSegmentArgs {
input: SegmentInputs
prevSegment: Sketch['value'][number]
prevSegment: Sketch['paths'][number]
id: string
pathToNode: PathToNode
isDraftSegment?: boolean
@ -72,7 +72,7 @@ interface CreateSegmentArgs {
interface UpdateSegmentArgs {
input: SegmentInputs
prevSegment: Sketch['value'][number]
prevSegment: Sketch['paths'][number]
group: Group
sceneInfra: SceneInfra
scale?: number
@ -147,6 +147,7 @@ class StraightSegment implements SegmentUtils {
segmentGroup.name = STRAIGHT_SEGMENT
segmentGroup.userData = {
type: STRAIGHT_SEGMENT,
draft: isDraftSegment,
id,
from,
to,
@ -347,6 +348,7 @@ class TangentialArcToSegment implements SegmentUtils {
mesh.name = meshName
group.userData = {
type: TANGENTIAL_ARC_TO_SEGMENT,
draft: isDraftSegment,
id,
from,
to,
@ -515,11 +517,18 @@ class CircleSegment implements SegmentUtils {
const meshType = isDraftSegment ? CIRCLE_SEGMENT_DASH : CIRCLE_SEGMENT_BODY
const arrowGroup = createArrowhead(scale, theme, color)
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
// A radius indicator that appears from the center to the perimeter
const radiusIndicatorGroup = createLengthIndicator({
from: center,
to: [center[0] + radius, center[1]],
scale,
})
arcMesh.userData.type = meshType
arcMesh.name = meshType
group.userData = {
type: CIRCLE_SEGMENT,
draft: isDraftSegment,
id,
from,
radius,
@ -532,7 +541,7 @@ class CircleSegment implements SegmentUtils {
}
group.name = CIRCLE_SEGMENT
group.add(arcMesh, arrowGroup, circleCenterGroup)
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
const updateOverlaysCallback = this.update({
prevSegment,
input,
@ -564,6 +573,9 @@ class CircleSegment implements SegmentUtils {
group.userData.radius = radius
group.userData.prevSegment = prevSegment
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const radiusLengthIndicator = group.getObjectByName(
SEGMENT_LENGTH_LABEL
) as Group
const circleCenterHandle = group.getObjectByName(
CIRCLE_CENTER_HANDLE
) as Group
@ -581,11 +593,14 @@ class CircleSegment implements SegmentUtils {
}
if (arrowGroup) {
arrowGroup.position.set(
center[0] + Math.cos(Math.PI / 4) * radius,
center[1] + Math.sin(Math.PI / 4) * radius,
0
)
// The arrowhead is placed at the perimeter of the circle,
// pointing up and to the right
const arrowPoint = {
x: center[0] + Math.cos(Math.PI / 4) * radius,
y: center[1] + Math.sin(Math.PI / 4) * radius,
}
arrowGroup.position.set(arrowPoint.x, arrowPoint.y, 0)
const arrowheadAngle = Math.PI / 4
arrowGroup.quaternion.setFromUnitVectors(
@ -596,6 +611,31 @@ class CircleSegment implements SegmentUtils {
arrowGroup.visible = isHandlesVisible
}
if (radiusLengthIndicator) {
// The radius indicator is placed at the midpoint of the radius,
// at a 45 degree CCW angle from the positive X-axis
const indicatorPoint = {
x: center[0] + (Math.cos(Math.PI / 4) * radius) / 2,
y: center[1] + (Math.sin(Math.PI / 4) * radius) / 2,
}
const labelWrapper = radiusLengthIndicator.getObjectByName(
SEGMENT_LENGTH_LABEL_TEXT
) as CSS2DObject
const labelWrapperElem = labelWrapper.element as HTMLDivElement
const label = labelWrapperElem.children[0] as HTMLParagraphElement
label.innerText = `${roundOff(radius)}`
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
const isPlaneBackFace = center[0] > indicatorPoint.x
label.style.setProperty(
'--degree',
`${isPlaneBackFace ? '45' : '-45'}deg`
)
label.style.setProperty('--x', `0px`)
label.style.setProperty('--y', `0px`)
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
radiusLengthIndicator.visible = isHandlesVisible
}
if (circleCenterHandle) {
circleCenterHandle.position.set(center[0], center[1], 0)
circleCenterHandle.scale.set(scale, scale, scale)

View File

@ -135,7 +135,9 @@ function CommandArgOptionInput({
<Combobox.Input
id="option-input"
ref={inputRef}
onChange={(event) => setQuery(event.target.value)}
onChange={(event) =>
!event.target.disabled && setQuery(event.target.value)
}
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k')
@ -175,9 +177,18 @@ function CommandArgOptionInput({
<Combobox.Option
key={option.name}
value={option}
disabled={option.disabled}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
>
<p className="flex-grow">{option.name} </p>
<p
className={`flex-grow ${
(option.disabled &&
'text-chalkboard-70 dark:text-chalkboard-50 cursor-not-allowed') ||
''
}`}
>
{option.name}
</p>
{option.value === currentOption?.value && (
<small className="text-chalkboard-70 dark:text-chalkboard-50">
current

View File

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

View File

@ -23,6 +23,7 @@ export function LowerRightControls({
}) {
const location = useLocation()
const filePath = useAbsoluteFilePath()
const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'

View File

@ -0,0 +1,123 @@
import { createContext, useEffect, useState } from 'react'
import { engineCommandManager } from 'lib/singletons'
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
import { isDesktop } from 'lib/isDesktop'
import { components } from 'lib/machine-api'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
export type MachinesListing = Array<
components['schemas']['MachineInfoResponse']
>
export interface MachineManager {
machines: MachinesListing
machineApiIp: string | null
currentMachine: components['schemas']['MachineInfoResponse'] | null
noMachinesReason: () => string | undefined
setCurrentMachine: (
m: components['schemas']['MachineInfoResponse'] | null
) => void
}
export const MachineManagerContext = createContext<MachineManager>({
machines: [],
machineApiIp: null,
currentMachine: null,
setCurrentMachine: (
_: components['schemas']['MachineInfoResponse'] | null
) => {},
noMachinesReason: () => undefined,
})
export const MachineManagerProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [machines, setMachines] = useState<MachinesListing>([])
const [machineApiIp, setMachineApiIp] = useState<string | null>(null)
const [currentMachine, setCurrentMachine] = useState<
components['schemas']['MachineInfoResponse'] | null
>(null)
const commandBarActor = CommandsContext.useActorRef()
// Get the reason message for why there are no machines.
const noMachinesReason = (): string | undefined => {
if (machines.length > 0) {
return undefined
}
if (machineApiIp === null) {
return 'Machine API server was not discovered'
}
return 'Machine API server was discovered, but no machines are available'
}
useEffect(() => {
if (!isDesktop()) return
const update = async () => {
const _machineApiIp = await window.electron.getMachineApiIp()
if (_machineApiIp === null) return
setMachineApiIp(_machineApiIp)
const _machines = await window.electron.listMachines(_machineApiIp)
setMachines(_machines)
}
// Start a background job to update the machines every ten seconds.
// If MDNS is already watching, this timeout will wait until it's done to trigger the
// finding again.
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
const timeoutLoop = () => {
clearTimeout(timeoutId)
timeoutId = setTimeout(
toSync(async () => {
await update()
timeoutLoop()
}, reportRejection),
1000
)
}
timeoutLoop()
update().catch(reportRejection)
}, [])
// Update engineCommandManager's copy of this data.
useEffect(() => {
const machineManagerNext = {
machines,
machineApiIp,
currentMachine,
noMachinesReason,
setCurrentMachine,
}
engineCommandManager.machineManager = machineManagerNext
commandBarActor.send({
type: 'Set machine manager',
data: machineManagerNext,
})
}, [machines, machineApiIp, currentMachine])
return (
<MachineManagerContext.Provider
value={{
machines,
machineApiIp,
currentMachine,
setCurrentMachine,
noMachinesReason,
}}
>
{' '}
{children}{' '}
</MachineManagerContext.Provider>
)
}

View File

@ -1,5 +1,11 @@
import { useMachine } from '@xstate/react'
import React, { createContext, useEffect, useMemo, useRef } from 'react'
import React, {
createContext,
useEffect,
useMemo,
useRef,
useContext,
} from 'react'
import {
Actor,
AnyStateMachine,
@ -28,7 +34,7 @@ import {
editorManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { machineManager } from 'lib/machineManager'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { useHotkeys } from 'react-hotkeys-hook'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import {
@ -69,7 +75,7 @@ import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@codemirror/state'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, reportRejection, trap } from 'lib/trap'
@ -84,6 +90,7 @@ import {
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -116,6 +123,7 @@ export const ModelingMachineProvider = ({
} = useSettingsAuthContext()
const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), [])
@ -138,6 +146,8 @@ export const ModelingMachineProvider = ({
// >
// )
const machineManager = useContext(MachineManagerContext)
const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine.provide({
actions: {
@ -406,18 +416,35 @@ export const ModelingMachineProvider = ({
return {}
}
),
Make: ({ event }) => {
Make: ({ context, event }) => {
if (event.type !== 'Make') return
// Check if we already have an export intent.
if (engineCommandManager.exportIntent) {
if (engineCommandManager.exportInfo) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportIntent = ExportIntent.Make
engineCommandManager.exportInfo = {
intent: ExportIntent.Make,
name: file?.name || '',
}
// Set the current machine.
machineManager.currentMachine = event.data.machine
// Due to our use of singeton pattern, we need to do this to reliably
// update this object across React and non-React boundary.
// We need to do this eagerly because of the exportToEngine call below.
if (engineCommandManager.machineManager === null) {
console.warn(
"engineCommandManager.machineManager is null. It shouldn't be at this point. Aborting operation."
)
return
} else {
engineCommandManager.machineManager.currentMachine =
event.data.machine
}
// Update the rest of the UI that needs to know the current machine
context.machineManager.setCurrentMachine(event.data.machine)
const format: Models['OutputFormat_type'] = {
type: 'stl',
@ -443,12 +470,16 @@ export const ModelingMachineProvider = ({
},
'Engine export': ({ event }) => {
if (event.type !== 'Export') return
if (engineCommandManager.exportIntent) {
if (engineCommandManager.exportInfo) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportIntent = ExportIntent.Save
engineCommandManager.exportInfo = {
intent: ExportIntent.Save,
// This never gets used its only for make.
name: '',
}
const format = {
...event.data,
@ -635,6 +666,7 @@ export const ModelingMachineProvider = ({
input.plane
)
await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.enableRotate = false
sceneInfra.camControls.syncDirection = 'clientToEngine'
await letEngineAnimateAndSyncCamAfter(
@ -985,6 +1017,7 @@ export const ModelingMachineProvider = ({
...modelingMachineDefaultContext.store,
...persistedContext,
},
machineManager,
},
// devTools: true,
}

View File

@ -95,7 +95,7 @@ export const processMemory = (programMemory: ProgramMemory) => {
return rest
})
} else if (!err(sg)) {
processedMemory[key] = sg.value.map(({ __geoMeta, ...rest }: Path) => {
processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if ((val.type as any) === 'Function') {

View File

@ -1,6 +1,12 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable'
import { MouseEventHandler, useCallback, useEffect, useMemo } from 'react'
import {
MouseEventHandler,
useCallback,
useEffect,
useMemo,
useContext,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
import Tooltip from 'components/Tooltip'
@ -13,7 +19,7 @@ import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider'
import { machineManager } from 'lib/machineManager'
import { MachineManagerContext } from 'components/MachineManagerProvider'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -29,6 +35,7 @@ function getPlatformString(): 'web' | 'desktop' {
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext()
const { settings } = useSettingsAuthContext()

View File

@ -1,7 +1,9 @@
import { Popover } from '@headlessui/react'
import { useContext } from 'react'
import Tooltip from './Tooltip'
import { machineManager } from 'lib/machineManager'
import { isDesktop } from 'lib/isDesktop'
import { components } from 'lib/machine-api'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { CustomIcon } from './CustomIcon'
export const NetworkMachineIndicator = ({
@ -9,8 +11,12 @@ export const NetworkMachineIndicator = ({
}: {
className?: string
}) => {
const machineCount = machineManager.machineCount()
const reason = machineManager.noMachinesReason()
const {
noMachinesReason,
machines,
machines: { length: machineCount },
} = useContext(MachineManagerContext)
const reason = noMachinesReason()
return isDesktop() ? (
<Popover className="relative">
@ -46,19 +52,35 @@ export const NetworkMachineIndicator = ({
</div>
{machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{Object.entries(machineManager.machines).map(
([hostname, machine]) => (
<li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">
{machine.make_model.model ||
machine.make_model.manufacturer ||
'Unknown Machine'}
</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Hostname {hostname}
</p>
</li>
)
{machines.map(
(machine: components['schemas']['MachineInfoResponse']) => {
return (
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.id.toUpperCase()}</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{machine.make_model.model}
</p>
{machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter && (
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Nozzle Diameter: {machine.extra.nozzle_diameter}
</p>
)}
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{`Status: ${machine.state.state
.charAt(0)
.toUpperCase()}${machine.state.state.slice(1)}`}
{machine.state.state === 'failed' && machine.state.message
? ` (${machine.state.message})`
: ''}
{machine.state.state === 'running' && machine.progress
? ` (${Math.round(machine.progress)}%)`
: ''}
</p>
</li>
)
}
)}
</ul>
)}

View File

@ -4,14 +4,14 @@ import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { isDesktop } from '../lib/isDesktop'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo } from 'react'
import { Fragment, useMemo, useContext } from 'react'
import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
import { engineCommandManager } from 'lib/singletons'
import { machineManager } from 'lib/machineManager'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
@ -96,6 +96,8 @@ function ProjectMenuPopover({
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const machineManager = useContext(MachineManagerContext)
const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
@ -106,7 +108,7 @@ function ProjectMenuPopover({
(c) => c.name === obj.name && c.groupId === obj.groupId
)
)
const machineCount = machineManager.machineCount()
const machineCount = machineManager.machines.length
// We filter this memoized list so that no orphan "break" elements are rendered.
const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>(

View File

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

View File

@ -255,10 +255,14 @@ export const Stream = () => {
}, [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

View File

@ -11,7 +11,7 @@ export const kclHighlight = styleTags({
nil: t.null,
'AddOp MultOp ExpOp': t.arithmeticOperator,
BangOp: t.logicOperator,
CompOp: t.logicOperator,
CompOp: t.compareOperator,
'Equals Arrow': t.definitionOperator,
PipeOperator: t.controlOperator,
String: t.string,

View File

@ -90,7 +90,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
MultOp { "/" | "*" | "\\" }
ExpOp { "^" }
BangOp { "!" }
CompOp { $[<>] "="? | "!=" | "==" }
CompOp { "==" | "!=" | "<=" | ">=" | "<" | ">" }
Equals { "=" }
Arrow { "=>" }
PipeOperator { "|>" }

View File

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

View File

@ -40,9 +40,7 @@ export class KclManager {
nonCodeMeta: {
nonCodeNodes: {},
start: [],
digest: null,
},
digest: null,
}
private _execState: ExecState = emptyExecState()
private _programMemory: ProgramMemory = ProgramMemory.empty()
@ -208,9 +206,7 @@ export class KclManager {
nonCodeMeta: {
nonCodeNodes: {},
start: [],
digest: null,
},
digest: null,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ const mySketch001 = startSketchOn('XY')
sourceRange: [46, 71],
},
},
value: [
paths: [
{
type: 'ToPoint',
tag: null,
@ -96,7 +96,7 @@ const mySketch001 = startSketchOn('XY')
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
value: [
paths: [
{
type: 'ToPoint',
from: [0, 0],
@ -172,7 +172,6 @@ const sk2 = startSketchOn('XY')
start: 114,
type: 'TagDeclarator',
value: 'p',
digest: null,
},
id: expect.any(String),
sourceRange: [95, 117],
@ -203,7 +202,7 @@ const sk2 = startSketchOn('XY')
info: expect.any(Object),
},
},
value: [
paths: [
{
type: 'ToPoint',
from: [0, 0],
@ -223,7 +222,6 @@ const sk2 = startSketchOn('XY')
start: 114,
type: 'TagDeclarator',
value: 'p',
digest: null,
},
__geoMeta: {
id: expect.any(String),
@ -266,7 +264,6 @@ const sk2 = startSketchOn('XY')
start: 417,
type: 'TagDeclarator',
value: 'o',
digest: null,
},
id: expect.any(String),
sourceRange: [399, 420],
@ -297,7 +294,7 @@ const sk2 = startSketchOn('XY')
info: expect.any(Object),
},
},
value: [
paths: [
{
type: 'ToPoint',
from: [0, 0],
@ -317,7 +314,6 @@ const sk2 = startSketchOn('XY')
start: 417,
type: 'TagDeclarator',
value: 'o',
digest: null,
},
__geoMeta: {
id: expect.any(String),

View File

@ -18,6 +18,9 @@ export default class CodeManager {
#updateState: (arg: string) => void = () => {}
private _currentFilePath: string | null = null
private _hotkeys: { [key: string]: () => void } = {}
private timeoutWriter: ReturnType<typeof setTimeout> | undefined = undefined
public writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
constructor() {
if (isDesktop()) {
@ -115,7 +118,12 @@ export default class CodeManager {
async writeToFile() {
if (isDesktop()) {
setTimeout(() => {
// Only write our buffer contents to file once per second. Any faster
// and file-system watchers which read, will receive empty data during
// writes.
clearTimeout(this.timeoutWriter)
this.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
this.timeoutWriter = setTimeout(() => {
// Wait one event loop to give a chance for params to be set
// Save the file to disk
this._currentFilePath &&
@ -126,7 +134,7 @@ export default class CodeManager {
console.error('error saving file', err)
toast.error('Error saving file, please check file permissions')
})
})
}, 1000)
} else {
safeLSSetItem(PERSIST_CODE_KEY, this.code)
}

View File

@ -58,7 +58,7 @@ 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?.value
const minusGeo = mem.get('mySketch')?.value?.paths
expect(minusGeo).toEqual([
{
type: 'ToPoint',
@ -73,7 +73,6 @@ const newVar = myVar + 1`
start: 89,
type: 'TagDeclarator',
value: 'myPath',
digest: null,
},
},
{
@ -99,7 +98,6 @@ const newVar = myVar + 1`
start: 143,
type: 'TagDeclarator',
value: 'rightPath',
digest: null,
},
},
])
@ -177,7 +175,7 @@ const newVar = myVar + 1`
info: expect.any(Object),
},
},
value: [
paths: [
{
type: 'ToPoint',
to: [1, 1],
@ -201,7 +199,6 @@ const newVar = myVar + 1`
start: 109,
type: 'TagDeclarator',
value: 'myPath',
digest: null,
},
},
{
@ -370,7 +367,7 @@ describe('testing math operators', () => {
const mem = await exe(code)
const sketch = sketchFromKclValue(mem.get('part001'), 'part001')
// result of `-legLen(5, min(3, 999))` should be -4
const yVal = (sketch as Sketch).value?.[0]?.to?.[1]
const yVal = (sketch as Sketch).paths?.[0]?.to?.[1]
expect(yVal).toBe(-4)
})
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
@ -388,8 +385,8 @@ describe('testing math operators', () => {
const mem = await exe(code)
const sketch = sketchFromKclValue(mem.get('part001'), 'part001')
// expect -legLen(segLen('seg01'), myVar) to equal -4 setting the y value back to 0
expect((sketch as Sketch).value?.[1]?.from).toEqual([3, 4])
expect((sketch as Sketch).value?.[1]?.to).toEqual([6, 0])
expect((sketch as Sketch).paths?.[1]?.from).toEqual([3, 4])
expect((sketch as Sketch).paths?.[1]?.to).toEqual([6, 0])
const removedUnaryExp = code.replace(
`-legLen(segLen(seg01), myVar)`,
`legLen(segLen(seg01), myVar)`
@ -401,7 +398,7 @@ describe('testing math operators', () => {
)
// without the minus sign, the y value should be 8
expect((removedUnaryExpMemSketch as Sketch).value?.[1]?.to).toEqual([6, 8])
expect((removedUnaryExpMemSketch as Sketch).paths?.[1]?.to).toEqual([6, 8])
})
it('with nested callExpression and binaryExpression', async () => {
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'

View File

@ -100,15 +100,15 @@ describe('Testing findUniqueName', () => {
it('should find a unique name', () => {
const result = findUniqueName(
JSON.stringify([
{ type: 'Identifier', name: 'yo01', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo01', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0 },
] satisfies Identifier[]),
'yo',
2
@ -123,8 +123,7 @@ describe('Testing addSketchTo', () => {
body: [],
start: 0,
end: 0,
nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null },
digest: null,
nonCodeMeta: { nonCodeNodes: {}, start: [] },
},
'yz'
)

View File

@ -241,7 +241,6 @@ export function mutateObjExpProp(
value: updateWith,
start: 0,
end: 0,
digest: null,
})
}
}
@ -579,7 +578,6 @@ export function createLiteral(value: string | number): Literal {
end: 0,
value,
raw: `${value}`,
digest: null,
}
}
@ -588,7 +586,7 @@ export function createTagDeclarator(value: string): TagDeclarator {
type: 'TagDeclarator',
start: 0,
end: 0,
digest: null,
value,
}
}
@ -598,7 +596,7 @@ export function createIdentifier(name: string): Identifier {
type: 'Identifier',
start: 0,
end: 0,
digest: null,
name,
}
}
@ -608,7 +606,6 @@ export function createPipeSubstitution(): PipeSubstitution {
type: 'PipeSubstitution',
start: 0,
end: 0,
digest: null,
}
}
@ -624,12 +621,11 @@ export function createCallExpressionStdLib(
type: 'Identifier',
start: 0,
end: 0,
digest: null,
name,
},
optional: false,
arguments: args,
digest: null,
}
}
@ -645,12 +641,11 @@ export function createCallExpression(
type: 'Identifier',
start: 0,
end: 0,
digest: null,
name,
},
optional: false,
arguments: args,
digest: null,
}
}
@ -661,7 +656,7 @@ export function createArrayExpression(
type: 'ArrayExpression',
start: 0,
end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(),
elements,
}
@ -674,7 +669,7 @@ export function createPipeExpression(
type: 'PipeExpression',
start: 0,
end: 0,
digest: null,
body,
nonCodeMeta: nonCodeMetaEmpty(),
}
@ -690,13 +685,13 @@ export function createVariableDeclaration(
type: 'VariableDeclaration',
start: 0,
end: 0,
digest: null,
declarations: [
{
type: 'VariableDeclarator',
start: 0,
end: 0,
digest: null,
id: createIdentifier(varName),
init,
},
@ -713,14 +708,14 @@ export function createObjectExpression(properties: {
type: 'ObjectExpression',
start: 0,
end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(),
properties: Object.entries(properties).map(([key, value]) => ({
type: 'ObjectProperty',
start: 0,
end: 0,
key: createIdentifier(key),
digest: null,
value,
})),
}
@ -734,7 +729,7 @@ export function createUnaryExpression(
type: 'UnaryExpression',
start: 0,
end: 0,
digest: null,
operator,
argument,
}
@ -749,7 +744,7 @@ export function createBinaryExpression([left, operator, right]: [
type: 'BinaryExpression',
start: 0,
end: 0,
digest: null,
operator,
left,
right,
@ -1139,5 +1134,5 @@ export async function deleteFromSelection(
}
const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, start: [], digest: null }
return { nonCodeNodes: {}, start: [] }
}

View File

@ -41,7 +41,7 @@ beforeAll(async () => {
},
})
})
}, 20_000)
}, 30_000)
afterAll(() => {
engineCommandManager.tearDown()

View File

@ -717,7 +717,7 @@ export function isLinesParallelAndConstrained(
constraintType === 'angle' || constraintLevel === 'full'
// get the previous segment
const prevSegment = sg.value[secondaryIndex - 1]
const prevSegment = sg.paths[secondaryIndex - 1]
const prevSourceRange = prevSegment.__geoMeta.sourceRange
const isParallelAndConstrained =

View File

@ -28,6 +28,7 @@ import {
} from 'lib/constants'
import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
import { MachineManager } from 'components/MachineManagerProvider'
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000
@ -50,6 +51,11 @@ export enum ExportIntent {
Make = 'make',
}
export interface ExportInfo {
intent: ExportIntent
name: string
}
type ClientMetrics = Models['ClientMetrics_type']
interface WebRTCClientMetrics extends ClientMetrics {
@ -1354,7 +1360,7 @@ export class EngineCommandManager extends EventTarget {
* export in progress. Otherwise it is an enum value of the intent.
* Another export cannot be started if one is already in progress.
*/
private _exportIntent: ExportIntent | null = null
private _exportInfo: ExportInfo | null = null
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
subscriptions: {
@ -1410,12 +1416,15 @@ export class EngineCommandManager extends EventTarget {
(() => {}) as any
kclManager: null | KclManager = null
set exportIntent(intent: ExportIntent | null) {
this._exportIntent = intent
// The current "manufacturing machine" aka 3D printer, CNC, etc.
public machineManager: MachineManager | null = null
set exportInfo(info: ExportInfo | null) {
this._exportInfo = info
}
get exportIntent() {
return this._exportIntent
get exportInfo() {
return this._exportInfo
}
start({
@ -1607,7 +1616,7 @@ export class EngineCommandManager extends EventTarget {
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
if (this.exportIntent === null || this.pendingExport === undefined) {
if (this.exportInfo === null || this.pendingExport === undefined) {
toast.error(
'Export intent was not set, but export data was received'
)
@ -1617,7 +1626,7 @@ export class EngineCommandManager extends EventTarget {
return
}
switch (this.exportIntent) {
switch (this.exportInfo.intent) {
case ExportIntent.Save: {
exportSave(event.data, this.pendingExport.toastId).then(() => {
this.pendingExport?.resolve(null)
@ -1625,21 +1634,28 @@ export class EngineCommandManager extends EventTarget {
break
}
case ExportIntent.Make: {
exportMake(event.data, this.pendingExport.toastId).then(
(result) => {
if (result) {
this.pendingExport?.resolve(null)
} else {
this.pendingExport?.reject('Failed to make export')
}
},
this.pendingExport?.reject
)
if (!this.machineManager) {
console.warn('Some how, no manufacturing machine is selected.')
break
}
exportMake(
event.data,
this.exportInfo.name,
this.pendingExport.toastId,
this.machineManager
).then((result) => {
if (result) {
this.pendingExport?.resolve(null)
} else {
this.pendingExport?.reject('Failed to make export')
}
}, this.pendingExport?.reject)
break
}
}
// Set the export intent back to null.
this.exportIntent = null
this.exportInfo = null
return
}
@ -1953,15 +1969,15 @@ export class EngineCommandManager extends EventTarget {
return Promise.resolve(null)
} else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => {
if (this.exportIntent === null) {
if (this.exportIntent === null) {
if (this.exportInfo === null) {
if (this.exportInfo === null) {
toast.error('Export intent was not set, but export is being sent')
console.error('Export intent was not set, but export is being sent')
return
}
}
const toastId = toast.loading(
this.exportIntent === ExportIntent.Save
this.exportInfo.intent === ExportIntent.Save
? EXPORT_TOAST_MESSAGES.START
: MAKE_TOAST_MESSAGES.START
)
@ -1975,7 +1991,7 @@ export class EngineCommandManager extends EventTarget {
resolve(passThrough)
},
reject: (reason: string) => {
this.exportIntent = null
this.exportInfo = null
reject(reason)
},
commandId: command.cmd_id,

View File

@ -18,7 +18,7 @@ class FileSystemManager {
return Promise.resolve(window.electron.path.join(dir, path))
}
async readFile(path: string): Promise<Uint8Array | void> {
async readFile(path: string): Promise<Uint8Array> {
// Using local file system only works from desktop.
if (!isDesktop()) {
return Promise.reject(

View File

@ -64,7 +64,7 @@ const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"')
export type Coords2d = [number, number]
export function getCoordsFromPaths(skGroup: Sketch, index = 0): Coords2d {
const currentPath = skGroup?.value?.[index]
const currentPath = skGroup?.paths?.[index]
if (!currentPath && skGroup?.start) {
return skGroup.start.to
} else if (!currentPath) {
@ -1704,7 +1704,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
varName
)
if (err(sketch)) return sketch
const intersectPath = sketch.value.find(
const intersectPath = sketch.paths.find(
({ tag }: Path) => tag && tag.value === intersectTagName
)
let offset = 0
@ -1823,11 +1823,10 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
start: 0,
end: 0,
body: [],
digest: null,
nonCodeMeta: {
start: [],
nonCodeNodes: [],
digest: null,
},
},
pathToNode,

View File

@ -18,7 +18,7 @@ export function getSketchSegmentFromPathToNode(
pathToNode: PathToNode
):
| {
segment: Sketch['value'][number]
segment: Sketch['paths'][number]
index: number
}
| Error {
@ -39,15 +39,15 @@ export function getSketchSegmentFromSourceRange(
[rangeStart, rangeEnd]: SourceRange
):
| {
segment: Sketch['value'][number]
segment: Sketch['paths'][number]
index: number
}
| Error {
const lineIndex = sketch.value.findIndex(
const lineIndex = sketch.paths.findIndex(
({ __geoMeta: { sourceRange } }: Path) =>
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
)
const line = sketch.value[lineIndex]
const line = sketch.paths[lineIndex]
if (line) {
return {
segment: line,

View File

@ -1732,7 +1732,7 @@ export function transformAstSketchLines({
if (err(_segment)) return _segment
referencedSegment = _segment.segment
} else {
referencedSegment = sketch.value.find(
referencedSegment = sketch.paths.find(
(path) => path.tag?.value === _referencedSegmentName
)
}

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

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

View File

@ -3,7 +3,6 @@ import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections'
import { machineManager } from 'lib/machineManager'
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type']
@ -187,20 +186,41 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
machine.make_model.model ||
machine.make_model.manufacturer ||
'Unknown Machine',
options: () => {
return Object.entries(machineManager.machines).map(
([_, machine]) => ({
name: `${machine.id} (${
machine.make_model.model || machine.make_model.manufacturer
}) via ${machineManager.machineApiIp || 'the local network'}`,
isCurrent: false,
value: machine as components['schemas']['MachineInfoResponse'],
})
)
},
defaultValue: () => {
options: (commandBarContext) => {
return Object.values(
machineManager.machines
commandBarContext.machineManager?.machines || []
).map((machine: components['schemas']['MachineInfoResponse']) => ({
name:
`${machine.id} (${
machine.make_model.model || machine.make_model.manufacturer
}) (${machine.state.state})` +
(machine.hardware_configuration &&
machine.hardware_configuration.type !== 'none' &&
machine.hardware_configuration.config.nozzle_diameter
? ` - Nozzle Diameter: ${machine.hardware_configuration.config.nozzle_diameter}`
: '') +
(machine.hardware_configuration &&
machine.hardware_configuration.type !== 'none' &&
machine.hardware_configuration.config.filaments &&
machine.hardware_configuration.config.filaments[0]
? ` - ${
machine.hardware_configuration.config.filaments[0].name
} #${
machine.hardware_configuration.config &&
machine.hardware_configuration.config.filaments[0].color?.slice(
0,
6
)
}`
: ''),
isCurrent: false,
disabled: machine.state.state !== 'idle',
value: machine,
}))
},
defaultValue: (commandBarContext) => {
return Object.values(
commandBarContext.machineManager.machines || []
)[0] as components['schemas']['MachineInfoResponse']
},
},

View File

@ -5,6 +5,7 @@ import { Selection } from './selections'
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
import { commandBarMachine } from 'machines/commandBarMachine'
import { ReactNode } from 'react'
import { MachineManager } from 'components/MachineManagerProvider'
type Icon = CustomIconName
const PLATFORMS = ['both', 'web', 'desktop'] as const
@ -127,6 +128,7 @@ export type CommandArgumentConfig<
| ((
commandBarContext: {
argumentsToSubmit: Record<string, unknown>
machineManager?: MachineManager
}, // Should be the commandbarMachine's context, but it creates a circular dependency
machineContext?: C
) => CommandArgumentOption<OutputType>[])
@ -258,5 +260,6 @@ export type CommandArgumentWithName<
export type CommandArgumentOption<A> = {
name: string
isCurrent?: boolean
disabled?: boolean
value: A
}

View File

@ -92,6 +92,7 @@ export const MAKE_TOAST_MESSAGES = {
NO_MACHINE_API_IP: 'No machine api ip available',
NO_CURRENT_MACHINE: 'No current machine available',
NO_MACHINE_ID: 'No machine id available',
NO_NAME: 'No name provided',
ERROR_STARTING_PRINT: 'Error while starting print',
SUCCESS: 'Started print successfully',
}

View File

@ -448,7 +448,9 @@ export const readProjectSettingsFile = async (
}
}
const configToml = await window.electron.readFile(settingsPath)
const configToml = await window.electron.readFile(settingsPath, {
encoding: 'utf-8',
})
const configObj = parseProjectSettings(configToml)
if (err(configObj)) {
return Promise.reject(configObj)
@ -467,7 +469,9 @@ export const readAppSettingsFile = async () => {
// The file exists, read it and parse it.
if (window.electron.exists(settingsPath)) {
const configToml = await window.electron.readFile(settingsPath)
const configToml = await window.electron.readFile(settingsPath, {
encoding: 'utf-8',
})
const parsedAppConfig = parseAppSettings(configToml)
if (err(parsedAppConfig)) {
return Promise.reject(parsedAppConfig)
@ -527,7 +531,9 @@ export const readTokenFile = async () => {
let settingsPath = await getTokenFilePath()
if (window.electron.exists(settingsPath)) {
const token: string = await window.electron.readFile(settingsPath)
const token: string = await window.electron.readFile(settingsPath, {
encoding: 'utf-8',
})
if (!token) return ''
return token

View File

@ -1,5 +1,5 @@
import { deserialize_files } from 'wasm-lib/pkg/wasm_lib'
import { machineManager } from './machineManager'
import { MachineManager } from 'components/MachineManagerProvider'
import toast from 'react-hot-toast'
import { components } from './machine-api'
import ModelingAppFile from './modelingAppFile'
@ -8,9 +8,17 @@ import { MAKE_TOAST_MESSAGES } from './constants'
// Make files locally from an export call.
export async function exportMake(
data: ArrayBuffer,
toastId: string
name: string,
toastId: string,
machineManager: MachineManager
): Promise<Response | null> {
if (machineManager.machineCount() === 0) {
if (name === '') {
console.error(MAKE_TOAST_MESSAGES.NO_NAME)
toast.error(MAKE_TOAST_MESSAGES.NO_NAME, { id: toastId })
return null
}
if (machineManager.machines.length === 0) {
console.error(MAKE_TOAST_MESSAGES.NO_MACHINES)
toast.error(MAKE_TOAST_MESSAGES.NO_MACHINES, { id: toastId })
return null
@ -39,7 +47,7 @@ export async function exportMake(
const params: components['schemas']['PrintParameters'] = {
machine_id: machineId,
job_name: 'Exported Job', // TODO: make this the project name.
job_name: name,
}
try {
console.log('params', params)

View File

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

View File

@ -1,101 +0,0 @@
import { isDesktop } from './isDesktop'
import { components } from './machine-api'
import { reportRejection } from './trap'
import { toSync } from './utils'
export type MachinesListing = Array<
components['schemas']['MachineInfoResponse']
>
export class MachineManager {
private _isDesktop: boolean = isDesktop()
private _machines: MachinesListing = []
private _machineApiIp: string | null = null
private _currentMachine: components['schemas']['MachineInfoResponse'] | null =
null
constructor() {
if (!this._isDesktop) {
return
}
this.updateMachines().catch(reportRejection)
}
start() {
if (!this._isDesktop) {
return
}
// Start a background job to update the machines every ten seconds.
// If MDNS is already watching, this timeout will wait until it's done to trigger the
// finding again.
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
const timeoutLoop = () => {
clearTimeout(timeoutId)
timeoutId = setTimeout(
toSync(async () => {
await this.updateMachineApiIp()
await this.updateMachines()
timeoutLoop()
}, reportRejection),
10000
)
}
timeoutLoop()
}
get machines(): MachinesListing {
return this._machines
}
machineCount(): number {
return this._machines.length
}
get machineApiIp(): string | null {
return this._machineApiIp
}
// Get the reason message for why there are no machines.
noMachinesReason(): string | undefined {
if (this.machineCount() > 0) {
return undefined
}
if (this.machineApiIp === null) {
return 'Machine API server was not discovered'
}
return 'Machine API server was discovered, but no machines are available'
}
get currentMachine(): components['schemas']['MachineInfoResponse'] | null {
return this._currentMachine
}
set currentMachine(
machine: components['schemas']['MachineInfoResponse'] | null
) {
this._currentMachine = machine
}
private async updateMachines(): Promise<void> {
if (!this._isDesktop) {
return
}
this._machines = await window.electron.listMachines()
}
private async updateMachineApiIp(): Promise<void> {
if (!this._isDesktop) {
return
}
this._machineApiIp = await window.electron.getMachineApiIp()
}
}
export const machineManager = new MachineManager()
machineManager.start()

View File

@ -14,6 +14,7 @@ import { codeManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import { getProjectInfo } from './desktop'
import { createSettings } from './settings/initialSettings'
import { normalizeLineEndings } from 'lib/codeEditor'
// The root loader simply resolves the settings and any errors that
// occurred during the settings load
@ -108,7 +109,9 @@ export const fileLoader: LoaderFunction = async (
)
}
code = await window.electron.readFile(currentFilePath)
code = await window.electron.readFile(currentFilePath, {
encoding: 'utf-8',
})
code = normalizeLineEndings(code)
// Update both the state and the editor's code.
@ -182,7 +185,3 @@ export const homeLoader: LoaderFunction = async (): Promise<
}
return {}
}
const normalizeLineEndings = (str: string, normalized = '\n') => {
return str.replace(/\r?\n/g, normalized)
}

View File

@ -7,6 +7,7 @@ import {
} from 'lib/commandTypes'
import { Selections } from 'lib/selections'
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
import { MachineManager } from 'components/MachineManagerProvider'
export type CommandBarContext = {
commands: Command[]
@ -14,6 +15,7 @@ export type CommandBarContext = {
currentArgument?: CommandArgument<unknown> & { name: string }
selectionRanges: Selections
argumentsToSubmit: { [x: string]: unknown }
machineManager: MachineManager
}
export type CommandBarMachineEvent =
@ -71,6 +73,7 @@ export type CommandBarMachineEvent =
type: 'Change current argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
}
| { type: 'Set machine manager'; data: MachineManager }
export const commandBarMachine = setup({
types: {
@ -90,6 +93,12 @@ export const commandBarMachine = setup({
}
},
}),
'Set machine manager': assign({
machineManager: ({ event, context }) => {
if (event.type !== 'Set machine manager') return context.machineManager
return event.data
},
}),
'Execute command': ({ context, event }) => {
const { selectedCommand } = context
if (!selectedCommand) return
@ -339,6 +348,13 @@ export const commandBarMachine = setup({
codeBasedSelections: [],
},
argumentsToSubmit: {},
machineManager: {
machines: [],
machineApiIp: null,
currentMachine: null,
setCurrentMachine: () => {},
noMachinesReason: () => undefined,
},
},
id: 'Command Bar',
initial: 'Closed',
@ -520,6 +536,11 @@ export const commandBarMachine = setup({
},
},
on: {
'Set machine manager': {
reenter: false,
actions: 'Set machine manager',
},
Close: {
target: '.Closed',
},

View File

@ -64,6 +64,7 @@ import toast from 'react-hot-toast'
import { ToolbarModeName } from 'lib/toolbar'
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
import { Vector3 } from 'three'
import { MachineManager } from 'components/MachineManagerProvider'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@ -301,6 +302,7 @@ export const getPersistedContext = (): Partial<PersistedModelingContext> => {
export interface ModelingMachineContext {
currentMode: ToolbarModeName
currentTool: SketchTool
machineManager: MachineManager
selection: string[]
selectionRanges: Selections
sketchDetails: SketchDetails | null
@ -315,6 +317,13 @@ export interface ModelingMachineContext {
export const modelingMachineDefaultContext: ModelingMachineContext = {
currentMode: 'modeling',
currentTool: 'none',
machineManager: {
machines: [],
machineApiIp: null,
currentMachine: null,
setCurrentMachine: () => {},
noMachinesReason: () => undefined,
},
selection: [],
selectionRanges: {
otherSelections: [],

View File

@ -37,8 +37,6 @@ if (!process.env.NODE_ENV)
// dotenv override when present
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
console.log(process.env)
process.env.VITE_KC_API_WS_MODELING_URL ??=
'wss://api.zoo.dev/ws/modeling/commands'
process.env.VITE_KC_API_BASE_URL ??= 'https://api.zoo.dev'
@ -238,6 +236,7 @@ ipcMain.handle('find_machine_api', () => {
const ip = service.addresses[0]
const port = service.port
// We want to return the ip address of the machine API.
console.log(`Machine API found at ${ip}:${port}`)
resolve(`${ip}:${port}`)
}
)

View File

@ -4,7 +4,7 @@ import fs from 'node:fs/promises'
import os from 'node:os'
import fsSync from 'node:fs'
import packageJson from '../package.json'
import { MachinesListing } from 'lib/machineManager'
import { MachinesListing } from 'components/MachineManagerProvider'
import chokidar from 'chokidar'
const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)
@ -30,22 +30,51 @@ const isMac = os.platform() === 'darwin'
const isWindows = os.platform() === 'win32'
const isLinux = os.platform() === 'linux'
let fsWatchListeners = new Map<string, ReturnType<typeof chokidar.watch>>()
let fsWatchListeners = new Map<
string,
Map<
string,
{
watcher: ReturnType<typeof chokidar.watch>
callback: (eventType: string, path: string) => void
}
>
>()
const watchFileOn = (path: string, callback: (path: string) => void) => {
const watcherMaybe = fsWatchListeners.get(path)
if (watcherMaybe) return
const watcher = chokidar.watch(path)
const watchFileOn = (
path: string,
key: string,
callback: (eventType: string, path: string) => void
) => {
let watchers = fsWatchListeners.get(path)
if (!watchers) {
watchers = new Map()
}
const watcher = chokidar.watch(path, { depth: 1 })
watcher.on('all', callback)
fsWatchListeners.set(path, watcher)
watchers.set(key, { watcher, callback })
fsWatchListeners.set(path, watchers)
}
const watchFileOff = (path: string) => {
const watcher = fsWatchListeners.get(path)
if (!watcher) return
watcher.unwatch(path)
fsWatchListeners.delete(path)
const watchFileOff = (path: string, key: string) => {
const watchers = fsWatchListeners.get(path)
if (!watchers) return
const data = watchers.get(key)
if (!data) {
console.warn(
"Trying to remove a watcher, callback that doesn't exist anymore. Suspicious."
)
return
}
const { watcher, callback } = data
watcher.off('all', callback)
watchers.delete(key)
if (watchers.size === 0) {
fsWatchListeners.delete(path)
} else {
fsWatchListeners.set(path, watchers)
}
}
const readFile = (path: string) => fs.readFile(path, 'utf-8')
const readFile = fs.readFile
// It seems like from the node source code this does not actually block but also
// don't trust me on that (jess).
const exists = (path: string) => fsSync.existsSync(path)
@ -77,11 +106,12 @@ const kittycad = (access: string, args: any) =>
// We could probably do this from the renderer side, but I fear CORS will
// bite our butts.
const listMachines = async (): Promise<MachinesListing> => {
const machineApi = await ipcRenderer.invoke('find_machine_api')
if (!machineApi) return []
return fetch(`http://${machineApi}/machines`).then((resp) => resp.json())
const listMachines = async (
machineApiAddr: string
): Promise<MachinesListing> => {
return fetch(`http://${machineApiAddr}/machines`).then((resp) => {
return resp.json()
})
}
const getMachineApiIp = async (): Promise<String | null> =>

View File

@ -23,6 +23,9 @@ import { codeManager, editorManager, kclManager } from 'lib/singletons'
import { bracket } from 'lib/exampleKcl'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { EngineConnectionStateType } from 'lang/std/engineConnection'
export const kbdClasses =
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
@ -80,8 +83,20 @@ export const onboardingRoutes = [
]
export function useDemoCode() {
const { overallState, immediateState } = useNetworkContext()
useEffect(() => {
if (!editorManager.editorView || codeManager.code === bracket) return
// Don't run if the editor isn't loaded or the code is already the bracket
if (!editorManager.editorView || codeManager.code === bracket) {
return
}
// Don't run if the network isn't healthy or the connection isn't established
if (
overallState !== NetworkHealthState.Ok ||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
) {
return
}
setTimeout(
toSync(async () => {
codeManager.updateCodeStateEditor(bracket)
@ -89,7 +104,7 @@ export function useDemoCode() {
await codeManager.writeToFile()
}, reportRejection)
)
}, [editorManager.editorView])
}, [editorManager.editorView, immediateState, overallState])
}
export function useNextClick(newStatus: string) {

128
src/wasm-lib/Cargo.lock generated
View File

@ -121,9 +121,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.89"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
dependencies = [
"backtrace",
]
@ -176,7 +176,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -187,7 +187,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -204,7 +204,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -465,7 +465,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -656,7 +656,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -667,7 +667,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -722,7 +722,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"synstructure",
]
@ -751,7 +751,7 @@ dependencies = [
"rustfmt-wrapper",
"serde",
"serde_tokenstream",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -762,7 +762,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -789,7 +789,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -827,7 +827,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -988,7 +988,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1084,7 +1084,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1542,7 +1542,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.22"
version = "0.2.23"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1612,12 +1612,12 @@ dependencies = [
"pretty_assertions",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
name = "kcl-test-server"
version = "0.1.14"
version = "0.1.15"
dependencies = [
"anyhow",
"hyper 0.14.30",
@ -1644,9 +1644,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.3.23"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71b6f0c34165939697548dd0c94221200dbb8b5d1c84b5d8e803e70f9f720ea7"
checksum = "f6359cc0a1bbccbcf78775eea17a033cf2aa89d3fe6a9784f8ce94e5f882c185"
dependencies = [
"anyhow",
"async-trait",
@ -1684,9 +1684,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.68"
version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3aedfcc1d8ea9995ec3eb78a6743c585c9380475c48701797f107489b696aa"
checksum = "c6d2160dcb0e5373b1242a760dbf17fb9c12de523c410c11b145381c852b377b"
dependencies = [
"anyhow",
"chrono",
@ -1716,7 +1716,7 @@ dependencies = [
"kittycad-modeling-cmds-macros-impl",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1727,7 +1727,7 @@ checksum = "6607507a8a0e4273b943179f0a3ef8e90712308d1d3095246040c29cfdbf985b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2112,7 +2112,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.5",
"structmeta",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2126,7 +2126,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.5",
"structmeta",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2166,7 +2166,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2224,7 +2224,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2337,9 +2337,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.88"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
@ -2391,7 +2391,7 @@ dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2404,7 +2404,7 @@ dependencies = [
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2586,9 +2586,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@ -2925,7 +2925,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2965,9 +2965,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
dependencies = [
"serde_derive",
]
@ -2983,13 +2983,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3000,14 +3000,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"indexmap 2.6.0",
"itoa",
@ -3024,7 +3024,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3045,7 +3045,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3182,7 +3182,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3193,7 +3193,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3215,7 +3215,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3237,9 +3237,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.79"
version = "2.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
dependencies = [
"proc-macro2",
"quote",
@ -3263,7 +3263,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3326,22 +3326,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3437,7 +3437,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3579,7 +3579,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3607,7 +3607,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3689,7 +3689,7 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"termcolor",
]
@ -3865,7 +3865,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3927,7 +3927,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"wasm-bindgen-shared",
]
@ -3962,7 +3962,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -4328,7 +4328,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]

View File

@ -71,8 +71,8 @@ members = [
[workspace.dependencies]
http = "1"
kittycad = { version = "0.3.23", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.68", features = ["websocket"] }
kittycad = { version = "0.3.25", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.71", features = ["websocket"] }
[[test]]
name = "executor"
@ -83,6 +83,6 @@ name = "modify"
path = "tests/modify/main.rs"
# Example: how to point modeling-api at a different repo (e.g. a branch or a local clone)
#[patch."https://github.com/KittyCAD/modeling-api"]
#[patch.crates-io]
#kittycad-modeling-cmds = { path = "../../../modeling-api/modeling-cmds" }
#kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }

View File

@ -17,13 +17,13 @@ convert_case = "0.6.0"
once_cell = "1.20.2"
proc-macro2 = "1"
quote = "1"
regex = "1.10"
serde = { version = "1.0.210", features = ["derive"] }
regex = "1.11"
serde = { version = "1.0.213", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.79", features = ["full"] }
syn = { version = "2.0.85", features = ["full"] }
[dev-dependencies]
anyhow = "1.0.89"
anyhow = "1.0.91"
expectorate = "1.1.0"
pretty_assertions = "1.4.1"
rustfmt-wrapper = "0.2.1"

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2.0.79", features = ["full"] }
syn = { version = "2.0.85", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1.4.1"

View File

@ -12,7 +12,7 @@ fn basic() {
let expected = Program {
start: 0,
end: 11,
body: vec![BodyItem::VariableDeclaration(VariableDeclaration {
body: vec![BodyItem::VariableDeclaration(Box::new(VariableDeclaration {
start: 0,
end: 11,
declarations: vec![VariableDeclarator {
@ -36,7 +36,7 @@ fn basic() {
visibility: ItemVisibility::Default,
kind: VariableKind::Const,
digest: None,
})],
}))],
non_code_meta: NonCodeMeta::default(),
digest: None,
};

View File

@ -1,15 +1,15 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.14"
version = "0.1.15"
edition = "2021"
license = "MIT"
[dependencies]
anyhow = "1.0.89"
anyhow = "1.0.91"
hyper = { version = "0.14.29", features = ["http1", "server", "tcp"] }
kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.210", features = ["derive"] }
serde = { version = "1.0.213", features = ["derive"] }
serde_json = "1.0.128"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }

View File

@ -31,7 +31,7 @@ pub struct ServerArgs {
/// Where to find the engine.
/// If none, uses the prod engine.
/// This is useful for testing a local engine instance.
/// Overridden by the $LOCAL_ENGINE_ADDR environment variable.
/// Overridden by the $ZOO_HOST environment variable.
pub engine_address: Option<String>,
}
@ -44,8 +44,8 @@ impl ServerArgs {
num_engine_conns: pargs.opt_value_from_str("--num-engine-conns")?.unwrap_or(1),
engine_address: pargs.opt_value_from_str("--engine-address")?,
};
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
println!("Overriding engine address via $LOCAL_ENGINE_ADDR");
if let Ok(addr) = std::env::var("ZOO_HOST") {
println!("Overriding engine address via $ZOO_HOST");
args.engine_address = Some(addr);
}
println!("Config is {args:?}");

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.22"
version = "0.2.23"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -11,7 +11,7 @@ keywords = ["kcl", "KittyCAD", "CAD"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { version = "1.0.89", features = ["backtrace"] }
anyhow = { version = "1.0.91", features = ["backtrace"] }
async-recursion = "1.1.1"
async-trait = "0.1.83"
base64 = "0.22.1"
@ -38,11 +38,11 @@ pyo3 = { version = "0.22.5", optional = true }
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1", "preserve_order"] }
serde = { version = "1.0.210", features = ["derive"] }
serde = { version = "1.0.213", features = ["derive"] }
serde_json = "1.0.128"
sha2 = "0.10.8"
tabled = { version = "0.15.0", optional = true }
thiserror = "1.0.64"
thiserror = "1.0.65"
toml = "0.8.19"
ts-rs = { version = "10.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] }

View File

@ -53,8 +53,11 @@ pub struct Program {
pub start: usize,
pub end: usize,
pub body: Vec<BodyItem>,
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -451,7 +454,7 @@ pub(crate) use impl_value_meta;
pub enum BodyItem {
ImportStatement(Box<ImportStatement>),
ExpressionStatement(ExpressionStatement),
VariableDeclaration(VariableDeclaration),
VariableDeclaration(Box<VariableDeclaration>),
ReturnStatement(ReturnStatement),
}
@ -837,6 +840,8 @@ pub struct NonCodeNode {
pub end: usize,
pub value: NonCodeValue,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -981,6 +986,8 @@ pub struct NonCodeMeta {
pub non_code_nodes: HashMap<usize, Vec<NonCodeNode>>,
pub start: Vec<NonCodeNode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1053,6 +1060,8 @@ pub struct ImportItem {
pub start: usize,
pub end: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1105,6 +1114,8 @@ pub struct ImportStatement {
pub path: String,
pub raw_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1146,6 +1157,8 @@ pub struct ExpressionStatement {
pub end: usize,
pub expression: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1162,6 +1175,8 @@ pub struct CallExpression {
pub arguments: Vec<Expr>,
pub optional: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1321,6 +1336,8 @@ pub struct VariableDeclaration {
pub visibility: ItemVisibility,
pub kind: VariableKind, // Change to enum if there are specific values
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1575,6 +1592,8 @@ pub struct VariableDeclarator {
/// The value of the variable.
pub init: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1606,6 +1625,8 @@ pub struct Literal {
pub value: LiteralValue,
pub raw: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1662,6 +1683,8 @@ pub struct Identifier {
pub end: usize,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1703,6 +1726,8 @@ pub struct TagDeclarator {
#[serde(rename = "value")]
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1821,6 +1846,8 @@ pub struct PipeSubstitution {
pub start: usize,
pub end: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1859,6 +1886,8 @@ pub struct ArrayExpression {
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1934,6 +1963,8 @@ pub struct ArrayRangeExpression {
/// Is the `end_element` included in the range?
pub end_inclusive: bool,
// TODO (maybe) comments on range components?
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2000,6 +2031,8 @@ pub struct ObjectExpression {
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2067,6 +2100,8 @@ pub struct ObjectProperty {
pub key: Identifier,
pub value: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2197,6 +2232,8 @@ pub struct MemberExpression {
pub property: LiteralIdentifier,
pub computed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2256,6 +2293,8 @@ pub struct BinaryExpression {
pub left: BinaryPart,
pub right: BinaryPart,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2438,6 +2477,8 @@ pub struct UnaryExpression {
pub operator: UnaryOperator,
pub argument: BinaryPart,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2513,8 +2554,11 @@ pub struct PipeExpression {
// TODO: Only the first body expression can be any Value.
// The rest will be CallExpression, and the AST type should reflect this.
pub body: Vec<Expr>,
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2650,6 +2694,8 @@ pub struct Parameter {
/// Is the parameter optional?
pub optional: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2665,13 +2711,15 @@ pub struct FunctionExpression {
#[serde(skip)]
pub return_type: Option<FnArgType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
impl_value_meta!(FunctionExpression);
#[derive(Debug, PartialEq, Clone)]
pub struct RequiredParamAfterOptionalParam(pub Parameter);
pub struct RequiredParamAfterOptionalParam(pub Box<Parameter>);
impl std::fmt::Display for RequiredParamAfterOptionalParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -2703,7 +2751,7 @@ impl FunctionExpression {
if param.optional {
found_optional = true;
} else if found_optional {
return Err(RequiredParamAfterOptionalParam(param.clone()));
return Err(RequiredParamAfterOptionalParam(Box::new(param.clone())));
}
}
let boundary = self.params.partition_point(|param| !param.optional);
@ -2751,6 +2799,8 @@ pub struct ReturnStatement {
pub end: usize,
pub argument: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}

View File

@ -65,93 +65,9 @@ impl MemberExpression {
}))
}
}
pub fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
#[derive(Debug)]
enum Property {
Number(usize),
String(String),
}
impl Property {
fn type_name(&self) -> &'static str {
match self {
Property::Number(_) => "number",
Property::String(_) => "string",
}
}
}
let property_src: SourceRange = self.property.clone().into();
let property_sr = vec![property_src];
let property: Property = match self.property.clone() {
LiteralIdentifier::Identifier(identifier) => {
let name = identifier.name;
if !self.computed {
// Treat the property as a literal
Property::String(name.to_string())
} else {
// Actually evaluate memory to compute the property.
let prop = exec_state.memory.get(&name, property_src)?;
let KclValue::UserVal(prop) = prop else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
),
}));
};
match prop.value {
JValue::Number(ref num) => {
num
.as_u64()
.and_then(|x| usize::try_from(x).ok())
.map(Property::Number)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name}'s value is not a valid property/index, you can only use a string or int (>= 0) here",
),
})
})?
}
JValue::String(ref x) => Property::String(x.to_owned()),
_ => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array",
),
}));
}
}
}
}
LiteralIdentifier::Literal(literal) => {
let value = literal.value.clone();
match value {
LiteralValue::IInteger(x) => {
if let Ok(x) = u64::try_from(x) {
Property::Number(x.try_into().unwrap())
} else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
}));
}
}
LiteralValue::String(s) => Property::String(s),
_ => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: "Only strings or ints (>= 0) can be properties/indexes".to_owned(),
}));
}
}
}
};
let property = Property::try_from(self.computed, self.property.clone(), exec_state, self.into())?;
let object = match &self.object {
// TODO: Don't use recursion here, use a loop.
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
@ -635,13 +551,13 @@ impl ArrayRangeExpression {
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let start = parse_json_number_as_u64(&start, (&*self.start_element).into())?;
let start = parse_json_number_as_i64(&start, (&*self.start_element).into())?;
let metadata = Metadata::from(&*self.end_element);
let end = ctx
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let end = parse_json_number_as_u64(&end, (&*self.end_element).into())?;
let end = parse_json_number_as_i64(&end, (&*self.end_element).into())?;
if end < start {
return Err(KclError::Semantic(KclErrorDetails {
@ -687,9 +603,9 @@ impl ObjectExpression {
}
}
pub fn parse_json_number_as_u64(j: &serde_json::Value, source_range: SourceRange) -> Result<u64, KclError> {
fn parse_json_number_as_i64(j: &serde_json::Value, source_range: SourceRange) -> Result<i64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_u64().ok_or_else(|| {
n.as_i64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
@ -783,3 +699,105 @@ impl IfExpression {
.map(|expr| expr.unwrap())
}
}
#[derive(Debug)]
enum Property {
Number(usize),
String(String),
}
impl Property {
fn try_from(
computed: bool,
value: LiteralIdentifier,
exec_state: &ExecState,
sr: SourceRange,
) -> Result<Self, KclError> {
let property_sr = vec![sr];
let property_src: SourceRange = value.clone().into();
match value {
LiteralIdentifier::Identifier(identifier) => {
let name = identifier.name;
if !computed {
// Treat the property as a literal
Ok(Property::String(name.to_string()))
} else {
// Actually evaluate memory to compute the property.
let prop = exec_state.memory.get(&name, property_src)?;
let KclValue::UserVal(prop) = prop else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
),
}));
};
jvalue_to_prop(&prop.value, property_sr, &name)
}
}
LiteralIdentifier::Literal(literal) => {
let value = literal.value.clone();
match value {
LiteralValue::IInteger(x) => {
if let Ok(x) = u64::try_from(x) {
Ok(Property::Number(x.try_into().unwrap()))
} else {
Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
}))
}
}
LiteralValue::String(s) => Ok(Property::String(s)),
_ => Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![sr],
message: "Only strings or ints (>= 0) can be properties/indexes".to_owned(),
})),
}
}
}
}
}
fn jvalue_to_prop(value: &JValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
let make_err = |message: String| {
Err::<Property, _>(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message,
}))
};
const MUST_BE_POSINT: &str = "indices must be whole positive numbers";
const TRY_INT: &str = "try using the int() function to make this a whole number";
match value {
JValue::Number(ref num) => {
let maybe_uint = num.as_u64().and_then(|x| usize::try_from(x).ok());
if let Some(uint) = maybe_uint {
Ok(Property::Number(uint))
} else if let Some(iint) = num.as_i64() {
make_err(format!("'{iint}' is not a valid index, {MUST_BE_POSINT}"))
} else if let Some(fnum) = num.as_f64() {
if fnum < 0.0 {
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}"))
} else if fnum.fract() == 0.0 {
make_err(format!("'{fnum:.1}' is stored as a fractional number but indices must be whole numbers, {TRY_INT}"))
} else {
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}, {TRY_INT}"))
}
} else {
make_err(format!("'{num}' is not a valid index, {MUST_BE_POSINT}"))
}
}
JValue::String(ref x) => Ok(Property::String(x.to_owned())),
_ => {
make_err(format!("{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array"))
}
}
}
impl Property {
fn type_name(&self) -> &'static str {
match self {
Property::Number(_) => "number",
Property::String(_) => "string",
}
}
}

View File

@ -1127,7 +1127,7 @@ pub struct TagEngineInfo {
/// The sketch the tag is on.
pub sketch: uuid::Uuid,
/// The path the tag is on.
pub path: Option<BasePath>,
pub path: Option<Path>,
/// The surface information for the tag.
pub surface: Option<ExtrudeSurface>,
}
@ -1137,10 +1137,10 @@ pub struct TagEngineInfo {
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct Sketch {
/// The id of the sketch (this will change when the engine's reference to it changes.
/// The id of the sketch (this will change when the engine's reference to it changes).
pub id: uuid::Uuid,
/// The paths in the sketch.
pub value: Vec<Path>,
pub paths: Vec<Path>,
/// What the sketch is on (can be a plane or a face).
pub on: SketchSurface,
/// The starting path.
@ -1206,7 +1206,7 @@ impl Sketch {
tag_identifier.info = Some(TagEngineInfo {
id: base.geo_meta.id,
sketch: self.id,
path: Some(base.clone()),
path: Some(current_path.clone()),
surface: None,
});
@ -1215,7 +1215,7 @@ impl Sketch {
/// Get the path most recently sketched.
pub(crate) fn latest_path(&self) -> Option<&Path> {
self.value.last()
self.paths.last()
}
/// The "pen" is an imaginary pen drawing the path.
@ -1656,6 +1656,43 @@ pub enum Path {
#[serde(flatten)]
base: BasePath,
},
/// A circular arc, not necessarily tangential to the current point.
Arc {
#[serde(flatten)]
base: BasePath,
/// Center of the circle that this arc is drawn on.
center: [f64; 2],
/// Radius of the circle that this arc is drawn on.
radius: f64,
},
}
/// What kind of path is this?
#[derive(Display)]
enum PathType {
ToPoint,
Base,
TangentialArc,
TangentialArcTo,
Circle,
Horizontal,
AngledLineTo,
Arc,
}
impl From<&Path> for PathType {
fn from(value: &Path) -> Self {
match value {
Path::ToPoint { .. } => Self::ToPoint,
Path::TangentialArcTo { .. } => Self::TangentialArcTo,
Path::TangentialArc { .. } => Self::TangentialArc,
Path::Circle { .. } => Self::Circle,
Path::Horizontal { .. } => Self::Horizontal,
Path::AngledLineTo { .. } => Self::AngledLineTo,
Path::Base { .. } => Self::Base,
Path::Arc { .. } => Self::Arc,
}
}
}
impl Path {
@ -1668,6 +1705,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.geo_meta.id,
Path::TangentialArc { base, .. } => base.geo_meta.id,
Path::Circle { base, .. } => base.geo_meta.id,
Path::Arc { base, .. } => base.geo_meta.id,
}
}
@ -1680,6 +1718,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.tag.clone(),
Path::TangentialArc { base, .. } => base.tag.clone(),
Path::Circle { base, .. } => base.tag.clone(),
Path::Arc { base, .. } => base.tag.clone(),
}
}
@ -1692,6 +1731,47 @@ impl Path {
Path::TangentialArcTo { base, .. } => base,
Path::TangentialArc { base, .. } => base,
Path::Circle { base, .. } => base,
Path::Arc { base, .. } => base,
}
}
/// Where does this path segment start?
pub fn get_from(&self) -> &[f64; 2] {
&self.get_base().from
}
/// Where does this path segment end?
pub fn get_to(&self) -> &[f64; 2] {
&self.get_base().to
}
/// Length of this path segment, in cartesian plane.
pub fn length(&self) -> f64 {
match self {
Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
linear_distance(self.get_from(), self.get_to())
}
Self::TangentialArc {
base: _,
center,
ccw: _,
}
| Self::TangentialArcTo {
base: _,
center,
ccw: _,
} => {
// The radius can be calculated as the linear distance between `to` and `center`,
// or between `from` and `center`. They should be the same.
let radius = linear_distance(self.get_from(), center);
debug_assert_eq!(radius, linear_distance(self.get_to(), center));
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
}
Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
Self::Arc { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
}
}
}
@ -1704,10 +1784,22 @@ impl Path {
Path::TangentialArcTo { base, .. } => Some(base),
Path::TangentialArc { base, .. } => Some(base),
Path::Circle { base, .. } => Some(base),
Path::Arc { base, .. } => Some(base),
}
}
}
/// Compute the straight-line distance between a pair of (2D) points.
#[rustfmt::skip]
fn linear_distance(
[x0, y0]: &[f64; 2],
[x1, y1]: &[f64; 2]
) -> f64 {
let y_sq = (y1 - y0).powi(2);
let x_sq = (x1 - x0).powi(2);
(y_sq + x_sq).sqrt()
}
/// An extrude surface.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -1888,9 +1980,73 @@ impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
}
}
/// Create a new zoo api client.
#[cfg(not(target_arch = "wasm32"))]
pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> Result<kittycad::Client> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.connection_verbose(true)
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let zoo_token_env = std::env::var("ZOO_API_TOKEN");
let token = if let Some(token) = token {
token
} else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
if let Ok(zoo_token) = zoo_token_env {
if zoo_token != token {
return Err(anyhow::anyhow!(
"Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
token,
zoo_token
));
}
}
token
} else if let Ok(token) = zoo_token_env {
token
} else {
return Err(anyhow::anyhow!(
"No API token found in environment variables. Use KITTYCAD_API_TOKEN or ZOO_API_TOKEN"
));
};
// Create the client.
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
// Set an engine address if it's set.
let kittycad_host_env = std::env::var("KITTYCAD_HOST");
if let Some(addr) = engine_addr {
client.set_base_url(addr);
} else if let Ok(addr) = std::env::var("ZOO_HOST") {
if let Ok(kittycad_host) = kittycad_host_env {
if kittycad_host != addr {
return Err(anyhow::anyhow!(
"Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
kittycad_host,
addr
));
}
}
client.set_base_url(addr);
} else if let Ok(addr) = kittycad_host_env {
client.set_base_url(addr);
}
Ok(client)
}
impl ExecutorContext {
/// Create a new default executor context.
/// Also returns the response HTTP headers from the server.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
let (ws, _headers) = client
@ -1935,6 +2091,35 @@ impl ExecutorContext {
})
}
/// Create a new default executor context.
/// With a kittycad client.
/// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
/// variables.
/// But also allows for passing in a token and engine address directly.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_with_client(
settings: ExecutorSettings,
token: Option<String>,
engine_addr: Option<String>,
) -> Result<Self> {
// Create the client.
let client = new_zoo_client(token, engine_addr)?;
let ctx = Self::new(&client, settings).await?;
Ok(ctx)
}
/// Create a new default executor context.
/// With the default kittycad client.
/// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
/// variables.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_with_default_client(settings: ExecutorSettings) -> Result<Self> {
// Create the client.
let ctx = Self::new_with_client(settings, None, None).await?;
Ok(ctx)
}
pub fn is_mock(&self) -> bool {
self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
}
@ -1942,35 +2127,7 @@ impl ExecutorContext {
/// For executing unit tests.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_for_unit_test(units: UnitLength, engine_addr: Option<String>) -> Result<Self> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"));
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.connection_verbose(true)
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
// Create the client.
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
// Set a local engine address if it's set.
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
client.set_base_url(addr);
}
if let Some(addr) = engine_addr {
client.set_base_url(addr);
}
let ctx = ExecutorContext::new(
&client,
let ctx = ExecutorContext::new_with_client(
ExecutorSettings {
units,
highlight_edges: true,
@ -1978,6 +2135,8 @@ impl ExecutorContext {
show_grid: false,
replay: None,
},
None,
engine_addr,
)
.await?;
Ok(ctx)

View File

@ -3,41 +3,13 @@ use std::sync::{Arc, RwLock};
use anyhow::Result;
use tower_lsp::LanguageServer;
fn new_zoo_client() -> kittycad::Client {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.connection_verbose(true)
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
// Create the client.
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
// Set a local engine address if it's set.
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
client.set_base_url(addr);
}
client
}
// Create a fake kcl lsp server for testing.
pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
let stdlib = crate::std::StdLib::new();
let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&stdlib)?;
let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&stdlib)?;
let zoo_client = new_zoo_client();
let zoo_client = crate::executor::new_zoo_client(None, None)?;
let executor_ctx = if execute {
Some(crate::executor::ExecutorContext::new(&zoo_client, Default::default()).await?)

View File

@ -1342,7 +1342,7 @@ fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> {
}
/// Parse a variable/constant declaration.
fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
fn declaration(i: TokenSlice) -> PResult<Box<VariableDeclaration>> {
let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
.parse_next(i)?
.map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
@ -1404,7 +1404,7 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
.map_err(|e| e.cut())?;
let end = val.end();
Ok(VariableDeclaration {
Ok(Box::new(VariableDeclaration {
start,
end,
declarations: vec![VariableDeclarator {
@ -1417,7 +1417,7 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
visibility,
kind,
digest: None,
})
}))
}
impl TryFrom<Token> for Identifier {

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "Literal",
@ -22,8 +21,6 @@ expression: actual
"start": 4,
"end": 5,
"value": 2,
"raw": "2",
"digest": null
},
"digest": null
"raw": "2"
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "Literal",
@ -22,8 +21,6 @@ expression: actual
"start": 2,
"end": 3,
"value": 2,
"raw": "2",
"digest": null
},
"digest": null
"raw": "2"
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "Literal",
@ -22,8 +21,6 @@ expression: actual
"start": 3,
"end": 4,
"value": 2,
"raw": "2",
"digest": null
},
"digest": null
"raw": "2"
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "BinaryExpression",
@ -28,8 +27,7 @@ expression: actual
"start": 4,
"end": 5,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -37,10 +35,7 @@ expression: actual
"start": 8,
"end": 9,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
"raw": "3"
}
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "BinaryExpression",
@ -28,8 +27,7 @@ expression: actual
"start": 6,
"end": 7,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -37,10 +35,7 @@ expression: actual
"start": 10,
"end": 11,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
"raw": "3"
}
}
}

View File

@ -19,8 +19,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "BinaryExpression",
@ -34,8 +33,7 @@ expression: actual
"start": 6,
"end": 7,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -43,12 +41,9 @@ expression: actual
"start": 10,
"end": 11,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
"raw": "3"
}
}
},
"right": {
"type": "Literal",
@ -56,8 +51,6 @@ expression: actual
"start": 16,
"end": 17,
"value": 4,
"raw": "4",
"digest": null
},
"digest": null
"raw": "4"
}
}

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