Compare commits

...

31 Commits

Author SHA1 Message Date
57e030a0ad Check settings file exists before checking its contents 2024-10-31 10:32:58 -04:00
0adecf4f58 Merge branch 'main' into franknoirot/adhoc/await-settings-write-test 2024-10-31 07:08:00 -07:00
26e995dc3f Snap to origin and axis behavior for profile starts and segments (#4344)
* Visualize draft point when near axes (only works on XY rn due to quaternion rotation issue)

* Slightly better quaternion rotation

* Actually snap new profiles to the X and Y axis

* Add snapping behavior while dragging

* Fix flickering on non-XY planes

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

* Add new test to verify snapping behavior

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

* All feedback except absolute lines

* Use `lineTo` for lines that have snapped

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

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

* Re-run CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-31 10:04:38 -04:00
08e411e1a3 Test fix: Don't click the settings close button until we confirm a write 2024-10-31 10:01:48 -04:00
a8b816a3e2 Added test to ensure array push is immutable (#4361)
added test to ensure array push is immutable
2024-10-30 23:04:26 +00:00
43bec115c0 Refactor source ranges into a generic node type (#4350)
* WIP

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

* Fix formatting

* Fix yarn build:wasm

* Fix ts_rs bindings

* Fix tsc errors

* Fix wasm TS types

* Add minimal failing test

* Rename field to avoid name collisions

* Remove node wrapper around NonCodeMeta

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

* Rename Node to BoxNode

* Fix lints

* Fix lint by boxing literals

* Rename UnboxedNode to Node

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

* Update docs

* Update snapshots

* initial trait

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

* update docs

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

* updates

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

* gross hack for TagNode

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

* extend gross hack

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

* fix EnvRef bullshit

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

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

* Fix test errors after merging main

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

* Confirm

* Change to use simpler map_err

* Add comment

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2024-10-30 20:52:17 +00:00
0c6c646fe7 KCL: New simulation test pipeline (#4351)
The idea behind this is to test all the various stages of executing KCL
separately, i.e.

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-10-29 23:12:44 -04:00
6b105897f7 Bump handlebars from 6.1.0 to 6.2.0 in /src/wasm-lib (#4330)
Bumps [handlebars](https://github.com/sunng87/handlebars-rust) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/sunng87/handlebars-rust/releases)
- [Changelog](https://github.com/sunng87/handlebars-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sunng87/handlebars-rust/compare/v6.1.0...v6.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-10-29 23:01:31 -04:00
9ff51de301 fix auth test in engine (#4354)
* fix auth test in engine

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

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

* emoty

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 19:24:24 -07:00
c161f578fd KCL test for subset of poop chute (#4343)
This would have caught the regression in https://github.com/KittyCAD/modeling-app/pull/4333

which had to be reverted in https://github.com/KittyCAD/modeling-app/pull/4339
2024-10-30 02:17:48 +00:00
4804eedf3e Bump react-router-dom from 6.26.1 to 6.27.0 (#4286)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.26.1 to 6.27.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@6.27.0/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.27.0/packages/react-router-dom)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 22:17:19 -04:00
99db31a6a4 Tests: remove all timeouts and pasting into editor from file name collision PW test (#4352)
remove all timeouts and pasting into editor from file name collision PW test
2024-10-29 21:42:53 -04:00
90b57ec202 Add lsystem.kcl to tests (#4146)
* Add lsystem.kcl to tests

* Reduce iterations

* Fix the user settings flake shit (NOTE TO ALL FUTURE PEOPLE MODELING-APP DOES NOT WAIT FOR I/O IN SOME CASES BEFORE ROUTER NAVIGATION)
2024-10-29 21:40:31 -04:00
3f86f99f5e Fix just lint and yarn script to check all targets (#4348)
* Fix just lint to check all targets

* Fix yarn test:rust to lint all targets

* Remove redundant options

* Change cargo --all to --workspace

* Update readme to use just command
2024-10-29 19:46:59 +00:00
83e2b093a6 Deflake settings persistence desktop test by verifying we have written to the disk before continuing (#4349) 2024-10-29 12:31:52 -04:00
58f7e0086d Fix CI docs generation after #4329 (#4347)
Fix CI
2024-10-29 14:39:50 +00:00
c147b3bfa2 Error if assertEqual's epsilon value is invalid (#4329)
Without this, you can get a funny error message, such as:

    assert failed because 42 != 42: number is not 42

I'm assuming an epsilon of zero is never useful because we're dealing
with floating point.
2024-10-28 22:51:10 -04:00
7103ded32a Guptaarnav 2024 10 28 (#4341)
* accessing toast error correctly

* wrapping try-catch around fs.stat on cli arg

* implemented array push

* changing arg execution order for sketch arc

* addressing sketchFromKclValue error for Sketches in Uservals

* addressing 'update to He inside a test not wrapped in act(...' error

* yarn fmt fix

* implemented polygon stdlib function

* changing polygon inscribed arg description in docs

* addressing cargo clippy warning

* Add tangential arc unavailable reason tooltip

* fixing tsc errors

* preventing hidden dirs from showing up as projects and prohibits renaming projects as hidden

* adding unit test for desktop listProjects

* showing no completions when last typed word is a number

* fmt

* Make clippy happy

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

* yarn tsc fix: added missing toast import in Home.tsx

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

* regenerating markdown docs for incoming merge from main

---------

Co-authored-by: arnav <arnav@agupta.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-28 20:52:51 -04:00
81279aa4e8 Revert "Track tagged geometry with an enum, not options (#4333)" (#4339)
This reverts commit 4a62862ca0.
2024-10-28 20:20:45 -04:00
550c8ae165 Chore: separate out projectMachine from Home route (#4109)
* Rename `homeMachine` and accessories to `projectsMachine`

* Separate out `/home` route from `projectsMachine`

* Add logic to navigate out from deleted or renamed project

* Show a warning in the command palette for deleting a project

* Make it navigate when you create a project

* Update "New project" button to use command bar flow
Closes #2585

* More explicit warning message text

* Make projects watching code not run in web

* Tests first version: nested loops

* Tests second version: flattened

* Remove console logs

* Fix tsc

* @jtran feedback, use the type guard util

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

* Fix tests that relied on one-click, no-navigation project creation

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

This reverts commit 7545b61b49.

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

This reverts commit 3d2e48732c.

* Add a mask to the state indicator to client-side scale test

* 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: ubuntu-latest)

* Fix lint

* Fix tsc

* Fix a couple stray tests that still relied on the old way of creating projects

* De-flake another text that could be thrown off by toast-based selectors

* FMT

* Dumb test error because I was rushing

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

* Ahhh more flaky toasts, they're everywhere!

* 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

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

* Re-run CI

* Fix one test added since this PR was made

* Fix a few tests that failed due to changes since PR was made

* Prevent double selector issue in Ubuntu test

* Prevent *a different* double selector issue

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-28 16:18:06 -04:00
05610bb0f3 Show top level dir (#4165)
* Reload FileTree and File when changed externally

* Added tests

* Show project root in files pane

* Cut off titles that are too long

* Fix tests
2024-10-28 14:29:47 -04:00
4a62862ca0 Track tagged geometry with an enum, not options (#4333)
Tags can refer to either a surface, or a path. Tags track what they're
tagging with two fields: `Option<Path>` and `Option<Surface>`. This means
as more things can be tagged, we'll have a bunch of options and only one
can be Some at a given time. This should be an enum instead, so the
compiler can enforce that only one thing is being tagged.

Also I want to allow tags to refer to points, so I'd like to make this
enum before I add a new possible variant.
2024-10-28 15:51:21 +00:00
a4783d4951 Separate debug/release electron-builder to help mac job (#4313) 2024-10-28 13:41:32 +00:00
30cfac06b8 Patch bump for node v21 (#4239)
* Loosen nvmrc to v21 (no minor, no patch)

* Bump to v21.7.3
2024-10-28 10:28:36 +00:00
c5509dabb1 Fix weirdly specific logic for querying extruded face info (#4320) 2024-10-26 18:16:21 -05:00
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
259 changed files with 45878 additions and 15003 deletions

View File

@ -109,17 +109,8 @@ jobs:
platform: linux
runs-on: ${{ matrix.os }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
steps:
- uses: actions/checkout@v4
@ -181,8 +172,25 @@ jobs:
smksp_cert_sync.exe
shell: cmd
- name: Build the app
run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }}
- name: Build the app (debug)
if: ${{ env.BUILD_RELEASE == 'false' }}
# electron-builder doesn't have a concept of release vs debug,
# this is just not doing any codesign or release yml generation
run: yarn electron-builder --config
- name: Build the app (release)
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always
- name: List artifacts in out/
run: ls -R out
@ -226,7 +234,17 @@ jobs:
- name: Build the app (updater-test)
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}

View File

@ -37,4 +37,4 @@ jobs:
# We specifically want to test the disable-println feature
# Since it is not enabled by default, we need to specify it
# This is used in kcl-lsp
cargo check --all --features disable-println --features pyo3 --features cli
cargo check --workspace --features disable-println --features pyo3 --features cli

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

2
.nvmrc
View File

@ -1 +1 @@
v21.7.1
v21.7.3

View File

@ -74,10 +74,12 @@ layout: manual
* [`patternTransform`](kcl/patternTransform)
* [`pi`](kcl/pi)
* [`polar`](kcl/polar)
* [`polygon`](kcl/polygon)
* [`pow`](kcl/pow)
* [`profileStart`](kcl/profileStart)
* [`profileStartX`](kcl/profileStartX)
* [`profileStartY`](kcl/profileStartY)
* [`push`](kcl/push)
* [`reduce`](kcl/reduce)
* [`rem`](kcl/rem)
* [`revolve`](kcl/revolve)

60
docs/kcl/polygon.md Normal file

File diff suppressed because one or more lines are too long

38
docs/kcl/push.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -23,11 +23,11 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -43,10 +43,10 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -62,12 +62,12 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -83,12 +83,12 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -104,11 +104,11 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -124,12 +124,12 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -145,13 +145,13 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -23,12 +23,12 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
| `path` |`string`| | No |
| `raw_path` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -44,10 +44,10 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ExpressionStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `expression` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -63,12 +63,12 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `VariableDeclaration`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -84,10 +84,10 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ReturnStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `argument` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -15,10 +15,10 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -16,6 +16,6 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `bindings` |`object`| | No |
| `parent` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `parent` |`integer`| | No |

View File

@ -24,11 +24,11 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| An expression can be evaluated to yield a single KCL value. | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -44,10 +44,10 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -63,10 +63,10 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -82,12 +82,12 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -103,11 +103,11 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`FunctionExpression`](/docs/kcl/types/FunctionExpression)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -123,12 +123,12 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| An expression can be evaluated to yield a single KCL value. | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -144,11 +144,11 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `body` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -164,9 +164,9 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeSubstitution`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -182,11 +182,11 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `elements` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -202,12 +202,12 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -223,11 +223,11 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ObjectExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `properties` |`[` [`ObjectProperty`](/docs/kcl/types/ObjectProperty) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -243,12 +243,12 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| An expression can be evaluated to yield a single KCL value. | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| An expression can be evaluated to yield a single KCL value. | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -264,11 +264,11 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -284,13 +284,13 @@ An expression can be evaluated to yield a single KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -307,8 +307,8 @@ KCL value for an optional parameter which was not given an argument. (remember,
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `None`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -15,10 +15,10 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -15,9 +15,9 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -17,8 +17,8 @@ layout: manual
|----------|------|-------------|----------|
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -59,10 +59,10 @@ Any KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -23,10 +23,10 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -42,11 +42,11 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -23,12 +23,12 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
@ -44,10 +44,10 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -16,7 +16,7 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `nonCodeNodes` |`object`| | No |
| `start` |`[` [`NonCodeNode`](/docs/kcl/types/NonCodeNode) `]`| | No |
| `startNodes` |`[` [`NonCodeNode`](/docs/kcl/types/NonCodeNode) `]`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -15,9 +15,9 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |[`NonCodeValue`](/docs/kcl/types/NonCodeValue)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -15,10 +15,10 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `key` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `value` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

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

@ -0,0 +1,24 @@
---
title: "PolygonData"
excerpt: "Data for drawing a polygon"
layout: manual
---
Data for drawing a polygon
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `radius` |`number`| The radius of the polygon | No |
| `numSides` |`integer`| The number of sides in the polygon | No |
| `center` |`[number, number]`| The center point of the polygon | No |
| `inscribed` |`boolean`| Whether the polygon is inscribed (true) or circumscribed (false) about a circle with the specified radius | No |

View File

@ -16,10 +16,10 @@ A KCL program top level, or function body.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `body` |`[` [`BodyItem`](/docs/kcl/types/BodyItem) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| A KCL program top level, or function body. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -16,7 +16,7 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `environments` |`[` [`Environment`](/docs/kcl/types/Environment) `]`| | No |
| `currentEnv` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `currentEnv` |`integer`| | No |
| `return` |[`KclValue`](/docs/kcl/types/KclValue)| | No |

View File

@ -15,10 +15,10 @@ layout: manual
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `id` |[`Identifier`](/docs/kcl/types/Identifier)| The identifier of the variable. | No |
| `init` |[`Expr`](/docs/kcl/types/Expr)| The value of the variable. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -86,7 +86,7 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 + 0.01}], %)
|> line([-${commonPoints.num2}, 0], %)`)
|> lineTo([0, ${commonPoints.num3}], %)`)
}
// deselect line tool

View File

@ -3,6 +3,7 @@ import { test, expect } from './fixtures/fixtureSetup'
import * as fsp from 'fs/promises'
import * as fs from 'fs'
import {
createProject,
executorInputPath,
getUtils,
setup,
@ -114,20 +115,15 @@ test.describe('when using the file tree to', () => {
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
renameFile,
editorTextMatches,
} = await getUtils(tronApp.page, test)
const { panesOpen, pasteCodeInEditor, renameFile, editorTextMatches } =
await getUtils(tronApp.page, test)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
await createProject({ name: 'project-000', page: tronApp.page })
// File the main.kcl with contents
const kclCube = await fsp.readFile(
@ -167,15 +163,14 @@ test.describe('when using the file tree to', () => {
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
const { panesOpen, createAndSelectProject, createNewFile } =
await getUtils(tronApp.page, test)
const { panesOpen, createNewFile } = await getUtils(tronApp.page, test)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files'])
await createAndSelectProject('project-000')
await createProject({ name: 'project-000', page: tronApp.page })
await createNewFile('')
await createNewFile('')
@ -198,62 +193,74 @@ test.describe('when using the file tree to', () => {
test(
'create a new file with the same name as an existing file cancels the operation',
{ tag: '@electron' },
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
async (
{ browser: _, tronApp, homePage, scene, editor, toolbar },
testInfo
) => {
const projectName = 'cube'
const mainFile = 'main.kcl'
const secondFile = 'cylinder.kcl'
const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8')
const kclCylinder = await fsp.readFile(
executorInputPath('cylinder.kcl'),
'utf-8'
)
await tronApp.initialise({
fixtures: { homePage, scene, editor, toolbar },
folderSetupFn: async (dir) => {
const cubeDir = join(dir, projectName)
await fsp.mkdir(cubeDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.kcl'),
join(cubeDir, mainFile)
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(cubeDir, secondFile)
)
},
})
const {
openKclCodePanel,
openFilePanel,
createAndSelectProject,
pasteCodeInEditor,
createNewFileAndSelect,
renameFile,
selectFile,
editorTextMatches,
waitForPageLoad,
} = await getUtils(tronApp.page, _test)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await test.step(`Setup: Open project and navigate to ${secondFile}`, async () => {
await homePage.expectState({
projectCards: [
{
title: projectName,
fileCount: 2,
folderCount: 2, // TODO: This is a pre-existing bug, there are no folders within the project
},
],
sortBy: 'last-modified-desc',
})
await homePage.openProject(projectName)
await waitForPageLoad()
await openFilePanel()
await selectFile(secondFile)
})
await createAndSelectProject('project-000')
await openKclCodePanel()
await openFilePanel()
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
await test.step(`Attempt to rename ${secondFile} to ${mainFile}`, async () => {
await renameFile(secondFile, mainFile)
})
// 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',
'utf-8'
)
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 () => {
await selectFile(kcl1)
await test.step(`Postcondition: ${mainFile} still has the original content`, async () => {
await selectFile(mainFile)
await editorTextMatches(kclCube)
})
await tronApp.page.waitForTimeout(500)
await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => {
await selectFile(kcl2)
await test.step(`Postcondition: ${secondFile} still exists with the original content`, async () => {
await selectFile(secondFile)
await editorTextMatches(kclCylinder)
})
await tronApp?.close?.()
await tronApp.close()
}
)
@ -263,20 +270,15 @@ test.describe('when using the file tree to', () => {
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
deleteFile,
editorTextMatches,
} = await getUtils(tronApp.page, _test)
const { panesOpen, pasteCodeInEditor, deleteFile, editorTextMatches } =
await getUtils(tronApp.page, _test)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
await createProject({ name: 'project-000', page: tronApp.page })
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
@ -284,11 +286,11 @@ test.describe('when using the file tree to', () => {
)
await pasteCodeInEditor(kclCube)
const kcl1 = 'main.kcl'
const mainFile = 'main.kcl'
await deleteFile(kcl1)
await deleteFile(mainFile)
await test.step(`Postcondition: ${kcl1} is recreated but has no content`, async () => {
await test.step(`Postcondition: ${mainFile} is recreated but has no content`, async () => {
await editorTextMatches('')
})
@ -306,7 +308,6 @@ test.describe('when using the file tree to', () => {
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
createNewFile,
openDebugPanel,
@ -318,7 +319,7 @@ test.describe('when using the file tree to', () => {
tronApp.page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
await createProject({ name: 'project-000', page: tronApp.page })
// Create a small file
const kclCube = await fsp.readFile(
@ -722,7 +723,7 @@ _test.describe('Renaming in the file tree', () => {
})
await _test.step('Rename the folder', async () => {
await page.waitForTimeout(60000)
await page.waitForTimeout(1000)
await folderToRename.click({ button: 'right' })
await _expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()

View File

@ -1,6 +1,11 @@
import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import { sansWhitespace } from '../test-utils'
import {
closePane,
checkIfPaneIsOpen,
openPane,
sansWhitespace,
} from '../test-utils'
interface EditorState {
activeLines: Array<string>
@ -11,6 +16,7 @@ interface EditorState {
export class EditorFixture {
public page: Page
private paneButtonTestId = 'code-pane-button'
private diagnosticsTooltip!: Locator
private diagnosticsGutterIcon!: Locator
private codeContent!: Locator
@ -31,19 +37,32 @@ export class EditorFixture {
private _expectEditorToContain =
(not = false) =>
(
async (
code: string,
{
shouldNormalise = false,
timeout = 5_000,
}: { shouldNormalise?: boolean; timeout?: number } = {}
) => {
const wasPaneOpen = await this.checkIfPaneIsOpen()
if (!wasPaneOpen) {
await this.openPane()
}
const resetPane = async () => {
if (!wasPaneOpen) {
await this.closePane()
}
}
if (!shouldNormalise) {
const expectStart = expect(this.codeContent)
if (not) {
return expectStart.not.toContainText(code, { timeout })
const result = await expectStart.not.toContainText(code, { timeout })
await resetPane()
return result
}
return expectStart.toContainText(code, { timeout })
const result = await expectStart.toContainText(code, { timeout })
await resetPane()
return result
}
const normalisedCode = code.replaceAll(/\s+/g, '').trim()
const expectStart = expect.poll(
@ -56,9 +75,13 @@ export class EditorFixture {
}
)
if (not) {
return expectStart.not.toContain(normalisedCode)
const result = await expectStart.not.toContain(normalisedCode)
await resetPane()
return result
}
return expectStart.toContain(normalisedCode)
const result = await expectStart.toContain(normalisedCode)
await resetPane()
return result
}
expectEditor = {
toContain: this._expectEditorToContain(),
@ -115,4 +138,13 @@ export class EditorFixture {
code = code.replace(findCode, replaceCode)
await this.codeContent.fill(code)
}
checkIfPaneIsOpen() {
return checkIfPaneIsOpen(this.page, this.paneButtonTestId)
}
closePane() {
return closePane(this.page, this.paneButtonTestId)
}
openPane() {
return openPane(this.page, this.paneButtonTestId)
}
}

View File

@ -20,6 +20,7 @@ export class AuthenticatedApp {
public readonly page: Page
public readonly context: BrowserContext
public readonly testInfo: TestInfo
public readonly viewPortSize = { width: 1000, height: 500 }
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.page = page
@ -36,7 +37,7 @@ export class AuthenticatedApp {
;(window as any).playwrightSkipFilePicker = true
}, code)
await this.page.setViewportSize({ width: 1000, height: 500 })
await this.page.setViewportSize(this.viewPortSize)
await u.waitForAuthSkipAppStart()
}

View File

@ -10,7 +10,13 @@ import {
} from '../test-utils'
type mouseParams = {
pixelDiff: number
pixelDiff?: number
}
type mouseDragToParams = mouseParams & {
fromPoint: { x: number; y: number }
}
type mouseDragFromParams = mouseParams & {
toPoint: { x: number; y: number }
}
type SceneSerialised = {
@ -20,6 +26,13 @@ type SceneSerialised = {
}
}
type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean>
type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean>
type DragFromHandler = (
dragParams: mouseDragFromParams
) => Promise<void | boolean>
export class SceneFixture {
public page: Page
@ -55,7 +68,7 @@ export class SceneFixture {
x: number,
y: number,
{ steps }: { steps: number } = { steps: 20 }
) =>
): [ClickHandler, MoveHandler] =>
[
(clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) {
@ -78,6 +91,47 @@ export class SceneFixture {
return this.page.mouse.move(x, y, { steps })
},
] as const
makeDragHelpers = (
x: number,
y: number,
{ steps }: { steps: number } = { steps: 20 }
): [DragToHandler, DragFromHandler] =>
[
(dragToParams: mouseDragToParams) => {
if (dragToParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() =>
this.page.dragAndDrop('#stream', '#stream', {
sourcePosition: dragToParams.fromPoint,
targetPosition: { x, y },
}),
dragToParams.pixelDiff
)
}
return this.page.dragAndDrop('#stream', '#stream', {
sourcePosition: dragToParams.fromPoint,
targetPosition: { x, y },
})
},
(dragFromParams: mouseDragFromParams) => {
if (dragFromParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() =>
this.page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x, y },
targetPosition: dragFromParams.toPoint,
}),
dragFromParams.pixelDiff
)
}
return this.page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x, y },
targetPosition: dragFromParams.toPoint,
})
},
] as const
/** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
*

View File

@ -7,6 +7,7 @@ export class ToolbarFixture {
extrudeButton!: Locator
startSketchBtn!: Locator
lineBtn!: Locator
rectangleBtn!: Locator
exitSketchBtn!: Locator
editSketchBtn!: Locator
@ -24,6 +25,7 @@ export class ToolbarFixture {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line')
this.rectangleBtn = page.getByTestId('corner-rectangle')
this.exitSketchBtn = page.getByTestId('sketch-exit')
this.editSketchBtn = page.getByText('Edit Sketch')

View File

@ -7,6 +7,7 @@ import {
setupElectron,
tearDown,
executorInputPath,
createProject,
} from './test-utils'
import { bracket } from 'lib/exampleKcl'
import { onboardingPaths } from 'routes/Onboarding/paths'
@ -74,13 +75,8 @@ test.describe('Onboarding tests', () => {
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 createProject({ name: 'project-link', page })
await test.step(`Ensure the engine connection works by testing the sketch button`, async () => {
await u.waitForPageLoad()
})
@ -425,7 +421,9 @@ test(
const restartConfirmationButton = page.getByRole('button', {
name: 'Make a new project',
})
const tutorialProjectIndicator = page.getByText('Tutorial Project 00')
const tutorialProjectIndicator = page
.getByTestId('project-sidebar-toggle')
.filter({ hasText: 'Tutorial Project 00' })
const tutorialModalText = page.getByText('Welcome to Modeling App!')
const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
const userMenuButton = page.getByTestId('user-sidebar-toggle')

View File

@ -451,3 +451,103 @@ sketch002 = startSketchOn(extrude001, seg03)
}
)
})
test(`Verify axis and origin snapping`, async ({
app,
editor,
toolbar,
scene,
}) => {
// Constants and locators
// These are mappings from screenspace to KCL coordinates,
// until we merge in our coordinate system helpers
const xzPlane = [
app.viewPortSize.width * 0.65,
app.viewPortSize.height * 0.3,
] as const
const originSloppy = {
screen: [
app.viewPortSize.width / 2 + 3, // 3px off the center of the screen
app.viewPortSize.height / 2,
],
kcl: [0, 0],
} as const
const xAxisSloppy = {
screen: [
app.viewPortSize.width * 0.75,
app.viewPortSize.height / 2 - 3, // 3px off the X-axis
],
kcl: [16.95, 0],
} as const
const offYAxis = {
screen: [
app.viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range
app.viewPortSize.height * 0.3,
],
kcl: [6.78, 6.78],
} as const
const yAxisSloppy = {
screen: [
app.viewPortSize.width / 2 + 5, // 5px off the Y-axis
app.viewPortSize.height * 0.3,
],
kcl: [0, 6.78],
} as const
const [clickOnXzPlane, moveToXzPlane] = scene.makeMouseHelpers(...xzPlane)
const [clickOriginSloppy] = scene.makeMouseHelpers(...originSloppy.screen)
const [clickXAxisSloppy, moveXAxisSloppy] = scene.makeMouseHelpers(
...xAxisSloppy.screen
)
const [dragToOffYAxis, dragFromOffAxis] = scene.makeDragHelpers(
...offYAxis.screen
)
const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
segmentOnXAxis: `lineTo([${xAxisSloppy.kcl[0]}, ${xAxisSloppy.kcl[1]}], %)`,
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
}
await app.initialise()
await test.step(`Start a sketch on the XZ plane`, async () => {
await editor.closePane()
await toolbar.startSketchPlaneSelection()
await moveToXzPlane()
await clickOnXzPlane()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane)
})
await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => {
await clickOriginSloppy()
await editor.expectEditor.toContain(expectedCodeSnippets.pointAtOrigin)
})
await test.step(`Add a segment on x-axis after moving the mouse a bit, verify it snaps`, async () => {
await moveXAxisSloppy()
await clickXAxisSloppy()
await editor.expectEditor.toContain(expectedCodeSnippets.segmentOnXAxis)
})
await test.step(`Unequip line tool`, async () => {
await toolbar.lineBtn.click()
await expect(toolbar.lineBtn).not.toHaveAttribute('aria-pressed', 'true')
})
await test.step(`Drag the origin point up and to the right, verify it's past snapping`, async () => {
await dragToOffYAxis({
fromPoint: { x: originSloppy.screen[0], y: originSloppy.screen[1] },
})
await editor.expectEditor.toContain(
expectedCodeSnippets.afterSegmentDraggedOffYAxis
)
})
await test.step(`Drag the origin point left to the y-axis, verify it snaps back`, async () => {
await dragFromOffAxis({
toPoint: { x: yAxisSloppy.screen[0], y: yAxisSloppy.screen[1] },
})
await editor.expectEditor.toContain(
expectedCodeSnippets.afterSegmentDraggedOnYAxis
)
})
})

View File

@ -7,7 +7,7 @@ import {
Paths,
setupElectron,
tearDown,
createProjectAndRenameIt,
createProject,
} from './test-utils'
import fsp from 'fs/promises'
import fs from 'fs'
@ -503,21 +503,261 @@ test(
}
)
test.describe(`Project management commands`, () => {
test(
`Rename from project page`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const projectName = `my_project_to_rename`
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/${projectName}/main.kcl`
)
},
})
const u = await getUtils(page)
// Constants and locators
const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'rename project' })
const projectNameOption = page.getByRole('option', { name: projectName })
const projectRenamedName = `project-000`
// const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const commandContinueButton = page.getByRole('button', {
name: 'Continue',
})
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully renamed`)
await test.step(`Setup`, async () => {
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await projectHomeLink.click()
await u.waitForPageLoad()
})
await test.step(`Run rename command via command palette`, async () => {
await commandButton.click()
await commandOption.click()
await projectNameOption.click()
await expect(commandContinueButton).toBeVisible()
await commandContinueButton.click()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await expect(toastMessage).toBeVisible()
})
// TODO: in future I'd like the behavior to be to
// navigate to the new project's page directly,
// see ProjectContextProvider.tsx:158
await test.step(`Check the project was renamed and we navigated home`, async () => {
await expect(projectHomeLink.first()).toBeVisible()
await expect(projectHomeLink.first()).toContainText(projectRenamedName)
})
await electronApp.close()
}
)
test(
`Delete from project page`,
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
const projectName = `my_project_to_delete`
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/${projectName}/main.kcl`
)
},
})
const u = await getUtils(page)
// Constants and locators
const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'delete project' })
const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully deleted`)
const noProjectsMessage = page.getByText('No Projects found')
await test.step(`Setup`, async () => {
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await projectHomeLink.click()
await u.waitForPageLoad()
})
await test.step(`Run delete command via command palette`, async () => {
await commandButton.click()
await commandOption.click()
await projectNameOption.click()
await expect(commandWarning).toBeVisible()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await expect(toastMessage).toBeVisible()
})
await test.step(`Check the project was deleted and we navigated home`, async () => {
await expect(noProjectsMessage).toBeVisible()
})
await electronApp.close()
}
)
test(
`Rename from home page`,
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
const projectName = `my_project_to_rename`
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/${projectName}/main.kcl`
)
},
})
// Constants and locators
const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'rename project' })
const projectNameOption = page.getByRole('option', { name: projectName })
const projectRenamedName = `project-000`
const commandContinueButton = page.getByRole('button', {
name: 'Continue',
})
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully renamed`)
await test.step(`Setup`, async () => {
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await expect(projectHomeLink).toBeVisible()
})
await test.step(`Run rename command via command palette`, async () => {
await commandButton.click()
await commandOption.click()
await projectNameOption.click()
await expect(commandContinueButton).toBeVisible()
await commandContinueButton.click()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await expect(toastMessage).toBeVisible()
})
await test.step(`Check the project was renamed`, async () => {
await expect(
page.getByRole('link', { name: projectRenamedName })
).toBeVisible()
await expect(projectHomeLink).not.toHaveText(projectName)
})
await electronApp.close()
}
)
test(
`Delete from home page`,
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
const projectName = `my_project_to_delete`
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/${projectName}/main.kcl`
)
},
})
// Constants and locators
const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'delete project' })
const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully deleted`)
const noProjectsMessage = page.getByText('No Projects found')
await test.step(`Setup`, async () => {
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await expect(projectHomeLink).toBeVisible()
})
await test.step(`Run delete command via command palette`, async () => {
await commandButton.click()
await commandOption.click()
await projectNameOption.click()
await expect(commandWarning).toBeVisible()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await expect(toastMessage).toBeVisible()
})
await test.step(`Check the project was deleted`, async () => {
await expect(projectHomeLink).not.toBeVisible()
await expect(noProjectsMessage).toBeVisible()
})
await electronApp.close()
}
)
})
test(
'File in the file pane should open with a single click',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const projectName = 'router-template-slate'
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl`
`${dir}/${projectName}/main.kcl`
)
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/router-template-slate/otherThingToClickOn.kcl`
`${dir}/${projectName}/otherThingToClickOn.kcl`
)
},
})
@ -526,7 +766,7 @@ test(
page.on('console', console.log)
await page.getByText('router-template-slate').click()
await page.getByText(projectName).click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
@ -643,7 +883,7 @@ test(
page.on('console', console.log)
await test.step('delete the middle project, i.e. the bracket project', async () => {
const project = page.getByText('bracket')
const project = page.getByTestId('project-link').getByText('bracket')
await project.hover()
await project.focus()
@ -687,10 +927,10 @@ test(
})
await test.step('Check we can still create a project', async () => {
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText('project-000')).toBeVisible()
await createProject({ name: 'project-000', page, returnHome: true })
await expect(
page.getByTestId('project-link').filter({ hasText: 'project-000' })
).toBeVisible()
})
await electronApp.close()
@ -867,17 +1107,16 @@ test.fixme(
const pointOnModel = { x: 660, y: 250 }
const expectedStartCamZPosition = 15633.47
// Constants and locators
const projectLinks = page.getByTestId('project-link')
// expect to see text "No Projects found"
await expect(page.getByText('No Projects found')).toBeVisible()
await page.getByRole('button', { name: 'New project' }).click()
await createProject({ name: 'project-000', page, returnHome: true })
await expect(projectLinks.getByText('project-000')).toBeVisible()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText('project-000')).toBeVisible()
await page.getByText('project-000').click()
await projectLinks.getByText('project-000').click()
await u.waitForPageLoad()
@ -936,16 +1175,10 @@ extrude001 = extrude(200, sketch001)`)
page.getByRole('button', { name: 'New project' })
).toBeVisible()
const createProject = async (projectNum: number) => {
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
const projectNumStr = projectNum.toString().padStart(3, '0')
await expect(page.getByText(`project-${projectNumStr}`)).toBeVisible()
}
for (let i = 1; i <= 10; i++) {
await createProject(i)
const name = `project-${i.toString().padStart(3, '0')}`
await createProject({ name, page, returnHome: true })
await expect(projectLinks.getByText(name)).toBeVisible()
}
await electronApp.close()
}
@ -1120,11 +1353,10 @@ test(
await page.getByTestId('settings-close-button').click()
await expect(page.getByText('No Projects found')).toBeVisible()
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText(`project-000`)).toBeVisible()
await createProject({ name: 'project-000', page, returnHome: true })
await expect(
page.getByTestId('project-link').filter({ hasText: 'project-000' })
).toBeVisible()
})
await test.step('We can change back to the original root project directory', async () => {
@ -1265,6 +1497,7 @@ test(
'i_shape.kcl',
'kittycad_svg.kcl',
'lego.kcl',
'lsystem.kcl',
'math.kcl',
'member_expression_sketch.kcl',
'mike_stress_test.kcl',
@ -1450,7 +1683,7 @@ test(
page.on('console', console.log)
await test.step('Should create and name a project called wrist brace', async () => {
await createProjectAndRenameIt({ name: 'wrist brace', page })
await createProject({ name: 'wrist brace', page, returnHome: true })
})
await test.step('Should go through onboarding', async () => {

View File

@ -637,7 +637,6 @@ test.describe('Sketch tests', () => {
|> revolve({ axis: "X" }, %)`)
})
test('Can add multiple sketches', async ({ page }) => {
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
@ -675,15 +674,16 @@ test.describe('Sketch tests', () => {
await click00r(50, 0)
await page.waitForTimeout(100)
codeStr += ` |> line(${toSU([50, 0])}, %)`
codeStr += ` |> lineTo(${toSU([50, 0])}, %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 50)
codeStr += ` |> line(${toSU([0, 50])}, %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(-50, 0)
codeStr += ` |> line(${toSU([-50, 0])}, %)`
let clickCoords = await click00r(-50, 0)
expect(clickCoords).not.toBeUndefined()
codeStr += ` |> lineTo(${toSU(clickCoords!)}, %)`
await expect(u.codeLocator).toHaveText(codeStr)
// exit the sketch, reset relative clicker
@ -709,8 +709,10 @@ test.describe('Sketch tests', () => {
codeStr += ` |> startProfileAt([2.03, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr)
// TODO: I couldn't use `toSU` here because of some rounding error causing
// it to be off by 0.01
await click00r(30, 0)
codeStr += ` |> line([2.04, 0], %)`
codeStr += ` |> lineTo([4.07, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 30)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

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

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -219,7 +219,7 @@ test.describe('Test network and connection issues', () => {
|> startProfileAt([12.34, -12.34], %)
|> line([12.34, 0], %)
|> line([-12.34, 12.34], %)
|> line([-12.34, 0], %)
|> lineTo([0, -12.34], %)
`)

View File

@ -45,7 +45,9 @@ export const commonPoints = {
startAt: '[7.19, -9.7]',
num1: 7.25,
num2: 14.44,
}
/** The Y-value of a common lineTo move we perform in tests */
num3: -2.44,
} as const
/** A semi-reliable color to check the default XZ plane on
* in dark mode in the default camera position
@ -118,15 +120,32 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
)
}
async function openPane(page: Page, testId: string) {
const locator = page.getByTestId(testId)
await expect(locator).toBeVisible()
const isOpen = (await locator?.getAttribute('aria-pressed')) === 'true'
export async function checkIfPaneIsOpen(page: Page, testId: string) {
const paneButtonLocator = page.getByTestId(testId)
await expect(paneButtonLocator).toBeVisible()
return (await paneButtonLocator?.getAttribute('aria-pressed')) === 'true'
}
export async function openPane(page: Page, testId: string) {
const paneButtonLocator = page.getByTestId(testId)
await expect(paneButtonLocator).toBeVisible()
const isOpen = await checkIfPaneIsOpen(page, testId)
if (!isOpen) {
await locator.click()
await expect(locator).toHaveAttribute('aria-pressed', 'true')
await paneButtonLocator.click()
}
await expect(paneButtonLocator).toHaveAttribute('aria-pressed', 'true')
}
export async function closePane(page: Page, testId: string) {
const paneButtonLocator = page.getByTestId(testId)
await expect(paneButtonLocator).toBeVisible()
const isOpen = await checkIfPaneIsOpen(page, testId)
if (isOpen) {
await paneButtonLocator.click()
}
await expect(paneButtonLocator).toHaveAttribute('aria-pressed', 'false')
}
async function openKclCodePanel(page: Page) {
@ -467,20 +486,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
return text.replace(/\s+/g, '')
},
createAndSelectProject: async (hasText: string) => {
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()
}
)
},
editorTextMatches: async (code: string) => {
const editor = page.locator(editorSelector)
return expect(editor).toHaveText(code, { useInnerText: true })
@ -520,6 +525,9 @@ export async function getUtils(page: Page, test_?: typeof test) {
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.click()
await expect(page.getByTestId('project-sidebar-toggle')).toContainText(
name
)
})
},
@ -980,30 +988,25 @@ export async function isOutOfViewInScrollContainer(
return isOutOfView
}
export async function createProjectAndRenameIt({
export async function createProject({
name,
page,
returnHome = false,
}: {
name: string
page: Page
returnHome?: boolean
}) {
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await test.step(`Create project and navigate to it`, async () => {
await page.getByRole('button', { name: 'New project' }).click()
await page.getByRole('textbox', { name: 'Name' }).fill(name)
await page.getByRole('button', { name: 'Continue' }).click()
await expect(page.getByText(`project-000`)).toBeVisible()
await page.getByText(`project-000`).hover()
await page.getByText(`project-000`).focus()
await page.getByLabel('sketch').first().click()
await page.waitForTimeout(100)
// type the name passed in
await page.keyboard.press('Backspace')
await page.keyboard.type(name)
await page.getByLabel('checkmark').last().click()
if (returnHome) {
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
await page.getByTestId('app-logo').click()
}
})
}
export function executorInputPath(fileName: string): string {

View File

@ -96,7 +96,7 @@ test.describe('Testing selections', () => {
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 + 0.01}], %)
|> line([-${commonPoints.num2}, 0], %)`)
|> lineTo([0, ${commonPoints.num3}], %)`)
// deselect line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
@ -157,7 +157,9 @@ test.describe('Testing selections', () => {
await emptySpaceClick()
// check the same selection again by putting cursor in code first then selecting axis
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await page
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
.click()
await page.keyboard.down('Shift')
await constrainButton.click()
await expect(absYButton).toBeDisabled()
@ -180,7 +182,9 @@ test.describe('Testing selections', () => {
process.platform === 'linux' ? 'Control' : 'Meta'
)
await page.waitForTimeout(100)
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await page
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
.click()
await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(500)

View File

@ -1,5 +1,6 @@
import { test, expect } from '@playwright/test'
import * as fsp from 'fs/promises'
import * as fs from 'fs'
import { join } from 'path'
import {
getUtils,
@ -7,6 +8,7 @@ import {
setupElectron,
tearDown,
executorInputPath,
createProject,
} from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
@ -265,10 +267,15 @@ test.describe('Testing settings', () => {
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
const projectName = 'bracket'
const {
electronApp,
page,
dir: projectDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
const bracketDir = join(dir, projectName)
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
@ -280,6 +287,12 @@ test.describe('Testing settings', () => {
await page.setViewportSize({ width: 1200, height: 500 })
// Selectors and constants
const tempProjectSettingsFilePath = join(
projectDirName,
projectName,
PROJECT_SETTINGS_FILE_NAME
)
const tempUserSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const userThemeColor = '120'
const projectThemeColor = '50'
const settingsOpenButton = page.getByRole('link', {
@ -292,6 +305,21 @@ test.describe('Testing settings', () => {
const projectLink = page.getByText('bracket')
const logoLink = page.getByTestId('app-logo')
async function confirmThemeWasWritten(filePath: string, value: string) {
return expect
.poll(
async () => {
const fileExists = await fs.existsSync(filePath)
return fileExists ? fsp.readFile(filePath, 'utf-8') : ''
},
{
message: 'Setting should now be written to the file',
timeout: 5_000,
}
)
.toContain(`themeColor = "${value}"`)
}
await test.step('Set user theme color on home', async () => {
await expect(settingsOpenButton).toBeVisible()
await settingsOpenButton.click()
@ -299,6 +327,7 @@ test.describe('Testing settings', () => {
await expect(userSettingsTab).toBeChecked()
await themeColorSetting.fill(userThemeColor)
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
await confirmThemeWasWritten(tempUserSettingsFilePath, userThemeColor)
await settingsCloseButton.click()
})
@ -310,6 +339,11 @@ test.describe('Testing settings', () => {
await expect(projectSettingsTab).toBeChecked()
await themeColorSetting.fill(projectThemeColor)
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
// Make sure that the project settings file has been written to before continuing
await confirmThemeWasWritten(
tempProjectSettingsFilePath,
projectThemeColor
)
await settingsCloseButton.click()
})
@ -323,6 +357,7 @@ test.describe('Testing settings', () => {
await test.step(`Navigate back to the home view and see user setting applied`, async () => {
await logoLink.click()
await page.screenshot({ path: 'out.png' })
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
})
@ -428,8 +463,7 @@ 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 createProject({ name: 'project-000', page })
await changeColor('58')
await expect(logoLink).toHaveCSS('--primary-hue', '58')
})
@ -447,7 +481,7 @@ test.describe('Testing settings', () => {
test(
'project settings reload on external change',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
async ({ browserName: _ }, testInfo) => {
const {
electronApp,
page,
@ -465,11 +499,7 @@ test.describe('Testing settings', () => {
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()
await createProject({ name: 'project-000', page })
const changeColorFs = async (color: string) => {
const tempSettingsFilePath = join(

View File

@ -1,5 +1,11 @@
import { test, expect, Page } from '@playwright/test'
import { getUtils, setup, tearDown, setupElectron } from './test-utils'
import {
getUtils,
setup,
tearDown,
setupElectron,
createProject,
} from './test-utils'
import { join } from 'path'
import fs from 'fs'
@ -700,17 +706,17 @@ test(
const fileExists = () =>
fs.existsSync(join(dir, projectName, textToCadFileName))
const {
createAndSelectProject,
openFilePanel,
openKclCodePanel,
waitForPageLoad,
} = await getUtils(page, test)
const { openFilePanel, openKclCodePanel, waitForPageLoad } = await getUtils(
page,
test
)
await page.setViewportSize({ width: 1200, height: 500 })
// Locators
const projectMenuButton = page.getByRole('button', { name: projectName })
const projectMenuButton = page
.getByTestId('project-sidebar-toggle')
.filter({ hasText: projectName })
const textToCadFileButton = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: textToCadFileName }),
})
@ -719,7 +725,7 @@ test(
)
// Create and navigate to the project
await createAndSelectProject('project-000')
await createProject({ name: 'project-000', page })
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await waitForPageLoad()

2
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

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.26.1",
"version": "0.26.2",
"private": true,
"productName": "Zoo Modeling App",
"author": {
@ -56,7 +56,7 @@
"react-json-view": "^1.21.3",
"react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2",
"react-router-dom": "^6.26.1",
"react-router-dom": "^6.27.0",
"sketch-helpers": "^0.0.4",
"three": "^0.166.1",
"ua-parser-js": "^1.0.37",
@ -76,7 +76,7 @@
"build:both": "vite build",
"build:both:local": "yarn build:wasm && vite build",
"pretest": "yarn remove-importmeta",
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
"test:rust": "(cd src/wasm-lib && cargo test --workspace && cargo clippy --workspace --all-targets)",
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver:bg": "yarn pretest && http-server ./public --cors -p 3000 &",

View File

@ -34,6 +34,11 @@
"title": "Car Wheel Assembly",
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
},
{
"file": "dodecahedron.kcl",
"title": "Hollow Dodecahedron",
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
},
{
"file": "enclosure.kcl",
"title": "Enclosure",
@ -54,6 +59,11 @@
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
},
{
"file": "food-service-spatula.kcl",
"title": "Food Service Spatula",
"description": "Use these spatulas for mixing, flipping, and scraping."
},
{
"file": "french-press.kcl",
"title": "French Press",
@ -61,7 +71,7 @@
},
{
"file": "gear.kcl",
"title": "Gear",
"title": "Spur Gear",
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
},
{

0
release-notes.md Normal file
View File

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,
@ -42,6 +43,7 @@ import { coreDump } from 'lang/wasm'
import { useMemo } from 'react'
import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -49,17 +51,22 @@ 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: (
<CommandBarProvider>
<SettingsAuthProvider>
<LspProvider>
<KclContextProvider>
<AppStateProvider>
<Outlet />
</AppStateProvider>
</KclContextProvider>
<ProjectsContextProvider>
<KclContextProvider>
<AppStateProvider>
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
</AppStateProvider>
</KclContextProvider>
</ProjectsContextProvider>
</LspProvider>
</SettingsAuthProvider>
</CommandBarProvider>

View File

@ -100,6 +100,11 @@ export function Toolbar({
function resolveItemConfig(
maybeIconConfig: ToolbarItem
): ToolbarItemResolved {
const isDisabled =
disableAllButtons ||
maybeIconConfig.status !== 'available' ||
maybeIconConfig.disabled?.(state) === true
return {
...maybeIconConfig,
title:
@ -113,10 +118,11 @@ export function Toolbar({
typeof maybeIconConfig.hotkey === 'string'
? maybeIconConfig.hotkey
: maybeIconConfig.hotkey?.(state),
disabled:
disableAllButtons ||
maybeIconConfig.status !== 'available' ||
maybeIconConfig.disabled?.(state) === true,
disabled: isDisabled,
disabledReason:
typeof maybeIconConfig.disabledReason === 'function'
? maybeIconConfig.disabledReason(state)
: maybeIconConfig.disabledReason,
disableHotkey: maybeIconConfig.disableHotkey?.(state),
status: maybeIconConfig.status,
}
@ -273,6 +279,8 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps
}) {
const { state } = useModelingContext()
useHotkeys(
itemConfig.hotkey || '',
() => {
@ -336,6 +344,17 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
)}
</div>
<p className="px-2 text-ch font-sans">{itemConfig.description}</p>
{/* Add disabled reason if item is disabled */}
{itemConfig.disabled && itemConfig.disabledReason && (
<>
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
<p className="px-2 text-ch font-sans text-chalkboard-70 dark:text-chalkboard-40">
{typeof itemConfig.disabledReason === 'function'
? itemConfig.disabledReason(state)
: itemConfig.disabledReason}
</p>
</>
)}
{itemConfig.links.length > 0 && (
<>
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />

View File

@ -44,6 +44,7 @@ import {
import { ActionButton } from 'components/ActionButton'
import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Node } from 'wasm-lib/kcl/bindings/Node'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false)
@ -201,7 +202,7 @@ const Overlay = ({
let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
const _node1 = getNodeFromPath<CallExpression>(
const _node1 = getNodeFromPath<Node<CallExpression>>(
kclManager.ast,
overlay.pathToNode,
'CallExpression'
@ -381,7 +382,7 @@ export async function deleteSegment({
pathToNode: PathToNode
sketchDetails: SketchDetails | null
}) {
let modifiedAst: Program | Error = kclManager.ast
let modifiedAst: Node<Program> | Error = kclManager.ast
const dependentRanges = findUsesOfTagInPipe(modifiedAst, pathToNode)
const shouldContinueSegDelete = dependentRanges.length

View File

@ -19,6 +19,8 @@ import {
import {
ARROWHEAD,
AXIS_GROUP,
DRAFT_POINT,
DRAFT_POINT_GROUP,
getSceneScale,
INTERSECTION_PLANE_LAYER,
OnClickCallbackArgs,
@ -53,7 +55,7 @@ import {
editorManager,
} from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst } from 'lang/langHelpers'
import { executeAst, ToolTip } from 'lang/langHelpers'
import {
createProfileStartHandle,
SegmentUtils,
@ -92,6 +94,7 @@ import { err, reportRejection, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
import { SegmentInputs } from 'lang/std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node'
type DraftSegment = 'line' | 'tangentialArcTo'
@ -313,6 +316,27 @@ export class SceneEntities {
const intersectionPlane = this.scene.getObjectByName(RAYCASTABLE_PLANE)
if (intersectionPlane) this.scene.remove(intersectionPlane)
}
getDraftPoint() {
return this.scene.getObjectByName(DRAFT_POINT)
}
createDraftPoint({ point, group }: { point: Vector2; group: Group }) {
const dummy = new Mesh()
dummy.position.set(0, 0, 0)
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
const draftPoint = createProfileStartHandle({
isDraft: true,
from: [point.x, point.y],
scale,
theme: sceneInfra._theme,
})
draftPoint.layers.set(SKETCH_LAYER)
group.add(draftPoint)
}
removeDraftPoint() {
const draftPoint = this.getDraftPoint()
if (draftPoint) draftPoint.removeFromParent()
}
setupNoPointsListener({
sketchDetails,
@ -321,22 +345,78 @@ export class SceneEntities {
sketchDetails: SketchDetails
afterClick: (args: OnClickCallbackArgs) => void
}) {
// Create a THREEjs plane to raycast clicks onto
// TODO: Consolidate shared logic between this and setupSketch
// Which should just fire when the sketch mode is entered,
// instead of in these two separate XState states.
this.createIntersectionPlane()
const draftPointGroup = new Group()
draftPointGroup.name = DRAFT_POINT_GROUP
sketchDetails.origin &&
draftPointGroup.position.set(...sketchDetails.origin)
if (!(sketchDetails.yAxis && sketchDetails)) {
console.error('No sketch quaternion or sketch details found')
return
}
this.currentSketchQuaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
draftPointGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
this.scene.add(draftPointGroup)
const quaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
// Position the click raycast plane
if (this.intersectionPlane) {
this.intersectionPlane.setRotationFromQuaternion(quaternion)
this.intersectionPlane.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
}
this.intersectionPlane!.setRotationFromQuaternion(quaternion)
this.intersectionPlane!.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
sceneInfra.setCallbacks({
onMove: (args) => {
if (!args.intersects.length) return
const axisIntersection = args.intersects.find(
(sceneObject) =>
sceneObject.object.name === X_AXIS ||
sceneObject.object.name === Y_AXIS
)
if (!axisIntersection) return
const { intersectionPoint } = args
// We're hovering over an axis, so we should show a draft point
const snappedPoint = intersectionPoint.twoD.clone()
if (axisIntersection.object.name === X_AXIS) {
snappedPoint.setComponent(1, 0)
} else {
snappedPoint.setComponent(0, 0)
}
// Either create a new one or update the existing one
const draftPoint = this.getDraftPoint()
if (!draftPoint) {
this.createDraftPoint({
point: snappedPoint,
group: draftPointGroup,
})
} else {
// Ignore if there are huge jumps in the mouse position,
// that is likely a strange behavior
if (
draftPoint.position.distanceTo(
new Vector3(snappedPoint.x, snappedPoint.y, 0)
) > 100
) {
return
}
draftPoint.position.set(snappedPoint.x, snappedPoint.y, 0)
}
},
onMouseLeave: () => {
this.removeDraftPoint()
},
onClick: async (args) => {
this.removeDraftPoint()
if (!args) return
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
@ -346,10 +426,25 @@ export class SceneEntities {
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return
// Snap to either or both axes
// if the click intersects their meshes
const yAxisIntersection = args.intersects.find(
(sceneObject) => sceneObject.object.name === Y_AXIS
)
const xAxisIntersection = args.intersects.find(
(sceneObject) => sceneObject.object.name === X_AXIS
)
const snappedClickPoint = {
x: yAxisIntersection ? 0 : intersectionPoint.twoD.x,
y: xAxisIntersection ? 0 : intersectionPoint.twoD.y,
}
const addStartProfileAtRes = addStartProfileAt(
kclManager.ast,
sketchDetails.sketchPathToNode,
[intersectionPoint.twoD.x, intersectionPoint.twoD.y]
[snappedClickPoint.x, snappedClickPoint.y]
)
if (trap(addStartProfileAtRes)) return
@ -357,6 +452,7 @@ export class SceneEntities {
await kclManager.updateAst(modifiedAst, false)
this.removeIntersectionPlane()
this.scene.remove(draftPointGroup)
// Now perform the caller-specified action
afterClick(args)
@ -374,14 +470,14 @@ export class SceneEntities {
selectionRanges,
}: {
sketchPathToNode: PathToNode
maybeModdedAst: Program
maybeModdedAst: Node<Program>
draftExpressionsIndices?: { start: number; end: number }
forward: [number, number, number]
up: [number, number, number]
position?: [number, number, number]
selectionRanges?: Selections
}): Promise<{
truncatedAst: Program
truncatedAst: Node<Program>
programMemoryOverride: ProgramMemory
sketch: Sketch
variableDeclarationName: string
@ -429,12 +525,7 @@ export class SceneEntities {
const dummy = new Mesh()
// TODO: When we actually have sketch positions and rotations we can use them here.
dummy.position.set(0, 0, 0)
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, dummy)) /
sceneInfra._baseUnitMultiplier
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
const segPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
@ -445,8 +536,9 @@ export class SceneEntities {
from: sketch.start.from,
id: sketch.start.__geoMeta.id,
pathToNode: segPathToNode,
scale: factor,
scale,
theme: sceneInfra._theme,
isDraft: false,
})
_profileStart.layers.set(SKETCH_LAYER)
_profileStart.traverse((child) => {
@ -522,7 +614,7 @@ export class SceneEntities {
id: segment.__geoMeta.id,
pathToNode: segPathToNode,
isDraftSegment,
scale: factor,
scale,
texture: sceneInfra.extraSegmentTexture,
theme: sceneInfra._theme,
isSelected,
@ -566,7 +658,7 @@ export class SceneEntities {
}
updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode,
modifiedAst: Program | Error,
modifiedAst: Node<Program> | Error,
forward: [number, number, number],
up: [number, number, number],
origin: [number, number, number]
@ -659,12 +751,14 @@ export class SceneEntities {
const { intersectionPoint } = args
let intersection2d = intersectionPoint?.twoD
const profileStart = args.intersects
const intersectsProfileStart = args.intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
let modifiedAst
if (profileStart) {
// Snapping logic for the profile start handle
if (intersectsProfileStart) {
const lastSegment = sketch.paths.slice(-1)[0]
modifiedAst = addCallExpressionsToPipe({
node: kclManager.ast,
@ -697,19 +791,39 @@ export class SceneEntities {
})
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
} else if (intersection2d) {
const intersectsYAxis = args.intersects.find(
(sceneObject) => sceneObject.object.name === Y_AXIS
)
const intersectsXAxis = args.intersects.find(
(sceneObject) => sceneObject.object.name === X_AXIS
)
const lastSegment = sketch.paths.slice(-1)[0]
const snappedPoint = {
x: intersectsYAxis ? 0 : intersection2d.x,
y: intersectsXAxis ? 0 : intersection2d.y,
}
let resolvedFunctionName: ToolTip = 'line'
// This might need to become its own function if we want more
// case-based logic for different segment types
if (lastSegment.type === 'TangentialArcTo') {
resolvedFunctionName = 'tangentialArcTo'
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) {
// We consider a point placed on axes or origin to be absolute
resolvedFunctionName = 'lineTo'
}
const tmp = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
input: {
type: 'straight-segment',
from: [lastSegment.to[0], lastSegment.to[1]],
to: [intersection2d.x, intersection2d.y],
to: [snappedPoint.x, snappedPoint.y],
},
fnName:
lastSegment.type === 'TangentialArcTo'
? 'tangentialArcTo'
: 'line',
fnName: resolvedFunctionName,
pathToNode: sketchPathToNode,
})
if (trap(tmp)) return Promise.reject(tmp)
@ -721,7 +835,7 @@ export class SceneEntities {
}
await kclManager.executeAstMock(modifiedAst)
if (profileStart) {
if (intersectsProfileStart) {
sceneInfra.modelingSend({ type: 'CancelSketch' })
} else {
await this.setUpDraftSegment(
@ -1202,7 +1316,7 @@ export class SceneEntities {
}
prepareTruncatedMemoryAndAst = (
sketchPathToNode: PathToNode,
ast?: Program,
ast?: Node<Program>,
draftSegment?: DraftSegment
) =>
prepareTruncatedMemoryAndAst(
@ -1223,20 +1337,35 @@ export class SceneEntities {
sketchPathToNode: PathToNode
intersects: Intersection<Object3D<Object3DEventMap>>[]
draftInfo?: {
truncatedAst: Program
truncatedAst: Node<Program>
programMemoryOverride: ProgramMemory
variableDeclarationName: string
}
}) {
const profileStart =
const intersectsProfileStart =
draftInfo &&
intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
const intersection2d = profileStart
? new Vector2(profileStart.position.x, profileStart.position.y)
const intersection2d = intersectsProfileStart
? new Vector2(
intersectsProfileStart.position.x,
intersectsProfileStart.position.y
)
: _intersection2d
const intersectsYAxis = intersects.find(
(sceneObject) => sceneObject.object.name === Y_AXIS
)
const intersectsXAxis = intersects.find(
(sceneObject) => sceneObject.object.name === X_AXIS
)
const snappedPoint = new Vector2(
intersectsYAxis ? 0 : intersection2d.x,
intersectsXAxis ? 0 : intersection2d.y
)
const group = getParentGroup(object, SEGMENT_BODIES_PLUS_PROFILE_START)
const subGroup = getParentGroup(object, [ARROWHEAD, CIRCLE_CENTER_HANDLE])
if (!group) return
@ -1256,10 +1385,10 @@ export class SceneEntities {
group.userData.from[0],
group.userData.from[1],
]
const dragTo: [number, number] = [intersection2d.x, intersection2d.y]
const dragTo: [number, number] = [snappedPoint.x, snappedPoint.y]
let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast }
const _node = getNodeFromPath<CallExpression>(
const _node = getNodeFromPath<Node<CallExpression>>(
modifiedAst,
pathToNode,
'CallExpression'
@ -1271,7 +1400,7 @@ export class SceneEntities {
let modded:
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNode: PathToNode
}
| Error
@ -1566,7 +1695,7 @@ export class SceneEntities {
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
if (trap(updatedAst)) return
const _node = getNodeFromPath<CallExpression>(
const _node = getNodeFromPath<Node<CallExpression>>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
@ -1701,12 +1830,12 @@ export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
function prepareTruncatedMemoryAndAst(
sketchPathToNode: PathToNode,
ast: Program,
ast: Node<Program>,
programMemory: ProgramMemory,
draftSegment?: DraftSegment
):
| {
truncatedAst: Program
truncatedAst: Node<Program>
programMemoryOverride: ProgramMemory
variableDeclarationName: string
}
@ -1714,7 +1843,7 @@ function prepareTruncatedMemoryAndAst(
const bodyIndex = Number(sketchPathToNode?.[1]?.[0]) || 0
const _ast = structuredClone(ast)
const _node = getNodeFromPath<VariableDeclaration>(
const _node = getNodeFromPath<Node<VariableDeclaration>>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
@ -1764,15 +1893,15 @@ function prepareTruncatedMemoryAndAst(
).body.slice(-1)[0].start = lastPipeItem.start
_ast.end = lastPipeItem.end
const varDec = _ast.body[bodyIndex] as VariableDeclaration
const varDec = _ast.body[bodyIndex] as Node<VariableDeclaration>
varDec.end = lastPipeItem.end
const declarator = varDec.declarations[0]
declarator.end = lastPipeItem.end
const init = declarator.init as PipeExpression
const init = declarator.init as Node<PipeExpression>
init.end = lastPipeItem.end
init.body.slice(-1)[0].end = lastPipeItem.end
}
const truncatedAst: Program = {
const truncatedAst: Node<Program> = {
..._ast,
body: [structuredClone(_ast.body[bodyIndex])],
}

View File

@ -30,6 +30,7 @@ import { MouseState, SegmentOverlayPayload } from 'machines/modelingMachine'
import { getAngle, throttle } from 'lib/utils'
import { Themes } from 'lib/theme'
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { orthoScale, perspScale } from './helpers'
type SendType = ReturnType<typeof useModelingContext>['send']
@ -49,6 +50,10 @@ export const RAYCASTABLE_PLANE = 'raycastable-plane'
export const X_AXIS = 'xAxis'
export const Y_AXIS = 'yAxis'
/** the THREEjs representation of the group surrounding a "snapped" point that is not yet placed */
export const DRAFT_POINT_GROUP = 'draft-point-group'
/** the THREEjs representation of a "snapped" point that is not yet placed */
export const DRAFT_POINT = 'draft-point'
export const AXIS_GROUP = 'axisGroup'
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
export const ARROWHEAD = 'arrowhead'
@ -60,6 +65,11 @@ export interface OnMouseEnterLeaveArgs {
selected: Object3D<Object3DEventMap>
dragSelected?: Object3D<Object3DEventMap>
mouseEvent: MouseEvent
/** The intersection of the mouse with the THREEjs raycast plane */
intersectionPoint?: {
twoD?: Vector2
threeD?: Vector3
}
}
interface OnDragCallbackArgs extends OnMouseEnterLeaveArgs {
@ -348,29 +358,42 @@ export class SceneInfra {
window.removeEventListener('resize', this.onWindowResize)
// Dispose of any other resources like geometries, materials, textures
}
getClientSceneScaleFactor(meshOrGroup: Mesh | Group) {
const orthoFactor = orthoScale(this.camControls.camera)
const factor =
(this.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(this.camControls.camera, meshOrGroup)) /
this._baseUnitMultiplier
return factor
}
getPlaneIntersectPoint = (): {
twoD?: Vector2
threeD?: Vector3
intersection: Intersection<Object3D<Object3DEventMap>>
} | null => {
// Get the orientations from the camera and mouse position
this.planeRaycaster.setFromCamera(
this.currentMouseVector,
this.camControls.camera
)
// Get the intersection of the ray with the default planes
const planeIntersects = this.planeRaycaster.intersectObjects(
this.scene.children,
true
)
const recastablePlaneIntersect = planeIntersects.find(
if (!planeIntersects.length) return null
// Find the intersection with the raycastable (or sketch) plane
const raycastablePlaneIntersection = planeIntersects.find(
(intersect) => intersect.object.name === RAYCASTABLE_PLANE
)
if (!planeIntersects.length) return null
if (!recastablePlaneIntersect) return { intersection: planeIntersects[0] }
const planePosition = planeIntersects[0].object.position
const inversePlaneQuaternion = planeIntersects[0].object.quaternion
.clone()
.invert()
const intersectPoint = planeIntersects[0].point
if (!raycastablePlaneIntersection)
return { intersection: planeIntersects[0] }
const planePosition = raycastablePlaneIntersection.object.position
const inversePlaneQuaternion =
raycastablePlaneIntersection.object.quaternion.clone().invert()
const intersectPoint = raycastablePlaneIntersection.point
let transformedPoint = intersectPoint.clone()
if (transformedPoint) {
transformedPoint.applyQuaternion(inversePlaneQuaternion)
@ -447,18 +470,26 @@ export class SceneInfra {
if (intersects[0]) {
const firstIntersectObject = intersects[0].object
const planeIntersectPoint = this.getPlaneIntersectPoint()
const intersectionPoint = {
twoD: planeIntersectPoint?.twoD,
threeD: planeIntersectPoint?.threeD,
}
if (this.hoveredObject !== firstIntersectObject) {
const hoveredObj = this.hoveredObject
this.hoveredObject = null
await this.onMouseLeave({
selected: hoveredObj,
mouseEvent: mouseEvent,
intersectionPoint,
})
this.hoveredObject = firstIntersectObject
await this.onMouseEnter({
selected: this.hoveredObject,
dragSelected: this.selected?.object,
mouseEvent: mouseEvent,
intersectionPoint,
})
if (!this.selected)
this.updateMouseState({

View File

@ -45,6 +45,7 @@ import {
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import {
ARROWHEAD,
DRAFT_POINT,
SceneInfra,
SEGMENT_LENGTH_LABEL,
SEGMENT_LENGTH_LABEL_OFFSET_PX,
@ -686,19 +687,20 @@ class CircleSegment implements SegmentUtils {
export function createProfileStartHandle({
from,
id,
pathToNode,
isDraft = false,
scale = 1,
theme,
isSelected,
...rest
}: {
from: Coords2d
id: string
pathToNode: PathToNode
scale?: number
theme: Themes
isSelected?: boolean
}) {
} & (
| { isDraft: true }
| { isDraft: false; id: string; pathToNode: PathToNode }
)) {
const group = new Group()
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
@ -711,13 +713,12 @@ export function createProfileStartHandle({
group.userData = {
type: PROFILE_START,
id,
from,
pathToNode,
isSelected,
baseColor,
...rest,
}
group.name = PROFILE_START
group.name = isDraft ? DRAFT_POINT : PROFILE_START
group.position.set(from[0], from[1], 0)
group.scale.set(scale, scale, scale)
return group

View File

@ -538,3 +538,19 @@ export const FileTreeInner = ({
</div>
)
}
export const FileTreeRoot = () => {
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project } = loaderData
// project.path should never be empty here but I guess during initial loading
// it can be.
return (
<div
className="max-w-xs text-ellipsis overflow-hidden cursor-pointer"
title={project?.path ?? ''}
>
{project?.name ?? ''}
</div>
)
}

View File

@ -4,7 +4,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { CustomIcon } from './CustomIcon'
import { useLocation, useNavigate } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { createAndOpenNewProject } from 'lib/desktopFS'
import { createAndOpenNewTutorialProject } from 'lib/desktopFS'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
@ -116,9 +116,10 @@ export function HelpMenu(props: React.PropsWithChildren) {
if (isInProject) {
navigate(filePath + PATHS.ONBOARDING.INDEX)
} else {
createAndOpenNewProject({ onProjectOpen, navigate }).catch(
reportRejection
)
createAndOpenNewTutorialProject({
onProjectOpen,
navigate,
}).catch(reportRejection)
}
}}
>

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 {
@ -85,6 +91,7 @@ import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
import { Node } from 'wasm-lib/kcl/bindings/Node'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -140,6 +147,8 @@ export const ModelingMachineProvider = ({
// >
// )
const machineManager = useContext(MachineManagerContext)
const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine.provide({
actions: {
@ -408,7 +417,7 @@ export const ModelingMachineProvider = ({
return {}
}
),
Make: ({ event }) => {
Make: ({ context, event }) => {
if (event.type !== 'Make') return
// Check if we already have an export intent.
if (engineCommandManager.exportInfo) {
@ -422,7 +431,21 @@ export const ModelingMachineProvider = ({
}
// 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',
@ -949,7 +972,7 @@ export const ModelingMachineProvider = ({
})
let parsed = parse(recast(kclManager.ast))
if (trap(parsed)) return Promise.reject(parsed)
parsed = parsed as Program
parsed = parsed as Node<Program>
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
moveValueIntoNewVariablePath(
@ -960,7 +983,7 @@ export const ModelingMachineProvider = ({
)
parsed = parse(recast(_modifiedAst))
if (trap(parsed)) return Promise.reject(parsed)
parsed = parsed as Program
parsed = parsed as Node<Program>
if (!pathToReplacedNode)
return Promise.reject(new Error('No path to replaced node'))
@ -995,6 +1018,7 @@ export const ModelingMachineProvider = ({
...modelingMachineDefaultContext.store,
...persistedContext,
},
machineManager,
},
// devTools: true,
}

View File

@ -1,3 +1,4 @@
import { ReactNode } from 'react'
import styles from './ModelingPane.module.css'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { ActionButton } from 'components/ActionButton'
@ -6,22 +7,24 @@ import { CustomIconName } from 'components/CustomIcon'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from 'components/ActionIcon'
export interface ModelingPaneProps
extends React.PropsWithChildren,
React.HTMLAttributes<HTMLDivElement> {
export interface ModelingPaneProps {
id: string
children: ReactNode | ReactNode[]
className?: string
icon?: CustomIconName | IconDefinition
title: string
title: ReactNode
Menu?: React.ReactNode | React.FC
detailsTestId?: string
onClose: () => void
}
export const ModelingPaneHeader = ({
id,
icon,
title,
Menu,
onClose,
}: Pick<ModelingPaneProps, 'icon' | 'title' | 'Menu' | 'onClose'>) => {
}: Pick<ModelingPaneProps, 'id' | 'icon' | 'title' | 'Menu' | 'onClose'>) => {
return (
<div className={styles.header}>
<div className="flex gap-2 items-center flex-1">
@ -34,7 +37,7 @@ export const ModelingPaneHeader = ({
bgClassName="!bg-transparent"
/>
)}
<span>{title}</span>
<span data-testid={id + '-header'}>{title}</span>
</div>
{Menu instanceof Function ? <Menu /> : Menu}
<ActionButton
@ -86,6 +89,7 @@ export const ModelingPane = ({
}
>
<ModelingPaneHeader
id={id}
icon={icon}
title={title}
Menu={Menu}

View File

@ -88,8 +88,12 @@ export const MemoryPane = () => {
export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {}
for (const [key, val] of programMemory?.visibleEntries()) {
if (typeof val.value !== 'function') {
const sg = sketchFromKclValue(val, null)
if (
(val.type === 'UserVal' && val.value.type === 'Sketch') ||
// @ts-ignore
(val.type !== 'Function' && val.type !== 'UserVal')
) {
const sg = sketchFromKclValue(val, key)
if (val.type === 'Solid') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
@ -98,15 +102,16 @@ export const processMemory = (programMemory: ProgramMemory) => {
processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if ((val.type as any) === 'Function') {
processedMemory[key] = `__function(${(val as any)?.expression?.params
?.map?.(({ identifier }: any) => identifier?.name || '')
.join(', ')})__`
} else {
processedMemory[key] = val.value
}
} else if (key !== 'log') {
processedMemory[key] = '__function__'
//@ts-ignore
} else if (val.type === 'Function') {
processedMemory[key] = `__function(${(val as any)?.expression?.params
?.map?.(({ identifier }: any) => identifier?.name || '')
.join(', ')})__`
} else {
processedMemory[key] = val.value
}
}
return processedMemory

View File

@ -6,7 +6,7 @@ import { MouseEventHandler, ReactNode } from 'react'
import { MemoryPane, MemoryPaneMenu } from './MemoryPane'
import { LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
import { FileTreeInner, FileTreeMenu, FileTreeRoot } from 'components/FileTree'
import { useKclContext } from 'lang/KclProvider'
import { editorManager } from 'lib/singletons'
import { ContextFrom } from 'xstate'
@ -38,7 +38,8 @@ interface PaneCallbackProps {
export type SidebarPane = {
id: SidebarType
title: string
title: ReactNode
sidebarName?: string
icon: CustomIconName | IconDefinition
keybinding: string
Content: ReactNode | React.FC
@ -49,7 +50,7 @@ export type SidebarPane = {
export type SidebarAction = {
id: string
title: string
title: ReactNode
icon: CustomIconName
iconClassName?: string // Just until we get rid of FontAwesome icons
keybinding: string
@ -78,7 +79,8 @@ export const sidebarPanes: SidebarPane[] = [
},
{
id: 'files',
title: 'Project Files',
title: <FileTreeRoot />,
sidebarName: 'Project Files',
icon: 'folder',
Content: FileTreeInner,
keybinding: 'Shift + F',

View File

@ -1,6 +1,13 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable'
import { MouseEventHandler, useCallback, useEffect, useMemo } from 'react'
import {
MouseEventHandler,
useCallback,
useEffect,
useMemo,
ReactNode,
useContext,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
import Tooltip from 'components/Tooltip'
@ -13,7 +20,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 +36,7 @@ function getPlatformString(): 'web' | 'desktop' {
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext()
const { settings } = useSettingsAuthContext()
@ -263,7 +271,8 @@ interface ModelingPaneButtonProps
extends React.HTMLAttributes<HTMLButtonElement> {
paneConfig: {
id: string
title: string
title: ReactNode
sidebarName?: string
icon: CustomIconName | IconDefinition
keybinding: string
iconClassName?: string
@ -292,7 +301,10 @@ function ModelingPaneButton({
<button
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
onClick={onClick}
name={paneConfig.title}
name={
paneConfig.sidebarName ??
(typeof paneConfig.title === 'string' ? paneConfig.title : '')
}
data-testid={paneConfig.id + '-pane-button'}
disabled={disabledText !== undefined}
aria-disabled={disabledText !== undefined}
@ -308,7 +320,7 @@ function ModelingPaneButton({
}
/>
<span className="sr-only">
{paneConfig.title}
{paneConfig.sidebarName ?? paneConfig.title}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<Tooltip
@ -317,7 +329,7 @@ function ModelingPaneButton({
hoverOnly
>
<span className="flex-1">
{paneConfig.title}
{paneConfig.sidebarName ?? paneConfig.title}
{disabledText !== undefined ? ` (${disabledText})` : ''}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>

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,9 +11,12 @@ export const NetworkMachineIndicator = ({
}: {
className?: string
}) => {
const machineCount = machineManager.machineCount()
const reason = machineManager.noMachinesReason()
const machines = machineManager.machines
const {
noMachinesReason,
machines,
machines: { length: machineCount },
} = useContext(MachineManagerContext)
const reason = noMachinesReason()
return isDesktop() ? (
<Popover className="relative">
@ -47,34 +52,36 @@ export const NetworkMachineIndicator = ({
</div>
{machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{machines.map((machine) => {
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>
)
})}
{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>
)}
</Popover.Panel>

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

@ -0,0 +1,289 @@
import { useMachine } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { projectsMachine } from 'machines/projectsMachine'
import { createContext, useEffect, useState } from 'react'
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
import { useLspContext } from './LspProvider'
import toast from 'react-hot-toast'
import { useLocation, useNavigate } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import {
createNewProjectDirectory,
listProjects,
renameProjectDirectory,
} from 'lib/desktop'
import {
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
} from 'lib/desktopFS'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
import { isDesktop } from 'lib/isDesktop'
type MachineContext<T extends AnyStateMachine> = {
state?: StateFrom<T>
send: Prop<Actor<T>, 'send'>
}
export const ProjectsMachineContext = createContext(
{} as MachineContext<typeof projectsMachine>
)
/**
* Watches the project directory and provides project management-related commands,
* like "Create project", "Open project", "Delete project", etc.
*
* If in the future we implement full-fledge project management in the web version,
* we can unify these components but for now, we need this to be only for the desktop version.
*/
export const ProjectsContextProvider = ({
children,
}: {
children: React.ReactNode
}) => {
return isDesktop() ? (
<ProjectsContextDesktop>{children}</ProjectsContextDesktop>
) : (
<ProjectsContextWeb>{children}</ProjectsContextWeb>
)
}
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
return (
<ProjectsMachineContext.Provider
value={{
state: undefined,
send: () => {},
}}
>
{children}
</ProjectsMachineContext.Provider>
)
}
const ProjectsContextDesktop = ({
children,
}: {
children: React.ReactNode
}) => {
const navigate = useNavigate()
const location = useLocation()
const { commandBarSend } = useCommandsContext()
const { onProjectOpen } = useLspContext()
const {
settings: { context: settings },
} = useSettingsAuthContext()
useEffect(() => {
console.log(
'project directory changed',
settings.app.projectDirectory.current
)
}, [settings.app.projectDirectory.current])
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectPaths, projectsDir } = useProjectsLoader([
projectsLoaderTrigger,
])
// Re-read projects listing if the projectDir has any updates.
useFileSystemWatcher(
async () => {
return setProjectsLoaderTrigger(projectsLoaderTrigger + 1)
},
projectsDir ? [projectsDir] : []
)
const [state, send, actor] = useMachine(
projectsMachine.provide({
actions: {
navigateToProject: ({ context, event }) => {
const nameFromEventData =
'data' in event &&
event.data &&
'name' in event.data &&
event.data.name
const nameFromOutputData =
'output' in event &&
event.output &&
'name' in event.output &&
event.output.name
const name = nameFromEventData || nameFromOutputData
if (name) {
let projectPath =
context.defaultDirectory + window.electron.path.sep + name
onProjectOpen(
{
name,
path: projectPath,
},
null
)
commandBarSend({ type: 'Close' })
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
projectPath
)}`
navigate(newPathName)
}
},
navigateToProjectIfNeeded: ({ event }) => {
if (
event.type.startsWith('xstate.done.actor.') &&
'output' in event
) {
const isInAProject = location.pathname.startsWith(PATHS.FILE)
const isInDeletedProject =
event.type === 'xstate.done.actor.delete-project' &&
isInAProject &&
decodeURIComponent(location.pathname).includes(event.output.name)
if (isInDeletedProject) {
navigate(PATHS.HOME)
return
}
const isInRenamedProject =
event.type === 'xstate.done.actor.rename-project' &&
isInAProject &&
decodeURIComponent(location.pathname).includes(
event.output.oldName
)
if (isInRenamedProject) {
// TODO: In future, we can navigate to the new project path
// directly, but we need to coordinate with
// @lf94's useFileSystemWatcher in SettingsAuthProvider.tsx:224
// Because it's beating us to the punch and updating the route
// const newPathName = location.pathname.replace(
// encodeURIComponent(event.output.oldName),
// encodeURIComponent(event.output.newName)
// )
// navigate(newPathName)
return
}
}
},
toastSuccess: ({ event }) =>
toast.success(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
'message' in event.output &&
typeof event.output.message === 'string' &&
event.output.message) ||
''
),
toastError: ({ event }) =>
toast.error(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
typeof event.output === 'string' &&
event.output) ||
''
),
},
actors: {
readProjects: fromPromise(() => listProjects()),
createProject: fromPromise(async ({ input }) => {
let name = (
input && 'name' in input && input.name
? input.name
: settings.projects.defaultProjectName.current
).trim()
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = getNextProjectIndex(name, input.projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProjectDirectory(name)
return {
message: `Successfully created "${name}"`,
name,
}
}),
renameProject: fromPromise(async ({ input }) => {
const {
oldName,
newName,
defaultProjectName,
defaultDirectory,
projects,
} = input
let name = newName ? newName : defaultProjectName
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
console.log('from Project')
await renameProjectDirectory(
window.electron.path.join(defaultDirectory, oldName),
name
)
return {
message: `Successfully renamed "${oldName}" to "${name}"`,
oldName: oldName,
newName: name,
}
}),
deleteProject: fromPromise(async ({ input }) => {
await window.electron.rm(
window.electron.path.join(input.defaultDirectory, input.name),
{
recursive: true,
}
)
return {
message: `Successfully deleted "${input.name}"`,
name: input.name,
}
}),
},
guards: {
'Has at least 1 project': ({ event }) => {
if (event.type !== 'xstate.done.actor.read-projects') return false
console.log(`from has at least 1 project: ${event.output.length}`)
return event.output.length ? event.output.length >= 1 : false
},
},
}),
{
input: {
projects: projectPaths,
defaultProjectName: settings.projects.defaultProjectName.current,
defaultDirectory: settings.app.projectDirectory.current,
},
}
)
useEffect(() => {
send({ type: 'Read projects', data: {} })
}, [projectPaths])
// register all project-related command palette commands
useStateMachineCommands({
machineId: 'projects',
send,
state,
commandBarConfig: projectsCommandBarConfig,
actor,
})
return (
<ProjectsMachineContext.Provider
value={{
state,
send,
}}
>
{children}
</ProjectsMachineContext.Provider>
)
}

View File

@ -15,7 +15,10 @@ import { SettingsFieldInput } from './SettingsFieldInput'
import toast from 'react-hot-toast'
import { APP_VERSION } from 'routes/Settings'
import { PATHS } from 'lib/paths'
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
import {
createAndOpenNewTutorialProject,
getSettingsFolderPaths,
} from 'lib/desktopFS'
import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { ForwardedRef, forwardRef, useEffect } from 'react'
import { useLspContext } from 'components/LspProvider'
@ -79,7 +82,7 @@ export const AllSettingsFields = forwardRef(
} else {
// If we're in the global settings, create a new project and navigate
// to the onboarding start in that project
await createAndOpenNewProject({ onProjectOpen, navigate })
await createAndOpenNewTutorialProject({ onProjectOpen, navigate })
}
}
}

View File

@ -14,6 +14,7 @@ import {
import { TransformInfo } from 'lang/std/stdTypes'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export function setEqualLengthInfo({
selectionRanges,
@ -86,7 +87,7 @@ export function applyConstraintEqualLength({
selectionRanges: Selections
}):
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNodeMap: PathToNodeMap
}
| Error {

View File

@ -13,6 +13,7 @@ import {
import { TransformInfo } from 'lang/std/stdTypes'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export function horzVertInfo(
selectionRanges: Selections,
@ -55,11 +56,11 @@ export function horzVertInfo(
export function applyConstraintHorzVert(
selectionRanges: Selections,
horOrVert: 'vertical' | 'horizontal',
ast: Program,
ast: Node<Program>,
programMemory: ProgramMemory
):
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNodeMap: PathToNodeMap
}
| Error {

View File

@ -19,6 +19,7 @@ import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
const getModalInfo = createInfoModal(GetInfoModal)
@ -136,7 +137,7 @@ export async function applyConstraintIntersect({
}: {
selectionRanges: Selections
}): Promise<{
modifiedAst: Program
modifiedAst: Node<Program>
pathToNodeMap: PathToNodeMap
}> {
const info = intersectInfo({

View File

@ -13,6 +13,7 @@ import {
import { TransformInfo } from 'lang/std/stdTypes'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export function removeConstrainingValuesInfo({
selectionRanges,
@ -77,7 +78,7 @@ export function applyRemoveConstrainingValues({
pathToNodes?: Array<PathToNode>
}):
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNodeMap: PathToNodeMap
}
| Error {

View File

@ -23,6 +23,7 @@ import {
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
@ -161,7 +162,7 @@ export function applyConstraintAxisAlign({
constraint: 'snapToYAxis' | 'snapToXAxis'
}):
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNodeMap: PathToNodeMap
}
| Error {

View File

@ -18,6 +18,7 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lib/singletons'
import { Selections } from 'lib/selections'
import { cleanErrs, err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
const getModalInfo = createInfoModal(GetInfoModal)
@ -185,7 +186,7 @@ export function applyConstraintHorzVertAlign({
constraint: 'setHorzDistance' | 'setVertDistance'
}):
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNodeMap: PathToNodeMap
}
| Error {

View File

@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import UserSidebarMenu from './UserSidebarMenu'
import {
Route,
@ -13,7 +13,7 @@ import { CommandBarProvider } from './CommandBar/CommandBarProvider'
type User = Models['User_type']
describe('UserSidebarMenu tests', () => {
test("Renders user's name and email if available", () => {
test("Renders user's name and email if available", async () => {
const userWellFormed: User = {
id: '8675309',
name: 'Test User',
@ -39,13 +39,19 @@ describe('UserSidebarMenu tests', () => {
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
expect(screen.getByTestId('username')).toHaveTextContent(
userWellFormed.name || ''
)
expect(screen.getByTestId('email')).toHaveTextContent(userWellFormed.email)
await waitFor(() => {
expect(screen.getByTestId('username')).toHaveTextContent(
userWellFormed.name || ''
)
})
await waitFor(() => {
expect(screen.getByTestId('email')).toHaveTextContent(
userWellFormed.email
)
})
})
test("Renders just the user's email if no name is available", () => {
test("Renders just the user's email if no name is available", async () => {
const userNoName: User = {
id: '8675309',
email: 'kittycad.sidebar.test@example.com',
@ -71,10 +77,12 @@ describe('UserSidebarMenu tests', () => {
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
expect(screen.getByTestId('username')).toHaveTextContent(userNoName.email)
await waitFor(() => {
expect(screen.getByTestId('username')).toHaveTextContent(userNoName.email)
})
})
test('Renders a menu button if no user avatar is available', () => {
test('Renders a menu button if no user avatar is available', async () => {
const userNoAvatar: User = {
id: '8675309',
name: 'Test User',
@ -98,9 +106,11 @@ describe('UserSidebarMenu tests', () => {
</TestWrap>
)
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent(
'User menu'
)
await waitFor(() => {
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent(
'User menu'
)
})
})
})

View File

@ -0,0 +1,6 @@
import { ProjectsMachineContext } from 'components/ProjectsContextProvider'
import { useContext } from 'react'
export const useProjectsContext = () => {
return useContext(ProjectsMachineContext)
}

View File

@ -5,7 +5,7 @@ import { useCommandsContext } from './useCommandsContext'
import { modelingMachine } from 'machines/modelingMachine'
import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine'
import { projectsMachine } from 'machines/projectsMachine'
import {
Command,
StateMachineCommandSetConfig,
@ -22,7 +22,7 @@ export type AllMachines =
| typeof modelingMachine
| typeof settingsMachine
| typeof authMachine
| typeof homeMachine
| typeof projectsMachine
interface UseStateMachineCommandsArgs<
T extends AllMachines,

View File

@ -21,9 +21,10 @@ import {
import { getNodeFromPath } from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
import { Node } from 'wasm-lib/kcl/bindings/Node'
interface ExecuteArgs {
ast?: Program
ast?: Node<Program>
zoomToFit?: boolean
executionId?: number
zoomOnRangeAndType?: {
@ -33,13 +34,13 @@ interface ExecuteArgs {
}
export class KclManager {
private _ast: Program = {
private _ast: Node<Program> = {
body: [],
start: 0,
end: 0,
nonCodeMeta: {
nonCodeNodes: {},
start: [],
startNodes: [],
},
}
private _execState: ExecState = emptyExecState()
@ -55,7 +56,7 @@ export class KclManager {
engineCommandManager: EngineCommandManager
private _isExecutingCallback: (arg: boolean) => void = () => {}
private _astCallBack: (arg: Program) => void = () => {}
private _astCallBack: (arg: Node<Program>) => void = () => {}
private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {}
private _logsCallBack: (arg: string[]) => void = () => {}
private _kclErrorsCallBack: (arg: KCLError[]) => void = () => {}
@ -181,7 +182,7 @@ export class KclManager {
setWasmInitFailed,
}: {
setProgramMemory: (arg: ProgramMemory) => void
setAst: (arg: Program) => void
setAst: (arg: Node<Program>) => void
setLogs: (arg: string[]) => void
setKclErrors: (arg: KCLError[]) => void
setIsExecuting: (arg: boolean) => void
@ -205,12 +206,12 @@ export class KclManager {
end: 0,
nonCodeMeta: {
nonCodeNodes: {},
start: [],
startNodes: [],
},
}
}
safeParse(code: string): Program | null {
safeParse(code: string): Node<Program> | null {
const ast = parse(code)
this.lints = []
this.kclErrors = []
@ -377,7 +378,7 @@ export class KclManager {
Array.from(this.engineCommandManager.artifactGraph).forEach(
([commandId, artifact]) => {
if (!('codeRef' in artifact)) return
const _node1 = getNodeFromPath<CallExpression>(
const _node1 = getNodeFromPath<Node<CallExpression>>(
this.ast,
artifact.codeRef.pathToNode,
'CallExpression'
@ -441,7 +442,7 @@ export class KclManager {
// but should probably have think about which of the function to keep
// This always updates the code state and editor and writes to the file system.
async updateAst(
ast: Program,
ast: Node<Program>,
execute: boolean,
optionalParams?: {
focusPath?: Array<PathToNode>
@ -452,7 +453,7 @@ export class KclManager {
}
}
): Promise<{
newAst: Program
newAst: Node<Program>
selections?: Selections
}> {
const newCode = recast(ast)
@ -588,7 +589,7 @@ export class KclManager {
}
// Determines if there is no KCL code which means it is executing a blank KCL file
_isAstEmpty(ast: Program) {
_isAstEmpty(ast: Node<Program>) {
return ast.start === 0 && ast.end === 0 && ast.body.length === 0
}
}

View File

@ -12,6 +12,7 @@ import { EngineCommandManager } from 'lang/std/engineConnection'
import { KCLError } from 'lang/errors'
import { Diagnostic } from '@codemirror/lint'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ToolTip =
| 'lineTo'
@ -52,7 +53,7 @@ export async function executeAst({
programMemoryOverride,
idGenerator,
}: {
ast: Program
ast: Node<Program>
engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean
programMemoryOverride?: ProgramMemory

View File

@ -21,6 +21,7 @@ import { enginelessExecutor } from '../lib/testHelpers'
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
import { err } from 'lib/trap'
import { SimplifiedArgDetails } from './std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node'
beforeAll(async () => {
await initPromise
@ -109,7 +110,7 @@ describe('Testing findUniqueName', () => {
{ 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[]),
] satisfies Node<Identifier>[]),
'yo',
2
)
@ -123,7 +124,7 @@ describe('Testing addSketchTo', () => {
body: [],
start: 0,
end: 0,
nonCodeMeta: { nonCodeNodes: {}, start: [] },
nonCodeMeta: { nonCodeNodes: {}, startNodes: [] },
},
'yz'
)

View File

@ -42,12 +42,13 @@ import { SimplifiedArgDetails } from './std/stdTypes'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export function startSketchOnDefault(
node: Program,
node: Node<Program>,
axis: DefaultPlaneStr,
name = ''
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
): { modifiedAst: Node<Program>; id: string; pathToNode: PathToNode } {
const _node = { ...node }
const _name =
name || findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SKETCH)
@ -76,10 +77,10 @@ export function startSketchOnDefault(
}
export function addStartProfileAt(
node: Program,
node: Node<Program>,
pathToNode: PathToNode,
at: [number, number]
): { modifiedAst: Program; pathToNode: PathToNode } | Error {
): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error {
const _node1 = getNodeFromPath<VariableDeclaration>(
node,
pathToNode,
@ -114,7 +115,7 @@ export function addStartProfileAt(
}
export function addSketchTo(
node: Program,
node: Node<Program>,
axis: 'xy' | 'xz' | 'yz',
name = ''
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
@ -210,7 +211,7 @@ export function mutateArrExp(node: Expr, updateWith: ArrayExpression): boolean {
export function mutateObjExpProp(
node: Expr,
updateWith: Literal | ArrayExpression,
updateWith: Node<Literal> | Node<ArrayExpression>,
key: string
): boolean {
if (node.type === 'ObjectExpression') {
@ -248,13 +249,13 @@ export function mutateObjExpProp(
}
export function extrudeSketch(
node: Program,
node: Node<Program>,
pathToNode: PathToNode,
shouldPipe = false,
distance: Expr = createLiteral(4)
):
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNode: PathToNode
pathToExtrudeArg: PathToNode
}
@ -343,13 +344,13 @@ export function extrudeSketch(
}
export function revolveSketch(
node: Program,
node: Node<Program>,
pathToNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4)
):
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNode: PathToNode
pathToRevolveArg: PathToNode
}
@ -439,7 +440,7 @@ export function revolveSketch(
}
export function sketchOnExtrudedFace(
node: Program,
node: Node<Program>,
sketchPathToNode: PathToNode,
extrudePathToNode: PathToNode,
info: ExtrudeFacePlane['faceInfo'] = { type: 'wall' }
@ -571,7 +572,7 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
}
export function createLiteral(value: string | number): Literal {
export function createLiteral(value: string | number): Node<Literal> {
return {
type: 'Literal',
start: 0,
@ -581,7 +582,7 @@ export function createLiteral(value: string | number): Literal {
}
}
export function createTagDeclarator(value: string): TagDeclarator {
export function createTagDeclarator(value: string): Node<TagDeclarator> {
return {
type: 'TagDeclarator',
start: 0,
@ -591,7 +592,7 @@ export function createTagDeclarator(value: string): TagDeclarator {
}
}
export function createIdentifier(name: string): Identifier {
export function createIdentifier(name: string): Node<Identifier> {
return {
type: 'Identifier',
start: 0,
@ -601,7 +602,7 @@ export function createIdentifier(name: string): Identifier {
}
}
export function createPipeSubstitution(): PipeSubstitution {
export function createPipeSubstitution(): Node<PipeSubstitution> {
return {
type: 'PipeSubstitution',
start: 0,
@ -612,7 +613,7 @@ export function createPipeSubstitution(): PipeSubstitution {
export function createCallExpressionStdLib(
name: string,
args: CallExpression['arguments']
): CallExpression {
): Node<CallExpression> {
return {
type: 'CallExpression',
start: 0,
@ -632,7 +633,7 @@ export function createCallExpressionStdLib(
export function createCallExpression(
name: string,
args: CallExpression['arguments']
): CallExpression {
): Node<CallExpression> {
return {
type: 'CallExpression',
start: 0,
@ -651,7 +652,7 @@ export function createCallExpression(
export function createArrayExpression(
elements: ArrayExpression['elements']
): ArrayExpression {
): Node<ArrayExpression> {
return {
type: 'ArrayExpression',
start: 0,
@ -664,7 +665,7 @@ export function createArrayExpression(
export function createPipeExpression(
body: PipeExpression['body']
): PipeExpression {
): Node<PipeExpression> {
return {
type: 'PipeExpression',
start: 0,
@ -680,7 +681,7 @@ export function createVariableDeclaration(
init: VariableDeclarator['init'],
visibility: VariableDeclaration['visibility'] = 'default',
kind: VariableDeclaration['kind'] = 'const'
): VariableDeclaration {
): Node<VariableDeclaration> {
return {
type: 'VariableDeclaration',
start: 0,
@ -703,7 +704,7 @@ export function createVariableDeclaration(
export function createObjectExpression(properties: {
[key: string]: Expr
}): ObjectExpression {
}): Node<ObjectExpression> {
return {
type: 'ObjectExpression',
start: 0,
@ -724,7 +725,7 @@ export function createObjectExpression(properties: {
export function createUnaryExpression(
argument: UnaryExpression['argument'],
operator: UnaryExpression['operator'] = '-'
): UnaryExpression {
): Node<UnaryExpression> {
return {
type: 'UnaryExpression',
start: 0,
@ -739,7 +740,7 @@ export function createBinaryExpression([left, operator, right]: [
BinaryExpression['left'],
BinaryExpression['operator'],
BinaryExpression['right']
]): BinaryExpression {
]): Node<BinaryExpression> {
return {
type: 'BinaryExpression',
start: 0,
@ -754,19 +755,19 @@ export function createBinaryExpression([left, operator, right]: [
export function createBinaryExpressionWithUnary([left, right]: [
BinaryExpression['left'],
BinaryExpression['right']
]): BinaryExpression {
]): Node<BinaryExpression> {
if (right.type === 'UnaryExpression' && right.operator === '-')
return createBinaryExpression([left, '-', right.argument])
return createBinaryExpression([left, '+', right])
}
export function giveSketchFnCallTag(
ast: Program,
ast: Node<Program>,
range: Selection['range'],
tag?: string
):
| {
modifiedAst: Program
modifiedAst: Node<Program>
tag: string
isTagExisting: boolean
pathToNode: PathToNode
@ -801,7 +802,7 @@ export function giveSketchFnCallTag(
}
export function moveValueIntoNewVariablePath(
ast: Program,
ast: Node<Program>,
programMemory: ProgramMemory,
pathToNode: PathToNode,
variableName: string
@ -834,12 +835,12 @@ export function moveValueIntoNewVariablePath(
}
export function moveValueIntoNewVariable(
ast: Program,
ast: Node<Program>,
programMemory: ProgramMemory,
sourceRange: Selection['range'],
variableName: string
): {
modifiedAst: Program
modifiedAst: Node<Program>
pathToReplacedNode?: PathToNode
} {
const meta = isNodeSafeToReplace(ast, sourceRange)
@ -872,17 +873,17 @@ export function moveValueIntoNewVariable(
*/
export function deleteSegmentFromPipeExpression(
dependentRanges: SourceRange[],
modifiedAst: Program,
modifiedAst: Node<Program>,
programMemory: ProgramMemory,
code: string,
pathToNode: PathToNode
): Program | Error {
): Node<Program> | Error {
let _modifiedAst = structuredClone(modifiedAst)
dependentRanges.forEach((range) => {
const path = getNodePathFromSourceRange(_modifiedAst, range)
const callExp = getNodeFromPath<CallExpression>(
const callExp = getNodeFromPath<Node<CallExpression>>(
_modifiedAst,
path,
'CallExpression',
@ -928,11 +929,11 @@ export function deleteSegmentFromPipeExpression(
export function removeSingleConstraintInfo(
pathToCallExp: PathToNode,
argDetails: SimplifiedArgDetails,
ast: Program,
ast: Node<Program>,
programMemory: ProgramMemory
):
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNodeMap: PathToNodeMap
}
| false {
@ -954,12 +955,12 @@ export function removeSingleConstraintInfo(
}
export async function deleteFromSelection(
ast: Program,
ast: Node<Program>,
selection: Selection,
programMemory: ProgramMemory,
getFaceDetails: (id: string) => Promise<Models['FaceIsPlanar_type']> = () =>
({} as any)
): Promise<Program | Error> {
): Promise<Node<Program> | Error> {
const astClone = structuredClone(ast)
const range = selection.range
const path = getNodePathFromSourceRange(ast, range)
@ -1134,5 +1135,5 @@ export async function deleteFromSelection(
}
const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, start: [] }
return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 }
}

View File

@ -36,11 +36,12 @@ import {
getSweepFromSuspectedPath,
} from 'lang/std/artifactGraph'
import { kclManager, engineCommandManager, editorManager } from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node'
// Apply Fillet To Selection
export function applyFilletToSelection(
ast: Program,
ast: Node<Program>,
selection: Selections,
radius: KclCommandValue
): void | Error {
@ -55,10 +56,10 @@ export function applyFilletToSelection(
}
export function modifyAstCloneWithFilletAndTag(
ast: Program,
ast: Node<Program>,
selection: Selections,
radius: KclCommandValue
): { modifiedAst: Program; pathToFilletNode: Array<PathToNode> } | Error {
): { modifiedAst: Node<Program>; pathToFilletNode: Array<PathToNode> } | Error {
let clonedAst = structuredClone(ast)
const clonedAstForGetExtrude = structuredClone(ast)
@ -246,7 +247,7 @@ export function getPathToExtrudeForSegmentSelection(
}
async function updateAstAndFocus(
modifiedAst: Program,
modifiedAst: Node<Program>,
pathToFilletNode: Array<PathToNode>
) {
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
@ -258,7 +259,7 @@ async function updateAstAndFocus(
}
function mutateAstWithTagForSketchSegment(
astClone: Program,
astClone: Node<Program>,
pathToSegmentNode: PathToNode
): { modifiedAst: Program; tag: string } | Error {
const segmentNode = getNodeFromPath<CallExpression>(
@ -292,7 +293,7 @@ function mutateAstWithTagForSketchSegment(
function getEdgeTagCall(
tag: string,
selectionType: string
): Identifier | CallExpression {
): Node<Identifier | CallExpression> {
let tagCall: Expr = createIdentifier(tag)
// Modify the tag based on selectionType
@ -426,7 +427,7 @@ export const hasValidFilletSelection = ({
code,
}: {
selectionRanges: Selections
ast: Program
ast: Node<Program>
code: string
}) => {
// check if there is anything filletable in the scene
@ -454,7 +455,7 @@ export const hasValidFilletSelection = ({
for (const selection of selectionRanges.codeBasedSelections) {
// check if all selections are in sketchLineHelperMap
const path = getNodePathFromSourceRange(ast, selection.range)
const segmentNode = getNodeFromPath<CallExpression>(
const segmentNode = getNodeFromPath<Node<CallExpression>>(
ast,
path,
'CallExpression'
@ -534,7 +535,7 @@ export const isTagUsedInFillet = ({
ast,
callExp,
}: {
ast: Program
ast: Node<Program>
callExp: CallExpression
}): Array<EdgeTypes> => {
const tag = getTagFromCallExpression(callExp)

View File

@ -29,6 +29,7 @@ import {
} from './std/sketchcombos'
import { err } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node'
/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -121,12 +122,13 @@ export function getNodeFromPathCurry(
}
function moreNodePathFromSourceRange(
node:
node: Node<
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement,
| ReturnStatement
>,
sourceRange: Selection['range'],
previousPath: PathToNode = [['body', '']]
): PathToNode {
@ -344,15 +346,16 @@ export function getNodePathFromSourceRange(
return path
}
type KCLNode =
type KCLNode = Node<
| Expr
| ExpressionStatement
| VariableDeclaration
| VariableDeclarator
| ReturnStatement
>
export function traverse(
node: KCLNode | Program,
node: KCLNode | Node<Program>,
option: {
enter?: (node: KCLNode, pathToNode: PathToNode) => void
leave?: (node: KCLNode) => void
@ -512,9 +515,9 @@ export function findAllPreviousVariables(
}
type ReplacerFn = (
_ast: Program,
_ast: Node<Program>,
varName: string
) => { modifiedAst: Program; pathToReplaced: PathToNode } | Error
) => { modifiedAst: Node<Program>; pathToReplaced: PathToNode } | Error
export function isNodeSafeToReplacePath(
ast: Program,
@ -583,12 +586,12 @@ export function isNodeSafeToReplacePath(
}
export function isNodeSafeToReplace(
ast: Program,
ast: Node<Program>,
sourceRange: [number, number]
):
| {
isSafe: boolean
value: Expr
value: Node<Expr>
replacer: ReplacerFn
}
| Error {
@ -837,7 +840,7 @@ export function findUsesOfTagInPipe(
? String(thirdParam.value)
: thirdParam.name
const varDec = getNodeFromPath<VariableDeclaration>(
const varDec = getNodeFromPath<Node<VariableDeclaration>>(
ast,
pathToNode,
'VariableDeclaration'
@ -898,7 +901,7 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
}
/** File must contain at least one sketch that has not been extruded already */
export function doesSceneHaveSweepableSketch(ast: Program) {
export function doesSceneHaveSweepableSketch(ast: Node<Program>) {
const theMap: any = {}
traverse(ast as any, {
enter(node) {

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
@ -1415,6 +1416,9 @@ export class EngineCommandManager extends EventTarget {
(() => {}) as any
kclManager: null | KclManager = null
// The current "manufacturing machine" aka 3D printer, CNC, etc.
public machineManager: MachineManager | null = null
set exportInfo(info: ExportInfo | null) {
this._exportInfo = info
}
@ -1630,10 +1634,16 @@ export class EngineCommandManager extends EventTarget {
break
}
case ExportIntent.Make: {
if (!this.machineManager) {
console.warn('Some how, no manufacturing machine is selected.')
break
}
exportMake(
event.data,
this.exportInfo.name,
this.pendingExport.toastId
this.pendingExport.toastId,
this.machineManager
).then((result) => {
if (result) {
this.pendingExport?.resolve(null)

View File

@ -17,6 +17,7 @@ import {
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { enginelessExecutor } from '../../lib/testHelpers'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
const eachQuad: [number, [number, number]][] = [
[-315, [1, 1]],
@ -687,7 +688,7 @@ describe('testing getConstraintInfo', () => {
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<CallExpression>(
const callExp = getNodeFromPath<Node<CallExpression>>(
ast,
pathToNode,
'CallExpression'
@ -841,7 +842,7 @@ describe('testing getConstraintInfo', () => {
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<CallExpression>(
const callExp = getNodeFromPath<Node<CallExpression>>(
ast,
pathToNode,
'CallExpression'
@ -1197,7 +1198,7 @@ describe('testing getConstraintInfo', () => {
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<CallExpression>(
const callExp = getNodeFromPath<Node<CallExpression>>(
ast,
pathToNode,
'CallExpression'

View File

@ -55,6 +55,7 @@ import { err } from 'lib/trap'
import { perpendicularDistance } from 'sketch-helpers'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { EdgeCutInfo } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node'
const STRAIGHT_SEGMENT_ERR = new Error(
'Invalid input, expected "straight-segment"'
@ -1785,7 +1786,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
)
}
if (intersectTag !== -1) {
const tag = firstArg.properties[intersectTag]?.value as Identifier
const tag = firstArg.properties[intersectTag]?.value as Node<Identifier>
const pathToTagProp: PathToNode = [
...pathToObjectExp,
[intersectTag, 'index'],
@ -1825,7 +1826,9 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
body: [],
nonCodeMeta: {
start: [],
start: 0,
end: 0,
startNodes: [],
nonCodeNodes: [],
},
},
@ -1865,7 +1868,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
} as const
export function changeSketchArguments(
node: Program,
node: Node<Program>,
programMemory: ProgramMemory,
sourceRangeOrPath:
| {
@ -1877,7 +1880,7 @@ export function changeSketchArguments(
pathToNode: PathToNode
},
input: SegmentInputs
): { modifiedAst: Program; pathToNode: PathToNode } | Error {
): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error {
const _node = { ...node }
const thePath =
sourceRangeOrPath.type === 'sourceRange'
@ -1906,7 +1909,7 @@ export function changeSketchArguments(
}
export function getConstraintInfo(
callExpression: CallExpression,
callExpression: Node<CallExpression>,
code: string,
pathToNode: PathToNode
): ConstrainInfo[] {
@ -1944,7 +1947,7 @@ export function compareVec2Epsilon2(
}
interface CreateLineFnCallArgs {
node: Program
node: Node<Program>
programMemory: ProgramMemory
input: SegmentInputs
fnName: ToolTip
@ -1961,7 +1964,7 @@ export function addNewSketchLn({
spliceBetween = false,
}: CreateLineFnCallArgs):
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNode: PathToNode
}
| Error {
@ -1971,8 +1974,12 @@ export function addNewSketchLn({
return new Error('not a sketch line helper')
}
getNodeFromPath<VariableDeclarator>(node, pathToNode, 'VariableDeclarator')
getNodeFromPath<PipeExpression | CallExpression>(
getNodeFromPath<Node<VariableDeclarator>>(
node,
pathToNode,
'VariableDeclarator'
)
getNodeFromPath<Node<PipeExpression | CallExpression>>(
node,
pathToNode,
'PipeExpression'
@ -1991,13 +1998,13 @@ export function addCallExpressionsToPipe({
pathToNode,
expressions,
}: {
node: Program
node: Node<Program>
programMemory: ProgramMemory
pathToNode: PathToNode
expressions: CallExpression[]
expressions: Node<CallExpression>[]
}) {
const _node = { ...node }
const pipeExpression = getNodeFromPath<PipeExpression>(
const pipeExpression = getNodeFromPath<Node<PipeExpression>>(
_node,
pathToNode,
'PipeExpression'
@ -2046,7 +2053,7 @@ export function replaceSketchLine({
replaceExistingCallback,
referencedSegment,
}: {
node: Program
node: Node<Program>
programMemory: ProgramMemory
pathToNode: PathToNode
fnName: ToolTip
@ -2055,7 +2062,7 @@ export function replaceSketchLine({
referencedSegment?: Path
}):
| {
modifiedAst: Program
modifiedAst: Node<Program>
valueUsedInTransform?: number
pathToNode: PathToNode
}
@ -2107,7 +2114,7 @@ function addTagToChamfer(
edgeCutMeta: EdgeCutInfo | null
):
| {
modifiedAst: Program
modifiedAst: Node<Program>
tag: string
}
| Error {
@ -2234,7 +2241,7 @@ export function addTagForSketchOnFace(
edgeCutMeta: EdgeCutInfo | null
):
| {
modifiedAst: Program
modifiedAst: Node<Program>
tag: string
}
| Error {
@ -2272,12 +2279,14 @@ function isAngleLiteral(lineArugement: Expr): boolean {
: false
}
type addTagFn = (a: AddTagInfo) => { modifiedAst: Program; tag: string } | Error
type addTagFn = (
a: AddTagInfo
) => { modifiedAst: Node<Program>; tag: string } | Error
function addTag(tagIndex = 2): addTagFn {
return ({ node, pathToNode }) => {
const _node = { ...node }
const callExpr = getNodeFromPath<CallExpression>(
const callExpr = getNodeFromPath<Node<CallExpression>>(
_node,
pathToNode,
'CallExpression'

View File

@ -49,6 +49,7 @@ import {
getSketchSegmentFromSourceRange,
} from './sketchConstraints'
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type LineInputsType =
| 'xAbsolute'
@ -325,7 +326,7 @@ const setHorzVertDistanceCreateNode =
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
let finalValue: Expr = createBinaryExpressionWithUnary([
let finalValue: Node<Expr> = createBinaryExpressionWithUnary([
createSegEnd(referenceSegName, !index),
forceValueUsedInTransform || createLiteral(valueUsedInTransform),
])
@ -683,6 +684,14 @@ const transformMap: TransformMap = {
tag
),
},
xAbs: {
tooltip: 'lineTo',
createNode: setAbsDistanceCreateNode('x'),
},
yAbs: {
tooltip: 'lineTo',
createNode: setAbsDistanceCreateNode('y'),
},
},
xAbsolute: {
equalLength: {
@ -1541,7 +1550,7 @@ export function transformSecondarySketchLinesTagFirst({
forceSegName,
forceValueUsedInTransform,
}: {
ast: Program
ast: Node<Program>
selectionRanges: Selections
transformInfos: TransformInfo[]
programMemory: ProgramMemory
@ -1549,7 +1558,7 @@ export function transformSecondarySketchLinesTagFirst({
forceValueUsedInTransform?: BinaryPart
}):
| {
modifiedAst: Program
modifiedAst: Node<Program>
valueUsedInTransform?: number
pathToNodeMap: PathToNodeMap
tagInfo: {
@ -1620,7 +1629,7 @@ export function transformAstSketchLines({
forceValueUsedInTransform,
referencedSegmentRange,
}: {
ast: Program
ast: Node<Program>
selectionRanges: Selections | PathToNode[]
transformInfos: TransformInfo[]
programMemory: ProgramMemory
@ -1629,7 +1638,7 @@ export function transformAstSketchLines({
referencedSegmentRange?: Selection['range']
}):
| {
modifiedAst: Program
modifiedAst: Node<Program>
valueUsedInTransform?: number
pathToNodeMap: PathToNodeMap
}
@ -1647,7 +1656,7 @@ export function transformAstSketchLines({
const getNode = getNodeFromPathCurry(node, _pathToNode)
const callExp = getNode<CallExpression>('CallExpression')
const callExp = getNode<Node<CallExpression>>('CallExpression')
if (err(callExp)) return callExp
const varDec = getNode<VariableDeclarator>('VariableDeclarator')
if (err(varDec)) return varDec
@ -1806,13 +1815,16 @@ function createSegAngle(referenceSegName: string): BinaryPart {
return createCallExpression('segAng', [createIdentifier(referenceSegName)])
}
function createSegEnd(referenceSegName: string, isX: boolean): CallExpression {
function createSegEnd(
referenceSegName: string,
isX: boolean
): Node<CallExpression> {
return createCallExpression(isX ? 'segEndX' : 'segEndY', [
createIdentifier(referenceSegName),
])
}
function createLastSeg(isX: boolean): CallExpression {
function createLastSeg(isX: boolean): Node<CallExpression> {
return createCallExpression(isX ? 'lastSegX' : 'lastSegY', [
createPipeSubstitution(),
])
@ -1830,7 +1842,7 @@ export function getConstraintLevelFromSourceRange(
ast: Program | Error
): Error | { range: [number, number]; level: ConstraintLevel } {
if (err(ast)) return ast
const nodeMeta = getNodeFromPath<CallExpression>(
const nodeMeta = getNodeFromPath<Node<CallExpression>>(
ast,
getNodePathFromSourceRange(ast, cursorRange),
'CallExpression'

View File

@ -11,16 +11,17 @@ import {
BinaryPart,
} from '../wasm'
import { LineInputsType } from './sketchcombos'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export interface ModifyAstBase {
node: Program
node: Node<Program>
// TODO #896: Remove ProgramMemory from this interface
previousProgramMemory: ProgramMemory
pathToNode: PathToNode
}
export interface AddTagInfo {
node: Program
node: Node<Program>
pathToNode: PathToNode
}
@ -134,7 +135,7 @@ type _InputArg<T> =
* Which is why a union type is used that can be type narrowed using the {@link RawArg.type} property
* {@link RawArg.expr} is common to all of these types
*/
export type InputArg = _InputArg<Expr>
export type InputArg = _InputArg<Node<Expr>>
/**
* {@link RawArg.expr} is the literal equivalent of whatever current expression is
@ -142,7 +143,7 @@ export type InputArg = _InputArg<Expr>
* but of course works for expressions like myVar + someFn() etc too
* This is useful in cases where we want to "un-constrain" inputs to segments
*/
type RawArg = _InputArg<Literal>
type RawArg = _InputArg<Node<Literal>>
export type InputArgs = Array<InputArg>
@ -186,7 +187,7 @@ export type CreateStdLibSketchCallExpr = (args: {
inputs: InputArgs
rawArgs: RawArgs
referenceSegName: string
tag?: Expr
tag?: Node<Expr>
forceValueUsedInTransform?: BinaryPart
referencedSegment?: Path
}) => CreatedSketchExprResult | Error
@ -215,26 +216,26 @@ export interface ConstrainInfo {
export interface SketchLineHelper {
add: (a: addCall) =>
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNode: PathToNode
valueUsedInTransform?: number
}
| Error
updateArgs: (a: updateArgs) =>
| {
modifiedAst: Program
modifiedAst: Node<Program>
pathToNode: PathToNode
}
| Error
getTag: (a: CallExpression) => string | Error
addTag: (a: AddTagInfo) =>
| {
modifiedAst: Program
modifiedAst: Node<Program>
tag: string
}
| Error
getConstraintInfo: (
callExp: CallExpression,
callExp: Node<CallExpression>,
code: string,
pathToNode: PathToNode
) => ConstrainInfo[]

13
src/lang/wasm.test.ts Normal file
View File

@ -0,0 +1,13 @@
import { err } from 'lib/trap'
import { parse } from './wasm'
import { enginelessExecutor } from 'lib/testHelpers'
it('can execute parsed AST', async () => {
const code = `x = 1
// A comment.`
const ast = parse(code)
expect(err(ast)).toEqual(false)
const execState = await enginelessExecutor(ast)
expect(err(ast)).toEqual(false)
expect(execState.memory.get('x')?.value).toEqual(1)
})

View File

@ -42,6 +42,7 @@ import { ExecState as RawExecState } from '../wasm-lib/kcl/bindings/ExecState'
import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory'
import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -122,11 +123,11 @@ export const initPromise = initialise()
export const rangeTypeFix = (ranges: number[][]): [number, number][] =>
ranges.map(([start, end]) => [start, end])
export const parse = (code: string | Error): Program | Error => {
export const parse = (code: string | Error): Node<Program> | Error => {
if (err(code)) return code
try {
const program: Program = parse_wasm(code)
const program: Node<Program> = parse_wasm(code)
return program
} catch (e: any) {
// throw e
@ -378,7 +379,7 @@ export function sketchFromKclValue(
}
export const executor = async (
node: Program,
node: Node<Program>,
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager,
@ -402,7 +403,7 @@ export const executor = async (
}
export const _executor = async (
node: Program,
node: Node<Program>,
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager,
@ -493,13 +494,13 @@ export function lexer(str: string): Token[] | Error {
export const modifyAstForSketch = async (
engineCommandManager: EngineCommandManager,
ast: Program,
ast: Node<Program>,
variableName: string,
currentPlane: string,
engineId: string
): Promise<Program> => {
): Promise<Node<Program>> => {
try {
const updatedAst: Program = await modify_ast_for_sketch_wasm(
const updatedAst: Node<Program> = await modify_ast_for_sketch_wasm(
engineCommandManager,
JSON.stringify(ast),
variableName,

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,41 +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
}) (${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 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

@ -1,7 +1,8 @@
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { homeMachine } from 'machines/homeMachine'
import { projectsMachine } from 'machines/projectsMachine'
export type HomeCommandSchema = {
export type ProjectsCommandSchema = {
'Read projects': {}
'Create project': {
name: string
@ -18,9 +19,9 @@ export type HomeCommandSchema = {
}
}
export const homeCommandBarConfig: StateMachineCommandSetConfig<
typeof homeMachine,
HomeCommandSchema
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
typeof projectsMachine,
ProjectsCommandSchema
> = {
'Open project': {
icon: 'arrowRight',
@ -53,6 +54,11 @@ export const homeCommandBarConfig: StateMachineCommandSetConfig<
icon: 'close',
description: 'Delete a project',
needsReview: true,
reviewMessage: ({ argumentsToSubmit }) =>
CommandBarOverwriteWarning({
heading: 'Are you sure you want to delete?',
message: `This will permanently delete the project "${argumentsToSubmit.name}" and all its contents.`,
}),
args: {
name: {
inputType: 'options',

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