Compare commits

...

40 Commits

Author SHA1 Message Date
c825eac27e Cut release v0.11.0 (#826)
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2023-10-17 18:15:46 -04:00
82e8a491c4 honour sketch mode after execute (#885)
* enable stay in sketch mode after execute

* clean up
2023-10-17 17:03:02 -04:00
93e806fc99 don't query program memory for add sketch line (#895)
* don't query program memory for add sketch line

* add issue TODO

* fmt

* fix test tsc
2023-10-18 07:30:03 +11:00
f1a14f1e3d Guard all the vaules in the Metrics for undefined. (#891)
We've seen a few cases where the WebRTC metrics report contains
undefined values when the stream hasn't started yet. JavaScript, when we
send to the backend, drops `undefined` members of the object, for
example:

```
> JSON.stringify({rtc_frames_dropped: undefined})
< '{}'
```

This will fail the backend validation logic and cause an error to get
emitted to the front-end reporting the missing key. I don't think this
does anything to the session, but it's an error we can avoid by guarding
more of the statistics with a || 0.

Some of the values had this already, this just adds a few more.

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2023-10-17 16:55:45 +00:00
57c01ec3a2 Use platform-agnostic file separators (#890)
* Use platform-agnostic path separators

* Fix file settings by fixing absolute file path

* Fix missing home link in AppHeader

* Found so many more instances of raw "/" characters

* Tiny Settings style fix

* Clean up onboarding behavior for XState and multi-file
2023-10-17 12:31:14 -04:00
ce951d7c12 Add https://tauri.localhost for Windows (#886)
* Add https://tauri.localhost for Windows
Fixes #878

* Comments and info logs
2023-10-17 09:37:16 -04:00
0aa2a6cee7 more clean up (#889) 2023-10-17 09:08:17 +00:00
ba8f5d9785 unused vars (#887) 2023-10-17 05:34:35 +00:00
50a133b2fa add wasm error banner (#882)
add wasm error banner
2023-10-17 01:07:48 +00:00
3b15bc12f7 Franknoirot/multi file (#844)
* Fix unrelated bug, settings button in the home sidebar
doesn't go to the home settings after my previous fixes to routes

* Turn on "Replay Onboarding" button in home settings

* Add icons

* Add Tooltip component

* Rough-in of sidebar styling and add initial File Tree

* Polish basic styling

* Show nested files and directories

* Add tests

* use camelCase for entrypointMetadata

* Add ability to switch files via links

* Revert "Improve Prop Typings for Modals. Remove instances of `any`. (… (#813)

Revert "Improve Prop Typings for Modals. Remove instances of `any`. (#792)"

This reverts commit 629f326f4c.

* ffmpeg instructions (#814)

* Formatting

* Remove folder names from display in app header

* Highlight current file, open folders it's within

* Navigate on double click, delete on Cmd + Esc
+ highlight focused folders

* Migrate to an XState machine, add create new file

* Add ability to create folders (with naive names)
+ remove command bar stuff for now

* Use route loader data to instantiate the kcl code

* Clean up some unused things

* Add ability to rename files

* Add ability to rename folders

* Add keyboard shortcuts for creating files/folders

* Tooltip style tweaks

* Polish + re-execute when switching files with a connection

* Reset code before navigating via file tree

* Don't invoke `readProject` if you're in a browser

* Show files and folders for projects on home page

* Don't highlight folders further down the file tree

* @jgomez720 and @jessfraz feedback:
+ indentation markers
+ proper file icon
+ bump down font size
+ touch up colors

* Tune down spacing, allow scroll overflow

* Fix formatting

* Update src/lib/tauriFS.ts

* Add a confirmation dialog when deleting

Signed-off-by: Frank Noirot <frank@kittycad.io>

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-10-16 13:28:41 -04:00
8eedee328b Add nightly releases (#736)
* WIP nightly release
Will fix #708

* semver compatible date

* shell: bash

* Update

* WIP

* WIP

* Revert and move to artifact for data sharing

* Update date

* Clean up

* Testing publish-apps-nightly

* Add config override

* Missing checkout

* Remove v

* Unified publish-apps-release step

* Add comment and commit sha

* Remove the remove-if-safe guards

* Final touches, cron to 4am UTC

* Add checks back
2023-10-16 11:49:23 -04:00
49b321feb5 Tauri Upgrade 1.5.0 (#823)
* Tauri Upgrade 1.5.0

* Add APPLE_TEAM_ID secret
2023-10-16 11:04:06 -04:00
35b5ad7d9b refactor selections (#876)
* migrate selection types

* extract selection event into selections.ts

* move code-mirror selection functions into selections.ts

* move more selection logit out of code mirror and engine connection

* add selection functions pure

* tidy up naming

* write a novel about how selections work

* final comments
2023-10-16 10:20:05 +00:00
8fad9ef3c2 fix vertex selection (#869) 2023-10-16 15:29:02 +11:00
b257b202c3 Add modal typing back in, and clean up old constraints code (#865)
* Revert "Revert "Improve Prop Typings for Modals. Remove instances of `any`. (… (#813)"

This reverts commit 9822576077.

* tsc

* refactor all buttons

* add parallel constraint

* typegen?

* add constraint removal constraint

* add perpendicular distance constraint

* state diagram layout

* fmt

* improve modal typing for setAngleLength
2023-10-15 21:54:38 +00:00
c6af62797d holes (#848)
* start of holes

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>

* updates

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

* updates;

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

* close it

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

* Fix holes in jess's branch (#857)

tweak

* holes

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

* bump version

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

* fix image

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: mlfarrell <michael@kittycad.io>
2023-10-13 12:02:46 -07:00
16a9acad56 Bump kittycad from 0.2.32 to 0.2.33 in /src-tauri (#851)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.2.32 to 0.2.33.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.2.32...v0.2.33)

---
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>
2023-10-13 10:26:36 -07:00
8a80a88ad3 update images (#856)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-10-13 10:26:22 -07:00
71d1bb70ef Fix move only working first time (#850)
* nuke fixIdMappings

* update readme

* remove sourceRangeMap as it was redundant

repeated state already covered by the artifactMap

* bug fix, name conflict

* bug fix

* update artifact map when sketch is first created

* update artifact map for line generation too

* fmt

* update move state to allow selections

* allow for selection of vertices

some what hacky, but better than nothing

* unnecessary react hook dependency

* generic react linting

* move working for non-execute case

* block partial contrained things too for now
2023-10-13 09:47:46 -07:00
4853872614 Bump serde from 1.0.188 to 1.0.189 in /src-tauri (#852)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.188 to 1.0.189.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.188...v1.0.189)

---
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>
2023-10-13 09:47:29 -07:00
1ca5204a1a Bump kittycad from 0.2.32 to 0.2.33 in /src/wasm-lib (#853)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.2.32 to 0.2.33.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.2.32...v0.2.33)

---
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>
2023-10-13 09:46:43 -07:00
7baed0b5bd Bump serde from 1.0.188 to 1.0.189 in /src/wasm-lib (#854)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.188 to 1.0.189.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.188...v1.0.189)

---
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>
2023-10-13 09:46:23 -07:00
e4969857bd Bump openapitor from c122a9b to 7e087ec in /src/wasm-lib (#855)
Bumps [openapitor](https://github.com/KittyCAD/kittycad.rs) from `c122a9b` to `7e087ec`.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](c122a9b1d6...7e087ecaee)

---
updated-dependencies:
- dependency-name: openapitor
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-13 09:46:10 -07:00
9b7cc7afa4 save path id on path create (#849) 2023-10-13 10:27:25 +11:00
714917429e Release kcl-lib 0.1.34 (#843) 2023-10-12 12:27:01 -05:00
5af9c6b22d Typo: tangental should be tangential (#842)
* Typo: tangental should be tangential

* Run executor tests in serial

* Fix typo in image file names
2023-10-12 11:50:54 -05:00
396a994fe6 Add unit test (#841) 2023-10-12 10:56:20 -05:00
872da51da5 Bump tauri from 1.5.1 to 1.5.2 in /src-tauri (#836)
Bumps [tauri](https://github.com/tauri-apps/tauri) from 1.5.1 to 1.5.2.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v1.5.1...tauri-v1.5.2)

---
updated-dependencies:
- dependency-name: tauri
  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>
2023-10-12 08:50:44 -07:00
05cd8cfec9 Bump kittycad from 0.2.31 to 0.2.32 in /src-tauri (#837)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.2.31 to 0.2.32.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.2.31...v0.2.32)

---
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>
2023-10-12 08:50:35 -07:00
2a02f6e039 New parser built in Winnow (#731)
* New parser built with Winnow

This new parser uses [winnow](docs.rs/winnow) to replace the handwritten recursive parser.

## Differences

I think the Winnow parser is more readable than handwritten one, due to reusing standard combinators. If you have a parsre like `p` or `q` you can combine them with standard functions like `repeat(0..4, p)`, `opt(p)`, `alt((p, q))` and `separated1(p, ", ")`. This IMO makes it more readable once you know what those standard functions do.

It's also more accurate now -- e.g. the parser no longer swallows whitespace between comments, or inserts it where there was none before. It no longer changes // comments to /* comments depending on the surrounding whitespace. 

Primary form of testing was running the same KCL program through both the old and new parsers and asserting that both parsers produce the same AST. See the test `parser::parser_impl::tests::check_parsers_work_the_same`. But occasionally the new and old parsers disagree. This is either:

- Innocuous (e.g. disagreeing on whether a comment starts at the preceding whitespace or at the //)
- Helpful (e.g. new parser recognizes comments more accurately, preserving the difference between // and /* comments)
- Acceptably bad (e.g. new parser sometimes outputs worse error messages, TODO in #784)

so those KCL programs have their own unit tests in `parser_impl.rs` demonstrating the behaviour.

If you'd like to review this PR, it's arguably more important to review changes to the existing unit tests rather than the new parser itself. Because changes to the unit tests show where my parser changes behaviour -- usually for the better, occasionally for the worse (e.g. a worse error message than before). I think overall the improvements are worth it that I'd like to merge it without spending another week fixing it up -- we can fix the error messages in a follow-up PR.

## Performance

| Benchmark | Old parser (this branch) | New parser (this branch) | Speedup |
| ------------- | ------------- | ------------- | ------------- |
| Pipes on pipes  | 922 ms  | 42 ms | 21x |
| Kitt SVG  | 148 ms  | 7 ms | 21x |

There's definitely still room to improve performance:

- https://github.com/KittyCAD/modeling-app/issues/839
- https://github.com/KittyCAD/modeling-app/issues/840

## Winnow

Y'all know I love [Nom](docs.rs/nom) and I've blogged about it a lot. But I'm very happy using Winnow, a fork. It's got some really nice usability improvements. While writing this PR I found some bugs or unclear docs in Winnow:

- https://github.com/winnow-rs/winnow/issues/339
- https://github.com/winnow-rs/winnow/issues/341
- https://github.com/winnow-rs/winnow/issues/342
- https://github.com/winnow-rs/winnow/issues/344

The maintainer was quick to close them and release new versions within a few hours, so I feel very confident building the parser on this library. It's a clear improvement over Nom and it's used in toml-edit (and therefore within Cargo) and Gitoxide, so it's becoming a staple of the Rust ecosystem, which adds confidence.

Closes #716 
Closes #815 
Closes #599
2023-10-12 09:42:37 -05:00
5b90686e5e fix for culling (#835)
* start of fix for culling;

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

* update lock

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

* updates

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-10-11 18:30:04 -07:00
298269d117 Bump tokio from 1.32.0 to 1.33.0 in /src-tauri (#822)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.32.0 to 1.33.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.32.0...tokio-1.33.0)

---
updated-dependencies:
- dependency-name: tokio
  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>
2023-10-11 17:47:35 -07:00
b379f6518f Add Language Data's Comment Tokens to Support Toggle Commenting Shortcuts in the Editor (#809)
Add Language Data's Comment Tokens to Support Toggle Comment Shortcuts

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2023-10-11 16:59:54 -07:00
6b22c8789d Bump tokio from 1.32.0 to 1.33.0 in /src/wasm-lib (#820)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.32.0 to 1.33.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.32.0...tokio-1.33.0)

---
updated-dependencies:
- dependency-name: tokio
  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>
2023-10-11 16:57:33 -07:00
cb4683e70b Bump proc-macro2 from 1.0.67 to 1.0.69 in /src/wasm-lib (#810)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.67 to 1.0.69.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.67...1.0.69)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2023-10-11 16:57:18 -07:00
0a020d9959 Bump tauri-plugin-fs-extra from 9f27e6e to 9b20f28 in /src-tauri (#828)
Bumps [tauri-plugin-fs-extra](https://github.com/tauri-apps/plugins-workspace) from `9f27e6e` to `9b20f28`.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](9f27e6e441...9b20f28d74)

---
updated-dependencies:
- dependency-name: tauri-plugin-fs-extra
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-11 16:57:08 -07:00
7aae3dccdc Use route loader data to instantiate the kcl code (#832) 2023-10-11 16:29:22 -04:00
818bf96d0b update rendered images (#831)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-10-11 11:49:55 -07:00
03bc2eaf22 Docs: std.md now displays correct number of members of an array (#825)
* First draft up

* removed print statements

* running cargo fmt

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2023-10-11 11:12:21 -07:00
8ad1476c13 wait to execute code until planes are ready (#830)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-10-11 13:40:54 -04:00
116 changed files with 8328 additions and 2713 deletions

View File

@ -7,6 +7,10 @@ on:
- main - main
release: release:
types: [published] types: [published]
schedule:
- cron: '0 4 * * *'
# Daily at 04:00 AM UTC
# Will checkout the last commit from the default branch (main as of 2023-10-04)
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@ -42,8 +46,6 @@ jobs:
build-test-web: build-test-web:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
outputs:
version: ${{ steps.export_version.outputs.version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -66,11 +68,37 @@ jobs:
- run: yarn test:cov - run: yarn test:cov
prepare-json-files:
runs-on: ubuntu-20.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Set nightly version
if: github.event_name == 'schedule'
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
echo "$(jq --arg url 'https://dl.kittycad.io/releases/modeling-app/test/nightly/last_update.json' \
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
- uses: actions/upload-artifact@v3
if: github.event_name == 'schedule'
with:
path: |
package.json
src-tauri/tauri.conf.json
- id: export_version - id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT" run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-apps: build-apps:
needs: [check-format, build-test-web, check-types] needs: [check-format, build-test-web, prepare-json-files, check-types]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
@ -78,6 +106,15 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
- name: install ubuntu system dependencies - name: install ubuntu system dependencies
if: matrix.os == 'ubuntu-20.04' if: matrix.os == 'ubuntu-20.04'
run: | run: |
@ -156,6 +193,7 @@ jobs:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with: with:
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
@ -165,12 +203,14 @@ jobs:
publish-apps-release: publish-apps-release:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
if: github.event_name == 'release' if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [build-test-web, build-apps] needs: [build-test-web, prepare-json-files, build-apps]
env: env:
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }} VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
PUB_DATE: ${{ github.event.release.created_at }} VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
NOTES: ${{ github.event.release.body }} PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
@ -180,9 +220,9 @@ jobs:
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig` DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig` LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig` WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V} RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
jq --null-input \ jq --null-input \
--arg version "v${VERSION_NO_V}" \ --arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \ --arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \ --arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \ --arg darwin_sig "$DARWIN_SIG" \
@ -218,9 +258,9 @@ jobs:
- name: Generate the download static endpoint - name: Generate the download static endpoint
run: | run: |
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V} RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
jq --null-input \ jq --null-input \
--arg version "v${VERSION_NO_V}" \ --arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \ --arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \ --arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \ --arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
@ -260,21 +300,22 @@ jobs:
path: artifact path: artifact
glob: '*/*itty*' glob: '*/*itty*'
parent: false parent: false
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }} destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket - name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3 uses: google-github-actions/upload-cloud-storage@v1.0.3
with: with:
path: last_update.json path: last_update.json
destination: dl.kittycad.io/releases/modeling-app destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket - name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3 uses: google-github-actions/upload-cloud-storage@v1.0.3
with: with:
path: last_download.json path: last_download.json
destination: dl.kittycad.io/releases/modeling-app destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github - name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
files: artifact/*/*itty* files: artifact/*/*itty*

View File

@ -29,6 +29,7 @@ The 3D view in KittyCAD Modeling App is just a video stream from our hosted geom
- [React](https://react.dev/) - [React](https://react.dev/)
- [Headless UI](https://headlessui.com/) - [Headless UI](https://headlessui.com/)
- [TailwindCSS](https://tailwindcss.com/) - [TailwindCSS](https://tailwindcss.com/)
- [XState](https://xstate.js.org/)
- Networking - Networking
- WebSockets (via [KittyCAD TS client](https://github.com/KittyCAD/kittycad.ts)) - WebSockets (via [KittyCAD TS client](https://github.com/KittyCAD/kittycad.ts))
- Code Editor - Code Editor
@ -56,7 +57,7 @@ yarn install
followed by: followed by:
``` ```
yarn build:wasm yarn build:wasm-dev
``` ```
That will build the WASM binary and put in the `public` dir (though gitignored) That will build the WASM binary and put in the `public` dir (though gitignored)
@ -97,7 +98,7 @@ but you will need to have install ffmpeg prior to.
## Tauri ## Tauri
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then To spin up up tauri dev, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
``` ```
yarn tauri dev yarn tauri dev

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.10.0", "version": "0.11.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.9.0", "@codemirror/autocomplete": "^6.9.0",
@ -16,7 +16,7 @@
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0", "@replit/codemirror-interact": "^6.3.0",
"@sentry/react": "^7.65.0", "@sentry/react": "^7.65.0",
"@tauri-apps/api": "^1.3.0", "@tauri-apps/api": "^1.5.0",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0", "@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1", "@testing-library/user-event": "^13.2.1",
@ -73,6 +73,7 @@
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000", "simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src", "fmt": "prettier --write ./src",
"fmt-check": "prettier --check ./src", "fmt-check": "prettier --check ./src",
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt", "build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm", "build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
@ -101,7 +102,7 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.22.9", "@babel/preset-env": "^7.22.9",
"@tauri-apps/cli": "^1.3.1", "@tauri-apps/cli": "^1.5.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/debounce-promise": "^3.1.6", "@types/debounce-promise": "^3.1.6",
"@types/isomorphic-fetch": "^0.0.36", "@types/isomorphic-fetch": "^0.0.36",

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 475 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 469 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 200 B

3
public/kcl-icon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z" fill="#D0FF00"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

22
src-tauri/Cargo.lock generated
View File

@ -1658,9 +1658,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.2.31" version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "539b323537b877fc8dd130362b8f1af9af8051c19208bb8bfd816ab7c330f2bb" checksum = "d341a81a4dfef43460d395c87d86c17e24affb96db0e7f4a35e8688f0e092344"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -3208,9 +3208,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.188" version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -3226,9 +3226,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.188" version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3712,9 +3712,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "1.5.1" version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0238c5063bf9613054149a1b6bce4935922e532b7d8211f36989a490a79806be" checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.21.2", "base64 0.21.2",
@ -3828,7 +3828,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs-extra" name = "tauri-plugin-fs-extra"
version = "0.0.0" version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#9f27e6e4415ddf6c40f846d50c0d95c768cded77" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#9b20f28d747f6ec3ba5a80bfcd5edc1d573b4c90"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -4007,9 +4007,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.32.0" version = "1.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",

View File

@ -16,13 +16,13 @@ tauri-build = { version = "1.5.0", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kittycad = "0.2.31" kittycad = "0.2.33"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "1.5.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] } tauri = { version = "1.5.2", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.32.0", features = ["time"] } tokio = { version = "1.33.0", features = ["time"] }
toml = "0.8.2" toml = "0.8.2"
[features] [features]

View File

@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "kittycad-modeling", "productName": "kittycad-modeling",
"version": "0.10.0" "version": "0.11.0"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@ -35,7 +35,7 @@ import { kclManager } from 'lang/KclSinglton'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
export function App() { export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData const { code: loadedCode, project, file } = useLoaderData() as IndexLoaderData
useHotKeyListener() useHotKeyListener()
const { const {
@ -86,8 +86,14 @@ export function App() {
// on mount, and overwrite any locally-stored code // on mount, and overwrite any locally-stored code
useEffect(() => { useEffect(() => {
if (isTauri() && loadedCode !== null) { if (isTauri() && loadedCode !== null) {
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
// If the engine is ready, promptly execute the loaded code
kclManager.setCodeAndExecute(loadedCode)
} else {
// Otherwise, just set the code and wait for the connection to complete
kclManager.setCode(loadedCode) kclManager.setCode(loadedCode)
} }
}
return () => { return () => {
// Clear code on unmount if in desktop app // Clear code on unmount if in desktop app
if (isTauri()) { if (isTauri()) {
@ -182,7 +188,7 @@ export function App() {
paneOpacity + paneOpacity +
(buttonDownInStream ? ' pointer-events-none' : '') (buttonDownInStream ? ' pointer-events-none' : '')
} }
project={project} project={{ project, file }}
enableMenu={true} enableMenu={true}
/> />
<ModalContainer /> <ModalContainer />

View File

@ -31,6 +31,7 @@ import {
} from './lib/tauriFS' } from './lib/tauriFS'
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api' import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
import DownloadAppBanner from './components/DownloadAppBanner' import DownloadAppBanner from './components/DownloadAppBanner'
import { WasmErrBanner } from './components/WasmErrBanner'
import { GlobalStateProvider } from './components/GlobalStateProvider' import { GlobalStateProvider } from './components/GlobalStateProvider'
import { import {
SETTINGS_PERSIST_KEY, SETTINGS_PERSIST_KEY,
@ -42,6 +43,8 @@ import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import ModelingMachineProvider from 'components/ModelingMachineProvider' import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { KclContextProvider } from 'lang/KclSinglton' import { KclContextProvider } from 'lang/KclSinglton'
import FileMachineProvider from 'components/FileMachineProvider'
import { sep } from '@tauri-apps/api/path'
if (VITE_KC_SENTRY_DSN && !TEST) { if (VITE_KC_SENTRY_DSN && !TEST) {
Sentry.init({ Sentry.init({
@ -101,10 +104,11 @@ export const BROWSER_FILE_NAME = 'new'
export type IndexLoaderData = { export type IndexLoaderData = {
code: string | null code: string | null
project?: ProjectWithEntryPointMetadata project?: ProjectWithEntryPointMetadata
file?: FileEntry
} }
export type ProjectWithEntryPointMetadata = FileEntry & { export type ProjectWithEntryPointMetadata = FileEntry & {
entrypoint_metadata: Metadata entrypointMetadata: Metadata
} }
export type HomeLoaderData = { export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[] projects: ProjectWithEntryPointMetadata[]
@ -142,12 +146,15 @@ const router = createBrowserRouter(
path: paths.FILE + '/:id', path: paths.FILE + '/:id',
element: ( element: (
<Auth> <Auth>
<Outlet /> <FileMachineProvider>
<KclContextProvider> <KclContextProvider>
<ModelingMachineProvider> <ModelingMachineProvider>
<Outlet />
<App /> <App />
</ModelingMachineProvider> </ModelingMachineProvider>
<WasmErrBanner />
</KclContextProvider> </KclContextProvider>
</FileMachineProvider>
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />} {!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</Auth> </Auth>
), ),
@ -177,21 +184,41 @@ const router = createBrowserRouter(
) )
} }
const defaultDir = persistedSettings.defaultDirectory || ''
if (params.id && params.id !== BROWSER_FILE_NAME) { if (params.id && params.id !== BROWSER_FILE_NAME) {
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files const decodedId = decodeURIComponent(params.id)
const code = await readTextFile(params.id + '/' + PROJECT_ENTRYPOINT) const projectAndFile = decodedId.replace(defaultDir + sep, '')
const entrypoint_metadata = await metadata( const firstSlashIndex = projectAndFile.indexOf(sep)
params.id + '/' + PROJECT_ENTRYPOINT const projectName = projectAndFile.slice(0, firstSlashIndex)
const projectPath = defaultDir + sep + projectName
const currentFileName = projectAndFile.slice(firstSlashIndex + 1)
if (firstSlashIndex === -1 || !currentFileName)
return redirect(
`${paths.FILE}/${encodeURIComponent(
`${params.id}${sep}${PROJECT_ENTRYPOINT}`
)}`
) )
const children = await readDir(params.id)
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
const code = await readTextFile(decodedId)
const entrypointMetadata = await metadata(
projectPath + sep + PROJECT_ENTRYPOINT
)
const children = await readDir(projectPath, { recursive: true })
return { return {
code, code,
project: { project: {
name: params.id.slice(params.id.lastIndexOf('/') + 1), name: projectName,
path: params.id, path: projectPath,
children, children,
entrypoint_metadata, entrypointMetadata,
},
file: {
name: currentFileName,
path: params.id,
}, },
} }
} }
@ -244,9 +271,9 @@ const router = createBrowserRouter(
isProjectDirectory isProjectDirectory
) )
const projects = await Promise.all( const projects = await Promise.all(
projectsNoMeta.map(async (p) => ({ projectsNoMeta.map(async (p: FileEntry) => ({
entrypoint_metadata: await metadata( entrypointMetadata: await metadata(
p.path + '/' + PROJECT_ENTRYPOINT p.path + sep + PROJECT_ENTRYPOINT
), ),
...p, ...p,
})) }))

View File

@ -1,26 +1,13 @@
import { useStore, toolTips, ToolTip } from './useStore' import { ToolTip } from './useStore'
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
import { getNodePathFromSourceRange } from './lang/queryAst'
// import { HorzVert } from './components/Toolbar/HorzVert'
// import { RemoveConstrainingValues } from './components/Toolbar/RemoveConstrainingValues'
// import { EqualLength } from './components/Toolbar/EqualLength'
// import { EqualAngle } from './components/Toolbar/EqualAngle'
// import { Intersect } from './components/Toolbar/Intersect'
// import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
// import { SetAngleLength } from './components/Toolbar/setAngleLength'
// import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
// import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
import { Fragment, WheelEvent, useRef, useMemo } from 'react' import { Fragment, WheelEvent, useRef, useMemo } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons' import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
import { Popover, Transition } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import styles from './Toolbar.module.css' import styles from './Toolbar.module.css'
import { v4 as uuidv4 } from 'uuid'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { ActionIcon } from 'components/ActionIcon' import { ActionIcon } from 'components/ActionIcon'
import { engineCommandManager } from './lang/std/engineConnection' import { engineCommandManager } from './lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager } from 'lang/KclSinglton'
export const sketchButtonClassnames = { export const sketchButtonClassnames = {
background: background:
@ -178,24 +165,6 @@ export const Toolbar = () => {
Extrude Extrude
</button> </button>
)} )}
{/* <HorzVert horOrVert="horizontal" />
<HorzVert horOrVert="vertical" />
<EqualLength />
<EqualAngle />
<SetHorzVertDistance buttonType="alignEndsVertically" />
<SetHorzVertDistance buttonType="setHorzDistance" />
<SetAbsDistance buttonType="snapToYAxis" />
<SetAbsDistance buttonType="xAbs" />
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
<SetAbsDistance buttonType="snapToXAxis" />
<SetHorzVertDistance buttonType="setVertDistance" />
<SetAbsDistance buttonType="yAbs" />
<SetAngleLength angleOrLength="setAngle" />
<SetAngleLength angleOrLength="setLength" />
<Intersect />
<RemoveConstrainingValues />
<SetAngleBetween /> */}
</span> </span>
) )
} }

View File

@ -1,6 +1,6 @@
import { Toolbar } from '../Toolbar' import { Toolbar } from '../Toolbar'
import UserSidebarMenu from './UserSidebarMenu' import UserSidebarMenu from './UserSidebarMenu'
import { ProjectWithEntryPointMetadata } from '../Router' import { IndexLoaderData } from '../Router'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import styles from './AppHeader.module.css' import styles from './AppHeader.module.css'
@ -8,7 +8,7 @@ import { NetworkHealthIndicator } from './NetworkHealthIndicator'
interface AppHeaderProps extends React.PropsWithChildren { interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean showToolbar?: boolean
project?: ProjectWithEntryPointMetadata project?: Omit<IndexLoaderData, 'code'>
className?: string className?: string
enableMenu?: boolean enableMenu?: boolean
} }
@ -32,7 +32,11 @@ export const AppHeader = ({
className className
} }
> >
<ProjectSidebarMenu renderAsLink={!enableMenu} project={project} /> <ProjectSidebarMenu
renderAsLink={!enableMenu}
project={project?.project}
file={project?.file}
/>
{/* Toolbar if the context deems it */} {/* Toolbar if the context deems it */}
{showToolbar && ( {showToolbar && (
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl"> <div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
@ -41,7 +45,7 @@ export const AppHeader = ({
)} )}
{/* If there are children, show them, otherwise show User menu */} {/* If there are children, show them, otherwise show User menu */}
{children || ( {children || (
<div className="ml-auto flex items-center gap-1"> <div className="flex items-center gap-1 ml-auto">
<NetworkHealthIndicator /> <NetworkHealthIndicator />
<UserSidebarMenu user={user} /> <UserSidebarMenu user={user} />
</div> </div>

View File

@ -1,7 +1,10 @@
export type CustomIconName = export type CustomIconName =
| 'createFile'
| 'createFolder'
| 'equal' | 'equal'
| 'exit' | 'exit'
| 'extrude' | 'extrude'
| 'file'
| 'horizontal' | 'horizontal'
| 'line' | 'line'
| 'move' | 'move'
@ -16,6 +19,38 @@ export const CustomIcon = ({
name: CustomIconName name: CustomIconName
} & React.SVGProps<SVGSVGElement>) => { } & React.SVGProps<SVGSVGElement>) => {
switch (name) { switch (name) {
case 'createFile':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z"
fill="currentColor"
/>
</svg>
)
case 'createFolder':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
fill="currentColor"
/>
</svg>
)
case 'equal': case 'equal':
return ( return (
<svg <svg
@ -61,6 +96,20 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'file':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5"
stroke="currentColor"
/>
</svg>
)
case 'horizontal': case 'horizontal':
return ( return (
<svg <svg

View File

@ -16,8 +16,8 @@ type StorageUnion = ExtractStorageTypes<OutputFormat>
interface ExportButtonProps extends React.PropsWithChildren { interface ExportButtonProps extends React.PropsWithChildren {
className?: { className?: {
button?: string button?: string
// If we wanted more classname configuration of sub-elements, icon?: string
// put them here bg?: string
} }
} }
@ -109,7 +109,11 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
<ActionButton <ActionButton
onClick={openModal} onClick={openModal}
Element="button" Element="button"
icon={{ icon: faFileExport }} icon={{
icon: faFileExport,
iconClassName: className?.icon,
bgClassName: className?.bg,
}}
className={className?.button} className={className?.button}
> >
{children || 'Export'} {children || 'Export'}

View File

@ -0,0 +1,158 @@
import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { IndexLoaderData, paths } from '../Router'
import React, { createContext } from 'react'
import { toast } from 'react-hot-toast'
import {
AnyStateMachine,
ContextFrom,
EventFrom,
InterpreterFrom,
Prop,
StateFrom,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { DEFAULT_FILE_NAME, fileMachine } from 'machines/fileMachine'
import {
createDir,
removeDir,
removeFile,
renameFile,
writeFile,
} from '@tauri-apps/api/fs'
import { FILE_EXT, readProject } from 'lib/tauriFS'
import { isTauri } from 'lib/isTauri'
import { sep } from '@tauri-apps/api/path'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'>
}
export const FileContext = createContext(
{} as MachineContext<typeof fileMachine>
)
export const FileMachineProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { setCommandBarOpen } = useCommandsContext()
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
const [state, send] = useMachine(fileMachine, {
context: {
project,
selectedDirectory: project,
},
actions: {
navigateToFile: (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine>
) => {
if (event.data && 'name' in event.data) {
setCommandBarOpen(false)
navigate(
`${paths.FILE}/${encodeURIComponent(
context.selectedDirectory + sep + event.data.name
)}`
)
}
},
toastSuccess: (_, event) =>
event.data && toast.success((event.data || '') + ''),
toastError: (_, event) => toast.error((event.data || '') + ''),
},
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri()
? await readProject(context.project.path)
: []
return {
...context.project,
children: newFiles,
}
},
createFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Create file'>
) => {
let name = event.data.name.trim() || DEFAULT_FILE_NAME
if (event.data.makeDir) {
await createDir(context.selectedDirectory.path + sep + name)
} else {
await writeFile(
context.selectedDirectory.path +
sep +
name +
(name.endsWith(FILE_EXT) ? '' : FILE_EXT),
''
)
}
return `Successfully created "${name}"`
},
renameFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Rename file'>
) => {
const { oldName, newName, isDir } = event.data
let name = newName ? newName : DEFAULT_FILE_NAME
await renameFile(
context.selectedDirectory.path + sep + oldName,
context.selectedDirectory.path +
sep +
name +
(name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
)
return (
oldName !== name && `Successfully renamed "${oldName}" to "${name}"`
)
},
deleteFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Delete file'>
) => {
const isDir = !!event.data.children
if (isDir) {
await removeDir(event.data.path, {
recursive: true,
}).catch((e) => console.error('Error deleting directory', e))
} else {
await removeFile(event.data.path).catch((e) =>
console.error('Error deleting file', e)
)
}
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
event.data.name
}"`
},
},
guards: {
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
if (event.type !== 'done.invoke.read-files') return false
return !!event?.data?.children && event.data.children.length > 0
},
},
})
return (
<FileContext.Provider
value={{
send,
state,
context: state.context, // just a convenience, can remove if we need to save on memory
}}
>
{children}
</FileContext.Provider>
)
}
export default FileMachineProvider

View File

@ -0,0 +1,16 @@
.folder {
position: relative;
}
.folder::after {
content: '';
width: 1px;
z-index: -1;
@apply absolute top-0 bottom-0;
left: calc(var(--indent-line-left, 1rem) + 0.25rem);
@apply bg-chalkboard-30;
}
:global(.dark) .folder::after {
@apply bg-chalkboard-80;
}

400
src/components/FileTree.tsx Normal file
View File

@ -0,0 +1,400 @@
import { IndexLoaderData, paths } from 'Router'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
import { FileEntry } from '@tauri-apps/api/fs'
import { Dispatch, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Dialog, Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext'
import { useHotkeys } from 'react-hotkeys-hook'
import { kclManager } from 'lang/KclSinglton'
import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
}
function RenameForm({
fileOrDir,
setIsRenaming,
level = 0,
}: {
fileOrDir: FileEntry
setIsRenaming: Dispatch<React.SetStateAction<boolean>>
level?: number
}) {
const { send } = useFileContext()
const inputRef = useRef<HTMLInputElement>(null)
function handleRenameSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setIsRenaming(false)
send({
type: 'Rename file',
data: {
oldName: fileOrDir.name || '',
newName: inputRef.current?.value || fileOrDir.name || '',
isDir: fileOrDir.children !== undefined,
},
})
}
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Escape') {
e.stopPropagation()
setIsRenaming(false)
}
}
return (
<form onSubmit={handleRenameSubmit}>
<label>
<span className="sr-only">Rename file</span>
<input
ref={inputRef}
type="text"
autoFocus
placeholder={fileOrDir.name}
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
onKeyDown={handleKeyDown}
onBlur={() => setIsRenaming(false)}
style={{ paddingInlineStart: getIndentationCSS(level) }}
/>
</label>
<button className="sr-only" type="submit">
Submit
</button>
</form>
)
}
function DeleteConfirmationDialog({
fileOrDir,
setIsOpen,
}: {
fileOrDir: FileEntry
setIsOpen: Dispatch<React.SetStateAction<boolean>>
}) {
const { send } = useFileContext()
return (
<Dialog
open={true}
onClose={() => setIsOpen(false)}
className="relative z-50"
>
<div className="fixed inset-0 bg-chalkboard-110/80 grid place-content-center">
<Dialog.Panel className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border border-destroy-80 max-w-2xl">
<Dialog.Title as="h2" className="text-2xl font-bold mb-4">
Delete {fileOrDir.children !== undefined ? 'Folder' : 'File'}
</Dialog.Title>
<Dialog.Description className="my-6">
This will permanently delete "{fileOrDir.name || 'this file'}"
{fileOrDir.children !== undefined
? ' and all of its contents. '
: '. '}
This action cannot be undone.
</Dialog.Description>
<div className="flex justify-between">
<ActionButton
Element="button"
onClick={async () => {
send({ type: 'Delete file', data: fileOrDir })
setIsOpen(false)
}}
icon={{
icon: faTrashAlt,
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
}}
className="hover:border-destroy-40 dark:hover:border-destroy-40"
>
Delete
</ActionButton>
<ActionButton Element="button" onClick={() => setIsOpen(false)}>
Cancel
</ActionButton>
</div>
</Dialog.Panel>
</div>
</Dialog>
)
}
const FileTreeItem = ({
project,
currentFile,
fileOrDir,
closePanel,
level = 0,
}: {
project?: IndexLoaderData['project']
currentFile?: IndexLoaderData['file']
fileOrDir: FileEntry
closePanel: (
focusableElement?:
| HTMLElement
| React.MutableRefObject<HTMLElement | null>
| undefined
) => void
level?: number
}) => {
const { send, context } = useFileContext()
const navigate = useNavigate()
const [isRenaming, setIsRenaming] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const isCurrentFile = fileOrDir.path === currentFile?.path
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
if (e.metaKey && e.key === 'Backspace') {
// Open confirmation dialog
setIsConfirmingDelete(true)
} else if (e.key === 'Enter') {
// Show the renaming form
setIsRenaming(true)
} else if (e.code === 'Space') {
openFile()
}
}
function openFile() {
if (fileOrDir.children !== undefined) return // Don't open directories
kclManager.setCode('')
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
closePanel()
}
return (
<>
{fileOrDir.children === undefined ? (
<li
className={
'group m-0 p-0 border-solid border-0 text-energy-100 hover:text-energy-70 hover:bg-energy-10/50 dark:text-energy-30 dark:hover:!text-energy-20 dark:hover:bg-energy-90/50 focus-within:bg-energy-10/80 dark:focus-within:bg-energy-80/50 hover:focus-within:bg-energy-10/80 dark:hover:focus-within:bg-energy-80/50 ' +
(isCurrentFile ? 'bg-energy-10/50 dark:bg-energy-90/50' : '')
}
>
{!isRenaming ? (
<button
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
style={{ paddingInlineStart: getIndentationCSS(level) }}
onDoubleClick={openFile}
onClick={(e) => e.currentTarget.focus()}
onKeyUp={handleKeyUp}
>
<KclIcon
className={
'inline-block w-3 ' +
(isCurrentFile
? 'text-energy-90 dark:text-energy-10'
: 'text-energy-50 dark:text-energy-50')
}
/>
{fileOrDir.name}
</button>
) : (
<RenameForm
fileOrDir={fileOrDir}
setIsRenaming={setIsRenaming}
level={level}
/>
)}
</li>
) : (
<Disclosure defaultOpen={currentFile?.path.includes(fileOrDir.path)}>
{({ open }) => (
<div className="group">
{!isRenaming ? (
<Disclosure.Button
className={
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 text-chalkboard-70 dark:text-chalkboard-30 hover:bg-energy-10/50 dark:hover:bg-energy-90/50' +
(context.selectedDirectory.path.includes(fileOrDir.path)
? ' group-focus-within:bg-chalkboard-20/50 dark:group-focus-within:bg-chalkboard-80/20 hover:group-focus-within:bg-chalkboard-20 dark:hover:group-focus-within:bg-chalkboard-80/20 group-active:bg-chalkboard-20/50 dark:group-active:bg-chalkboard-80/20 hover:group-active:bg-chalkboard-20/50 dark:hover:group-active:bg-chalkboard-80/20'
: '')
}
style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => e.currentTarget.focus()}
onClickCapture={(e) =>
send({ type: 'Set selected directory', data: fileOrDir })
}
onFocusCapture={(e) =>
send({ type: 'Set selected directory', data: fileOrDir })
}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
onKeyUp={handleKeyUp}
>
<FontAwesomeIcon
icon={faChevronRight}
className={
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
(open ? 'transform rotate-90' : '')
}
/>
{fileOrDir.name}
</Disclosure.Button>
) : (
<div
className="flex items-center"
style={{ paddingInlineStart: getIndentationCSS(level) }}
>
<FontAwesomeIcon
icon={faChevronRight}
className={
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
(open ? 'transform rotate-90' : '')
}
/>
<RenameForm
fileOrDir={fileOrDir}
setIsRenaming={setIsRenaming}
level={-1}
/>
</div>
)}
<Disclosure.Panel
className={styles.folder}
style={
{
'--indent-line-left': getIndentationCSS(level),
} as React.CSSProperties
}
>
<ul
className="m-0 p-0"
onClickCapture={(e) => {
send({ type: 'Set selected directory', data: fileOrDir })
}}
onFocusCapture={(e) =>
send({ type: 'Set selected directory', data: fileOrDir })
}
>
{fileOrDir.children?.map((child) => (
<FileTreeItem
fileOrDir={child}
project={project}
currentFile={currentFile}
closePanel={closePanel}
level={level + 1}
key={level + '-' + child.path}
/>
))}
</ul>
</Disclosure.Panel>
</div>
)}
</Disclosure>
)}
{isConfirmingDelete && (
<DeleteConfirmationDialog
fileOrDir={fileOrDir}
setIsOpen={setIsConfirmingDelete}
/>
)}
</>
)
}
interface FileTreeProps {
className?: string
file?: IndexLoaderData['file']
closePanel: (
focusableElement?:
| HTMLElement
| React.MutableRefObject<HTMLElement | null>
| undefined
) => void
}
export const FileTree = ({
className = '',
file,
closePanel,
}: FileTreeProps) => {
const { send, context } = useFileContext()
useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder)
async function createFile() {
send({ type: 'Create file', data: { name: '', makeDir: false } })
}
async function createFolder() {
send({ type: 'Create file', data: { name: '', makeDir: true } })
}
return (
<div className={className}>
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-30/50 dark:bg-chalkboard-70/50">
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
<ActionButton
Element="button"
icon={{
icon: 'createFile',
iconClassName: '!text-energy-80 dark:!text-energy-20',
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
}}
className="!p-0 border-none bg-transparent !outline-none"
onClick={createFile}
>
<Tooltip position="inlineStart" delay={750}>
Create File
</Tooltip>
</ActionButton>
<ActionButton
Element="button"
icon={{
icon: 'createFolder',
iconClassName: '!text-energy-80 dark:!text-energy-20',
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
}}
className="!p-0 border-none bg-transparent !outline-none"
onClick={createFolder}
>
<Tooltip position="inlineStart" delay={750}>
Create Folder
</Tooltip>
</ActionButton>
</div>
<div className="overflow-auto max-h-full pb-12">
<ul
className="m-0 p-0 text-sm"
onClickCapture={(e) => {
send({ type: 'Set selected directory', data: context.project })
}}
>
{sortProject(context.project.children || []).map((fileOrDir) => (
<FileTreeItem
project={context.project}
currentFile={file}
fileOrDir={fileOrDir}
closePanel={closePanel}
key={fileOrDir.path}
/>
))}
</ul>
</div>
</div>
)
}
function KclIcon({ className = '' }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z"
fill="currentColor"
/>
</svg>
)
}

View File

@ -24,9 +24,7 @@ import {
StateFrom, StateFrom,
} from 'xstate' } from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { invoke } from '@tauri-apps/api'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { VITE_KC_API_BASE_URL } from 'env'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>

View File

@ -16,7 +16,14 @@ import { engineCommandManager } from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { addStartSketch } from 'lang/modifyAst' import { addStartSketch } from 'lang/modifyAst'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { recast, parse, Program, VariableDeclarator } from 'lang/wasm' import {
recast,
parse,
Program,
VariableDeclarator,
PipeExpression,
CallExpression,
} from 'lang/wasm'
import { getNodeFromPath } from 'lang/queryAst' import { getNodeFromPath } from 'lang/queryAst'
import { import {
addCloseToPipe, addCloseToPipe,
@ -29,11 +36,9 @@ import { applyConstraintAngleBetween } from './Toolbar/SetAngleBetween'
import { applyConstraintAngleLength } from './Toolbar/setAngleLength' import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { pathMapToSelections } from 'lang/util' import { pathMapToSelections } from 'lang/util'
import { import { useStore } from 'useStore'
dispatchCodeMirrorCursor, import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
setCodeMirrorCursor, import { applyConstraintIntersect } from './Toolbar/Intersect'
useStore,
} from 'useStore'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -83,16 +88,16 @@ export const ModelingMachineProvider = ({
'show default planes': () => { 'show default planes': () => {
kclManager.showPlanes() kclManager.showPlanes()
}, },
'create path': async () => { 'create path': assign({
sketchEnginePathId: () => {
const sketchUuid = uuidv4() const sketchUuid = uuidv4()
const proms = [
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: sketchUuid, cmd_id: sketchUuid,
cmd: { cmd: {
type: 'start_path', type: 'start_path',
}, },
}), })
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -100,16 +105,22 @@ export const ModelingMachineProvider = ({
type: 'edit_mode_enter', type: 'edit_mode_enter',
target: sketchUuid, target: sketchUuid,
}, },
}), })
] return sketchUuid
await Promise.all(proms)
}, },
'AST start new sketch': assign((_, { data: { coords, axis } }) => { }),
// Something really weird must have happened for this to happen. 'AST start new sketch': assign(
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
if (!axis) { if (!axis) {
// Something really weird must have happened for this to happen.
console.error('axis is undefined for starting a new sketch') console.error('axis is undefined for starting a new sketch')
return {} return {}
} }
if (!segmentId) {
// Something really weird must have happened for this to happen.
console.error('segmentId is undefined for starting a new sketch')
return {}
}
const _addStartSketch = addStartSketch( const _addStartSketch = addStartSketch(
kclManager.ast, kclManager.ast,
@ -124,43 +135,102 @@ export const ModelingMachineProvider = ({
const _pathToNode = _addStartSketch.pathToNode const _pathToNode = _addStartSketch.pathToNode
const newCode = recast(_modifiedAst) const newCode = recast(_modifiedAst)
const astWithUpdatedSource = parse(newCode) const astWithUpdatedSource = parse(newCode)
const updatedPipeNode = getNodeFromPath<PipeExpression>(
astWithUpdatedSource,
_pathToNode
).node
const startProfileAtCallExp = updatedPipeNode.body.find(
(exp) =>
exp.type === 'CallExpression' &&
exp.callee.name === 'startProfileAt'
)
if (startProfileAtCallExp)
engineCommandManager.artifactMap[sketchEnginePathId] = {
type: 'result',
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
commandType: 'extend_path',
data: null,
raw: {} as any,
}
const lineCallExp = updatedPipeNode.body.find(
(exp) => exp.type === 'CallExpression' && exp.callee.name === 'line'
)
if (lineCallExp)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
commandType: 'extend_path',
parentId: sketchEnginePathId,
data: null,
raw: {} as any,
}
kclManager.executeAstMock(astWithUpdatedSource, true) kclManager.executeAstMock(astWithUpdatedSource, true)
return { return {
sketchPathToNode: _pathToNode, sketchPathToNode: _pathToNode,
} }
}), }
'AST add line segment': ({ sketchPathToNode }, { data: { coords } }) => { ),
'AST add line segment': async (
{ sketchPathToNode, sketchEnginePathId },
{ data: { coords, segmentId } }
) => {
if (!sketchPathToNode) return if (!sketchPathToNode) return
const lastCoord = coords[coords.length - 1] const lastCoord = coords[coords.length - 1]
const { node: varDec } = getNodeFromPath<VariableDeclarator>( const pathInfo = await engineCommandManager.sendSceneCommand({
kclManager.ast, type: 'modeling_cmd_req',
sketchPathToNode, cmd_id: uuidv4(),
'VariableDeclarator' cmd: {
type: 'path_get_info',
path_id: sketchEnginePathId,
},
})
const firstSegment = pathInfo?.data?.data?.segments.find(
(seg: any) => seg.command === 'line_to'
) )
const variableName = varDec.id.name const firstSegCoords = await engineCommandManager.sendSceneCommand({
const sketchGroup = kclManager.programMemory.root[variableName] type: 'modeling_cmd_req',
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return cmd_id: uuidv4(),
const initialCoords = sketchGroup.value[0].from cmd: {
type: 'curve_get_control_points',
curve_id: firstSegment.command_id,
},
})
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
const isClose = compareVec2Epsilon(initialCoords, [ const isClose = compareVec2Epsilon(
lastCoord.x, [startPathCoord.x, startPathCoord.y],
lastCoord.y, [lastCoord.x, lastCoord.y]
]) )
let _modifiedAst: Program let _modifiedAst: Program
if (!isClose) { if (!isClose) {
_modifiedAst = addNewSketchLn({ const newSketchLn = addNewSketchLn({
node: kclManager.ast, node: kclManager.ast,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
to: [lastCoord.x, lastCoord.y], to: [lastCoord.x, lastCoord.y],
from: [coords[0].x, coords[0].y],
fnName: 'line', fnName: 'line',
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
}).modifiedAst })
kclManager.executeAstMock(_modifiedAst, true) const _modifiedAst = newSketchLn.modifiedAst
// kclManager.updateAst(_modifiedAst, false) kclManager.executeAstMock(_modifiedAst, true).then(() => {
const lineCallExp = getNodeFromPath<CallExpression>(
kclManager.ast,
newSketchLn.pathToNode
).node
if (segmentId)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
commandType: 'extend_path',
parentId: sketchEnginePathId,
data: null,
raw: {} as any,
}
})
} else { } else {
_modifiedAst = addCloseToPipe({ _modifiedAst = addCloseToPipe({
node: kclManager.ast, node: kclManager.ast,
@ -209,25 +279,37 @@ export const ModelingMachineProvider = ({
// I've found this the best way to deal with the editor without causing an infinite loop // I've found this the best way to deal with the editor without causing an infinite loop
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it // and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
// because we want to respect the user manually placing the cursor too. // because we want to respect the user manually placing the cursor too.
const selectionRangeTypeMap = setCodeMirrorCursor({
// for more details on how selections see `src/lib/selections.ts`.
const { codeMirrorSelection, selectionRangeTypeMap } =
handleSelectionWithShift({
codeSelection: setSelections.selection, codeSelection: setSelections.selection,
currestSelections: selectionRanges, currestSelections: selectionRanges,
editorView,
isShiftDown, isShiftDown,
}) })
return { if (codeMirrorSelection) {
selectionRangeTypeMap, setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
})
} }
return { selectionRangeTypeMap }
} }
// This DOES NOT set the `selectionRanges` in xstate context // This DOES NOT set the `selectionRanges` in xstate context
// same as comment above // same as comment above
const selectionRangeTypeMap = dispatchCodeMirrorCursor({ const { codeMirrorSelection, selectionRangeTypeMap } =
handleSelectionBatch({
selections: setSelections.selection, selections: setSelections.selection,
editorView,
}) })
return { if (codeMirrorSelection) {
selectionRangeTypeMap, setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
})
} }
return { selectionRangeTypeMap }
}), }),
}, },
guards: { guards: {
@ -312,6 +394,22 @@ export const ModelingMachineProvider = ({
), ),
} }
}, },
'Get perpendicular distance info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
}, },
devTools: true, devTools: true,
}) })
@ -325,7 +423,13 @@ export const ModelingMachineProvider = ({
}) })
} }
}) })
}, [kclManager.defaultPlanes, modelingSend, modelingState.nextEvents]) }, [modelingSend, modelingState.nextEvents])
useEffect(() => {
kclManager.registerExecuteCallback(() => {
modelingSend({ type: 'Re-execute' })
})
}, [modelingSend])
// useStateMachineCommands({ // useStateMachineCommands({
// state: settingsState, // state: settingsState,

View File

@ -1,4 +1,4 @@
import { FormEvent, useState } from 'react' import { FormEvent, useEffect, useState } from 'react'
import { type ProjectWithEntryPointMetadata, paths } from '../Router' import { type ProjectWithEntryPointMetadata, paths } from '../Router'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
@ -8,7 +8,7 @@ import {
faTrashAlt, faTrashAlt,
faX, faX,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { FILE_EXT } from '../lib/tauriFS' import { FILE_EXT, getPartsCount, readProject } from '../lib/tauriFS'
import { Dialog } from '@headlessui/react' import { Dialog } from '@headlessui/react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -28,6 +28,8 @@ function ProjectCard({
useHotkeys('esc', () => setIsEditing(false)) useHotkeys('esc', () => setIsEditing(false))
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfParts, setNumberOfParts] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0)
function handleSave(e: FormEvent<HTMLFormElement>) { function handleSave(e: FormEvent<HTMLFormElement>) {
e.preventDefault() e.preventDefault()
@ -42,6 +44,17 @@ function ProjectCard({
: date.toLocaleTimeString() : date.toLocaleTimeString()
} }
useEffect(() => {
async function getNumberOfParts() {
const { kclFileCount, kclDirCount } = getPartsCount(
await readProject(project.path)
)
setNumberOfParts(kclFileCount)
setNumberOfFolders(kclDirCount)
}
getNumberOfParts()
}, [project.path])
return ( return (
<li <li
{...props} {...props}
@ -76,7 +89,7 @@ function ProjectCard({
</form> </form>
) : ( ) : (
<> <>
<div className="p-1 flex flex-col gap-2"> <div className="p-1 flex flex-col h-full gap-2">
<Link <Link
to={`${paths.FILE}/${encodeURIComponent(project.path)}`} to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
className="flex-1 text-liquid-100" className="flex-1 text-liquid-100"
@ -84,7 +97,14 @@ function ProjectCard({
{project.name?.replace(FILE_EXT, '')} {project.name?.replace(FILE_EXT, '')}
</Link> </Link>
<span className="text-chalkboard-60 text-xs"> <span className="text-chalkboard-60 text-xs">
Edited {getDisplayedTime(project.entrypoint_metadata.modifiedAt)} {numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 &&
`/ ${numberOfFolders} folder${
numberOfFolders === 1 ? '' : 's'
}`}
</span>
<span className="text-chalkboard-60 text-xs">
Edited {getDisplayedTime(project.entrypointMetadata.modifiedAt)}
</span> </span>
<div className="absolute bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"> <div className="absolute bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
<ActionButton <ActionButton

View File

@ -15,7 +15,7 @@ const projectWellFormed = {
path: '/some/path/Simple Box/main.kcl', path: '/some/path/Simple Box/main.kcl',
}, },
], ],
entrypoint_metadata: { entrypointMetadata: {
accessedAt: now, accessedAt: now,
blksize: 32, blksize: 32,
blocks: 32, blocks: 32,

View File

@ -1,18 +1,22 @@
import { Popover, Transition } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { faHome } from '@fortawesome/free-solid-svg-icons' import { faHome } from '@fortawesome/free-solid-svg-icons'
import { ProjectWithEntryPointMetadata, paths } from '../Router' import { IndexLoaderData, paths } from '../Router'
import { isTauri } from '../lib/isTauri' import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ExportButton } from './ExportButton' import { ExportButton } from './ExportButton'
import { Fragment } from 'react' import { Fragment } from 'react'
import { FileTree } from './FileTree'
import { sep } from '@tauri-apps/api/path'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
file,
renderAsLink = false, renderAsLink = false,
}: { }: {
renderAsLink?: boolean renderAsLink?: boolean
project?: Partial<ProjectWithEntryPointMetadata> project?: IndexLoaderData['project']
file?: IndexLoaderData['file']
}) => { }) => {
return renderAsLink ? ( return renderAsLink ? (
<Link <Link
@ -23,10 +27,10 @@ const ProjectSidebarMenu = ({
<img <img
src="/kitt-8bit-winking.svg" src="/kitt-8bit-winking.svg"
alt="KittyCAD App" alt="KittyCAD App"
className="h-9 w-auto" className="w-auto h-9"
/> />
<span <span
className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block" className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
data-testid="project-sidebar-link-name" data-testid="project-sidebar-link-name"
> >
{project?.name ? project.name : 'KittyCAD Modeling App'} {project?.name ? project.name : 'KittyCAD Modeling App'}
@ -41,11 +45,20 @@ const ProjectSidebarMenu = ({
<img <img
src="/kitt-8bit-winking.svg" src="/kitt-8bit-winking.svg"
alt="KittyCAD App" alt="KittyCAD App"
className="h-full w-auto" className="w-auto h-full"
/> />
<span className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block"> <div className="flex flex-col items-start py-0.5">
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'} <span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
{isTauri() && file?.name
? file.name.slice(file.name.lastIndexOf(sep) + 1)
: 'KittyCAD Modeling App'}
</span> </span>
{isTauri() && project?.name && (
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
{project.name}
</span>
)}
</div>
</Popover.Button> </Popover.Button>
<Transition <Transition
enter="duration-200 ease-out" enter="duration-200 ease-out"
@ -56,7 +69,7 @@ const ProjectSidebarMenu = ({
leaveTo="opacity-0" leaveTo="opacity-0"
as={Fragment} as={Fragment}
> >
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" /> <Popover.Overlay className="fixed inset-0 z-20 bg-chalkboard-110/50" />
</Transition> </Transition>
<Transition <Transition
@ -68,37 +81,53 @@ const ProjectSidebarMenu = ({
leaveTo="opacity-0 -translate-x-4" leaveTo="opacity-0 -translate-x-4"
as={Fragment} as={Fragment}
> >
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 dark:border-energy-100/50 shadow-md rounded-r-lg overflow-hidden"> <Popover.Panel
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100"> className="fixed inset-0 right-auto z-30 grid w-64 h-screen max-h-screen grid-cols-1 border rounded-r-lg shadow-md bg-chalkboard-10 dark:bg-chalkboard-100 border-energy-100 dark:border-energy-100/50"
style={{ gridTemplateRows: 'auto 1fr auto' }}
>
{({ close }) => (
<>
<div className="flex items-center gap-4 px-4 py-3 bg-energy-10/25 dark:bg-energy-110">
<img <img
src="/kitt-8bit-winking.svg" src="/kitt-8bit-winking.svg"
alt="KittyCAD App" alt="KittyCAD App"
className="h-9 w-auto" className="w-auto h-9"
/> />
<div> <div>
<p <p
className="m-0 text-energy-10 text-mono" className="m-0 text-chalkboard-100 dark:text-energy-10 text-mono"
data-testid="projectName" data-testid="projectName"
> >
{project?.name ? project.name : 'KittyCAD Modeling App'} {project?.name ? project.name : 'KittyCAD Modeling App'}
</p> </p>
{project?.entrypoint_metadata && ( {project?.entrypointMetadata && (
<p <p
className="m-0 text-energy-40 text-xs" className="m-0 text-xs text-chalkboard-100 dark:text-energy-40"
data-testid="createdAt" data-testid="createdAt"
> >
Created{' '} Created{' '}
{project?.entrypoint_metadata.createdAt.toLocaleDateString()} {project.entrypointMetadata.createdAt.toLocaleDateString()}
</p> </p>
)} )}
</div> </div>
</div> </div>
<div className="p-4 flex flex-col gap-2"> {isTauri() ? (
<FileTree
file={file}
className="overflow-hidden border-0 border-y border-energy-40 dark:border-energy-70"
closePanel={close}
/>
) : (
<div className="flex-1 overflow-hidden" />
)}
<div className="flex flex-col gap-2 p-4 bg-energy-10/25 dark:bg-energy-110">
<ExportButton <ExportButton
className={{ className={{
button: button:
'border-transparent dark:border-transparent dark:hover:border-energy-60', 'border-transparent dark:border-transparent hover:border-energy-60',
icon: 'text-energy-10 dark:text-energy-120',
bg: 'bg-energy-120 dark:bg-energy-10',
}} }}
> >
Export Model Export Model
@ -109,13 +138,17 @@ const ProjectSidebarMenu = ({
to={paths.HOME} to={paths.HOME}
icon={{ icon={{
icon: faHome, icon: faHome,
iconClassName: 'text-energy-10 dark:text-energy-120',
bgClassName: 'bg-energy-120 dark:bg-energy-10',
}} }}
className="border-transparent dark:border-transparent dark:hover:border-energy-60" className="border-transparent dark:border-transparent hover:border-energy-60"
> >
Go to Home Go to Home
</ActionButton> </ActionButton>
)} )}
</div> </div>
</>
)}
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>
</Popover> </Popover>

View File

@ -1,5 +1,6 @@
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { type InstanceProps, create } from 'react-modal-promise'
import { Value } from '../lang/wasm' import { Value } from '../lang/wasm'
import { import {
AvailableVars, AvailableVars,
@ -9,6 +10,28 @@ import {
CreateNewVariable, CreateNewVariable,
} from './AvailableVarsHelpers' } from './AvailableVarsHelpers'
type ModalResolve = {
value: string
sign: number
valueNode: Value
variableName?: string
newVariableInsertIndex: number
}
type ModalReject = boolean
type SetAngleLengthModalProps = InstanceProps<ModalResolve, ModalReject> & {
value: number
valueName: string
shouldCreateVariable?: boolean
}
export const createSetAngleLengthModal = create<
SetAngleLengthModalProps,
ModalResolve,
ModalReject
>
export const SetAngleLengthModal = ({ export const SetAngleLengthModal = ({
isOpen, isOpen,
onResolve, onResolve,
@ -16,20 +39,7 @@ export const SetAngleLengthModal = ({
value: initialValue, value: initialValue,
valueName, valueName,
shouldCreateVariable: initialShouldCreateVariable = false, shouldCreateVariable: initialShouldCreateVariable = false,
}: { }: SetAngleLengthModalProps) => {
isOpen: boolean
onResolve: (a: {
value: string
sign: number
valueNode: Value
variableName?: string
newVariableInsertIndex: number
}) => void
onReject: (a: any) => void
value: number
valueName: string
shouldCreateVariable: boolean
}) => {
const [sign, setSign] = useState(Math.sign(Number(initialValue))) const [sign, setSign] = useState(Math.sign(Number(initialValue)))
const [value, setValue] = useState(String(initialValue * sign)) const [value, setValue] = useState(String(initialValue * sign))
const [shouldCreateVariable, setShouldCreateVariable] = useState( const [shouldCreateVariable, setShouldCreateVariable] = useState(

View File

@ -1,5 +1,6 @@
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { type InstanceProps, create } from 'react-modal-promise'
import { Value } from '../lang/wasm' import { Value } from '../lang/wasm'
import { import {
AvailableVars, AvailableVars,
@ -9,6 +10,30 @@ import {
CreateNewVariable, CreateNewVariable,
} from './AvailableVarsHelpers' } from './AvailableVarsHelpers'
type ModalResolve = {
value: string
segName: string
valueNode: Value
variableName?: string
newVariableInsertIndex: number
sign: number
}
type ModalReject = boolean
type GetInfoModalProps = InstanceProps<ModalResolve, ModalReject> & {
segName: string
isSegNameEditable: boolean
value?: number
initialVariableName: string
}
export const createInfoModal = create<
GetInfoModalProps,
ModalResolve,
ModalReject
>
export const GetInfoModal = ({ export const GetInfoModal = ({
isOpen, isOpen,
onResolve, onResolve,
@ -17,25 +42,12 @@ export const GetInfoModal = ({
isSegNameEditable, isSegNameEditable,
value: initialValue, value: initialValue,
initialVariableName, initialVariableName,
}: { }: GetInfoModalProps) => {
isOpen: boolean
onResolve: (a: {
value: string
segName: string
valueNode: Value
variableName?: string
newVariableInsertIndex: number
sign: number
}) => void
onReject: (a: any) => void
segName: string
isSegNameEditable: boolean
value: number
initialVariableName: string
}) => {
const [sign, setSign] = useState(Math.sign(Number(initialValue))) const [sign, setSign] = useState(Math.sign(Number(initialValue)))
const [segName, setSegName] = useState(initialSegName) const [segName, setSegName] = useState(initialSegName)
const [value, setValue] = useState(String(Math.abs(initialValue))) const [value, setValue] = useState(
initialValue === undefined ? '' : String(Math.abs(initialValue))
)
const [shouldCreateVariable, setShouldCreateVariable] = useState(false) const [shouldCreateVariable, setShouldCreateVariable] = useState(false)
const { const {

View File

@ -4,19 +4,26 @@ import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { faPlus } from '@fortawesome/free-solid-svg-icons' import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { type InstanceProps, create } from 'react-modal-promise'
type ModalResolve = { variableName: string }
type ModalReject = boolean
type SetVarNameModalProps = InstanceProps<ModalResolve, ModalReject> & {
valueName: string
}
export const createSetVarNameModal = create<
SetVarNameModalProps,
ModalResolve,
ModalReject
>
export const SetVarNameModal = ({ export const SetVarNameModal = ({
isOpen, isOpen,
onResolve, onResolve,
onReject, onReject,
valueName, valueName,
}: { }: SetVarNameModalProps) => {
isOpen: boolean
onResolve: (a: { variableName?: string }) => void
onReject: (a: any) => void
value: number
valueName: string
}) => {
const { isNewVariableNameUnique, newVariableName, setNewVariableName } = const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
useCalc({ value: '', initialVariableName: valueName }) useCalc({ value: '', initialVariableName: valueName })

View File

@ -14,10 +14,11 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models' import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodeFromPath } from 'lang/queryAst' import { getNodeFromPath } from 'lang/queryAst'
import { Program, VariableDeclarator, modifyAstForSketch } from 'lang/wasm' import { VariableDeclarator, recast, parse, CallExpression } from 'lang/wasm'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager, useKclContext } from 'lang/KclSinglton' import { kclManager, useKclContext } from 'lang/KclSinglton'
import { changeSketchArguments } from 'lang/std/sketch'
export const Stream = ({ className = '' }) => { export const Stream = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -84,6 +85,12 @@ export const Stream = ({ className = '' }) => {
} }
if (state.matches('Sketch.Move Tool')) { if (state.matches('Sketch.Move Tool')) {
if (
state.matches('Sketch.Move Tool.No move') ||
state.matches('Sketch.Move Tool.Move with execute')
) {
return
}
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -209,7 +216,14 @@ export const Stream = ({ className = '' }) => {
} }
} }
send({ type: 'Add point', data: { coords, axis: currentAxis } }) send({
type: 'Add point',
data: {
coords,
axis: currentAxis,
segmentId: entities_modified[0],
},
})
} else if (state.matches('Sketch.Line Tool.Segment Added')) { } else if (state.matches('Sketch.Line Tool.Segment Added')) {
const curve = await engineCommandManager.sendSceneCommand({ const curve = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -221,7 +235,10 @@ export const Stream = ({ className = '' }) => {
}) })
const coords: { x: number; y: number }[] = const coords: { x: number; y: number }[] =
curve.data.data.control_points curve.data.data.control_points
send({ type: 'Add point', data: { coords, axis: null } }) send({
type: 'Add point',
data: { coords, axis: null, segmentId: entities_modified[0] },
})
} }
}) })
} else if ( } else if (
@ -255,8 +272,6 @@ export const Stream = ({ className = '' }) => {
context.sketchPathToNode, context.sketchPathToNode,
'VariableDeclarator' 'VariableDeclarator'
).node ).node
const variableName = varDec?.id?.name
// Get the current plane string for plane we are on. // Get the current plane string for plane we are on.
let currentPlaneString = '' let currentPlaneString = ''
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) { if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
@ -272,14 +287,73 @@ export const Stream = ({ className = '' }) => {
// error. // error.
if (currentPlaneString === '') return if (currentPlaneString === '') return
const updatedAst: Program = await modifyAstForSketch( const pathInfo = await engineCommandManager.sendSceneCommand({
engineCommandManager, type: 'modeling_cmd_req',
kclManager.ast, cmd_id: uuidv4(),
variableName, cmd: {
currentPlaneString, type: 'path_get_info',
context.sketchEnginePathId path_id: context.sketchEnginePathId,
},
})
const segmentsWithMappings = (
pathInfo?.data?.data?.segments as { command_id: string }[]
) )
kclManager.executeAstMock(updatedAst, true) .filter(({ command_id }) => {
return command_id && engineCommandManager.artifactMap[command_id]
})
.map(({ command_id }) => command_id)
const segment2dInfo = await Promise.all(
segmentsWithMappings.map(async (segmentId) => {
const response = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: segmentId,
},
})
const controlPoints: [
{ x: number; y: number },
{ x: number; y: number }
] = response.data.data.control_points
return {
controlPoints,
segmentId,
}
})
)
let modifiedAst = { ...kclManager.ast }
let code = kclManager.code
for (const controlPoint of segment2dInfo) {
const range =
engineCommandManager.artifactMap[controlPoint.segmentId].range
if (!range) continue
const from = controlPoint.controlPoints[0]
const to = controlPoint.controlPoints[1]
const modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
range,
[to.x, to.y],
[from.x, from.y]
)
modifiedAst = modded.modifiedAst
// update artifact map ranges now that we have updated the ast.
code = recast(modded.modifiedAst)
const astWithCurrentRanges = parse(code)
const updateNode = getNodeFromPath<CallExpression>(
astWithCurrentRanges,
modded.pathToNode
).node
engineCommandManager.artifactMap[controlPoint.segmentId].range = [
updateNode.start,
updateNode.end,
]
}
kclManager.executeAstMock(modifiedAst, true)
}) })
} }

View File

@ -11,22 +11,22 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { useMemo, useState } from 'react' import { useMemo } from 'react'
import { linter, lintGutter } from '@codemirror/lint' import { linter, lintGutter } from '@codemirror/lint'
import { Selections, useStore } from 'useStore' import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections'
import { LanguageServerClient } from 'editor/lsp' import { LanguageServerClient } from 'editor/lsp'
import kclLanguage from 'editor/lsp/language' import kclLanguage from 'editor/lsp/language'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { writeTextFile } from '@tauri-apps/api/fs' import { writeTextFile } from '@tauri-apps/api/fs'
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { import {
EditorView, EditorView,
addLineHighlight, addLineHighlight,
lineHighlightField, lineHighlightField,
} from 'editor/highlightextension' } from 'editor/highlightextension'
import { isOverlap, roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors' import { kclErrToDiagnostic } from 'lang/errors'
import { CSSRuleObject } from 'tailwindcss/types/config' import { CSSRuleObject } from 'tailwindcss/types/config'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
@ -111,18 +111,16 @@ export const TextEditor = ({
}, [lspClient, isLSPServerReady]) }, [lspClient, isLSPServerReady])
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => { // const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (newCode: string, viewUpdate: ViewUpdate) => { const onChange = (newCode: string) => {
kclManager.setCodeAndExecute(newCode) kclManager.setCodeAndExecute(newCode)
if (isTauri() && pathParams.id) { if (isTauri() && pathParams.id) {
// Save the file to disk // Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, newCode).catch( writeTextFile(pathParams.id, newCode).catch((err) => {
(err) => {
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254) // TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err) console.error('error saving file', err)
toast.error('Error saving file, please check file permissions') toast.error('Error saving file, please check file permissions')
} })
)
} }
if (editorView) { if (editorView) {
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) }) editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
@ -132,64 +130,17 @@ export const TextEditor = ({
if (!editorView) { if (!editorView) {
setEditorView(viewUpdate.view) setEditorView(viewUpdate.view)
} }
const ranges = viewUpdate.state.selection.ranges const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges,
selectionRangeTypeMap,
})
if (!eventInfo) return
const isChange = send(eventInfo.modelingEvent)
ranges.length !== selectionRanges.codeBasedSelections.length || eventInfo.engineEvents.forEach((event) =>
ranges.some(({ from, to }, i) => { engineCommandManager.sendSceneCommand(event)
return (
from !== selectionRanges.codeBasedSelections[i].range[0] ||
to !== selectionRanges.codeBasedSelections[i].range[1]
) )
})
if (!isChange) return
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
({ from, to }) => {
if (selectionRangeTypeMap[to]) {
return {
type: selectionRangeTypeMap[to],
range: [from, to],
}
}
return {
type: 'default',
range: [from, to],
}
}
)
const idBasedSelections = codeBasedSelections
.map(({ type, range }) => {
const hasOverlap = Object.entries(
engineCommandManager.sourceRangeMap || {}
).filter(([_, sourceRange]) => {
return isOverlap(sourceRange, range)
})
if (hasOverlap.length) {
return {
type,
id: hasOverlap[0][0],
}
}
})
.filter(Boolean) as any
engineCommandManager.cusorsSelected({
otherSelections: [],
idBasedSelections,
})
selectionRanges &&
send({
type: 'Set selection',
data: {
selectionType: 'mirrorCodeMirrorSelections',
selection: {
...selectionRanges,
codeBasedSelections,
},
},
})
} }
const editorExtensions = useMemo(() => { const editorExtensions = useMemo(() => {
@ -269,7 +220,7 @@ export const TextEditor = ({
} }
return extensions return extensions
}, [kclLSP, textWrapping]) }, [kclLSP, textWrapping, convertCallback])
return ( return (
<div <div

View File

@ -1,30 +1,23 @@
import { useState, useEffect } from 'react' import { toolTips } from '../../useStore'
import { toolTips, useStore } from '../../useStore' import { Selections } from 'lib/selections'
import { Value, VariableDeclarator } from '../../lang/wasm' import { Program, Value, VariableDeclarator } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
getNodeFromPath, getNodeFromPath,
} from '../../lang/queryAst' } from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import { import {
TransformInfo,
transformSecondarySketchLinesTagFirst, transformSecondarySketchLinesTagFirst,
getTransformInfos, getTransformInfos,
PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { ActionIcon } from 'components/ActionIcon'
import { sketchButtonClassnames } from 'Toolbar'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
/* export function equalAngleInfo({
export const EqualAngle = () => { selectionRanges,
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({ }: {
guiMode: s.guiMode, selectionRanges: Selections
selectionRanges: s.selectionRanges, }) {
setCursor: s.setCursor,
}))
const [enableEqual, setEnableEqual] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
useEffect(() => {
const paths = selectionRanges.codeBasedSelections.map(({ range }) => const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
getNodePathFromSourceRange(kclManager.ast, range) getNodePathFromSourceRange(kclManager.ast, range)
) )
@ -50,7 +43,7 @@ export const EqualAngle = () => {
toolTips.includes(node.callee.name as any) toolTips.includes(node.callee.name as any)
) )
const theTransforms = getTransformInfos( const transforms = getTransformInfos(
{ {
...selectionRanges, ...selectionRanges,
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1), codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
@ -58,45 +51,29 @@ export const EqualAngle = () => {
kclManager.ast, kclManager.ast,
'equalAngle' 'equalAngle'
) )
setTransformInfos(theTransforms)
const _enableEqual = const enabled =
!!secondaryVarDecs.length && !!secondaryVarDecs.length &&
isAllTooltips && isAllTooltips &&
isOthersLinkedToPrimary && isOthersLinkedToPrimary &&
theTransforms.every(Boolean) transforms.every(Boolean)
setEnableEqual(_enableEqual) return { enabled, transforms }
}, [guiMode, selectionRanges]) }
if (guiMode.mode !== 'sketch') return null
return ( export function applyConstraintEqualAngle({
<button selectionRanges,
onClick={async () => { }: {
if (!transformInfos) return selectionRanges: Selections
const { modifiedAst, pathToNodeMap } = }): {
transformSecondarySketchLinesTagFirst({ modifiedAst: Program
pathToNodeMap: PathToNodeMap
} {
const { transforms } = equalAngleInfo({ selectionRanges })
const { modifiedAst, pathToNodeMap } = transformSecondarySketchLinesTagFirst({
ast: kclManager.ast, ast: kclManager.ast,
selectionRanges, selectionRanges,
transformInfos, transformInfos: transforms,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
}) })
kclManager.updateAst(modifiedAst, true, { return { modifiedAst, pathToNodeMap }
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}
disabled={!enableEqual}
title="Parallel (or equal angle)"
className="group"
>
<ActionIcon
icon="parallel"
className="!p-0.5"
bgClassName={sketchButtonClassnames.background}
iconClassName={sketchButtonClassnames.icon}
size="md"
/>
Parallel
</button>
)
} }
*/

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { toolTips } from '../../useStore'
import { Selections, toolTips, useStore } from '../../useStore' import { Selections } from 'lib/selections'
import { Program, Value, VariableDeclarator } from '../../lang/wasm' import { Program, Value, VariableDeclarator } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -7,63 +7,12 @@ import {
} from '../../lang/queryAst' } from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import { import {
TransformInfo,
transformSecondarySketchLinesTagFirst, transformSecondarySketchLinesTagFirst,
getTransformInfos, getTransformInfos,
PathToNodeMap, PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { ActionIcon } from 'components/ActionIcon'
import { sketchButtonClassnames } from 'Toolbar'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
/*
export const EqualLength = () => {
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
guiMode: s.guiMode,
selectionRanges: s.selectionRanges,
setCursor: s.setCursor,
}))
const [enableEqual, setEnableEqual] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
useEffect(() => {
const { enabled, transforms } = setEqualLengthInfo({ selectionRanges })
setTransformInfos(transforms)
setEnableEqual(enabled)
}, [guiMode, selectionRanges])
if (guiMode.mode !== 'sketch') return null
return (
<button
onClick={() => {
if (!transformInfos) return
const { modifiedAst, pathToNodeMap } =
transformSecondarySketchLinesTagFirst({
ast: kclManager.ast,
selectionRanges,
transformInfos,
programMemory: kclManager.programMemory,
})
kclManager.updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}
disabled={!enableEqual}
className="group"
title="Equal Length"
>
<ActionIcon
icon="equal"
className="!p-0.5"
bgClassName={sketchButtonClassnames.background}
iconClassName={sketchButtonClassnames.icon}
size="md"
/>
Equal Length
</button>
)
}
*/
export function setEqualLengthInfo({ export function setEqualLengthInfo({
selectionRanges, selectionRanges,
}: { }: {

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { toolTips } from '../../useStore'
import { toolTips, useStore } from '../../useStore' import { Selections } from 'lib/selections'
import { Program, ProgramMemory, Value } from '../../lang/wasm' import { Program, ProgramMemory, Value } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -7,66 +7,10 @@ import {
} from '../../lang/queryAst' } from '../../lang/queryAst'
import { import {
PathToNodeMap, PathToNodeMap,
TransformInfo,
getTransformInfos, getTransformInfos,
transformAstSketchLines, transformAstSketchLines,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { ActionIcon } from 'components/ActionIcon'
import { sketchButtonClassnames } from 'Toolbar'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
import { Selections } from 'useStore'
/*
export const HorzVert = ({
horOrVert,
}: {
horOrVert: 'vertical' | 'horizontal'
}) => {
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
guiMode: s.guiMode,
selectionRanges: s.selectionRanges,
setCursor: s.setCursor,
}))
const [enableHorz, setEnableHorz] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
useEffect(() => {
const { enabled, transforms } = horzVertInfo(selectionRanges, horOrVert)
setTransformInfos(transforms)
setEnableHorz(enabled)
}, [guiMode, selectionRanges])
if (guiMode.mode !== 'sketch') return null
return (
<button
onClick={() => {
if (!transformInfos) return
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({
ast: kclManager.ast,
selectionRanges,
transformInfos,
programMemory: kclManager.programMemory,
referenceSegName: '',
})
kclManager.updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}
disabled={!enableHorz}
className="group"
title={horOrVert === 'horizontal' ? 'Horizontal' : 'Vertical'}
>
<ActionIcon
icon={horOrVert === 'horizontal' ? 'horizontal' : 'vertical'}
className="!p-0.5"
bgClassName={sketchButtonClassnames.background}
iconClassName={sketchButtonClassnames.icon}
size="md"
/>
{horOrVert === 'horizontal' ? 'Horizontal' : 'Vertical'}
</button>
)
}
*/
export function horzVertInfo( export function horzVertInfo(
selectionRanges: Selections, selectionRanges: Selections,
@ -110,7 +54,4 @@ export function applyConstraintHorzVert(
programMemory, programMemory,
referenceSegName: '', referenceSegName: '',
}) })
// kclManager.updateAst(modifiedAst, true, {
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
// })
} }

View File

@ -1,7 +1,6 @@
import { useState, useEffect } from 'react' import { toolTips } from '../../useStore'
import { create } from 'react-modal-promise' import { Selections } from 'lib/selections'
import { toolTips, useStore } from '../../useStore' import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
import { BinaryPart, Value, VariableDeclarator } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
getNodeFromPath, getNodeFromPath,
@ -9,33 +8,28 @@ import {
} from '../../lang/queryAst' } from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import { import {
TransformInfo,
transformSecondarySketchLinesTagFirst, transformSecondarySketchLinesTagFirst,
getTransformInfos, getTransformInfos,
PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { GetInfoModal } from '../SetHorVertDistanceModal' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createVariableDeclaration } from '../../lang/modifyAst' import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
const getModalInfo = create(GetInfoModal as any) const getModalInfo = createInfoModal(GetInfoModal)
/* export function intersectInfo({
export const Intersect = () => { selectionRanges,
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({ }: {
guiMode: s.guiMode, selectionRanges: Selections
selectionRanges: s.selectionRanges, }) {
setCursor: s.setCursor,
}))
const [enable, setEnable] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
const [forecdSelectionRanges, setForcedSelectionRanges] =
useState<typeof selectionRanges>()
useEffect(() => {
if (selectionRanges.codeBasedSelections.length < 2) { if (selectionRanges.codeBasedSelections.length < 2) {
setEnable(false) return {
setForcedSelectionRanges({ ...selectionRanges }) enabled: false,
return transforms: [],
forcedSelectionRanges: { ...selectionRanges },
}
} }
const previousSegment = const previousSegment =
@ -63,7 +57,6 @@ export const Intersect = () => {
: selectionRanges.codeBasedSelections?.[1], : selectionRanges.codeBasedSelections?.[1],
], ],
} }
setForcedSelectionRanges(_forcedSelectionRanges)
const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) => const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) =>
getNodePathFromSourceRange(kclManager.ast, range) getNodePathFromSourceRange(kclManager.ast, range)
@ -96,13 +89,11 @@ export const Intersect = () => {
const theTransforms = getTransformInfos( const theTransforms = getTransformInfos(
{ {
...selectionRanges, ...selectionRanges,
codeBasedSelections: codeBasedSelections: _forcedSelectionRanges.codeBasedSelections.slice(1),
_forcedSelectionRanges.codeBasedSelections.slice(1),
}, },
kclManager.ast, kclManager.ast,
'intersect' 'intersect'
) )
setTransformInfos(theTransforms)
const _enableEqual = const _enableEqual =
secondaryVarDecs.length === 1 && secondaryVarDecs.length === 1 &&
@ -110,19 +101,30 @@ export const Intersect = () => {
isOthersLinkedToPrimary && isOthersLinkedToPrimary &&
theTransforms.every(Boolean) && theTransforms.every(Boolean) &&
_forcedSelectionRanges?.codeBasedSelections?.[1]?.type === 'line-end' _forcedSelectionRanges?.codeBasedSelections?.[1]?.type === 'line-end'
setEnable(_enableEqual)
}, [guiMode, selectionRanges])
if (guiMode.mode !== 'sketch') return null
return ( return {
<button enabled: _enableEqual,
onClick={async () => { transforms: theTransforms,
if (!(transformInfos && forecdSelectionRanges)) return forcedSelectionRanges: _forcedSelectionRanges,
}
}
export async function applyConstraintIntersect({
selectionRanges,
}: {
selectionRanges: Selections
}): Promise<{
modifiedAst: Program
pathToNodeMap: PathToNodeMap
}> {
const { transforms, forcedSelectionRanges } = intersectInfo({
selectionRanges,
})
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } = const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
transformSecondarySketchLinesTagFirst({ transformSecondarySketchLinesTagFirst({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: JSON.parse(JSON.stringify(kclManager.ast)),
selectionRanges: forecdSelectionRanges, selectionRanges: forcedSelectionRanges,
transformInfos, transformInfos: transforms,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
}) })
const { const {
@ -132,35 +134,29 @@ export const Intersect = () => {
variableName, variableName,
newVariableInsertIndex, newVariableInsertIndex,
sign, sign,
}: {
segName: string
value: number
valueNode: Value
variableName?: string
newVariableInsertIndex: number
sign: number
} = await getModalInfo({ } = await getModalInfo({
segName: tagInfo?.tag, segName: tagInfo?.tag,
isSegNameEditable: !tagInfo?.isTagExisting, isSegNameEditable: !tagInfo?.isTagExisting,
value: valueUsedInTransform, value: valueUsedInTransform,
initialVariableName: 'offset', initialVariableName: 'offset',
} as any)
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
kclManager.updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
} else { if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
return {
modifiedAst,
pathToNodeMap,
}
}
// transform again but forcing certain values // transform again but forcing certain values
const finalValue = removeDoubleNegatives( const finalValue = removeDoubleNegatives(
valueNode as BinaryPart, valueNode as BinaryPart,
sign, sign,
variableName variableName
) )
const { modifiedAst: _modifiedAst, pathToNodeMap } = const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
transformSecondarySketchLinesTagFirst({ transformSecondarySketchLinesTagFirst({
ast: kclManager.ast, ast: kclManager.ast,
selectionRanges: forecdSelectionRanges, selectionRanges: forcedSelectionRanges,
transformInfos, transformInfos: transforms,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
forceSegName: segName, forceSegName: segName,
forceValueUsedInTransform: finalValue, forceValueUsedInTransform: finalValue,
@ -174,16 +170,8 @@ export const Intersect = () => {
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
} }
kclManager.updateAst(_modifiedAst, true, { return {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), modifiedAst: _modifiedAst,
}) pathToNodeMap: _pathToNodeMap,
} }
}}
disabled={!enable}
title="Set Perpendicular Distance"
>
Set Perpendicular Distance
</button>
)
} }
*/

View File

@ -1,27 +1,22 @@
import { useState, useEffect } from 'react' import { toolTips } from '../../useStore'
import { toolTips, useStore } from '../../useStore' import { Selections } from 'lib/selections'
import { Value } from '../../lang/wasm' import { Program, Value } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
getNodeFromPath, getNodeFromPath,
} from '../../lang/queryAst' } from '../../lang/queryAst'
import { import {
TransformInfo, PathToNodeMap,
getRemoveConstraintsTransforms, getRemoveConstraintsTransforms,
transformAstSketchLines, transformAstSketchLines,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
/* export function removeConstrainingValuesInfo({
export const RemoveConstrainingValues = () => { selectionRanges,
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({ }: {
guiMode: s.guiMode, selectionRanges: Selections
selectionRanges: s.selectionRanges, }) {
setCursor: s.setCursor,
}))
const [enableHorz, setEnableHorz] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
useEffect(() => {
const paths = selectionRanges.codeBasedSelections.map(({ range }) => const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
getNodePathFromSourceRange(kclManager.ast, range) getNodePathFromSourceRange(kclManager.ast, range)
) )
@ -35,41 +30,34 @@ export const RemoveConstrainingValues = () => {
) )
try { try {
const theTransforms = getRemoveConstraintsTransforms( const transforms = getRemoveConstraintsTransforms(
selectionRanges, selectionRanges,
kclManager.ast, kclManager.ast,
'removeConstrainingValues' 'removeConstrainingValues'
) )
setTransformInfos(theTransforms)
const _enableHorz = isAllTooltips && theTransforms.every(Boolean) const enabled = isAllTooltips && transforms.every(Boolean)
setEnableHorz(_enableHorz) return { enabled, transforms }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
return { enabled: false, transforms: [] }
}
} }
}, [guiMode, selectionRanges])
if (guiMode.mode !== 'sketch') return null
return ( export function applyRemoveConstrainingValues({
<button selectionRanges,
onClick={() => { }: {
if (!transformInfos) return selectionRanges: Selections
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({ }): {
modifiedAst: Program
pathToNodeMap: PathToNodeMap
} {
const { transforms } = removeConstrainingValuesInfo({ selectionRanges })
return transformAstSketchLines({
ast: kclManager.ast, ast: kclManager.ast,
selectionRanges, selectionRanges,
transformInfos, transformInfos: transforms,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
referenceSegName: '', referenceSegName: '',
}) })
kclManager.updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}
disabled={!enableHorz}
title="Remove Constraining Values"
>
Remove Constraining Values
</button>
)
} }
*/

View File

@ -1,18 +1,19 @@
import { useState, useEffect } from 'react' import { toolTips } from '../../useStore'
import { create } from 'react-modal-promise' import { Selections } from 'lib/selections'
import { toolTips, useStore } from '../../useStore' import { BinaryPart, Program, Value } from '../../lang/wasm'
import { Value } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
getNodeFromPath, getNodeFromPath,
} from '../../lang/queryAst' } from '../../lang/queryAst'
import { import {
TransformInfo,
getTransformInfos, getTransformInfos,
transformAstSketchLines, transformAstSketchLines,
ConstraintType, PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { SetAngleLengthModal } from '../SetAngleLengthModal' import {
SetAngleLengthModal,
createSetAngleLengthModal,
} from '../SetAngleLengthModal'
import { import {
createIdentifier, createIdentifier,
createVariableDeclaration, createVariableDeclaration,
@ -20,40 +21,29 @@ import {
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
const getModalInfo = create(SetAngleLengthModal as any) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
type ButtonType = 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis' type Constraint = 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis'
const buttonLabels: Record<ButtonType, string> = { export function absDistanceInfo({
xAbs: 'Set distance from X Axis', selectionRanges,
yAbs: 'Set distance from Y Axis', constraint,
snapToYAxis: 'Snap To Y Axis', }: {
snapToXAxis: 'Snap To X Axis', selectionRanges: Selections
} constraint: Constraint
}) {
/* const disType =
export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => { constraint === 'xAbs' || constraint === 'yAbs'
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({ ? constraint
guiMode: s.guiMode, : constraint === 'snapToYAxis'
selectionRanges: s.selectionRanges,
setCursor: s.setCursor,
}))
const disType: ConstraintType =
buttonType === 'xAbs' || buttonType === 'yAbs'
? buttonType
: buttonType === 'snapToYAxis'
? 'xAbs' ? 'xAbs'
: 'yAbs' : 'yAbs'
const [enableAngLen, setEnableAngLen] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
useEffect(() => {
const paths = selectionRanges.codeBasedSelections.map(({ range }) => const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
getNodePathFromSourceRange(kclManager.ast, range) getNodePathFromSourceRange(kclManager.ast, range)
) )
const nodes = paths.map( const nodes = paths.map(
(pathToNode) => (pathToNode) =>
getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression') getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression').node
.node
) )
const isAllTooltips = nodes.every( const isAllTooltips = nodes.every(
(node) => (node) =>
@ -61,12 +51,7 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
toolTips.includes(node.callee.name as any) toolTips.includes(node.callee.name as any)
) )
const theTransforms = getTransformInfos( const transforms = getTransformInfos(selectionRanges, kclManager.ast, disType)
selectionRanges,
kclManager.ast,
disType
)
setTransformInfos(theTransforms)
const enableY = const enableY =
disType === 'yAbs' && disType === 'yAbs' &&
@ -77,21 +62,29 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
selectionRanges.otherSelections.length === 1 && selectionRanges.otherSelections.length === 1 &&
selectionRanges.otherSelections[0] === 'y-axis' // select the y axis to set the distance from it i.e. x selectionRanges.otherSelections[0] === 'y-axis' // select the y axis to set the distance from it i.e. x
const _enableHorz = const enabled =
isAllTooltips && isAllTooltips &&
theTransforms.every(Boolean) && transforms.every(Boolean) &&
selectionRanges.codeBasedSelections.length === 1 && selectionRanges.codeBasedSelections.length === 1 &&
(enableX || enableY) (enableX || enableY)
setEnableAngLen(_enableHorz)
}, [guiMode, selectionRanges])
if (guiMode.mode !== 'sketch') return null
const isAlign = buttonType === 'snapToYAxis' || buttonType === 'snapToXAxis' return { enabled, transforms }
}
return ( export async function applyConstraintAbsDistance({
<button selectionRanges,
onClick={async () => { constraint,
if (!transformInfos) return }: {
selectionRanges: Selections
constraint: 'xAbs' | 'yAbs'
}): Promise<{
modifiedAst: Program
pathToNodeMap: PathToNodeMap
}> {
const transformInfos = absDistanceInfo({
selectionRanges,
constraint,
}).transforms
const { valueUsedInTransform } = transformAstSketchLines({ const { valueUsedInTransform } = transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: JSON.parse(JSON.stringify(kclManager.ast)),
selectionRanges: selectionRanges, selectionRanges: selectionRanges,
@ -99,20 +92,19 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
referenceSegName: '', referenceSegName: '',
}) })
try {
let forceVal = valueUsedInTransform || 0 let forceVal = valueUsedInTransform || 0
const { valueNode, variableName, newVariableInsertIndex, sign } = const { valueNode, variableName, newVariableInsertIndex, sign } =
await (!isAlign && await getModalInfo({
getModalInfo({
value: forceVal, value: forceVal,
valueName: disType === 'yAbs' ? 'yDis' : 'xDis', valueName: constraint === 'yAbs' ? 'yDis' : 'xDis',
} as any)) })
let finalValue = isAlign let finalValue = removeDoubleNegatives(
? createIdentifier('_0') valueNode as BinaryPart,
: removeDoubleNegatives(valueNode, sign, variableName) sign,
variableName
)
const { modifiedAst: _modifiedAst, pathToNodeMap } = const { modifiedAst: _modifiedAst, pathToNodeMap } = transformAstSketchLines({
transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: JSON.parse(JSON.stringify(kclManager.ast)),
selectionRanges: selectionRanges, selectionRanges: selectionRanges,
transformInfos, transformInfos,
@ -129,19 +121,32 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
} }
return { modifiedAst: _modifiedAst, pathToNodeMap }
}
kclManager.updateAst(_modifiedAst, true, { export function applyConstraintAxisAlign({
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), selectionRanges,
constraint,
}: {
selectionRanges: Selections
constraint: 'snapToYAxis' | 'snapToXAxis'
}): {
modifiedAst: Program
pathToNodeMap: PathToNodeMap
} {
const transformInfos = absDistanceInfo({
selectionRanges,
constraint,
}).transforms
let finalValue = createIdentifier('_0')
return transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)),
selectionRanges: selectionRanges,
transformInfos,
programMemory: kclManager.programMemory,
referenceSegName: '',
forceValueUsedInTransform: finalValue,
}) })
} catch (e) {
console.log('error', e)
} }
}}
disabled={!enableAngLen}
title={buttonLabels[buttonType]}
>
{buttonLabels[buttonType]}
</button>
)
}
*/

View File

@ -1,6 +1,5 @@
import { useState, useEffect } from 'react' import { toolTips } from '../../useStore'
import { create } from 'react-modal-promise' import { Selections } from 'lib/selections'
import { Selections, toolTips, useStore } from '../../useStore'
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm' import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -8,107 +7,16 @@ import {
} from '../../lang/queryAst' } from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import { import {
TransformInfo,
transformSecondarySketchLinesTagFirst, transformSecondarySketchLinesTagFirst,
getTransformInfos, getTransformInfos,
PathToNodeMap, PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { GetInfoModal } from '../SetHorVertDistanceModal' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createVariableDeclaration } from '../../lang/modifyAst' import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
const getModalInfo = create(GetInfoModal as any) const getModalInfo = createInfoModal(GetInfoModal)
/*
export const SetAngleBetween = () => {
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
guiMode: s.guiMode,
selectionRanges: s.selectionRanges,
setCursor: s.setCursor,
}))
const [enable, setEnable] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
useEffect(() => {
const { enabled, transforms } = angleBetweenInfo({ selectionRanges })
setTransformInfos(transforms)
setEnable(enabled)
}, [guiMode, selectionRanges])
if (guiMode.mode !== 'sketch') return null
return (
<button
onClick={async () => {
if (!transformInfos) return
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
transformSecondarySketchLinesTagFirst({
ast: JSON.parse(JSON.stringify(kclManager.ast)),
selectionRanges,
transformInfos,
programMemory: kclManager.programMemory,
})
const {
segName,
value,
valueNode,
variableName,
newVariableInsertIndex,
sign,
}: {
segName: string
value: number
valueNode: Value
variableName?: string
newVariableInsertIndex: number
sign: number
} = await getModalInfo({
segName: tagInfo?.tag,
isSegNameEditable: !tagInfo?.isTagExisting,
value: valueUsedInTransform,
initialVariableName: 'angle',
} as any)
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
kclManager.updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
} else {
const finalValue = removeDoubleNegatives(
valueNode as BinaryPart,
sign,
variableName
)
// transform again but forcing certain values
const { modifiedAst: _modifiedAst, pathToNodeMap } =
transformSecondarySketchLinesTagFirst({
ast: kclManager.ast,
selectionRanges,
transformInfos,
programMemory: kclManager.programMemory,
forceSegName: segName,
forceValueUsedInTransform: finalValue,
})
if (variableName) {
const newBody = [..._modifiedAst.body]
newBody.splice(
newVariableInsertIndex,
0,
createVariableDeclaration(variableName, valueNode)
)
_modifiedAst.body = newBody
}
kclManager.updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}
}}
disabled={!enable}
title="Set Angle Between"
>
Set Angle Between
</button>
)
}
*/
export function angleBetweenInfo({ export function angleBetweenInfo({
selectionRanges, selectionRanges,
@ -183,28 +91,17 @@ export async function applyConstraintAngleBetween({
variableName, variableName,
newVariableInsertIndex, newVariableInsertIndex,
sign, sign,
}: {
segName: string
value: number
valueNode: Value
variableName?: string
newVariableInsertIndex: number
sign: number
} = await getModalInfo({ } = await getModalInfo({
segName: tagInfo?.tag, segName: tagInfo?.tag,
isSegNameEditable: !tagInfo?.isTagExisting, isSegNameEditable: !tagInfo?.isTagExisting,
value: valueUsedInTransform, value: valueUsedInTransform,
initialVariableName: 'angle', initialVariableName: 'angle',
} as any) } as any)
if (segName === tagInfo?.tag && value === valueUsedInTransform) { if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
} }
// kclManager.updateAst(modifiedAst, true, {
// TODO handle cursor
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
// })
} }
const finalValue = removeDoubleNegatives( const finalValue = removeDoubleNegatives(
@ -235,8 +132,4 @@ export async function applyConstraintAngleBetween({
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap: _pathToNodeMap, pathToNodeMap: _pathToNodeMap,
} }
// kclManager.updateAst(_modifiedAst, true, {
// TODO handle cursor
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
// })
} }

View File

@ -1,6 +1,4 @@
import { useState, useEffect } from 'react' import { toolTips } from '../../useStore'
import { create } from 'react-modal-promise'
import { toolTips, useStore } from '../../useStore'
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm' import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -8,139 +6,17 @@ import {
} from '../../lang/queryAst' } from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import { import {
TransformInfo,
transformSecondarySketchLinesTagFirst, transformSecondarySketchLinesTagFirst,
getTransformInfos, getTransformInfos,
ConstraintType,
PathToNodeMap, PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { GetInfoModal } from '../SetHorVertDistanceModal' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst' import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
import { Selections } from 'useStore' import { Selections } from 'lib/selections'
const getModalInfo = create(GetInfoModal as any) const getModalInfo = createInfoModal(GetInfoModal)
type ButtonType =
| 'setHorzDistance'
| 'setVertDistance'
| 'alignEndsHorizontally'
| 'alignEndsVertically'
const buttonLabels: Record<ButtonType, string> = {
setHorzDistance: 'Set Horizontal Distance',
setVertDistance: 'Set Vertical Distance',
alignEndsHorizontally: 'Align Ends Horizontally',
alignEndsVertically: 'Align Ends Vertically',
}
/*
export const SetHorzVertDistance = ({
buttonType,
}: {
buttonType: ButtonType
}) => {
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
guiMode: s.guiMode,
selectionRanges: s.selectionRanges,
setCursor: s.setCursor,
}))
const constraint: ConstraintType =
buttonType === 'setHorzDistance' || buttonType === 'setVertDistance'
? buttonType
: buttonType === 'alignEndsHorizontally'
? 'setVertDistance'
: 'setHorzDistance'
const [enable, setEnable] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
useEffect(() => {
const { transforms, enabled } = horzVertDistanceInfo({
selectionRanges,
constraint,
})
setTransformInfos(transforms)
setEnable(enabled)
}, [guiMode, selectionRanges])
if (guiMode.mode !== 'sketch') return null
const isAlign =
buttonType === 'alignEndsHorizontally' ||
buttonType === 'alignEndsVertically'
return (
<button
onClick={async () => {
if (!transformInfos) return
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
transformSecondarySketchLinesTagFirst({
ast: JSON.parse(JSON.stringify(kclManager.ast)),
selectionRanges,
transformInfos,
programMemory: kclManager.programMemory,
})
const {
segName,
value,
valueNode,
variableName,
newVariableInsertIndex,
sign,
}: {
segName: string
value: number
valueNode: Value
variableName?: string
newVariableInsertIndex: number
sign: number
} = await (!isAlign &&
getModalInfo({
segName: tagInfo?.tag,
isSegNameEditable: !tagInfo?.isTagExisting,
value: valueUsedInTransform,
initialVariableName:
constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
} as any))
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
kclManager.updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
} else {
let finalValue = isAlign
? createLiteral(0)
: removeDoubleNegatives(valueNode as BinaryPart, sign, variableName)
// transform again but forcing certain values
const { modifiedAst: _modifiedAst, pathToNodeMap } =
transformSecondarySketchLinesTagFirst({
ast: kclManager.ast,
selectionRanges,
transformInfos,
programMemory: kclManager.programMemory,
forceSegName: segName,
forceValueUsedInTransform: finalValue,
})
if (variableName) {
const newBody = [..._modifiedAst.body]
newBody.splice(
newVariableInsertIndex,
0,
createVariableDeclaration(variableName, valueNode)
)
_modifiedAst.body = newBody
}
kclManager.updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}
}}
disabled={!enable}
title={buttonLabels[buttonType]}
>
{buttonLabels[buttonType]}
</button>
)
}
*/
export function horzVertDistanceInfo({ export function horzVertDistanceInfo({
selectionRanges, selectionRanges,
@ -201,7 +77,7 @@ export async function applyConstraintHorzVertDistance({
}: { }: {
selectionRanges: Selections selectionRanges: Selections
constraint: 'setHorzDistance' | 'setVertDistance' constraint: 'setHorzDistance' | 'setVertDistance'
isAlign?: boolean isAlign?: false
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program
pathToNodeMap: PathToNodeMap pathToNodeMap: PathToNodeMap
@ -224,29 +100,17 @@ export async function applyConstraintHorzVertDistance({
variableName, variableName,
newVariableInsertIndex, newVariableInsertIndex,
sign, sign,
}: { } = await getModalInfo({
segName: string
value: number
valueNode: Value
variableName?: string
newVariableInsertIndex: number
sign: number
} = await (!isAlign &&
getModalInfo({
segName: tagInfo?.tag, segName: tagInfo?.tag,
isSegNameEditable: !tagInfo?.isTagExisting, isSegNameEditable: !tagInfo?.isTagExisting,
value: valueUsedInTransform, value: valueUsedInTransform,
initialVariableName: constraint === 'setHorzDistance' ? 'xDis' : 'yDis', initialVariableName: constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
} as any)) } as any)
if (segName === tagInfo?.tag && value === valueUsedInTransform) { if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
} }
// TODO handle cursor stuff
// kclManager.updateAst(modifiedAst, true, {
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
// })
} else { } else {
let finalValue = isAlign let finalValue = isAlign
? createLiteral(0) ? createLiteral(0)
@ -274,10 +138,6 @@ export async function applyConstraintHorzVertDistance({
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
} }
// TODO handle cursor stuff
// kclManager.updateAst(_modifiedAst, true, {
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
// })
} }
} }
@ -307,8 +167,4 @@ export function applyConstraintHorzVertAlign({
modifiedAst: modifiedAst, modifiedAst: modifiedAst,
pathToNodeMap, pathToNodeMap,
} }
// TODO handle cursor stuff
// kclManager.updateAst(_modifiedAst, true, {
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
// })
} }

View File

@ -1,18 +1,19 @@
import { useState, useEffect } from 'react' import { toolTips } from '../../useStore'
import { create } from 'react-modal-promise' import { Selections } from 'lib/selections'
import { Selections, toolTips, useStore } from '../../useStore' import { BinaryPart, Program, Value } from '../../lang/wasm'
import { Program, Value } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
getNodeFromPath, getNodeFromPath,
} from '../../lang/queryAst' } from '../../lang/queryAst'
import { import {
PathToNodeMap, PathToNodeMap,
TransformInfo,
getTransformInfos, getTransformInfos,
transformAstSketchLines, transformAstSketchLines,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { SetAngleLengthModal } from '../SetAngleLengthModal' import {
SetAngleLengthModal,
createSetAngleLengthModal,
} from '../SetAngleLengthModal'
import { import {
createBinaryExpressionWithUnary, createBinaryExpressionWithUnary,
createIdentifier, createIdentifier,
@ -22,128 +23,7 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { normaliseAngle } from '../../lib/utils' import { normaliseAngle } from '../../lib/utils'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
const getModalInfo = create(SetAngleLengthModal as any) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
type ButtonType = 'setAngle' | 'setLength'
const buttonLabels: Record<ButtonType, string> = {
setAngle: 'Set Angle',
setLength: 'Set Length',
}
/*
export const SetAngleLength = ({
angleOrLength,
}: {
angleOrLength: ButtonType
}) => {
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
guiMode: s.guiMode,
selectionRanges: s.selectionRanges,
setCursor: s.setCursor,
}))
const [enableAngLen, setEnableAngLen] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
useEffect(() => {
const { enabled, transforms } = setAngleLengthInfo({
selectionRanges,
angleOrLength,
})
setTransformInfos(transforms)
setEnableAngLen(enabled)
}, [guiMode, selectionRanges])
if (guiMode.mode !== 'sketch') return null
return (
<button
onClick={async () => {
if (!transformInfos) return
const { valueUsedInTransform } = transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)),
selectionRanges,
transformInfos,
programMemory: kclManager.programMemory,
referenceSegName: '',
})
try {
const isReferencingYAxis =
selectionRanges.otherSelections.length === 1 &&
selectionRanges.otherSelections[0] === 'y-axis'
const isReferencingYAxisAngle =
isReferencingYAxis && angleOrLength === 'setAngle'
const isReferencingXAxis =
selectionRanges.otherSelections.length === 1 &&
selectionRanges.otherSelections[0] === 'x-axis'
const isReferencingXAxisAngle =
isReferencingXAxis && angleOrLength === 'setAngle'
let forceVal = valueUsedInTransform || 0
let calcIdentifier = createIdentifier('_0')
if (isReferencingYAxisAngle) {
calcIdentifier = createIdentifier(forceVal < 0 ? '_270' : '_90')
forceVal = normaliseAngle(forceVal + (forceVal < 0 ? 90 : -90))
} else if (isReferencingXAxisAngle) {
calcIdentifier = createIdentifier(
Math.abs(forceVal) > 90 ? '_180' : '_0'
)
forceVal =
Math.abs(forceVal) > 90
? normaliseAngle(forceVal - 180)
: forceVal
}
const { valueNode, variableName, newVariableInsertIndex, sign } =
await getModalInfo({
value: forceVal,
valueName: angleOrLength === 'setAngle' ? 'angle' : 'length',
shouldCreateVariable: true,
} as any)
let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
if (
isReferencingYAxisAngle ||
(isReferencingXAxisAngle && calcIdentifier.name !== '_0')
) {
finalValue = createBinaryExpressionWithUnary([
calcIdentifier,
finalValue,
])
}
const { modifiedAst: _modifiedAst, pathToNodeMap } =
transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)),
selectionRanges,
transformInfos,
programMemory: kclManager.programMemory,
referenceSegName: '',
forceValueUsedInTransform: finalValue,
})
if (variableName) {
const newBody = [..._modifiedAst.body]
newBody.splice(
newVariableInsertIndex,
0,
createVariableDeclaration(variableName, valueNode)
)
_modifiedAst.body = newBody
}
kclManager.updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
} catch (e) {
console.log('erorr', e)
}
}}
disabled={!enableAngLen}
title={buttonLabels[angleOrLength]}
>
{buttonLabels[angleOrLength]}
</button>
)
}
*/
export function setAngleLengthInfo({ export function setAngleLengthInfo({
selectionRanges, selectionRanges,
@ -220,8 +100,13 @@ export async function applyConstraintAngleLength({
value: forceVal, value: forceVal,
valueName: angleOrLength === 'setAngle' ? 'angle' : 'length', valueName: angleOrLength === 'setAngle' ? 'angle' : 'length',
shouldCreateVariable: true, shouldCreateVariable: true,
} as any) })
let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
let finalValue = removeDoubleNegatives(
valueNode as BinaryPart,
sign,
variableName
)
if ( if (
isReferencingYAxisAngle || isReferencingYAxisAngle ||
(isReferencingXAxisAngle && calcIdentifier.name !== '_0') (isReferencingXAxisAngle && calcIdentifier.name !== '_0')
@ -251,9 +136,6 @@ export async function applyConstraintAngleLength({
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
} }
// kclManager.updateAst(_modifiedAst, true, {
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
// })
} catch (e) { } catch (e) {
console.log('erorr', e) console.log('erorr', e)
throw e throw e

View File

@ -0,0 +1,229 @@
/* Adapted from https://github.com/argyleink/gui-challenges/blob/main/tooltips/tool-tip.css */
.tooltip {
/* internal CSS vars */
--_delay: 200ms;
--_p-inline: 1ch;
--_p-block: 4px;
--_triangle-size: 7px;
/* --_bg: hsl(0 0% 20%); */
--_bg: var(--chalkboard-10);
--_shadow-alpha: 20%;
/* Used to power spacing and layout for RTL languages */
--isRTL: -1;
/* Using conic gradients to get a clear tip triangle */
--_bottom-tip: conic-gradient(
from -30deg at bottom,
#0000,
#000 1deg 60deg,
#0000 61deg
)
bottom / 100% 50% no-repeat;
--_top-tip: conic-gradient(
from 150deg at top,
#0000,
#000 1deg 60deg,
#0000 61deg
)
top / 100% 50% no-repeat;
--_right-tip: conic-gradient(
from -120deg at right,
#0000,
#000 1deg 60deg,
#0000 61deg
)
right / 50% 100% no-repeat;
--_left-tip: conic-gradient(
from 60deg at left,
#0000,
#000 1deg 60deg,
#0000 61deg
)
left / 50% 100% no-repeat;
pointer-events: none;
user-select: none;
/* The parts that will be transitioned */
opacity: 0;
transform: translate(var(--_x, 0), var(--_y, 0));
transition: transform 0.15s ease-out, opacity 0.11s ease-out;
position: absolute;
z-index: 1;
inline-size: max-content;
max-inline-size: 25ch;
text-align: start;
font-family: var(--mono-font-family);
text-transform: none;
font-size: 0.9rem;
font-weight: normal;
line-height: initial;
letter-spacing: 0;
padding: var(--_p-block) var(--_p-inline);
margin: 0;
border-radius: 3px;
background: var(--_bg);
@apply text-chalkboard-110;
will-change: filter;
filter: drop-shadow(0 1px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 6px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}
:global(.dark) .tooltip {
--_bg: var(--chalkboard-110);
@apply text-chalkboard-10;
}
/* TODO we don't support a light theme yet */
/* @media (prefers-color-scheme: light) {
.tooltip {
--_bg: white;
--_shadow-alpha: 15%;
}
} */
.tooltip:dir(rtl) {
--isRTL: 1;
}
/* :has and :is are pretty fresh CSS pseudo-selectors, may not see full support */
:has(> .tooltip) {
position: relative;
}
:is(:hover, :focus-visible, :active) > .tooltip {
opacity: 1;
transition-delay: var(--_delay);
}
:is(:focus, :focus-visible, :focus-within) > .tooltip {
--_delay: 0 !important;
}
/* prepend some prose for screen readers only */
.tooltip::before {
content: '; Has tooltip: ';
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
/* tooltip shape is a pseudo element so we can cast a shadow */
.tooltip::after {
content: '';
background: var(--_bg);
position: absolute;
z-index: -1;
inset: 0;
mask: var(--_tip);
}
.tooltip.top,
.tooltip.blockStart,
.tooltip.bottom,
.tooltip.blockEnd {
text-align: center;
}
/* TOP || BLOCK-START */
.tooltip.top,
.tooltip.blockStart {
inset-inline-start: 50%;
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
.tooltip.top::after,
.tooltip.tooltip.blockStart::after {
--_tip: var(--_bottom-tip);
inset-block-end: calc(var(--_triangle-size) * -1);
border-block-end: var(--_triangle-size) solid transparent;
}
/* RIGHT || INLINE-END */
.tooltip.right,
.tooltip.inlineEnd {
inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
.tooltip.right::after,
.tooltip.tooltip.inlineEnd::after {
--_tip: var(--_left-tip);
inset-inline-start: calc(var(--_triangle-size) * -1);
border-inline-start: var(--_triangle-size) solid transparent;
}
.tooltip.right:dir(rtl)::after,
.tooltip.inlineEnd:dir(rtl)::after {
--_tip: var(--_right-tip);
}
/* BOTTOM || BLOCK-END */
.tooltip.bottom,
.tooltip.blockEnd {
inset-inline-start: 50%;
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
.tooltip.bottom::after,
.tooltip.tooltip.blockEnd::after {
--_tip: var(--_top-tip);
inset-block-start: calc(var(--_triangle-size) * -1);
border-block-start: var(--_triangle-size) solid transparent;
}
/* LEFT || INLINE-START */
.tooltip.left,
.tooltip.inlineStart {
inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
.tooltip.left::after,
.tooltip.tooltip.inlineStart::after {
--_tip: var(--_right-tip);
inset-inline-end: calc(var(--_triangle-size) * -1);
border-inline-end: var(--_triangle-size) solid transparent;
}
.tooltip.left:dir(rtl)::after,
.tooltip.inlineStart:dir(rtl)::after {
--_tip: var(--_left-tip);
}
@media (prefers-reduced-motion: no-preference) {
/* TOP || BLOCK-START */
:has(> :is(.tooltip.top, .tooltip.blockStart)):not(:hover, :active) .tooltip {
--_y: 3px;
}
/* RIGHT || INLINE-END */
:has(> :is(.tooltip.right, .tooltip.inlineEnd)):not(:hover, :active)
.tooltip {
--_x: calc(var(--isRTL) * -3px * -1);
}
/* BOTTOM || BLOCK-END */
:has(> :is(.tooltip.bottom, .tooltip.blockEnd)):not(:hover, :active)
.tooltip {
--_y: -3px;
}
/* BOTTOM || BLOCK-END */
:has(> :is(.tooltip.left, .tooltip.inlineStart)):not(:hover, :active)
.tooltip {
--_x: calc(var(--isRTL) * 3px * -1);
}
}

View File

@ -0,0 +1,37 @@
// We do use all the classes in this file currently, but we
// index into them with styles[position], which CSS Modules doesn't pick up.
// eslint-disable-next-line css-modules/no-unused-class
import styles from './Tooltip.module.css'
interface TooltipProps extends React.PropsWithChildren {
position?:
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'blockStart'
| 'blockEnd'
| 'inlineStart'
| 'inlineEnd'
className?: string
delay?: number
}
export default function Tooltip({
children,
position = 'top',
className,
delay = 200,
}: TooltipProps) {
return (
<div
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
inert="true"
role="tooltip"
className={styles.tooltip + ' ' + styles[position] + ' ' + className}
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
>
{children}
</div>
)
}

View File

@ -0,0 +1,63 @@
import { Dialog } from '@headlessui/react'
import { useState } from 'react'
import { ActionButton } from './ActionButton'
import { faX } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclSinglton'
export function WasmErrBanner() {
const [isBannerDismissed, setBannerDismissed] = useState(false)
const { wasmInitFailed } = useKclContext()
if (!wasmInitFailed) return null
return (
<Dialog
className="fixed inset-0 top-auto z-50 bg-warn-20 text-warn-80 px-8 py-4"
open={!isBannerDismissed}
onClose={() => ({})}
>
<Dialog.Panel className="max-w-3xl mx-auto">
<div className="flex gap-2 justify-between items-start">
<h2 className="text-xl font-bold mb-4">
Problem with our WASM blob :(
</h2>
<ActionButton
Element="button"
onClick={() => setBannerDismissed(true)}
icon={{
icon: faX,
bgClassName:
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
iconClassName:
'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
}}
className="!p-0 !bg-transparent !border-transparent"
/>
</div>
<p>
<a
href="https://webassembly.org/"
rel="noopener noreferrer"
target="_blank"
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
>
WASM or web assembly
</a>{' '}
is core part of how our app works. It might because you OS is not
up-to-date. If you're able to update your OS to a later version, try
that. If not create an issue on{' '}
<a
href="https://github.com/KittyCAD/modeling-app"
rel="noopener noreferrer"
target="_blank"
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
>
our Github
</a>
.
</p>
</Dialog.Panel>
</Dialog>
)
}

View File

@ -9,6 +9,7 @@ import { LanguageServerClient } from '.'
import { kclPlugin } from './plugin' import { kclPlugin } from './plugin'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { parser as jsParser } from '@lezer/javascript' import { parser as jsParser } from '@lezer/javascript'
import { EditorState } from '@uiw/react-codemirror'
const data = defineLanguageFacet({}) const data = defineLanguageFacet({})
@ -22,7 +23,25 @@ export default function kclLanguage(options: LanguageOptions): LanguageSupport {
// For now let's use the javascript parser. // For now let's use the javascript parser.
// It works really well and has good syntax highlighting. // It works really well and has good syntax highlighting.
// We can use our lsp for the rest. // We can use our lsp for the rest.
const lang = new Language(data, jsParser, [], 'kcl') const lang = new Language(
data,
jsParser,
[
EditorState.languageData.of(() => [
{
// https://codemirror.net/docs/ref/#commands.CommentTokens
commentTokens: {
line: '//',
block: {
open: '/*',
close: '*/',
},
},
},
]),
],
'kcl'
)
// Create our supporting extension. // Create our supporting extension.
const kclLsp = kclPlugin({ const kclLsp = kclPlugin({

View File

@ -7,6 +7,6 @@ export function useAbsoluteFilePath() {
return ( return (
paths.FILE + paths.FILE +
'/' + '/' +
encodeURIComponent(routeData?.project?.path || BROWSER_FILE_NAME) encodeURIComponent(routeData?.file?.path || BROWSER_FILE_NAME)
) )
} }

View File

@ -2,13 +2,22 @@ import { useEffect } from 'react'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { v4 as uuidv4 } from 'uuid'
import { SourceRange } from 'lang/wasm'
import { getEventForSelectWithPoint } from 'lib/selections'
export function useEngineConnectionSubscriptions() { export function useEngineConnectionSubscriptions() {
const { setHighlightRange, highlightRange } = useStore((s) => ({ const { setHighlightRange, highlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange, setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange, highlightRange: s.highlightRange,
})) }))
const { send } = useModelingContext() const { send, context } = useModelingContext()
interface RangeAndId {
id: string
range: SourceRange
}
useEffect(() => { useEffect(() => {
if (!engineCommandManager) return if (!engineCommandManager) return
@ -17,7 +26,7 @@ export function useEngineConnectionSubscriptions() {
callback: ({ data }) => { callback: ({ data }) => {
if (data?.entity_id) { if (data?.entity_id) {
const sourceRange = const sourceRange =
engineCommandManager.sourceRangeMap[data.entity_id] engineCommandManager.artifactMap?.[data.entity_id]?.range
setHighlightRange(sourceRange) setHighlightRange(sourceRange)
} else if ( } else if (
!highlightRange || !highlightRange ||
@ -29,27 +38,21 @@ export function useEngineConnectionSubscriptions() {
}) })
const unSubClick = engineCommandManager.subscribeTo({ const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point', event: 'select_with_point',
callback: ({ data }) => { callback: async (engineEvent) => {
if (!data?.entity_id) { const event = await getEventForSelectWithPoint(engineEvent, {
send({ sketchEnginePathId: context.sketchEnginePathId,
type: 'Set selection',
data: { selectionType: 'singleCodeCursor' },
})
return
}
const sourceRange = engineCommandManager.sourceRangeMap[data.entity_id]
send({
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: sourceRange, type: 'default' },
},
}) })
send(event)
}, },
}) })
return () => { return () => {
unSubHover() unSubHover()
unSubClick() unSubClick()
} }
}, [engineCommandManager, setHighlightRange, highlightRange]) }, [
engineCommandManager,
setHighlightRange,
highlightRange,
context.sketchEnginePathId,
])
} }

View File

@ -0,0 +1,6 @@
import { FileContext } from 'components/FileMachineProvider'
import { useContext } from 'react'
export const useFileContext = () => {
return useContext(FileContext)
}

View File

@ -1,12 +1,14 @@
import { SetVarNameModal } from 'components/SetVarNameModal' import {
SetVarNameModal,
createSetVarNameModal,
} from 'components/SetVarNameModal'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
import { moveValueIntoNewVariable } from 'lang/modifyAst' import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { create } from 'react-modal-promise'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
const getModalInfo = create(SetVarNameModal as any) const getModalInfo = createSetVarNameModal(SetVarNameModal)
export function useConvertToVariable() { export function useConvertToVariable() {
const { context } = useModelingContext() const { context } = useModelingContext()
@ -28,7 +30,7 @@ export function useConvertToVariable() {
try { try {
const { variableName } = await getModalInfo({ const { variableName } = await getModalInfo({
valueName: 'var', valueName: 'var',
} as any) })
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable( const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
kclManager.ast, kclManager.ast,

View File

@ -1,4 +1,5 @@
import { Selections, executeAst, executeCode } from 'useStore' import { executeAst, executeCode } from 'useStore'
import { Selections } from 'lib/selections'
import { KCLError } from './errors' import { KCLError } from './errors'
import { import {
EngineCommandManager, EngineCommandManager,
@ -16,6 +17,8 @@ import {
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
import { IndexLoaderData } from 'Router'
import { useLoaderData } from 'react-router-dom'
const PERSIST_CODE_TOKEN = 'persistCode' const PERSIST_CODE_TOKEN = 'persistCode'
@ -27,7 +30,7 @@ class KclManager {
end: 0, end: 0,
nonCodeMeta: { nonCodeMeta: {
nonCodeNodes: {}, nonCodeNodes: {},
start: null, start: [],
}, },
} }
private _programMemory: ProgramMemory = { private _programMemory: ProgramMemory = {
@ -37,6 +40,7 @@ class KclManager {
private _logs: string[] = [] private _logs: string[] = []
private _kclErrors: KCLError[] = [] private _kclErrors: KCLError[] = []
private _isExecuting = false private _isExecuting = false
private _wasmInitFailed = true
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
private _defferer = deferExecution((code: string) => { private _defferer = deferExecution((code: string) => {
@ -44,12 +48,14 @@ class KclManager {
this.executeAst(ast) this.executeAst(ast)
}, 600) }, 600)
private _isExecutingCallback: (a: boolean) => void = () => {} private _isExecutingCallback: (arg: boolean) => void = () => {}
private _codeCallBack: (arg: string) => void = () => {} private _codeCallBack: (arg: string) => void = () => {}
private _astCallBack: (arg: Program) => void = () => {} private _astCallBack: (arg: Program) => void = () => {}
private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {} private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {}
private _logsCallBack: (arg: string[]) => void = () => {} private _logsCallBack: (arg: string[]) => void = () => {}
private _kclErrorsCallBack: (arg: KCLError[]) => void = () => {} private _kclErrorsCallBack: (arg: KCLError[]) => void = () => {}
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
private _executeCallback: () => void = () => {}
get ast() { get ast() {
return this._ast return this._ast
@ -103,6 +109,14 @@ class KclManager {
this._isExecutingCallback(isExecuting) this._isExecutingCallback(isExecuting)
} }
get wasmInitFailed() {
return this._wasmInitFailed
}
set wasmInitFailed(wasmInitFailed) {
this._wasmInitFailed = wasmInitFailed
this._wasmInitFailedCallback(wasmInitFailed)
}
constructor(engineCommandManager: EngineCommandManager) { constructor(engineCommandManager: EngineCommandManager) {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
const storedCode = localStorage.getItem(PERSIST_CODE_TOKEN) const storedCode = localStorage.getItem(PERSIST_CODE_TOKEN)
@ -128,6 +142,7 @@ class KclManager {
setLogs, setLogs,
setKclErrors, setKclErrors,
setIsExecuting, setIsExecuting,
setWasmInitFailed,
}: { }: {
setCode: (arg: string) => void setCode: (arg: string) => void
setProgramMemory: (arg: ProgramMemory) => void setProgramMemory: (arg: ProgramMemory) => void
@ -135,6 +150,7 @@ class KclManager {
setLogs: (arg: string[]) => void setLogs: (arg: string[]) => void
setKclErrors: (arg: KCLError[]) => void setKclErrors: (arg: KCLError[]) => void
setIsExecuting: (arg: boolean) => void setIsExecuting: (arg: boolean) => void
setWasmInitFailed: (arg: boolean) => void
}) { }) {
this._codeCallBack = setCode this._codeCallBack = setCode
this._programMemoryCallBack = setProgramMemory this._programMemoryCallBack = setProgramMemory
@ -142,11 +158,26 @@ class KclManager {
this._logsCallBack = setLogs this._logsCallBack = setLogs
this._kclErrorsCallBack = setKclErrors this._kclErrorsCallBack = setKclErrors
this._isExecutingCallback = setIsExecuting this._isExecutingCallback = setIsExecuting
this._wasmInitFailedCallback = setWasmInitFailed
}
registerExecuteCallback(callback: () => void) {
this._executeCallback = callback
}
async ensureWasmInit() {
try {
await initPromise
if (this.wasmInitFailed) {
this.wasmInitFailed = false
}
} catch (e) {
this.wasmInitFailed = true
}
} }
async executeAst(ast: Program = this._ast, updateCode = false) { async executeAst(ast: Program = this._ast, updateCode = false) {
await this.ensureWasmInit()
this.isExecuting = true this.isExecuting = true
await initPromise
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({
ast, ast,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
@ -161,9 +192,10 @@ class KclManager {
this._code = recast(ast) this._code = recast(ast)
this._codeCallBack(this._code) this._codeCallBack(this._code)
} }
this._executeCallback()
} }
async executeAstMock(ast: Program = this._ast, updateCode = false) { async executeAstMock(ast: Program = this._ast, updateCode = false) {
await initPromise await this.ensureWasmInit()
const newCode = recast(ast) const newCode = recast(ast)
const newAst = parse(newCode) const newAst = parse(newCode)
await this?.engineCommandManager?.waitForReady await this?.engineCommandManager?.waitForReady
@ -183,8 +215,9 @@ class KclManager {
this._programMemory = programMemory this._programMemory = programMemory
} }
async executeCode(code?: string) { async executeCode(code?: string) {
await initPromise await this.ensureWasmInit()
await this?.engineCommandManager?.waitForReady await this?.engineCommandManager?.waitForReady
if (!this?.engineCommandManager?.planesInitialized()) return
const result = await executeCode({ const result = await executeCode({
engineCommandManager, engineCommandManager,
code: code || this._code, code: code || this._code,
@ -217,7 +250,7 @@ class KclManager {
end: 0, end: 0,
nonCodeMeta: { nonCodeMeta: {
nonCodeNodes: {}, nonCodeNodes: {},
start: null, start: [],
}, },
} }
this._programMemory = { this._programMemory = {
@ -302,6 +335,7 @@ const KclContext = createContext({
isExecuting: kclManager.isExecuting, isExecuting: kclManager.isExecuting,
errors: kclManager.kclErrors, errors: kclManager.kclErrors,
logs: kclManager.logs, logs: kclManager.logs,
wasmInitFailed: kclManager.wasmInitFailed,
}) })
export function useKclContext() { export function useKclContext() {
@ -313,12 +347,16 @@ export function KclContextProvider({
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
const [code, setCode] = useState(kclManager.code) // If we try to use this component anywhere but under the paths.FILE route it will fail
// Because useLoaderData assumes we are on within it's context.
const { code: loadedCode } = useLoaderData() as IndexLoaderData
const [code, setCode] = useState(loadedCode || kclManager.code)
const [programMemory, setProgramMemory] = useState(kclManager.programMemory) const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
const [ast, setAst] = useState(kclManager.ast) const [ast, setAst] = useState(kclManager.ast)
const [isExecuting, setIsExecuting] = useState(false) const [isExecuting, setIsExecuting] = useState(false)
const [errors, setErrors] = useState<KCLError[]>([]) const [errors, setErrors] = useState<KCLError[]>([])
const [logs, setLogs] = useState<string[]>([]) const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false)
useEffect(() => { useEffect(() => {
kclManager.registerCallBacks({ kclManager.registerCallBacks({
@ -328,6 +366,7 @@ export function KclContextProvider({
setLogs, setLogs,
setKclErrors: setErrors, setKclErrors: setErrors,
setIsExecuting, setIsExecuting,
setWasmInitFailed,
}) })
}, []) }, [])
return ( return (
@ -339,6 +378,7 @@ export function KclContextProvider({
isExecuting, isExecuting,
errors, errors,
logs, logs,
wasmInitFailed,
}} }}
> >
{children} {children}

View File

@ -141,42 +141,6 @@ const newVar = myVar + 1
}) })
describe('testing function declaration', () => { describe('testing function declaration', () => {
test('fn funcN = () => {}', () => {
const { body } = parse('fn funcN = () => {}')
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
expect(body).toEqual([
{
type: 'VariableDeclaration',
start: 0,
end: 19,
kind: 'fn',
declarations: [
{
type: 'VariableDeclarator',
start: 3,
end: 19,
id: {
type: 'Identifier',
start: 3,
end: 8,
name: 'funcN',
},
init: {
type: 'FunctionExpression',
start: 11,
end: 19,
params: [],
body: {
start: 17,
end: 19,
body: [],
},
},
},
],
},
])
})
test('fn funcN = (a, b) => {return a + b}', () => { test('fn funcN = (a, b) => {return a + b}', () => {
const { body } = parse( const { body } = parse(
['fn funcN = (a, b) => {', ' return a + b', '}'].join('\n') ['fn funcN = (a, b) => {', ' return a + b', '}'].join('\n')
@ -1513,22 +1477,23 @@ const key = 'c'`
const nonCodeMetaInstance = { const nonCodeMetaInstance = {
type: 'NonCodeNode', type: 'NonCodeNode',
start: code.indexOf('\n// this is a comment'), start: code.indexOf('\n// this is a comment'),
end: code.indexOf('const key'), end: code.indexOf('const key') - 1,
value: { value: {
type: 'blockComment', type: 'blockComment',
style: 'line',
value: 'this is a comment', value: 'this is a comment',
}, },
} }
const { nonCodeMeta } = parse(code) const { nonCodeMeta } = parse(code)
expect(nonCodeMeta.nonCodeNodes[0]).toEqual(nonCodeMetaInstance) expect(nonCodeMeta.nonCodeNodes[0][0]).toEqual(nonCodeMetaInstance)
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though) // extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
const codeWithExtraStartWhitespace = '\n\n\n' + code const codeWithExtraStartWhitespace = '\n\n\n' + code
const { nonCodeMeta: nonCodeMeta2 } = parse(codeWithExtraStartWhitespace) const { nonCodeMeta: nonCodeMeta2 } = parse(codeWithExtraStartWhitespace)
expect(nonCodeMeta2.nonCodeNodes[0].value).toStrictEqual( expect(nonCodeMeta2.nonCodeNodes[0][0].value).toStrictEqual(
nonCodeMetaInstance.value nonCodeMetaInstance.value
) )
expect(nonCodeMeta2.nonCodeNodes[0].start).not.toBe( expect(nonCodeMeta2.nonCodeNodes[0][0].start).not.toBe(
nonCodeMetaInstance.start nonCodeMetaInstance.start
) )
}) })
@ -1546,12 +1511,13 @@ const key = 'c'`
const indexOfSecondLineToExpression = 2 const indexOfSecondLineToExpression = 2
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
.nonCodeNodes .nonCodeNodes
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({ expect(sketchNonCodeMeta[indexOfSecondLineToExpression][0]).toEqual({
type: 'NonCodeNode', type: 'NonCodeNode',
start: 106, start: 106,
end: 166, end: 163,
value: { value: {
type: 'blockComment', type: 'inlineComment',
style: 'block',
value: 'this is\n a comment\n spanning a few lines', value: 'this is\n a comment\n spanning a few lines',
}, },
}) })
@ -1568,14 +1534,15 @@ const key = 'c'`
const { body } = parse(code) const { body } = parse(code)
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
.nonCodeNodes .nonCodeNodes[3][0]
expect(sketchNonCodeMeta[3]).toEqual({ expect(sketchNonCodeMeta).toEqual({
type: 'NonCodeNode', type: 'NonCodeNode',
start: 125, start: 125,
end: 141, end: 138,
value: { value: {
type: 'blockComment', type: 'blockComment',
value: 'a comment', value: 'a comment',
style: 'line',
}, },
}) })
}) })
@ -1693,11 +1660,7 @@ describe('parsing errors', () => {
} }
const theError = _theError as any const theError = _theError as any
expect(theError).toEqual( expect(theError).toEqual(
new KCLError( new KCLError('syntax', 'Unexpected token', [[27, 28]])
'unexpected',
'Unexpected token Token { token_type: Brace, start: 29, end: 30, value: "}" }',
[[29, 30]]
)
) )
}) })
}) })

View File

@ -104,7 +104,7 @@ describe('Testing addSketchTo', () => {
body: [], body: [],
start: 0, start: 0,
end: 0, end: 0,
nonCodeMeta: { nonCodeNodes: {}, start: null }, nonCodeMeta: { nonCodeNodes: {}, start: [] },
}, },
'yz' 'yz'
) )

View File

@ -1,4 +1,5 @@
import { Selection, ToolTip } from '../useStore' import { ToolTip } from '../useStore'
import { Selection } from 'lib/selections'
import { import {
Program, Program,
CallExpression, CallExpression,
@ -540,7 +541,7 @@ export function createPipeExpression(
start: 0, start: 0,
end: 0, end: 0,
body, body,
nonCodeMeta: { nonCodeNodes: {}, start: null }, nonCodeMeta: { nonCodeNodes: {}, start: [] },
} }
} }

View File

@ -1,4 +1,5 @@
import { Selection, ToolTip } from '../useStore' import { ToolTip } from '../useStore'
import { Selection } from 'lib/selections'
import { import {
BinaryExpression, BinaryExpression,
Program, Program,

View File

@ -272,21 +272,20 @@ const mySk1 = startSketchAt([0, 0])
` `
const { ast } = code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(`// comment at start expect(recasted).toBe(`/* comment at start */
const mySk1 = startSketchAt([0, 0]) const mySk1 = startSketchAt([0, 0])
|> lineTo([1, 1], %) |> lineTo([1, 1], %)
// comment here // comment here
|> lineTo({ to: [0, 1], tag: 'myTag' }, %) |> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|> lineTo([1, 1], %) |> lineTo([1, 1], %) /* and
/* and here */
here // a comment between pipe expression statements
a comment between pipe expression statements */
|> rx(90, %) |> rx(90, %)
// and another with just white space between others below // and another with just white space between others below
|> ry(45, %) |> ry(45, %)
|> rx(45, %) |> rx(45, %)
// one more for good measure /* one more for good measure */
`) `)
}) })
}) })

View File

@ -1,16 +1,9 @@
import { import { SourceRange } from 'lang/wasm'
ProgramMemory,
SourceRange,
Program,
VariableDeclarator,
} from 'lang/wasm'
import { Selections } from 'useStore'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
let lastMessage = '' let lastMessage = ''
@ -27,6 +20,7 @@ interface ResultCommand extends CommandInfo {
type: 'result' type: 'result'
data: any data: any
raw: WebSocketResponse raw: WebSocketResponse
headVertexId?: string
} }
interface FailedCommand extends CommandInfo { interface FailedCommand extends CommandInfo {
type: 'failed' type: 'failed'
@ -41,9 +35,6 @@ interface PendingCommand extends CommandInfo {
export interface ArtifactMap { export interface ArtifactMap {
[key: string]: ResultCommand | PendingCommand | FailedCommand [key: string]: ResultCommand | PendingCommand | FailedCommand
} }
export interface SourceRangeMap {
[key: string]: SourceRange
}
interface NewTrackArgs { interface NewTrackArgs {
conn: EngineConnection conn: EngineConnection
@ -459,18 +450,18 @@ export class EngineConnection {
videoTrackStats.forEach((videoTrackReport) => { videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') { if (videoTrackReport.type === 'inbound-rtp') {
client_metrics.rtc_frames_decoded = client_metrics.rtc_frames_decoded =
videoTrackReport.framesDecoded videoTrackReport.framesDecoded || 0
client_metrics.rtc_frames_dropped = client_metrics.rtc_frames_dropped =
videoTrackReport.framesDropped videoTrackReport.framesDropped || 0
client_metrics.rtc_frames_received = client_metrics.rtc_frames_received =
videoTrackReport.framesReceived videoTrackReport.framesReceived || 0
client_metrics.rtc_frames_per_second = client_metrics.rtc_frames_per_second =
videoTrackReport.framesPerSecond || 0 videoTrackReport.framesPerSecond || 0
client_metrics.rtc_freeze_count = client_metrics.rtc_freeze_count =
videoTrackReport.freezeCount || 0 videoTrackReport.freezeCount || 0
client_metrics.rtc_jitter_sec = videoTrackReport.jitter client_metrics.rtc_jitter_sec = videoTrackReport.jitter || 0.0
client_metrics.rtc_keyframes_decoded = client_metrics.rtc_keyframes_decoded =
videoTrackReport.keyFramesDecoded videoTrackReport.keyFramesDecoded || 0
client_metrics.rtc_total_freezes_duration_sec = client_metrics.rtc_total_freezes_duration_sec =
videoTrackReport.totalFreezesDuration || 0 videoTrackReport.totalFreezesDuration || 0
} else if (videoTrackReport.type === 'transport') { } else if (videoTrackReport.type === 'transport') {
@ -594,7 +585,6 @@ interface Subscription<T extends ModelTypes> {
export class EngineCommandManager { export class EngineCommandManager {
artifactMap: ArtifactMap = {} artifactMap: ArtifactMap = {}
sourceRangeMap: SourceRangeMap = {}
outSequence = 1 outSequence = 1
inSequence = 1 inSequence = 1
engineConnection?: EngineConnection engineConnection?: EngineConnection
@ -765,7 +755,6 @@ export class EngineCommandManager {
streamWidth: number streamWidth: number
streamHeight: number streamHeight: number
}) { }) {
console.log('handleResize', streamWidth, streamHeight)
if (!this.engineConnection?.isReady()) { if (!this.engineConnection?.isReady()) {
return return
} }
@ -856,7 +845,6 @@ export class EngineCommandManager {
} }
startNewSession() { startNewSession() {
this.artifactMap = {} this.artifactMap = {}
this.sourceRangeMap = {}
} }
subscribeTo<T extends ModelTypes>({ subscribeTo<T extends ModelTypes>({
event, event,
@ -922,30 +910,6 @@ export class EngineCommandManager {
this.engineConnection?.send(deletCmd) this.engineConnection?.send(deletCmd)
}) })
} }
cusorsSelected(selections: {
otherSelections: Selections['otherSelections']
idBasedSelections: { type: string; id: string }[]
}) {
if (!this.engineConnection?.isReady()) {
console.log('engine connection isnt ready')
return
}
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'select_clear',
},
cmd_id: uuidv4(),
})
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'select_add',
entities: selections.idBasedSelections.map((s) => s.id),
},
cmd_id: uuidv4(),
})
}
sendSceneCommand(command: EngineCommand): Promise<any> { sendSceneCommand(command: EngineCommand): Promise<any> {
if (this.engineConnection === undefined) { if (this.engineConnection === undefined) {
return Promise.resolve() return Promise.resolve()
@ -1006,7 +970,6 @@ export class EngineCommandManager {
if (this.engineConnection === undefined) { if (this.engineConnection === undefined) {
return Promise.resolve() return Promise.resolve()
} }
this.sourceRangeMap[id] = range
if (!this.engineConnection?.isReady()) { if (!this.engineConnection?.isReady()) {
return Promise.resolve() return Promise.resolve()
@ -1082,109 +1045,19 @@ export class EngineCommandManager {
} }
return command.promise return command.promise
} }
async waitForAllCommands( async waitForAllCommands(): Promise<{
ast?: Program,
programMemory?: ProgramMemory
): Promise<{
artifactMap: ArtifactMap artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
}> { }> {
const pendingCommands = Object.values(this.artifactMap).filter( const pendingCommands = Object.values(this.artifactMap).filter(
({ type }) => type === 'pending' ({ type }) => type === 'pending'
) as PendingCommand[] ) as PendingCommand[]
const proms = pendingCommands.map(({ promise }) => promise) const proms = pendingCommands.map(({ promise }) => promise)
await Promise.all(proms) await Promise.all(proms)
if (ast && programMemory) {
await this.fixIdMappings(ast, programMemory)
}
return { return {
artifactMap: this.artifactMap, artifactMap: this.artifactMap,
sourceRangeMap: this.sourceRangeMap,
} }
} }
private async fixIdMappings(ast: Program, programMemory: ProgramMemory) {
if (this.engineConnection === undefined) {
return
}
/* This is a temporary solution since the cmd_ids that are sent through when
sending 'extend_path' ids are not used as the segment ids.
We have a way to back fill them with 'path_get_info', however this relies on one
the sketchGroup array and the segements array returned from the server to be in
the same length and order. plus it's super hacky, we first use the path_id to get
the source range of the pipe expression then use the name of the variable to get
the sketchGroup from programMemory.
I feel queezy about relying on all these steps to always line up.
We have also had to pollute this EngineCommandManager class with knowledge of both the ast and programMemory
We should get the cmd_ids to match with the segment ids and delete this method.
*/
const pathInfoProms = []
for (const [id, artifact] of Object.entries(this.artifactMap)) {
if (artifact.commandType === 'start_path') {
pathInfoProms.push(
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: id,
},
}).then(({ data }) => ({
originalId: id,
segments: data?.data?.segments,
}))
)
}
}
const pathInfos = await Promise.all(pathInfoProms)
pathInfos.forEach(({ originalId, segments }) => {
const originalArtifact = this.artifactMap[originalId]
if (!originalArtifact || originalArtifact.type === 'pending') {
return
}
const pipeExpPath = getNodePathFromSourceRange(
ast,
originalArtifact.range
)
const pipeExp = getNodeFromPath<VariableDeclarator>(
ast,
pipeExpPath,
'VariableDeclarator'
).node
if (pipeExp.type !== 'VariableDeclarator') {
return
}
const variableName = pipeExp.id.name
const memoryItem = programMemory.root[variableName]
if (!memoryItem) {
return
} else if (memoryItem.type !== 'SketchGroup') {
return
}
const relevantSegments = segments.filter(
({ command_id }: { command_id: string | null }) => command_id
)
if (memoryItem.value.length !== relevantSegments.length) {
return
}
for (let i = 0; i < relevantSegments.length; i++) {
const engineSegment = relevantSegments[i]
const memorySegment = memoryItem.value[i]
const oldId = memorySegment.__geoMeta.id
const artifact = this.artifactMap[oldId]
delete this.artifactMap[oldId]
delete this.sourceRangeMap[oldId]
if (artifact) {
this.artifactMap[engineSegment.command_id] = artifact
this.sourceRangeMap[engineSegment.command_id] = artifact.range
}
}
})
}
private async initPlanes() { private async initPlanes() {
const [xy, yz, xz] = [ const [xy, yz, xz] = [
await this.createPlane({ await this.createPlane({
@ -1221,6 +1094,13 @@ export class EngineCommandManager {
}, },
}) })
} }
planesInitialized(): boolean {
return (
this.defaultPlanes.xy !== '' &&
this.defaultPlanes.yz !== '' &&
this.defaultPlanes.xz !== ''
)
}
onPlaneSelectCallback = (id: string) => {} onPlaneSelectCallback = (id: string) => {}
onPlaneSelected(callback: (id: string) => void) { onPlaneSelected(callback: (id: string) => void) {

View File

@ -138,6 +138,7 @@ show(mySketch001)`
node: ast, node: ast,
programMemory, programMemory,
to: [2, 3], to: [2, 3],
from: [0, 0],
fnName: 'lineTo', fnName: 'lineTo',
pathToNode: [ pathToNode: [
['body', ''], ['body', ''],

View File

@ -193,9 +193,6 @@ export const line: SketchLineHelper = {
pathToNode, pathToNode,
'VariableDeclarator' 'VariableDeclarator'
) )
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const newXVal = createLiteral(roundOff(to[0] - from[0], 2)) const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
const newYVal = createLiteral(roundOff(to[1] - from[1], 2)) const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
@ -209,7 +206,11 @@ export const line: SketchLineHelper = {
pipe.body[callIndex] = callExp pipe.body[callIndex] = callExp
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode, pathToNode: [
...pathToNode,
['body', 'PipeExpression'],
[callIndex, 'CallExpression'],
],
valueUsedInTransform, valueUsedInTransform,
} }
} }
@ -220,6 +221,14 @@ export const line: SketchLineHelper = {
]) ])
if (pipe.type === 'PipeExpression') { if (pipe.type === 'PipeExpression') {
pipe.body = [...pipe.body, callExp] pipe.body = [...pipe.body, callExp]
return {
modifiedAst: _node,
pathToNode: [
...pathToNode,
['body', 'PipeExpression'],
[pipe.body.length - 1, 'CallExpression'],
],
}
} else { } else {
varDec.init = createPipeExpression([varDec.init, callExp]) varDec.init = createPipeExpression([varDec.init, callExp])
} }
@ -909,7 +918,7 @@ export function changeSketchArguments(
sourceRange: SourceRange, sourceRange: SourceRange,
args: [number, number], args: [number, number],
from: [number, number] from: [number, number]
): { modifiedAst: Program } { ): { modifiedAst: Program; pathToNode: PathToNode } {
const _node = { ...node } const _node = { ...node }
const thePath = getNodePathFromSourceRange(_node, sourceRange) const thePath = getNodePathFromSourceRange(_node, sourceRange)
const { node: callExpression, shallowPath } = getNodeFromPath<CallExpression>( const { node: callExpression, shallowPath } = getNodeFromPath<CallExpression>(
@ -929,7 +938,7 @@ export function changeSketchArguments(
}) })
} }
throw new Error('not a sketch line helper') throw new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
} }
interface CreateLineFnCallArgs { interface CreateLineFnCallArgs {
@ -957,8 +966,10 @@ export function addNewSketchLn({
to, to,
fnName, fnName,
pathToNode, pathToNode,
}: Omit<CreateLineFnCallArgs, 'from'>): { from,
}: CreateLineFnCallArgs): {
modifiedAst: Program modifiedAst: Program
pathToNode: PathToNode
} { } {
const node = JSON.parse(JSON.stringify(_node)) const node = JSON.parse(JSON.stringify(_node))
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {} const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
@ -971,12 +982,6 @@ export function addNewSketchLn({
const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath< const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath<
PipeExpression | CallExpression PipeExpression | CallExpression
>(node, pathToNode, 'PipeExpression') >(node, pathToNode, 'PipeExpression')
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const last = sketch.value[sketch.value.length - 1] || sketch.start
const from = last.to
return add({ return add({
node, node,
previousProgramMemory, previousProgramMemory,

View File

@ -5,7 +5,7 @@ import {
transformAstSketchLines, transformAstSketchLines,
} from './sketchcombos' } from './sketchcombos'
import { getSketchSegmentFromSourceRange } from './sketchConstraints' import { getSketchSegmentFromSourceRange } from './sketchConstraints'
import { Selection } from '../../useStore' import { Selection } from 'lib/selections'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(() => initPromise)

View File

@ -7,7 +7,8 @@ import {
ConstraintType, ConstraintType,
getConstraintLevelFromSourceRange, getConstraintLevelFromSourceRange,
} from './sketchcombos' } from './sketchcombos'
import { Selections, ToolTip } from '../../useStore' import { ToolTip } from '../../useStore'
import { Selections } from 'lib/selections'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(() => initPromise)

View File

@ -1,5 +1,6 @@
import { TransformCallback } from './stdTypes' import { TransformCallback } from './stdTypes'
import { Selections, toolTips, ToolTip, Selection } from '../../useStore' import { toolTips, ToolTip } from '../../useStore'
import { Selections, Selection } from 'lib/selections'
import { import {
CallExpression, CallExpression,
Program, Program,

View File

@ -24,6 +24,7 @@ export interface PathReturn {
export interface ModifyAstBase { export interface ModifyAstBase {
node: Program node: Program
// TODO #896: Remove ProgramMemory from this interface
previousProgramMemory: ProgramMemory previousProgramMemory: ProgramMemory
pathToNode: PathToNode pathToNode: PathToNode
} }

View File

@ -1,4 +1,4 @@
import { Selections, StoreState } from '../useStore' import { Selections } from 'lib/selections'
import { Program, PathToNode } from './wasm' import { Program, PathToNode } from './wasm'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
import { ArtifactMap } from './std/engineConnection' import { ArtifactMap } from './std/engineConnection'

View File

@ -7,11 +7,7 @@ import init, {
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { import { EngineCommandManager } from './std/engineConnection'
EngineCommandManager,
ArtifactMap,
SourceRangeMap,
} from './std/engineConnection'
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn' import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem' import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
import type { Program } from '../wasm-lib/kcl/bindings/Program' import type { Program } from '../wasm-lib/kcl/bindings/Program'
@ -70,13 +66,16 @@ const initialise = async () => {
typeof window === 'undefined' typeof window === 'undefined'
? 'http://127.0.0.1:3000' ? 'http://127.0.0.1:3000'
: window.location.origin.includes('tauri://localhost') : window.location.origin.includes('tauri://localhost')
? 'tauri://localhost' ? 'tauri://localhost' // custom protocol for macOS
: window.location.origin.includes('tauri.localhost')
? 'https://tauri.localhost' // fallback for Windows
: window.location.origin.includes('localhost') : window.location.origin.includes('localhost')
? 'http://localhost:3000' ? 'http://localhost:3000'
: window.location.origin && window.location.origin !== 'null' : window.location.origin && window.location.origin !== 'null'
? window.location.origin ? window.location.origin
: 'http://localhost:3000' : 'http://localhost:3000'
const fullUrl = baseUrl + '/wasm_lib_bg.wasm' const fullUrl = baseUrl + '/wasm_lib_bg.wasm'
console.log(`Full URL for WASM: ${fullUrl}`)
const input = await fetch(fullUrl) const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer() const buffer = await input.arrayBuffer()
return init(buffer) return init(buffer)
@ -119,13 +118,7 @@ export const executor = async (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {}, return: null }, programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
planes: DefaultPlanes, planes: DefaultPlanes
// work around while the gemotry is still be stored on the frontend
// will be removed when the stream UI is added.
tempMapCallback: (a: {
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
}) => void = () => {}
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const _programMemory = await _executor( const _programMemory = await _executor(
@ -134,9 +127,7 @@ export const executor = async (
engineCommandManager, engineCommandManager,
planes planes
) )
const { artifactMap, sourceRangeMap } = await engineCommandManager.waitForAllCommands()
await engineCommandManager.waitForAllCommands(node, _programMemory)
tempMapCallback({ artifactMap, sourceRangeMap })
engineCommandManager.endSession() engineCommandManager.endSession()
return _programMemory return _programMemory

View File

@ -11,14 +11,14 @@ const wallMountL = 8
const bracket = startSketchOn('XY') const bracket = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([0, wallMountL], %) |> line([0, wallMountL], %)
|> tangentalArc({ |> tangentialArc({
radius: filletR, radius: filletR,
offset: 90 offset: 90
}, %) }, %)
|> line([-shelfMountL, 0], %) |> line([-shelfMountL, 0], %)
|> line([0, -thickness], %) |> line([0, -thickness], %)
|> line([shelfMountL, 0], %) |> line([shelfMountL, 0], %)
|> tangentalArc({ |> tangentialArc({
radius: filletR - thickness, radius: filletR - thickness,
offset: -90 offset: -90
}, %) }, %)

326
src/lib/selections.ts Normal file
View File

@ -0,0 +1,326 @@
import { Models } from '@kittycad/lib'
import { engineCommandManager } from 'lang/std/engineConnection'
import { SourceRange } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { v4 as uuidv4 } from 'uuid'
import { EditorSelection } from '@codemirror/state'
import { kclManager } from 'lang/KclSinglton'
import { SelectionRange } from '@uiw/react-codemirror'
import { isOverlap } from 'lib/utils'
/*
How selections work is complex due to the nature that we rely on the engine
to tell what has been selected after we send a click command. But than the
app needs these selections to be based on cursors, therefore the app must
be in control of selections. On top of that because we need to set cursor
positions in code-mirror for selections, both from app logic, and still
allow the user to add multiple cursors like a normal editor, it's best to
let code mirror control cursor positions and assosiate those source ranges
with entity ids from code-mirror events later.
So it's a lot of back and forth. conceptually the back and forth is:
1) we send a click command to the engine
2) the engine sends back ids of entities that were clicked
3) we associate that source ranges with those ids
4) we set the codemirror selection based on those source ranges (taking
into account if the user is holding shift to add to current selections
or not). we also create and remember a SelectionRangeTypeMap
5) Code mirror fires a an event that cursors have changed, we loop through
these ranges and associate them with entity ids again with the ArtifactMap,
but also we can pick up selection types using the SelectionRangeTypeMap
6) we clear all previous selections in the engine and set the new ones
The above is less likely to get stale but below is some more details,
because this wonders all over the code-base, I've tried to centeralise it
by putting relevant utils in this file. All of the functions below are
pure with the exception of getEventForSelectWithPoint which makes a call
to the engine, but it's a query call (not mutation) so I'm okay with this.
Actual side effects that change cursors or tell the engine what's selected
are still done throughout the in their relevant parts in the codebase.
In detail:
1) Click commands are mostly sent in stream.tsx search for
"select_with_point"
2) The handler for when the engine sends back entitiy ids calls
getEventForSelectWithPoint, it fires an XState event to update our
selections is xstate context
3 and 4) The XState handler for the above uses handleSelectionBatch and
handleSelectionWithShift to update the selections in xstate context as
well as returning our SelectionRangeTypeMap and a codeMirror specific
event to be dispatched.
5) The codeMirror handler for changes to the cursor uses
processCodeMirrorRanges to associate the ranges back with their original
types and the entity ids (the id can vary depending on the type, as
there's only one source range for a given segment, but depending on if
the user selected the segment directly or the vertex, the id will be
different)
6) We take all of the ids and create events for the engine with
resetAndSetEngineEntitySelectionCmds
An important note is that if a user changes the cursor directly themselves
then they skip directly to step 5, And these selections get a type of
"default".
There are a few more nuances than this, but best to find them in the code.
*/
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
export type Selection = {
type:
| 'default'
| 'line-end'
| 'line-mid'
| 'face'
| 'point'
| 'edge'
| 'line'
| 'arc'
| 'all'
range: SourceRange
}
export type Selections = {
otherSelections: Axis[]
codeBasedSelections: Selection[]
}
export interface SelectionRangeTypeMap {
[key: number]: Selection['type']
}
interface RangeAndId {
id: string
range: SourceRange
}
export async function getEventForSelectWithPoint(
{
data,
}: Extract<
Models['OkModelingCmdResponse_type'],
{ type: 'select_with_point' }
>,
{ sketchEnginePathId }: { sketchEnginePathId: string }
): Promise<ModelingMachineEvent> {
if (!data?.entity_id) {
return {
type: 'Set selection',
data: { selectionType: 'singleCodeCursor' },
}
}
const sourceRange = engineCommandManager.artifactMap[data.entity_id]?.range
if (engineCommandManager.artifactMap[data.entity_id]) {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: sourceRange, type: 'default' },
},
}
}
// selected a vertex
const res = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_curve_uuids_for_vertices',
vertex_ids: [data.entity_id],
path_id: sketchEnginePathId,
},
})
const curveIds = res?.data?.data?.curve_ids
const ranges: RangeAndId[] = curveIds
.map(
(id: string): RangeAndId => ({
id,
range: engineCommandManager.artifactMap[id].range,
})
)
.sort((a: RangeAndId, b: RangeAndId) => a.range[0] - b.range[0])
// default to the head of the curve selected
const _sourceRange = ranges?.[0].range
const artifact = engineCommandManager.artifactMap[ranges?.[0]?.id]
if (artifact.type === 'result') {
artifact.headVertexId = data.entity_id
}
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
// line-end is used to indicate that headVertexId should be sent to the engine as "selected"
// not the whole curve
selection: { range: _sourceRange, type: 'line-end' },
},
}
}
export function handleSelectionBatch({
selections,
}: {
selections: Selections
}): {
selectionRangeTypeMap: SelectionRangeTypeMap
codeMirrorSelection?: EditorSelection
} {
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionRangeTypeMap: SelectionRangeTypeMap = {}
selections.codeBasedSelections.forEach(({ range, type }) => {
if (range?.[1]) {
ranges.push(EditorSelection.cursor(range[1]))
selectionRangeTypeMap[range[1]] = type
}
})
if (ranges.length)
return {
selectionRangeTypeMap,
codeMirrorSelection: EditorSelection.create(
ranges,
selections.codeBasedSelections.length - 1
),
}
return {
selectionRangeTypeMap,
}
}
export function handleSelectionWithShift({
codeSelection,
currestSelections,
isShiftDown,
}: {
codeSelection?: Selection
currestSelections: Selections
isShiftDown: boolean
}): {
selectionRangeTypeMap: SelectionRangeTypeMap
codeMirrorSelection?: EditorSelection
} {
const code = kclManager.code
if (!codeSelection)
return handleSelectionBatch({
selections: {
otherSelections: currestSelections.otherSelections,
codeBasedSelections: [
{
range: [0, code.length ? code.length - 1 : 0],
type: 'default',
},
],
},
})
const selections: Selections = {
...currestSelections,
codeBasedSelections: isShiftDown
? [...currestSelections.codeBasedSelections, codeSelection]
: [codeSelection],
}
return handleSelectionBatch({ selections })
}
type SelectionToEngine = { type: Selection['type']; id: string }
export function processCodeMirrorRanges({
codeMirrorRanges,
selectionRanges,
selectionRangeTypeMap,
}: {
codeMirrorRanges: readonly SelectionRange[]
selectionRanges: Selections
selectionRangeTypeMap: SelectionRangeTypeMap
}): null | {
modelingEvent: ModelingMachineEvent
engineEvents: Models['WebSocketRequest_type'][]
} {
const isChange =
codeMirrorRanges.length !== selectionRanges.codeBasedSelections.length ||
codeMirrorRanges.some(({ from, to }, i) => {
return (
from !== selectionRanges.codeBasedSelections[i].range[0] ||
to !== selectionRanges.codeBasedSelections[i].range[1]
)
})
if (!isChange) return null
const codeBasedSelections: Selections['codeBasedSelections'] =
codeMirrorRanges.map(({ from, to }) => {
if (selectionRangeTypeMap[to]) {
return {
type: selectionRangeTypeMap[to],
range: [from, to],
}
}
return {
type: 'default',
range: [from, to],
}
})
const idBasedSelections: SelectionToEngine[] = codeBasedSelections
.map(({ type, range }): null | SelectionToEngine => {
// TODO #868: loops over all artifacts will become inefficient at a large scale
const entriesWithOverlap = Object.entries(
engineCommandManager.artifactMap || {}
).filter(([_, artifact]) => {
return artifact.range && isOverlap(artifact.range, range)
? artifact
: false
})
if (entriesWithOverlap.length) {
const [id, artifact] = entriesWithOverlap?.[0]
return {
type,
id:
type === 'line-end' &&
artifact.type === 'result' &&
artifact.headVertexId
? artifact.headVertexId
: id,
}
}
return null
})
.filter(Boolean) as any
if (!selectionRanges) return null
return {
modelingEvent: {
type: 'Set selection',
data: {
selectionType: 'mirrorCodeMirrorSelections',
selection: {
...selectionRanges,
codeBasedSelections,
},
},
},
engineEvents: resetAndSetEngineEntitySelectionCmds(idBasedSelections),
}
}
export function resetAndSetEngineEntitySelectionCmds(
selections: SelectionToEngine[]
): Models['WebSocketRequest_type'][] {
if (!engineCommandManager.engineConnection?.isReady()) {
console.log('engine connection isnt ready')
return []
}
return [
{
type: 'modeling_cmd_req',
cmd: {
type: 'select_clear',
},
cmd_id: uuidv4(),
},
{
type: 'modeling_cmd_req',
cmd: {
type: 'select_add',
entities: selections.map(({ id }) => id),
},
cmd_id: uuidv4(),
},
]
}

View File

@ -43,15 +43,12 @@ export function getSortFunction(sortBy: string) {
a: ProjectWithEntryPointMetadata, a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata b: ProjectWithEntryPointMetadata
) => { ) => {
if ( if (a.entrypointMetadata?.modifiedAt && b.entrypointMetadata?.modifiedAt) {
a.entrypoint_metadata?.modifiedAt &&
b.entrypoint_metadata?.modifiedAt
) {
return !sortBy || sortBy.includes('desc') return !sortBy || sortBy.includes('desc')
? b.entrypoint_metadata.modifiedAt.getTime() - ? b.entrypointMetadata.modifiedAt.getTime() -
a.entrypoint_metadata.modifiedAt.getTime() a.entrypointMetadata.modifiedAt.getTime()
: a.entrypoint_metadata.modifiedAt.getTime() - : a.entrypointMetadata.modifiedAt.getTime() -
b.entrypoint_metadata.modifiedAt.getTime() b.entrypointMetadata.modifiedAt.getTime()
} }
return 0 return 0
} }

View File

@ -1,10 +1,14 @@
import { FileEntry } from '@tauri-apps/api/fs'
import { import {
MAX_PADDING, MAX_PADDING,
deepFileFilter,
getNextProjectIndex, getNextProjectIndex,
getPartsCount,
interpolateProjectNameWithIndex, interpolateProjectNameWithIndex,
isRelevantFileOrDir,
} from './tauriFS' } from './tauriFS'
describe('Test file utility functions', () => { describe('Test project name utility functions', () => {
it('interpolates a project name without an index', () => { it('interpolates a project name without an index', () => {
expect(interpolateProjectNameWithIndex('test', 1)).toBe('test') expect(interpolateProjectNameWithIndex('test', 1)).toBe('test')
}) })
@ -46,3 +50,101 @@ describe('Test file utility functions', () => {
expect(getNextProjectIndex('new-project-$n', testFiles)).toBe(8) expect(getNextProjectIndex('new-project-$n', testFiles)).toBe(8)
}) })
}) })
describe('Test file tree utility functions', () => {
const baseFiles: FileEntry[] = [
{
name: 'show-me.kcl',
path: '/projects/show-me.kcl',
},
{
name: 'hide-me.jpg',
path: '/projects/hide-me.jpg',
},
{
name: '.gitignore',
path: '/projects/.gitignore',
},
]
const filteredBaseFiles: FileEntry[] = [
{
name: 'show-me.kcl',
path: '/projects/show-me.kcl',
},
]
it('Only includes files relevant to the project in a flat directory', () => {
expect(deepFileFilter(baseFiles, isRelevantFileOrDir)).toEqual(
filteredBaseFiles
)
})
const nestedFiles: FileEntry[] = [
...baseFiles,
{
name: 'show-me',
path: '/projects/show-me',
children: [
{
name: 'show-me-nested',
path: '/projects/show-me/show-me-nested',
children: baseFiles,
},
{
name: 'hide-me',
path: '/projects/show-me/hide-me',
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
},
],
},
{
name: 'hide-me',
path: '/projects/hide-me',
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
},
]
const filteredNestedFiles: FileEntry[] = [
...filteredBaseFiles,
{
name: 'show-me',
path: '/projects/show-me',
children: [
{
name: 'show-me-nested',
path: '/projects/show-me/show-me-nested',
children: filteredBaseFiles,
},
],
},
]
it('Only includes directories that include files relevant to the project in a nested directory', () => {
expect(deepFileFilter(nestedFiles, isRelevantFileOrDir)).toEqual(
filteredNestedFiles
)
})
const withHiddenDir: FileEntry[] = [
...baseFiles,
{
name: '.hide-me',
path: '/projects/.hide-me',
children: baseFiles,
},
]
it(`Hides folders that begin with a ".", even if they contain relevant files`, () => {
expect(deepFileFilter(withHiddenDir, isRelevantFileOrDir)).toEqual(
filteredBaseFiles
)
})
it(`Properly counts the number of relevant files and directories in a project`, () => {
expect(getPartsCount(nestedFiles)).toEqual({
kclFileCount: 2,
kclDirCount: 2,
})
})
})

View File

@ -5,16 +5,18 @@ import {
readDir, readDir,
writeTextFile, writeTextFile,
} from '@tauri-apps/api/fs' } from '@tauri-apps/api/fs'
import { documentDir, homeDir } from '@tauri-apps/api/path' import { documentDir, homeDir, sep } from '@tauri-apps/api/path'
import { isTauri } from './isTauri' import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router' import { ProjectWithEntryPointMetadata } from '../Router'
import { metadata } from 'tauri-plugin-fs-extra-api' import { metadata } from 'tauri-plugin-fs-extra-api'
import { bracket } from './exampleKcl'
const PROJECT_FOLDER = 'kittycad-modeling-projects' const PROJECT_FOLDER = 'kittycad-modeling-projects'
export const FILE_EXT = '.kcl' export const FILE_EXT = '.kcl'
export const PROJECT_ENTRYPOINT = 'main' + FILE_EXT export const PROJECT_ENTRYPOINT = 'main' + FILE_EXT
const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
export const MAX_PADDING = 7 export const MAX_PADDING = 7
const RELEVANT_FILE_TYPES = ['kcl']
// Initializes the project directory and returns the path // Initializes the project directory and returns the path
export async function initializeProjectDirectory(directory: string) { export async function initializeProjectDirectory(directory: string) {
@ -69,7 +71,7 @@ export async function getProjectsInDir(projectDir: string) {
const projectsWithMetadata = await Promise.all( const projectsWithMetadata = await Promise.all(
readProjects.map(async (p) => ({ readProjects.map(async (p) => ({
entrypoint_metadata: await metadata(p.path + '/' + PROJECT_ENTRYPOINT), entrypointMetadata: await metadata(p.path + sep + PROJECT_ENTRYPOINT),
...p, ...p,
})) }))
) )
@ -77,6 +79,135 @@ export async function getProjectsInDir(projectDir: string) {
return projectsWithMetadata return projectsWithMetadata
} }
export const isHidden = (fileOrDir: FileEntry) =>
!!fileOrDir.name?.startsWith('.')
export const isDir = (fileOrDir: FileEntry) =>
'children' in fileOrDir && fileOrDir.children !== undefined
export function deepFileFilter(
entries: FileEntry[],
filterFn: (f: FileEntry) => boolean
): FileEntry[] {
const filteredEntries: FileEntry[] = []
for (const fileOrDir of entries) {
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
const filteredChildren = deepFileFilter(fileOrDir.children, filterFn)
if (filterFn(fileOrDir)) {
filteredEntries.push({
...fileOrDir,
children: filteredChildren,
})
}
} else if (filterFn(fileOrDir)) {
filteredEntries.push(fileOrDir)
}
}
return filteredEntries
}
export function deepFileFilterFlat(
entries: FileEntry[],
filterFn: (f: FileEntry) => boolean
): FileEntry[] {
const filteredEntries: FileEntry[] = []
for (const fileOrDir of entries) {
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
const filteredChildren = deepFileFilterFlat(fileOrDir.children, filterFn)
if (filterFn(fileOrDir)) {
filteredEntries.push({
...fileOrDir,
children: filteredChildren,
})
}
filteredEntries.push(...filteredChildren)
} else if (filterFn(fileOrDir)) {
filteredEntries.push(fileOrDir)
}
}
return filteredEntries
}
// Read the contents of a project directory
// and return all relevant files and sub-directories recursively
export async function readProject(projectDir: string) {
const readFiles = await readDir(projectDir, {
recursive: true,
})
return deepFileFilter(readFiles, isRelevantFileOrDir)
}
// Given a read project, return the number of .kcl files,
// both in the root directory and in sub-directories,
// and folders that contain at least one .kcl file
export function getPartsCount(project: FileEntry[]) {
const flatProject = deepFileFilterFlat(project, isRelevantFileOrDir)
const kclFileCount = flatProject.filter((f) =>
f.name?.endsWith(FILE_EXT)
).length
const kclDirCount = flatProject.filter((f) => f.children !== undefined).length
return {
kclFileCount,
kclDirCount,
}
}
// Determines if a file or directory is relevant to the project
// i.e. not a hidden file or directory, and is a relevant file type
// or contains at least one relevant file (even if it's nested)
// or is a completely empty directory
export function isRelevantFileOrDir(fileOrDir: FileEntry) {
let isRelevantDir = false
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
isRelevantDir =
!isHidden(fileOrDir) &&
(fileOrDir.children.some(isRelevantFileOrDir) ||
fileOrDir.children.length === 0)
}
const isRelevantFile =
!isHidden(fileOrDir) &&
RELEVANT_FILE_TYPES.some((ext) => fileOrDir.name?.endsWith(ext))
return (
(isDir(fileOrDir) && isRelevantDir) || (!isDir(fileOrDir) && isRelevantFile)
)
}
// Deeply sort the files and directories in a project like VS Code does:
// The main.kcl file is always first, then files, then directories
// Files and directories are sorted alphabetically
export function sortProject(project: FileEntry[]): FileEntry[] {
const sortedProject = project.sort((a, b) => {
if (a.name === PROJECT_ENTRYPOINT) {
return -1
} else if (b.name === PROJECT_ENTRYPOINT) {
return 1
} else if (a.children === undefined && b.children !== undefined) {
return -1
} else if (a.children !== undefined && b.children === undefined) {
return 1
} else if (a.name && b.name) {
return a.name.localeCompare(b.name)
} else {
return 0
}
})
return sortedProject.map((fileOrDir: FileEntry) => {
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
return {
...fileOrDir,
children: sortProject(fileOrDir.children),
}
} else {
return fileOrDir
}
})
}
// Creates a new file in the default directory with the default project name // Creates a new file in the default directory with the default project name
// Returns the path to the new file // Returns the path to the new file
export async function createNewProject( export async function createNewProject(
@ -94,7 +225,7 @@ export async function createNewProject(
}) })
} }
await writeTextFile(path + '/' + PROJECT_ENTRYPOINT, '').catch((err) => { await writeTextFile(path + sep + PROJECT_ENTRYPOINT, bracket).catch((err) => {
console.error('Error creating new file:', err) console.error('Error creating new file:', err)
throw err throw err
}) })
@ -102,13 +233,13 @@ export async function createNewProject(
const m = await metadata(path) const m = await metadata(path)
return { return {
name: path.slice(path.lastIndexOf('/') + 1), name: path.slice(path.lastIndexOf(sep) + 1),
path: path, path: path,
entrypoint_metadata: m, entrypointMetadata: m,
children: [ children: [
{ {
name: PROJECT_ENTRYPOINT, name: PROJECT_ENTRYPOINT,
path: path + '/' + PROJECT_ENTRYPOINT, path: path + sep + PROJECT_ENTRYPOINT,
children: [], children: [],
}, },
], ],

View File

@ -93,6 +93,6 @@ export async function executor(
yz: uuidv4(), yz: uuidv4(),
xz: uuidv4(), xz: uuidv4(),
}) })
await engineCommandManager.waitForAllCommands(ast, programMemory) await engineCommandManager.waitForAllCommands()
return programMemory return programMemory
} }

178
src/machines/fileMachine.ts Normal file
View File

@ -0,0 +1,178 @@
import { assign, createMachine } from 'xstate'
import { ProjectWithEntryPointMetadata } from 'Router'
import { FileEntry } from '@tauri-apps/api/fs'
export const FILE_PERSIST_KEY = 'Last opened KCL files'
export const DEFAULT_FILE_NAME = 'Untitled'
export const fileMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
id: 'File machine',
initial: 'Reading files',
context: {
project: {} as ProjectWithEntryPointMetadata,
selectedDirectory: {} as FileEntry,
},
on: {
assign: {
actions: assign((_, event) => ({
...event.data,
})),
target: '.Reading files',
},
},
states: {
'Has no files': {
on: {
'Create file': {
target: 'Creating file',
},
},
},
'Has files': {
on: {
'Rename file': {
target: 'Renaming file',
},
'Create file': {
target: 'Creating file',
},
'Delete file': {
target: 'Deleting file',
},
'Open file': {
target: 'Opening file',
},
'Set selected directory': {
target: 'Has files',
actions: ['setSelectedDirectory'],
},
},
},
'Creating file': {
invoke: {
id: 'create-file',
src: 'createFile',
onDone: [
{
target: 'Reading files',
actions: ['toastSuccess'],
},
],
onError: [
{
target: 'Reading files',
actions: ['toastError'],
},
],
},
},
'Renaming file': {
invoke: {
id: 'rename-file',
src: 'renameFile',
onDone: [
{
target: '#File machine.Reading files',
actions: ['toastSuccess'],
},
],
onError: [
{
target: '#File machine.Reading files',
actions: ['toastError'],
},
],
},
},
'Deleting file': {
invoke: {
id: 'delete-file',
src: 'deleteFile',
onDone: [
{
actions: ['toastSuccess'],
target: '#File machine.Reading files',
},
],
onError: {
actions: ['toastError'],
target: '#File machine.Has files',
},
},
},
'Reading files': {
invoke: {
id: 'read-files',
src: 'readFiles',
onDone: [
{
cond: 'Has at least 1 file',
target: 'Has files',
actions: ['setFiles'],
},
{
target: 'Has no files',
actions: ['setFiles'],
},
],
onError: [
{
target: 'Has no files',
actions: ['toastError'],
},
],
},
},
'Opening file': {
entry: ['navigateToFile'],
},
},
schema: {
events: {} as
| { type: 'Open file'; data: { name: string } }
| {
type: 'Rename file'
data: { oldName: string; newName: string; isDir: boolean }
}
| { type: 'Create file'; data: { name: string; makeDir: boolean } }
| { type: 'Delete file'; data: FileEntry }
| { type: 'Set selected directory'; data: FileEntry }
| { type: 'navigate'; data: { name: string } }
| {
type: 'done.invoke.read-files'
data: ProjectWithEntryPointMetadata
}
| { type: 'assign'; data: { [key: string]: any } },
},
predictableActionArguments: true,
preserveActionOrder: true,
tsTypes: {} as import('./fileMachine.typegen').Typegen0,
},
{
actions: {
setFiles: assign((_, event) => {
return { project: event.data }
}),
setSelectedDirectory: assign((_, event) => {
return { selectedDirectory: event.data }
}),
},
}
)

View File

@ -0,0 +1,96 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true
internalEvents: {
'done.invoke.create-file': {
type: 'done.invoke.create-file'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.delete-file': {
type: 'done.invoke.delete-file'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.read-files': {
type: 'done.invoke.read-files'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.rename-file': {
type: 'done.invoke.rename-file'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'error.platform.create-file': {
type: 'error.platform.create-file'
data: unknown
}
'error.platform.delete-file': {
type: 'error.platform.delete-file'
data: unknown
}
'error.platform.read-files': {
type: 'error.platform.read-files'
data: unknown
}
'error.platform.rename-file': {
type: 'error.platform.rename-file'
data: unknown
}
'xstate.init': { type: 'xstate.init' }
}
invokeSrcNameMap: {
createFile: 'done.invoke.create-file'
deleteFile: 'done.invoke.delete-file'
readFiles: 'done.invoke.read-files'
renameFile: 'done.invoke.rename-file'
}
missingImplementations: {
actions: 'navigateToFile' | 'toastError' | 'toastSuccess'
delays: never
guards: 'Has at least 1 file'
services: 'createFile' | 'deleteFile' | 'readFiles' | 'renameFile'
}
eventsCausingActions: {
navigateToFile: 'Open file'
setFiles: 'done.invoke.read-files'
setSelectedDirectory: 'Set selected directory'
toastError:
| 'error.platform.create-file'
| 'error.platform.delete-file'
| 'error.platform.read-files'
| 'error.platform.rename-file'
toastSuccess:
| 'done.invoke.create-file'
| 'done.invoke.delete-file'
| 'done.invoke.rename-file'
}
eventsCausingDelays: {}
eventsCausingGuards: {
'Has at least 1 file': 'done.invoke.read-files'
}
eventsCausingServices: {
createFile: 'Create file'
deleteFile: 'Delete file'
readFiles:
| 'assign'
| 'done.invoke.create-file'
| 'done.invoke.delete-file'
| 'done.invoke.rename-file'
| 'error.platform.create-file'
| 'error.platform.rename-file'
| 'xstate.init'
renameFile: 'Rename file'
}
matchesStates:
| 'Creating file'
| 'Deleting file'
| 'Has files'
| 'Has no files'
| 'Opening file'
| 'Reading files'
| 'Renaming file'
tags: never
}

File diff suppressed because one or more lines are too long

View File

@ -8,10 +8,12 @@
"done.invoke.get-angle-info": { type: "done.invoke.get-angle-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." }; "done.invoke.get-angle-info": { type: "done.invoke.get-angle-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-horizontal-info": { type: "done.invoke.get-horizontal-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." }; "done.invoke.get-horizontal-info": { type: "done.invoke.get-horizontal-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-length-info": { type: "done.invoke.get-length-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." }; "done.invoke.get-length-info": { type: "done.invoke.get-length-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-perpendicular-distance-info": { type: "done.invoke.get-perpendicular-distance-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-vertical-info": { type: "done.invoke.get-vertical-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." }; "done.invoke.get-vertical-info": { type: "done.invoke.get-vertical-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"error.platform.get-angle-info": { type: "error.platform.get-angle-info"; data: unknown }; "error.platform.get-angle-info": { type: "error.platform.get-angle-info"; data: unknown };
"error.platform.get-horizontal-info": { type: "error.platform.get-horizontal-info"; data: unknown }; "error.platform.get-horizontal-info": { type: "error.platform.get-horizontal-info"; data: unknown };
"error.platform.get-length-info": { type: "error.platform.get-length-info"; data: unknown }; "error.platform.get-length-info": { type: "error.platform.get-length-info"; data: unknown };
"error.platform.get-perpendicular-distance-info": { type: "error.platform.get-perpendicular-distance-info"; data: unknown };
"error.platform.get-vertical-info": { type: "error.platform.get-vertical-info"; data: unknown }; "error.platform.get-vertical-info": { type: "error.platform.get-vertical-info"; data: unknown };
"xstate.init": { type: "xstate.init" }; "xstate.init": { type: "xstate.init" };
"xstate.stop": { type: "xstate.stop" }; "xstate.stop": { type: "xstate.stop" };
@ -20,13 +22,14 @@
"Get angle info": "done.invoke.get-angle-info"; "Get angle info": "done.invoke.get-angle-info";
"Get horizontal info": "done.invoke.get-horizontal-info"; "Get horizontal info": "done.invoke.get-horizontal-info";
"Get length info": "done.invoke.get-length-info"; "Get length info": "done.invoke.get-length-info";
"Get perpendicular distance info": "done.invoke.get-perpendicular-distance-info";
"Get vertical info": "done.invoke.get-vertical-info"; "Get vertical info": "done.invoke.get-vertical-info";
}; };
missingImplementations: { missingImplementations: {
actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute" | "toast extrude failed"; actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute" | "toast extrude failed";
delays: never; delays: never;
guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face"; guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face";
services: "Get angle info" | "Get horizontal info" | "Get length info" | "Get vertical info"; services: "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
}; };
eventsCausingActions: { eventsCausingActions: {
"AST add line segment": "Add point"; "AST add line segment": "Add point";
@ -37,40 +40,46 @@
"Clear selection": "Deselect all"; "Clear selection": "Deselect all";
"Constrain equal length": "Constrain equal length"; "Constrain equal length": "Constrain equal length";
"Constrain horizontally align": "Constrain horizontally align"; "Constrain horizontally align": "Constrain horizontally align";
"Constrain parallel": "Constrain parallel";
"Constrain remove constraints": "Constrain remove constraints";
"Constrain vertically align": "Constrain vertically align"; "Constrain vertically align": "Constrain vertically align";
"Make selection horizontal": "Make segment horizontal"; "Make selection horizontal": "Make segment horizontal";
"Make selection vertical": "Make segment vertical"; "Make selection vertical": "Make segment vertical";
"Modify AST": "Complete line"; "Modify AST": "Complete line";
"Remove from code-based selection": "Deselect edge" | "Deselect face" | "Deselect point"; "Remove from code-based selection": "Deselect edge" | "Deselect face" | "Deselect point";
"Remove from other selection": "Deselect axis"; "Remove from other selection": "Deselect axis";
"Set selection": "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-vertical-info"; "Set selection": "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info";
"Update code selection cursors": "Complete line" | "Deselect all" | "Deselect axis" | "Deselect edge" | "Deselect face" | "Deselect point" | "Deselect segment" | "Select edge" | "Select face" | "Select point" | "Select segment"; "Update code selection cursors": "Complete line" | "Deselect all" | "Deselect axis" | "Deselect edge" | "Deselect face" | "Deselect point" | "Deselect segment" | "Select edge" | "Select face" | "Select point" | "Select segment";
"create path": "Select default plane"; "create path": "Select default plane";
"default_camera_disable_sketch_mode": "Cancel"; "default_camera_disable_sketch_mode": "Cancel";
"edit mode enter": "Enter sketch"; "edit mode enter": "Enter sketch" | "Re-execute";
"edit_mode_exit": "Cancel"; "edit_mode_exit": "Cancel";
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-vertical-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-vertical-info"; "equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain parallel" | "Constrain remove constraints" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Re-execute" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
"hide default planes": "Cancel" | "Select default plane" | "xstate.stop"; "hide default planes": "Cancel" | "Select default plane" | "xstate.stop";
"reset sketch metadata": "Cancel" | "Select default plane"; "reset sketch metadata": "Cancel" | "Select default plane";
"set default plane id": "Select default plane"; "set default plane id": "Select default plane";
"set sketch metadata": "Enter sketch"; "set sketch metadata": "Enter sketch";
"set sketchMetadata from pathToNode": "Re-execute";
"set tool": "Equip new tool"; "set tool": "Equip new tool";
"set tool line": "Equip tool"; "set tool line": "Equip tool";
"set tool move": "Equip move tool"; "set tool move": "Equip move tool" | "Re-execute" | "Set selection";
"show default planes": "Enter sketch"; "show default planes": "Enter sketch";
"sketch exit execute": "Cancel" | "Complete line" | "xstate.stop"; "sketch exit execute": "Cancel" | "Complete line" | "xstate.stop";
"sketch mode enabled": "Enter sketch" | "Select default plane"; "sketch mode enabled": "Enter sketch" | "Re-execute" | "Select default plane";
"toast extrude failed": ""; "toast extrude failed": "";
}; };
eventsCausingDelays: { eventsCausingDelays: {
}; };
eventsCausingGuards: { eventsCausingGuards: {
"Can canstrain parallel": "Constrain parallel";
"Can constrain angle": "Constrain angle"; "Can constrain angle": "Constrain angle";
"Can constrain equal length": "Constrain equal length"; "Can constrain equal length": "Constrain equal length";
"Can constrain horizontal distance": "Constrain horizontal distance"; "Can constrain horizontal distance": "Constrain horizontal distance";
"Can constrain horizontally align": "Constrain horizontally align"; "Can constrain horizontally align": "Constrain horizontally align";
"Can constrain length": "Constrain length"; "Can constrain length": "Constrain length";
"Can constrain perpendicular distance": "Constrain perpendicular distance";
"Can constrain remove constraints": "Constrain remove constraints";
"Can constrain vertical distance": "Constrain vertical distance"; "Can constrain vertical distance": "Constrain vertical distance";
"Can constrain vertically align": "Constrain vertically align"; "Can constrain vertically align": "Constrain vertically align";
"Can make selection horizontal": "Make segment horizontal"; "Can make selection horizontal": "Make segment horizontal";
@ -82,6 +91,8 @@
"Selection contains point": "Deselect point"; "Selection contains point": "Deselect point";
"Selection is not empty": "Deselect all"; "Selection is not empty": "Deselect all";
"Selection is one face": "Enter sketch"; "Selection is one face": "Enter sketch";
"can move": "";
"can move with execute": "";
"has no selection": "extrude intent"; "has no selection": "extrude intent";
"has valid extrude selection": "" | "extrude intent"; "has valid extrude selection": "" | "extrude intent";
"is editing existing sketch": ""; "is editing existing sketch": "";
@ -90,9 +101,11 @@
"Get angle info": "Constrain angle"; "Get angle info": "Constrain angle";
"Get horizontal info": "Constrain horizontal distance"; "Get horizontal info": "Constrain horizontal distance";
"Get length info": "Constrain length"; "Get length info": "Constrain length";
"Get perpendicular distance info": "Constrain perpendicular distance";
"Get vertical info": "Constrain vertical distance"; "Get vertical info": "Constrain vertical distance";
}; };
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added"; }; }; matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
tags: never; tags: never;
} }

View File

@ -29,6 +29,7 @@ import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine' import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
import { sep } from '@tauri-apps/api/path'
// This route only opens in the Tauri desktop context for now, // This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types. // as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -58,7 +59,7 @@ const Home = () => {
setCommandBarOpen(false) setCommandBarOpen(false)
navigate( navigate(
`${paths.FILE}/${encodeURIComponent( `${paths.FILE}/${encodeURIComponent(
context.defaultDirectory + '/' + event.data.name context.defaultDirectory + sep + event.data.name
)}` )}`
) )
} }
@ -91,7 +92,7 @@ const Home = () => {
name = interpolateProjectNameWithIndex(name, nextIndex) name = interpolateProjectNameWithIndex(name, nextIndex)
} }
await createNewProject(context.defaultDirectory + '/' + name) await createNewProject(context.defaultDirectory + sep + name)
if (shouldUpdateDefaultProjectName) { if (shouldUpdateDefaultProjectName) {
sendToSettings({ sendToSettings({
@ -114,8 +115,8 @@ const Home = () => {
} }
await renameFile( await renameFile(
context.defaultDirectory + '/' + oldName, context.defaultDirectory + sep + oldName,
context.defaultDirectory + '/' + name context.defaultDirectory + sep + name
) )
return `Successfully renamed "${oldName}" to "${name}"` return `Successfully renamed "${oldName}" to "${name}"`
}, },
@ -123,7 +124,7 @@ const Home = () => {
context: ContextFrom<typeof homeMachine>, context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Delete project'> event: EventFrom<typeof homeMachine, 'Delete project'>
) => { ) => {
await removeDir(context.defaultDirectory + '/' + event.data.name, { await removeDir(context.defaultDirectory + sep + event.data.name, {
recursive: true, recursive: true,
}) })
return `Successfully deleted "${event.data.name}"` return `Successfully deleted "${event.data.name}"`
@ -172,9 +173,9 @@ const Home = () => {
} }
return ( return (
<div className="h-screen overflow-hidden relative flex flex-col"> <div className="relative flex flex-col h-screen overflow-hidden">
<AppHeader showToolbar={false} /> <AppHeader showToolbar={false} />
<div className="my-24 overflow-y-auto max-w-5xl w-full mx-auto"> <div className="w-full max-w-5xl px-4 mx-auto my-24 overflow-y-auto lg:px-0">
<section className="flex justify-between"> <section className="flex justify-between">
<h1 className="text-3xl text-bold">Your Projects</h1> <h1 className="text-3xl text-bold">Your Projects</h1>
<div className="flex"> <div className="flex">
@ -235,7 +236,7 @@ const Home = () => {
) : ( ) : (
<> <>
{projects.length > 0 ? ( {projects.length > 0 ? (
<ul className="my-8 w-full grid grid-cols-4 gap-4"> <ul className="grid w-full grid-cols-4 gap-4 my-8">
{projects.sort(getSortFunction(sort)).map((project) => ( {projects.sort(getSortFunction(sort)).map((project) => (
<ProjectCard <ProjectCard
key={project.name} key={project.name}
@ -246,7 +247,7 @@ const Home = () => {
))} ))}
</ul> </ul>
) : ( ) : (
<p className="rounded my-8 border border-dashed border-chalkboard-30 dark:border-chalkboard-70 p-4"> <p className="p-4 my-8 border border-dashed rounded border-chalkboard-30 dark:border-chalkboard-70">
No Projects found, ready to make your first one? No Projects found, ready to make your first one?
</p> </p>
)} )}

View File

@ -24,8 +24,15 @@ export default function Export() {
Try opening the project menu and clicking "Export Model". Try opening the project menu and clicking "Export Model".
</p> </p>
<p className="my-4"> <p className="my-4">
KittyCAD Modeling App uses our open-source extension proposal for KittyCAD Modeling App uses{' '}
the GLTF file format.{' '} <a
href="https://kittycad.io/gltf-format-extension"
rel="noopener noreferrer"
target="_blank"
>
our open-source extension proposal
</a>{' '}
for the GLTF file format.{' '}
<a <a
href="https://kittycad.io/docs/api/convert-cad-file" href="https://kittycad.io/docs/api/convert-cad-file"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -4,13 +4,23 @@ import { useDismiss } from '.'
import { useEffect } from 'react' import { useEffect } from 'react'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
import { useModelingContext } from 'hooks/useModelingContext'
export default function FutureWork() { export default function FutureWork() {
const { send } = useModelingContext()
const dismiss = useDismiss() const dismiss = useDismiss()
useEffect(() => { useEffect(() => {
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
// If the engine is ready, promptly execute the loaded code
kclManager.setCodeAndExecute(bracket)
} else {
// Otherwise, just set the code and wait for the connection to complete
kclManager.setCode(bracket) kclManager.setCode(bracket)
}, [kclManager.setCode]) }
send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode
}, [send])
return ( return (
<div className="fixed grid justify-center items-center inset-0 bg-chalkboard-100/50 z-50"> <div className="fixed grid justify-center items-center inset-0 bg-chalkboard-100/50 z-50">

View File

@ -10,6 +10,7 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { import {
PROJECT_ENTRYPOINT,
createNewProject, createNewProject,
getNextProjectIndex, getNextProjectIndex,
getProjectsInDir, getProjectsInDir,
@ -20,6 +21,7 @@ import { useNavigate } from 'react-router-dom'
import { paths } from 'Router' import { paths } from 'Router'
import { useEffect } from 'react' import { useEffect } from 'react'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSinglton'
import { sep } from '@tauri-apps/api/path'
function OnboardingWithNewFile() { function OnboardingWithNewFile() {
const navigate = useNavigate() const navigate = useNavigate()
@ -41,12 +43,16 @@ function OnboardingWithNewFile() {
ONBOARDING_PROJECT_NAME, ONBOARDING_PROJECT_NAME,
nextIndex nextIndex
) )
const newFile = await createNewProject(defaultDirectory + '/' + name) const newFile = await createNewProject(defaultDirectory + sep + name)
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`) navigate(
`${paths.FILE}/${encodeURIComponent(
newFile.path + sep + PROJECT_ENTRYPOINT
)}${paths.ONBOARDING.INDEX}`
)
} }
return ( return (
<div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50"> <div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="max-w-3xl bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded"> <div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
{!isTauri() ? ( {!isTauri() ? (
<> <>
<h1 className="text-2xl font-bold text-warn-80 dark:text-warn-10"> <h1 className="text-2xl font-bold text-warn-80 dark:text-warn-10">
@ -84,7 +90,7 @@ function OnboardingWithNewFile() {
</> </>
) : ( ) : (
<> <>
<h1 className="text-2xl font-bold flex gap-4 flex-wrap items-center"> <h1 className="flex flex-wrap items-center gap-4 text-2xl font-bold">
Would you like to create a new project? Would you like to create a new project?
</h1> </h1>
<section className="my-12"> <section className="my-12">
@ -110,7 +116,11 @@ function OnboardingWithNewFile() {
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="button" Element="button"
onClick={createAndOpenNewProject} onClick={() => {
createAndOpenNewProject()
kclManager.setCode(bracket)
dismiss()
}}
icon={{ icon: faArrowRight }} icon={{ icon: faArrowRight }}
> >
Make a new project Make a new project
@ -138,21 +148,22 @@ export default function Introduction() {
: '' : ''
const dismiss = useDismiss() const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.CAMERA) const next = useNextClick(onboardingPaths.CAMERA)
const isStarterCode = kclManager.code === '' || kclManager.code === bracket
useEffect(() => { useEffect(() => {
if (kclManager.code === '') kclManager.setCode(bracket) if (kclManager.code === '') kclManager.setCode(bracket)
}, [kclManager.code, kclManager.setCode]) }, [])
return !(kclManager.code !== '' && kclManager.code !== bracket) ? ( return isStarterCode ? (
<div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50"> <div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="max-w-3xl bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded"> <div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<h1 className="text-2xl font-bold flex gap-4 flex-wrap items-center"> <h1 className="flex flex-wrap items-center gap-4 text-2xl font-bold">
<img <img
src={`/kcma-logomark${getLogoTheme()}.svg`} src={`/kcma-logomark${getLogoTheme()}.svg`}
alt="KittyCAD Modeling App" alt="KittyCAD Modeling App"
className="max-w-full h-20" className="h-20 max-w-full"
/> />
<span className="bg-energy-10 text-energy-80 px-3 py-1 rounded-full text-base"> <span className="px-3 py-1 text-base rounded-full bg-energy-10 text-energy-80">
Alpha Alpha
</span> </span>
</h1> </h1>

View File

@ -11,7 +11,13 @@ export default function Sketching() {
const next = useNextClick(onboardingPaths.FUTURE_WORK) const next = useNextClick(onboardingPaths.FUTURE_WORK)
useEffect(() => { useEffect(() => {
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
// If the engine is ready, promptly execute the loaded code
kclManager.setCodeAndExecute('')
} else {
// Otherwise, just set the code and wait for the connection to complete
kclManager.setCode('') kclManager.setCode('')
}
}, []) }, [])
return ( return (

View File

@ -31,9 +31,11 @@ import {
interpolateProjectNameWithIndex, interpolateProjectNameWithIndex,
} from 'lib/tauriFS' } from 'lib/tauriFS'
import { ONBOARDING_PROJECT_NAME } from './Onboarding' import { ONBOARDING_PROJECT_NAME } from './Onboarding'
import { sep } from '@tauri-apps/api/path'
export const Settings = () => { export const Settings = () => {
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData const loaderData =
(useRouteLoaderData(paths.FILE) as IndexLoaderData) || undefined
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const isFileSettings = location.pathname.includes(paths.FILE) const isFileSettings = location.pathname.includes(paths.FILE)
@ -94,13 +96,13 @@ export const Settings = () => {
ONBOARDING_PROJECT_NAME, ONBOARDING_PROJECT_NAME,
nextIndex nextIndex
) )
const newFile = await createNewProject(defaultDirectory + '/' + name) const newFile = await createNewProject(defaultDirectory + sep + name)
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`) navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
} }
return ( return (
<div className="fixed inset-0 z-40 overflow-auto body-bg"> <div className="fixed inset-0 z-40 overflow-auto body-bg">
<AppHeader showToolbar={false} project={loaderData?.project}> <AppHeader showToolbar={false} project={loaderData}>
<ActionButton <ActionButton
Element="link" Element="link"
to={location.pathname.replace(paths.SETTINGS, '')} to={location.pathname.replace(paths.SETTINGS, '')}
@ -115,7 +117,7 @@ export const Settings = () => {
Close Close
</ActionButton> </ActionButton>
</AppHeader> </AppHeader>
<div className="max-w-5xl mx-auto my-24"> <div className="max-w-5xl mx-5 lg:mx-auto my-24">
<h1 className="text-4xl font-bold">User Settings</h1> <h1 className="text-4xl font-bold">User Settings</h1>
<p className="max-w-2xl mt-6"> <p className="max-w-2xl mt-6">
Don't see the feature you want? Check to see if it's on{' '} Don't see the feature you want? Check to see if it's on{' '}

View File

@ -1,42 +1,13 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { addLineHighlight, EditorView } from './editor/highlightextension' import { addLineHighlight, EditorView } from './editor/highlightextension'
import { import { parse, Program, _executor, ProgramMemory } from './lang/wasm'
parse, import { Selection } from 'lib/selections'
Program,
_executor,
ProgramMemory,
Position,
PathToNode,
Rotation,
SourceRange,
} from './lang/wasm'
import { enginelessExecutor } from './lib/testHelpers' import { enginelessExecutor } from './lib/testHelpers'
import { EditorSelection } from '@codemirror/state'
import { EngineCommandManager } from './lang/std/engineConnection' import { EngineCommandManager } from './lang/std/engineConnection'
import { KCLError } from './lang/errors' import { KCLError } from './lang/errors'
import { kclManager } from 'lang/KclSinglton'
import { DefaultPlanes } from './wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from './wasm-lib/kcl/bindings/DefaultPlanes'
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
export type Selection = {
type:
| 'default'
| 'line-end'
| 'line-mid'
| 'face'
| 'point'
| 'edge'
| 'line'
| 'arc'
| 'all'
range: SourceRange
}
export type Selections = {
otherSelections: Axis[]
codeBasedSelections: Selection[]
}
export type ToolTip = export type ToolTip =
| 'lineTo' | 'lineTo'
| 'line' | 'line'
@ -77,10 +48,6 @@ export type PaneType =
| 'logs' | 'logs'
| 'lspMessages' | 'lspMessages'
export interface SelectionRangeTypeMap {
[key: number]: Selection['type']
}
export interface StoreState { export interface StoreState {
editorView: EditorView | null editorView: EditorView | null
setEditorView: (editorView: EditorView) => void setEditorView: (editorView: EditorView) => void
@ -257,7 +224,7 @@ export async function executeCode({
body: [], body: [],
nonCodeMeta: { nonCodeMeta: {
nonCodeNodes: {}, nonCodeNodes: {},
start: null, start: [],
}, },
}, },
} }
@ -316,7 +283,7 @@ export async function executeAst({
defaultPlanes defaultPlanes
)) ))
await engineCommandManager.waitForAllCommands(ast, programMemory) await engineCommandManager.waitForAllCommands()
return { return {
logs: [], logs: [],
errors: [], errors: [],
@ -345,79 +312,3 @@ export async function executeAst({
} }
} }
} }
export function dispatchCodeMirrorCursor({
selections,
editorView,
}: {
selections: Selections
editorView: EditorView
}): {
selectionRangeTypeMap: SelectionRangeTypeMap
} {
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionRangeTypeMap: SelectionRangeTypeMap = {}
selections.codeBasedSelections.forEach(({ range, type }) => {
if (range?.[1]) {
ranges.push(EditorSelection.cursor(range[1]))
selectionRangeTypeMap[range[1]] = type
}
})
setTimeout(() => {
ranges.length &&
editorView.dispatch({
selection: EditorSelection.create(
ranges,
selections.codeBasedSelections.length - 1
),
})
})
return {
selectionRangeTypeMap,
}
}
export function setCodeMirrorCursor({
codeSelection,
currestSelections,
editorView,
isShiftDown,
}: {
codeSelection?: Selection
currestSelections: Selections
editorView: EditorView
isShiftDown: boolean
}): SelectionRangeTypeMap {
// This DOES NOT set the `selectionRanges` in xstate context
// instead it updates/dispatches to the editor, which in turn updates the xstate context
// I've found this the best way to deal with the editor without causing an infinite loop
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
// because we want to respect the user manually placing the cursor too.
const code = kclManager.code
if (!codeSelection) {
const selectionRangeTypeMap = dispatchCodeMirrorCursor({
editorView,
selections: {
otherSelections: currestSelections.otherSelections,
codeBasedSelections: [
{
range: [0, code.length ? code.length - 1 : 0],
type: 'default',
},
],
},
})
return selectionRangeTypeMap
}
const selections: Selections = {
...currestSelections,
codeBasedSelections: isShiftDown
? [...currestSelections.codeBasedSelections, codeSelection]
: [codeSelection],
}
const selectionRangeTypeMap = dispatchCodeMirrorCursor({
editorView,
selections,
})
return selectionRangeTypeMap
}

View File

@ -1390,7 +1390,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.33" version = "0.1.35"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-recursion", "async-recursion",
@ -1426,9 +1426,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.2.31" version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "539b323537b877fc8dd130362b8f1af9af8051c19208bb8bfd816ab7c330f2bb" checksum = "d341a81a4dfef43460d395c87d86c17e24affb96db0e7f4a35e8688f0e092344"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1733,7 +1733,7 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]] [[package]]
name = "openapitor" name = "openapitor"
version = "0.0.9" version = "0.0.9"
source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#c122a9b1d6afe51c25e545b5e0bbeb91d367e6d2" source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#7e087ecaee2fdfdbdbe8648e769213130f777c45"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -2047,9 +2047,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.67" version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2549,9 +2549,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.188" version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -2567,9 +2567,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.188" version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3077,9 +3077,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.32.0" version = "1.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",

View File

@ -11,7 +11,7 @@ crate-type = ["cdylib"]
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] } bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" } kcl-lib = { path = "kcl" }
kittycad = { version = "0.2.31", default-features = false, features = ["js"] } kittycad = { version = "0.2.33", default-features = false, features = ["js"] }
serde_json = "1.0.107" serde_json = "1.0.107"
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] } uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.87" wasm-bindgen = "0.2.87"
@ -20,10 +20,10 @@ wasm-bindgen-futures = "0.4.37"
[dev-dependencies] [dev-dependencies]
anyhow = "1" anyhow = "1"
image = "0.24.7" image = "0.24.7"
kittycad = "0.2.31" kittycad = "0.2.33"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
reqwest = { version = "0.11.22", default-features = false } reqwest = { version = "0.11.22", default-features = false }
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] } tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.6.1" twenty-twenty = "0.6.1"
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] } uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }

View File

@ -14,7 +14,7 @@ proc-macro = true
convert_case = "0.6.0" convert_case = "0.6.0"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.189", features = ["derive"] }
serde_tokenstream = "0.2" serde_tokenstream = "0.2"
syn = { version = "2.0.38", features = ["full"] } syn = { version = "2.0.38", features = ["full"] }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language" description = "KittyCAD Language"
version = "0.1.33" version = "0.1.35"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@ -15,11 +15,11 @@ clap = { version = "4.4.6", features = ["cargo", "derive", "env", "unicode"], op
dashmap = "5.5.3" dashmap = "5.5.3"
derive-docs = { version = "0.1.4" } derive-docs = { version = "0.1.4" }
#derive-docs = { path = "../derive-docs" } #derive-docs = { path = "../derive-docs" }
kittycad = { version = "0.2.31", default-features = false, features = ["js"] } kittycad = { version = "0.2.33", default-features = false, features = ["js"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
parse-display = "0.8.2" parse-display = "0.8.2"
schemars = { version = "0.8", features = ["impl_json_schema", "url", "uuid1"] } schemars = { version = "0.8", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107" serde_json = "1.0.107"
thiserror = "1.0.49" thiserror = "1.0.49"
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] } ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
@ -37,7 +37,7 @@ web-sys = { version = "0.3.64", features = ["console"] }
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] } bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
futures = { version = "0.3.28" } futures = { version = "0.3.28" }
reqwest = { version = "0.11.22", default-features = false } reqwest = { version = "0.11.22", default-features = false }
tokio = { version = "1.32.0", features = ["full"] } tokio = { version = "1.33.0", features = ["full"] }
tokio-tungstenite = { version = "0.20.0", features = ["rustls-tls-native-roots"] } tokio-tungstenite = { version = "0.20.0", features = ["rustls-tls-native-roots"] }
tower-lsp = { version = "0.20.0", features = ["proposed"] } tower-lsp = { version = "0.20.0", features = ["proposed"] }
@ -55,7 +55,7 @@ criterion = "0.5.1"
expectorate = "1.1.0" expectorate = "1.1.0"
itertools = "0.11.0" itertools = "0.11.0"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] } tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros", "time"] }
[[bench]] [[bench]]
name = "compiler_benchmark" name = "compiler_benchmark"

View File

@ -6,36 +6,30 @@ pub fn bench_lex(c: &mut Criterion) {
c.bench_function("lex_pipes_on_pipes", |b| b.iter(|| lex(PIPES_PROGRAM))); c.bench_function("lex_pipes_on_pipes", |b| b.iter(|| lex(PIPES_PROGRAM)));
} }
pub fn bench_lex_parse(c: &mut Criterion) { pub fn bench_parse(c: &mut Criterion) {
c.bench_function("parse_lex_cube", |b| b.iter(|| lex_and_parse(CUBE_PROGRAM))); for (name, file) in [
c.bench_function("parse_lex_big_kitt", |b| b.iter(|| lex_and_parse(KITT_PROGRAM))); ("pipes_on_pipes", PIPES_PROGRAM),
c.bench_function("parse_lex_pipes_on_pipes", |b| b.iter(|| lex_and_parse(PIPES_PROGRAM))); ("big_kitt", KITT_PROGRAM),
("cube", CUBE_PROGRAM),
] {
let tokens = kcl_lib::token::lexer(file);
c.bench_function(&format!("parse_{name}"), move |b| {
let tok = tokens.clone();
b.iter(move || {
let parser = kcl_lib::parser::Parser::new(tok.clone());
black_box(parser.ast().unwrap());
})
});
}
} }
fn lex(program: &str) { fn lex(program: &str) {
black_box(kcl_lib::token::lexer(program)); black_box(kcl_lib::token::lexer(program));
} }
fn lex_and_parse(program: &str) { criterion_group!(benches, bench_lex, bench_parse);
let tokens = kcl_lib::token::lexer(program);
let parser = kcl_lib::parser::Parser::new(tokens);
black_box(parser.ast().unwrap());
}
criterion_group!(benches, bench_lex, bench_lex_parse);
criterion_main!(benches); criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl"); const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl"); const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = r#"fn cube = (pos, scale) => { const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const sg = startSketchAt(pos)
|> line([0, scale], %)
|> line([scale, 0], %)
|> line([0, -scale], %)
return sg
}
const b1 = cube([0,0], 10)
const pt1 = b1[0]
show(b1)"#;

View File

@ -63,10 +63,14 @@ impl Program {
.fold(String::new(), |mut output, (index, recast_str)| { .fold(String::new(), |mut output, (index, recast_str)| {
let start_string = if index == 0 { let start_string = if index == 0 {
// We need to indent. // We need to indent.
if let Some(start) = self.non_code_meta.start.clone() { if self.non_code_meta.start.is_empty() {
start.format(&indentation)
} else {
indentation.to_string() indentation.to_string()
} else {
self.non_code_meta
.start
.iter()
.map(|start| start.format(&indentation))
.collect()
} }
} else { } else {
// Do nothing, we already applied the indentation elsewhere. // Do nothing, we already applied the indentation elsewhere.
@ -82,7 +86,10 @@ impl Program {
}; };
let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) { let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
Some(custom_white_space_or_comment) => custom_white_space_or_comment.format(&indentation), Some(noncodes) => noncodes
.iter()
.map(|custom_white_space_or_comment| custom_white_space_or_comment.format(&indentation))
.collect::<String>(),
None => String::new(), None => String::new(),
}; };
let end_string = if custom_white_space_or_comment.is_empty() { let end_string = if custom_white_space_or_comment.is_empty() {
@ -707,30 +714,35 @@ pub struct NonCodeNode {
impl NonCodeNode { impl NonCodeNode {
pub fn value(&self) -> String { pub fn value(&self) -> String {
match &self.value { match &self.value {
NonCodeValue::InlineComment { value } => value.clone(), NonCodeValue::InlineComment { value, style: _ } => value.clone(),
NonCodeValue::BlockComment { value } => value.clone(), NonCodeValue::BlockComment { value, style: _ } => value.clone(),
NonCodeValue::NewLineBlockComment { value } => value.clone(), NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
NonCodeValue::NewLine => "\n\n".to_string(), NonCodeValue::NewLine => "\n\n".to_string(),
} }
} }
pub fn format(&self, indentation: &str) -> String { pub fn format(&self, indentation: &str) -> String {
match &self.value { match &self.value {
NonCodeValue::InlineComment { value } => format!(" // {}\n", value), NonCodeValue::InlineComment {
NonCodeValue::BlockComment { value } => { value,
style: CommentStyle::Line,
} => format!(" // {}\n", value),
NonCodeValue::InlineComment {
value,
style: CommentStyle::Block,
} => format!(" /* {} */", value),
NonCodeValue::BlockComment { value, style } => {
let add_start_new_line = if self.start == 0 { "" } else { "\n" }; let add_start_new_line = if self.start == 0 { "" } else { "\n" };
if value.contains('\n') { match style {
format!("{}{}/* {} */\n", add_start_new_line, indentation, value) CommentStyle::Block => format!("{}{}/* {} */", add_start_new_line, indentation, value),
} else { CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
format!("{}{}// {}\n", add_start_new_line, indentation, value)
} }
} }
NonCodeValue::NewLineBlockComment { value } => { NonCodeValue::NewLineBlockComment { value, style } => {
let add_start_new_line = if self.start == 0 { "" } else { "\n\n" }; let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
if value.contains('\n') { match style {
format!("{}{}/* {} */\n", add_start_new_line, indentation, value) CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
} else { CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
format!("{}{}// {}\n", add_start_new_line, indentation, value)
} }
} }
NonCodeValue::NewLine => "\n\n".to_string(), NonCodeValue::NewLine => "\n\n".to_string(),
@ -738,14 +750,27 @@ impl NonCodeNode {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum CommentStyle {
/// Like // foo
Line,
/// Like /* foo */
Block,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum NonCodeValue { pub enum NonCodeValue {
/// An inline comment. /// An inline comment.
/// An example of this is the following: `1 + 1 // This is an inline comment`. /// Here are examples:
/// `1 + 1 // This is an inline comment`.
/// `1 + 1 /* Here's another */`.
InlineComment { InlineComment {
value: String, value: String,
style: CommentStyle,
}, },
/// A block comment. /// A block comment.
/// An example of this is the following: /// An example of this is the following:
@ -759,11 +784,13 @@ pub enum NonCodeValue {
/// If it did it would be a `NewLineBlockComment`. /// If it did it would be a `NewLineBlockComment`.
BlockComment { BlockComment {
value: String, value: String,
style: CommentStyle,
}, },
/// A block comment that has a new line above it. /// A block comment that has a new line above it.
/// The user explicitly added a new line above the block comment. /// The user explicitly added a new line above the block comment.
NewLineBlockComment { NewLineBlockComment {
value: String, value: String,
style: CommentStyle,
}, },
// A new line like `\n\n` NOT a new line like `\n`. // A new line like `\n\n` NOT a new line like `\n`.
// This is also not a comment. // This is also not a comment.
@ -774,8 +801,8 @@ pub enum NonCodeValue {
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NonCodeMeta { pub struct NonCodeMeta {
pub non_code_nodes: HashMap<usize, NonCodeNode>, pub non_code_nodes: HashMap<usize, Vec<NonCodeNode>>,
pub start: Option<NonCodeNode>, pub start: Vec<NonCodeNode>,
} }
// implement Deserialize manually because we to force the keys of non_code_nodes to be usize // implement Deserialize manually because we to force the keys of non_code_nodes to be usize
@ -788,15 +815,16 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct NonCodeMetaHelper { struct NonCodeMetaHelper {
non_code_nodes: HashMap<String, NonCodeNode>, non_code_nodes: HashMap<String, Vec<NonCodeNode>>,
start: Option<NonCodeNode>, start: Vec<NonCodeNode>,
} }
let helper = NonCodeMetaHelper::deserialize(deserializer)?; let helper = NonCodeMetaHelper::deserialize(deserializer)?;
let mut non_code_nodes = HashMap::new(); let non_code_nodes = helper
for (key, value) in helper.non_code_nodes { .non_code_nodes
non_code_nodes.insert(key.parse().map_err(serde::de::Error::custom)?, value); .into_iter()
} .map(|(key, value)| Ok((key.parse().map_err(serde::de::Error::custom)?, value)))
.collect::<Result<HashMap<_, _>, _>>()?;
Ok(NonCodeMeta { Ok(NonCodeMeta {
non_code_nodes, non_code_nodes,
start: helper.start, start: helper.start,
@ -804,6 +832,12 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
} }
} }
impl NonCodeMeta {
pub fn insert(&mut self, i: usize, new: NonCodeNode) {
self.non_code_nodes.entry(i).or_default().push(new);
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -2385,7 +2419,9 @@ impl PipeExpression {
let mut s = statement.recast(options, indentation_level + 1, true); let mut s = statement.recast(options, indentation_level + 1, true);
let non_code_meta = self.non_code_meta.clone(); let non_code_meta = self.non_code_meta.clone();
if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) { if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
s += non_code_meta_value.format(&indentation).trim_end_matches('\n') for val in non_code_meta_value {
s += val.format(&indentation).trim_end_matches('\n')
}
} }
if index != self.body.len() - 1 { if index != self.body.len() - 1 {
@ -2869,13 +2905,32 @@ show(part001)"#;
recasted, recasted,
r#"fn myFn = () => { r#"fn myFn = () => {
// this is a comment // this is a comment
const yo = { a: { b: { c: '123' } } } const yo = { a: { b: { c: '123' } } } /* block
/* block
comment */ comment */
const key = 'c' const key = 'c'
// this is also a comment // this is also a comment
return things return things
} }
"#
);
}
#[test]
fn test_recast_comment_at_start() {
let test_program = r#"
/* comment at start */
const mySk1 = startSketchAt([0, 0])"#;
let tokens = crate::token::lexer(test_program);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"/* comment at start */
const mySk1 = startSketchAt([0, 0])
"# "#
); );
} }
@ -2913,9 +2968,8 @@ const mySk1 = startSketchOn('XY')
|> lineTo({ to: [0, 1], tag: 'myTag' }, %) |> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|> lineTo([1, 1], %) |> lineTo([1, 1], %)
/* and /* and
here here */
// a comment between pipe expression statements
a comment between pipe expression statements */
|> rx(90, %) |> rx(90, %)
// and another with just white space between others below // and another with just white space between others below
|> ry(45, %) |> ry(45, %)
@ -2988,16 +3042,19 @@ const things = "things"
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0); let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted.trim(), some_program_string.trim()); let expected = some_program_string.trim();
// Currently new parser removes an empty line
let actual = recasted.trim();
assert_eq!(actual, expected);
} }
#[test] #[test]
fn test_recast_comment_tokens_inside_strings() { fn test_recast_comment_tokens_inside_strings() {
let some_program_string = r#"let b = { let some_program_string = r#"let b = {
"end": 141, end: 141,
"start": 125, start: 125,
"type": "NonCodeNode", type: "NonCodeNode",
"value": " value: "
// a comment // a comment
" "
}"#; }"#;

View File

@ -316,7 +316,17 @@ pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<
if let Some(array_val) = &o.array { if let Some(array_val) = &o.array {
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items { if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
// Let's print out the object's properties. // Let's print out the object's properties.
match array_val.max_items {
Some(val) => {
return Ok((
format!("[{}]", (0..val).map(|_| "number").collect::<Vec<_>>().join(", ")),
false,
));
}
None => {
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false)); return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
}
};
} else if let Some(items) = &array_val.contains { } else if let Some(items) = &array_val.contains {
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false)); return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
} }

View File

@ -4,7 +4,7 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::executor::SourceRange; use crate::executor::SourceRange;
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS)] #[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
#[ts(export)] #[ts(export)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum KclError { pub enum KclError {
@ -28,7 +28,7 @@ pub enum KclError {
Engine(KclErrorDetails), Engine(KclErrorDetails),
} }
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)] #[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
#[ts(export)] #[ts(export)]
pub struct KclErrorDetails { pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")] #[serde(rename = "sourceRanges")]
@ -78,6 +78,22 @@ impl KclError {
KclError::Engine(e) => e.source_ranges.clone(), KclError::Engine(e) => e.source_ranges.clone(),
} }
} }
/// Get the inner error message.
pub fn message(&self) -> &str {
match &self {
KclError::Syntax(e) => &e.message,
KclError::Semantic(e) => &e.message,
KclError::Type(e) => &e.message,
KclError::Unimplemented(e) => &e.message,
KclError::Unexpected(e) => &e.message,
KclError::ValueAlreadyDefined(e) => &e.message,
KclError::UndefinedValue(e) => &e.message,
KclError::InvalidExpression(e) => &e.message,
KclError::Engine(e) => &e.message,
}
}
pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic { pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
let (message, _, _) = self.get_message_line_column(code); let (message, _, _) = self.get_message_line_column(code);
let source_ranges = self.source_ranges(); let source_ranges = self.source_ranges();

View File

@ -2,7 +2,7 @@ use std::{collections::HashMap, str::FromStr};
use crate::{ use crate::{
ast::types::{ ast::types::{
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement, ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CommentStyle, ExpressionStatement,
FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject, NonCodeMeta, FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject, NonCodeMeta,
NonCodeNode, NonCodeValue, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression, PipeSubstitution, NonCodeNode, NonCodeValue, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression, PipeSubstitution,
Program, ReturnStatement, UnaryExpression, UnaryOperator, Value, VariableDeclaration, VariableDeclarator, Program, ReturnStatement, UnaryExpression, UnaryOperator, Value, VariableDeclaration, VariableDeclarator,
@ -13,6 +13,8 @@ use crate::{
token::{Token, TokenType}, token::{Token, TokenType},
}; };
mod parser_impl;
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%"; pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
pub const PIPE_OPERATOR: &str = "|>"; pub const PIPE_OPERATOR: &str = "|>";
@ -179,13 +181,19 @@ impl Parser {
Ok(token) Ok(token)
} }
/// Use the new Winnow parser.
pub fn ast(&self) -> Result<Program, KclError> { pub fn ast(&self) -> Result<Program, KclError> {
parser_impl::run_parser(&mut self.tokens.as_slice())
}
/// Use the old handwritten recursive parser.
pub fn ast_old(&self) -> Result<Program, KclError> {
let body = self.make_body( let body = self.make_body(
0, 0,
vec![], vec![],
NonCodeMeta { NonCodeMeta {
non_code_nodes: HashMap::new(), non_code_nodes: HashMap::new(),
start: None, start: Vec::new(),
}, },
)?; )?;
let end = match self.get_token(body.last_index) { let end = match self.get_token(body.last_index) {
@ -209,7 +217,7 @@ impl Parser {
}) })
} }
pub fn make_literal(&self, index: usize) -> Result<Literal, KclError> { fn make_literal(&self, index: usize) -> Result<Literal, KclError> {
let token = self.get_token(index)?; let token = self.get_token(index)?;
let value = if token.token_type == TokenType::Number { let value = if token.token_type == TokenType::Number {
if let Ok(value) = token.value.parse::<i64>() { if let Ok(value) = token.value.parse::<i64>() {
@ -295,6 +303,11 @@ impl Parser {
)); ));
} }
let is_block_style = non_code_tokens
.first()
.map(|tok| matches!(tok.token_type, TokenType::BlockComment))
.unwrap_or_default();
let full_string = non_code_tokens let full_string = non_code_tokens
.iter() .iter()
.map(|t| { .map(|t| {
@ -336,11 +349,32 @@ impl Parser {
value: if start_end_string.starts_with("\n\n") && is_new_line_comment { value: if start_end_string.starts_with("\n\n") && is_new_line_comment {
// Preserve if they want a whitespace line before the comment. // Preserve if they want a whitespace line before the comment.
// But let's just allow one. // But let's just allow one.
NonCodeValue::NewLineBlockComment { value: full_string } NonCodeValue::NewLineBlockComment {
} else if is_new_line_comment { value: full_string,
NonCodeValue::BlockComment { value: full_string } style: if is_block_style {
CommentStyle::Block
} else { } else {
NonCodeValue::InlineComment { value: full_string } CommentStyle::Line
},
}
} else if is_new_line_comment {
NonCodeValue::BlockComment {
value: full_string,
style: if is_block_style {
CommentStyle::Block
} else {
CommentStyle::Line
},
}
} else {
NonCodeValue::InlineComment {
value: full_string,
style: if is_block_style {
CommentStyle::Block
} else {
CommentStyle::Line
},
}
}, },
}; };
Ok((Some(node), end_index - 1)) Ok((Some(node), end_index - 1))
@ -1033,7 +1067,7 @@ impl Parser {
let non_code_meta = match previous_non_code_meta { let non_code_meta = match previous_non_code_meta {
Some(meta) => meta, Some(meta) => meta,
None => NonCodeMeta { None => NonCodeMeta {
start: None, start: Vec::new(),
non_code_nodes: HashMap::new(), non_code_nodes: HashMap::new(),
}, },
}; };
@ -1064,7 +1098,7 @@ impl Parser {
let mut _non_code_meta: NonCodeMeta; let mut _non_code_meta: NonCodeMeta;
if let Some(node) = next_pipe.non_code_node { if let Some(node) = next_pipe.non_code_node {
_non_code_meta = non_code_meta; _non_code_meta = non_code_meta;
_non_code_meta.non_code_nodes.insert(previous_values.len(), node); _non_code_meta.insert(previous_values.len(), node);
} else { } else {
_non_code_meta = non_code_meta; _non_code_meta = non_code_meta;
} }
@ -1435,7 +1469,7 @@ impl Parser {
self.make_params(next_brace_or_comma_token.index, _previous_params) self.make_params(next_brace_or_comma_token.index, _previous_params)
} }
pub fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> { fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
let current_token = self.get_token(index)?; let current_token = self.get_token(index)?;
let next_token = self.next_meaningful_token(index, None)?; let next_token = self.next_meaningful_token(index, None)?;
if next_token.token.is_none() { if next_token.token.is_none() {
@ -1631,9 +1665,11 @@ impl Parser {
let next_token = self.next_meaningful_token(token_index, Some(0))?; let next_token = self.next_meaningful_token(token_index, Some(0))?;
if let Some(node) = &next_token.non_code_node { if let Some(node) = &next_token.non_code_node {
if previous_body.is_empty() { if previous_body.is_empty() {
non_code_meta.start = next_token.non_code_node; if let Some(next) = next_token.non_code_node {
non_code_meta.start.push(next);
}
} else { } else {
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone()); non_code_meta.insert(previous_body.len(), node.clone());
} }
} }
return self.make_body(next_token.index, previous_body, non_code_meta); return self.make_body(next_token.index, previous_body, non_code_meta);
@ -1641,14 +1677,14 @@ impl Parser {
let next = self.next_meaningful_token(token_index, None)?; let next = self.next_meaningful_token(token_index, None)?;
if let Some(node) = &next.non_code_node { if let Some(node) = &next.non_code_node {
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone()); non_code_meta.insert(previous_body.len(), node.clone());
} }
if token.token_type == TokenType::Keyword && VariableKind::from_str(&token.value).is_ok() { if token.token_type == TokenType::Keyword && VariableKind::from_str(&token.value).is_ok() {
let declaration = self.make_variable_declaration(token_index)?; let declaration = self.make_variable_declaration(token_index)?;
let next_thing = self.next_meaningful_token(declaration.last_index, None)?; let next_thing = self.next_meaningful_token(declaration.last_index, None)?;
if let Some(node) = &next_thing.non_code_node { if let Some(node) = &next_thing.non_code_node {
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone()); non_code_meta.insert(previous_body.len(), node.clone());
} }
let mut _previous_body = previous_body; let mut _previous_body = previous_body;
_previous_body.push(BodyItem::VariableDeclaration(VariableDeclaration { _previous_body.push(BodyItem::VariableDeclaration(VariableDeclaration {
@ -1669,7 +1705,7 @@ impl Parser {
let statement = self.make_return_statement(token_index)?; let statement = self.make_return_statement(token_index)?;
let next_thing = self.next_meaningful_token(statement.last_index, None)?; let next_thing = self.next_meaningful_token(statement.last_index, None)?;
if let Some(node) = &next_thing.non_code_node { if let Some(node) = &next_thing.non_code_node {
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone()); non_code_meta.insert(previous_body.len(), node.clone());
} }
let mut _previous_body = previous_body; let mut _previous_body = previous_body;
_previous_body.push(BodyItem::ReturnStatement(ReturnStatement { _previous_body.push(BodyItem::ReturnStatement(ReturnStatement {
@ -1693,7 +1729,7 @@ impl Parser {
let expression = self.make_expression_statement(token_index)?; let expression = self.make_expression_statement(token_index)?;
let next_thing = self.next_meaningful_token(expression.last_index, None)?; let next_thing = self.next_meaningful_token(expression.last_index, None)?;
if let Some(node) = &next_thing.non_code_node { if let Some(node) = &next_thing.non_code_node {
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone()); non_code_meta.insert(previous_body.len(), node.clone());
} }
let mut _previous_body = previous_body; let mut _previous_body = previous_body;
_previous_body.push(BodyItem::ExpressionStatement(ExpressionStatement { _previous_body.push(BodyItem::ExpressionStatement(ExpressionStatement {
@ -1716,7 +1752,7 @@ impl Parser {
&& next_thing_token.token_type == TokenType::Operator && next_thing_token.token_type == TokenType::Operator
{ {
if let Some(node) = &next_thing.non_code_node { if let Some(node) = &next_thing.non_code_node {
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone()); non_code_meta.insert(previous_body.len(), node.clone());
} }
let expression = self.make_expression_statement(token_index)?; let expression = self.make_expression_statement(token_index)?;
let mut _previous_body = previous_body; let mut _previous_body = previous_body;
@ -1749,7 +1785,7 @@ impl Parser {
last_index: next_token_index, last_index: next_token_index,
non_code_meta: NonCodeMeta { non_code_meta: NonCodeMeta {
non_code_nodes: HashMap::new(), non_code_nodes: HashMap::new(),
start: None, start: Vec::new(),
}, },
} }
} else { } else {
@ -1758,7 +1794,7 @@ impl Parser {
vec![], vec![],
NonCodeMeta { NonCodeMeta {
non_code_nodes: HashMap::new(), non_code_nodes: HashMap::new(),
start: None, start: Vec::new(),
}, },
)? )?
}; };
@ -1913,6 +1949,7 @@ const key = 'c'"#,
end: 60, end: 60,
value: NonCodeValue::BlockComment { value: NonCodeValue::BlockComment {
value: "this is a comment".to_string(), value: "this is a comment".to_string(),
style: CommentStyle::Line,
}, },
}), }),
31, 31,
@ -1966,6 +2003,35 @@ const key = 'c'"#,
); );
} }
#[test]
fn test_math_parse() {
let tokens = crate::token::lexer(r#"5 + "a""#);
let actual = Parser::new(tokens).ast().unwrap().body;
let expr = BinaryExpression {
start: 0,
end: 7,
operator: BinaryOperator::Add,
left: BinaryPart::Literal(Box::new(Literal {
start: 0,
end: 1,
value: serde_json::Value::Number(serde_json::Number::from(5)),
raw: "5".to_owned(),
})),
right: BinaryPart::Literal(Box::new(Literal {
start: 4,
end: 7,
value: serde_json::Value::String("a".to_owned()),
raw: r#""a""#.to_owned(),
})),
};
let expected = vec![BodyItem::ExpressionStatement(ExpressionStatement {
start: 0,
end: 7,
expression: Value::BinaryExpression(Box::new(expr)),
})];
assert_eq!(expected, actual);
}
#[test] #[test]
fn test_is_code_token() { fn test_is_code_token() {
let tokens = [ let tokens = [
@ -2600,7 +2666,7 @@ show(mySk1)"#;
vec![], vec![],
NonCodeMeta { NonCodeMeta {
non_code_nodes: HashMap::new(), non_code_nodes: HashMap::new(),
start: None, start: Vec::new(),
}, },
) )
.unwrap(); .unwrap();
@ -2636,10 +2702,7 @@ show(mySk1)"#;
})), })),
})), })),
})], })],
non_code_meta: NonCodeMeta { non_code_meta: NonCodeMeta::default(),
non_code_nodes: Default::default(),
start: None,
},
}; };
assert_eq!(result, expected_result); assert_eq!(result, expected_result);
@ -2812,10 +2875,6 @@ z(-[["#,
let parser = Parser::new(tokens); let parser = Parser::new(tokens);
let result = parser.ast(); let result = parser.ast();
assert!(result.is_err()); assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([1, 2])], message: "missing a closing brace for the function call" }"#
);
} }
#[test] #[test]
@ -2831,7 +2890,7 @@ z(-[["#,
// https://github.com/KittyCAD/modeling-app/issues/696 // https://github.com/KittyCAD/modeling-app/issues/696
assert_eq!( assert_eq!(
result.err().unwrap().to_string(), result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [], message: "file is empty" }"# r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
); );
} }
@ -2845,7 +2904,7 @@ z(-[["#,
// https://github.com/KittyCAD/modeling-app/issues/696 // https://github.com/KittyCAD/modeling-app/issues/696
assert_eq!( assert_eq!(
result.err().unwrap().to_string(), result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [], message: "file is empty" }"# r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
); );
} }
@ -2863,7 +2922,7 @@ e
.err() .err()
.unwrap() .unwrap()
.to_string() .to_string()
.contains("expected to be started on a identifier or literal")); .contains("expected whitespace, found ')' which is brace"));
} }
#[test] #[test]
@ -2872,7 +2931,11 @@ e
let parser = Parser::new(tokens); let parser = Parser::new(tokens);
let result = parser.ast(); let result = parser.ast();
assert!(result.is_err()); assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("expected another token")); assert!(result
.err()
.unwrap()
.to_string()
.contains("expected whitespace, found ')' which is brace"));
} }
#[test] #[test]
@ -2884,11 +2947,7 @@ e
let parser = Parser::new(tokens); let parser = Parser::new(tokens);
let result = parser.ast(); let result = parser.ast();
assert!(result.is_err()); assert!(result.is_err());
assert!(result assert!(result.err().unwrap().to_string().contains("Unexpected token"));
.err()
.unwrap()
.to_string()
.contains("unexpected end of expression"));
} }
#[test] #[test]
@ -2985,10 +3044,7 @@ e
}], }],
kind: VariableKind::Const, kind: VariableKind::Const,
})], })],
non_code_meta: NonCodeMeta { non_code_meta: NonCodeMeta::default(),
non_code_nodes: Default::default(),
start: None,
},
}; };
assert_eq!(result, expected_result); assert_eq!(result, expected_result);
@ -3022,7 +3078,9 @@ e
#[test] #[test]
fn test_error_stdlib_in_fn_name() { fn test_error_stdlib_in_fn_name() {
let some_program_string = r#"fn cos = () {}"#; let some_program_string = r#"fn cos = () => {
return 1
}"#;
let tokens = crate::token::lexer(some_program_string); let tokens = crate::token::lexer(some_program_string);
let parser = Parser::new(tokens); let parser = Parser::new(tokens);
let result = parser.ast(); let result = parser.ast();
@ -3123,9 +3181,12 @@ thing(false)
let parser = Parser::new(tokens); let parser = Parser::new(tokens);
let result = parser.ast(); let result = parser.ast();
assert!(result.is_err()); assert!(result.is_err());
// TODO: https://github.com/KittyCAD/modeling-app/issues/784
// Improve this error message.
// It should say that the compiler is expecting a function expression on the RHS.
assert_eq!( assert_eq!(
result.err().unwrap().to_string(), result.err().unwrap().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 2])], message: "Expected a `let` variable kind, found: `fn`" }"# r#"syntax: KclErrorDetails { source_ranges: [SourceRange([11, 18])], message: "Unexpected token" }"#
); );
} }
@ -3163,15 +3224,6 @@ let other_thing = 2 * cos(3)"#;
parser.ast().unwrap(); parser.ast().unwrap();
} }
#[test]
fn test_parse_pipes_on_pipes() {
let code = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
let tokens = crate::token::lexer(code);
let parser = Parser::new(tokens);
parser.ast().unwrap();
}
#[test] #[test]
fn test_negative_arguments() { fn test_negative_arguments() {
let some_program_string = r#"fn box = (p, h, l, w) => { let some_program_string = r#"fn box = (p, h, l, w) => {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
use winnow::error::{ErrorKind, ParseError, StrContext};
use crate::{
errors::{KclError, KclErrorDetails},
token::Token,
};
/// Accumulate context while backtracking errors
/// Very similar to [`winnow::error::ContextError`] type,
/// but the 'cause' field is always a [`KclError`],
/// instead of a dynamic [`std::error::Error`] trait object.
#[derive(Debug, Clone)]
pub struct ContextError<C = StrContext> {
pub context: Vec<C>,
pub cause: Option<KclError>,
}
impl From<ParseError<&[Token], ContextError>> for KclError {
fn from(err: ParseError<&[Token], ContextError>) -> Self {
let Some(last_token) = err.input().last() else {
return KclError::Syntax(KclErrorDetails {
source_ranges: Default::default(),
message: "file is empty".to_owned(),
});
};
let (input, offset, err) = (err.input().to_vec(), err.offset(), err.into_inner());
if let Some(e) = err.cause {
return e;
}
// See docs on `offset`.
if offset >= input.len() {
let context = err.context.first();
return KclError::Syntax(KclErrorDetails {
source_ranges: last_token.as_source_ranges(),
message: match context {
Some(what) => format!("Unexpected end of file. The compiler {what}"),
None => "Unexpected end of file while still parsing".to_owned(),
},
});
}
let bad_token = &input[offset];
// TODO: Add the Winnow parser context to the error.
// See https://github.com/KittyCAD/modeling-app/issues/784
KclError::Syntax(KclErrorDetails {
source_ranges: bad_token.as_source_ranges(),
message: "Unexpected token".to_owned(),
})
}
}
impl<C> From<KclError> for ContextError<C> {
fn from(e: KclError) -> Self {
Self {
context: Default::default(),
cause: Some(e),
}
}
}
impl<C> std::default::Default for ContextError<C> {
fn default() -> Self {
Self {
context: Default::default(),
cause: None,
}
}
}
impl<I, C> winnow::error::ParserError<I> for ContextError<C> {
#[inline]
fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self {
Self::default()
}
#[inline]
fn append(self, _input: &I, _kind: ErrorKind) -> Self {
self
}
#[inline]
fn or(self, other: Self) -> Self {
other
}
}
impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> {
#[inline]
fn add_context(mut self, _input: &I, ctx: C) -> Self {
self.context.push(ctx);
self
}
}
impl<C, I> winnow::error::FromExternalError<I, KclError> for ContextError<C> {
#[inline]
fn from_external_error(_input: &I, _kind: ErrorKind, e: KclError) -> Self {
let mut err = Self::default();
{
err.cause = Some(e);
}
err
}
}

View File

@ -25,13 +25,26 @@ pub async fn extrude(args: Args) -> Result<MemoryItem, KclError> {
}] }]
async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args) -> Result<Box<ExtrudeGroup>, KclError> { async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args) -> Result<Box<ExtrudeGroup>, KclError> {
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
// Extrude the element.
let cmd = kittycad::types::ModelingCmd::Extrude { args.send_modeling_cmd(
id,
kittycad::types::ModelingCmd::Extrude {
target: sketch_group.id, target: sketch_group.id,
distance: length, distance: length,
cap: true, cap: true,
}; },
args.send_modeling_cmd(id, cmd).await?; )
.await?;
// Bring the object to the front of the scene.
// See: https://github.com/KittyCAD/modeling-app/issues/806
args.send_modeling_cmd(
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::ObjectBringToFront {
object_id: sketch_group.id,
},
)
.await?;
Ok(Box::new(ExtrudeGroup { Ok(Box::new(ExtrudeGroup {
id, id,

View File

@ -63,9 +63,10 @@ impl StdLib {
Box::new(crate::std::sketch::StartProfileAt), Box::new(crate::std::sketch::StartProfileAt),
Box::new(crate::std::sketch::Close), Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc), Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::TangentalArc), Box::new(crate::std::sketch::TangentialArc),
Box::new(crate::std::sketch::TangentalArcTo), Box::new(crate::std::sketch::TangentialArcTo),
Box::new(crate::std::sketch::BezierCurve), Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::sketch::Hole),
Box::new(crate::std::math::Cos), Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin), Box::new(crate::std::math::Sin),
Box::new(crate::std::math::Tan), Box::new(crate::std::math::Tan),
@ -230,6 +231,42 @@ impl Args {
Ok((segment_name, sketch_group)) Ok((segment_name, sketch_group))
} }
fn get_sketch_groups(&self) -> Result<(Box<SketchGroup>, Box<SketchGroup>), KclError> {
let first_value = self.args.first().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
let sketch_group = if let MemoryItem::SketchGroup(sg) = first_value {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
let second_sketch_group = if let MemoryItem::SketchGroup(sg) = second_value {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
Ok((sketch_group, second_sketch_group))
}
fn get_sketch_group(&self) -> Result<Box<SketchGroup>, KclError> { fn get_sketch_group(&self) -> Result<Box<SketchGroup>, KclError> {
let first_value = self.args.first().ok_or_else(|| { let first_value = self.args.first().ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {

View File

@ -1080,11 +1080,11 @@ async fn inner_arc(data: ArcData, sketch_group: Box<SketchGroup>, args: Args) ->
Ok(new_sketch_group) Ok(new_sketch_group)
} }
/// Data to draw a tangental arc. /// Data to draw a tangential arc.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase", untagged)] #[serde(rename_all = "camelCase", untagged)]
pub enum TangentalArcData { pub enum TangentialArcData {
RadiusAndOffset { RadiusAndOffset {
/// Radius of the arc. /// Radius of the arc.
/// Not to be confused with Raiders of the Lost Ark. /// Not to be confused with Raiders of the Lost Ark.
@ -1103,20 +1103,20 @@ pub enum TangentalArcData {
Point([f64; 2]), Point([f64; 2]),
} }
/// Draw a tangental arc. /// Draw a tangential arc.
pub async fn tangental_arc(args: Args) -> Result<MemoryItem, KclError> { pub async fn tangential_arc(args: Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (TangentalArcData, Box<SketchGroup>) = args.get_data_and_sketch_group()?; let (data, sketch_group): (TangentialArcData, Box<SketchGroup>) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_tangental_arc(data, sketch_group, args).await?; let new_sketch_group = inner_tangential_arc(data, sketch_group, args).await?;
Ok(MemoryItem::SketchGroup(new_sketch_group)) Ok(MemoryItem::SketchGroup(new_sketch_group))
} }
/// Draw an arc. /// Draw an arc.
#[stdlib { #[stdlib {
name = "tangentalArc", name = "tangentialArc",
}] }]
async fn inner_tangental_arc( async fn inner_tangential_arc(
data: TangentalArcData, data: TangentialArcData,
sketch_group: Box<SketchGroup>, sketch_group: Box<SketchGroup>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
@ -1125,7 +1125,7 @@ async fn inner_tangental_arc(
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
let to = match &data { let to = match &data {
TangentalArcData::RadiusAndOffset { radius, offset } => { TangentialArcData::RadiusAndOffset { radius, offset } => {
// Calculate the end point from the angle and radius. // Calculate the end point from the angle and radius.
let end_angle = Angle::from_degrees(*offset); let end_angle = Angle::from_degrees(*offset);
let start_angle = Angle::from_degrees(0.0); let start_angle = Angle::from_degrees(0.0);
@ -1147,7 +1147,7 @@ async fn inner_tangental_arc(
.await?; .await?;
to.into() to.into()
} }
TangentalArcData::PointWithTag { to, .. } => { TangentialArcData::PointWithTag { to, .. } => {
args.send_modeling_cmd( args.send_modeling_cmd(
id, id,
ModelingCmd::ExtendPath { ModelingCmd::ExtendPath {
@ -1166,7 +1166,7 @@ async fn inner_tangental_arc(
*to *to
} }
TangentalArcData::Point(to) => { TangentialArcData::Point(to) => {
args.send_modeling_cmd( args.send_modeling_cmd(
id, id,
ModelingCmd::ExtendPath { ModelingCmd::ExtendPath {
@ -1207,11 +1207,11 @@ async fn inner_tangental_arc(
Ok(new_sketch_group) Ok(new_sketch_group)
} }
/// Data to draw a tangental arc to a specific point. /// Data to draw a tangential arc to a specific point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase", untagged)] #[serde(rename_all = "camelCase", untagged)]
pub enum TangentalArcToData { pub enum TangentialArcToData {
/// A point with a tag. /// A point with a tag.
PointWithTag { PointWithTag {
/// Where the arc should end. Must lie in the same plane as the current path pen position. Must not be colinear with current path pen position. /// Where the arc should end. Must lie in the same plane as the current path pen position. Must not be colinear with current path pen position.
@ -1223,27 +1223,27 @@ pub enum TangentalArcToData {
Point([f64; 2]), Point([f64; 2]),
} }
/// Draw a tangental arc to a specific point. /// Draw a tangential arc to a specific point.
pub async fn tangental_arc_to(args: Args) -> Result<MemoryItem, KclError> { pub async fn tangential_arc_to(args: Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (TangentalArcToData, Box<SketchGroup>) = args.get_data_and_sketch_group()?; let (data, sketch_group): (TangentialArcToData, Box<SketchGroup>) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_tangental_arc_to(data, sketch_group, args).await?; let new_sketch_group = inner_tangential_arc_to(data, sketch_group, args).await?;
Ok(MemoryItem::SketchGroup(new_sketch_group)) Ok(MemoryItem::SketchGroup(new_sketch_group))
} }
/// Draw an arc. /// Draw an arc.
#[stdlib { #[stdlib {
name = "tangentalArcTo", name = "tangentialArcTo",
}] }]
async fn inner_tangental_arc_to( async fn inner_tangential_arc_to(
data: TangentalArcToData, data: TangentialArcToData,
sketch_group: Box<SketchGroup>, sketch_group: Box<SketchGroup>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from: Point2d = sketch_group.get_coords_from_paths()?; let from: Point2d = sketch_group.get_coords_from_paths()?;
let to = match &data { let to = match &data {
TangentalArcToData::PointWithTag { to, .. } => to, TangentialArcToData::PointWithTag { to, .. } => to,
TangentalArcToData::Point(to) => to, TangentialArcToData::Point(to) => to,
}; };
let delta = [to[0] - from.x, to[1] - from.y]; let delta = [to[0] - from.x, to[1] - from.y];
@ -1270,7 +1270,7 @@ async fn inner_tangental_arc_to(
base: BasePath { base: BasePath {
from: from.into(), from: from.into(),
to: *to, to: *to,
name: if let TangentalArcToData::PointWithTag { tag, .. } = data { name: if let TangentialArcToData::PointWithTag { tag, .. } = data {
tag.to_string() tag.to_string()
} else { } else {
"".to_string() "".to_string()
@ -1395,6 +1395,50 @@ async fn inner_bezier_curve(
Ok(new_sketch_group) Ok(new_sketch_group)
} }
/// Use a sketch to cut a hole in another sketch.
pub async fn hole(args: Args) -> Result<MemoryItem, KclError> {
let (hole_sketch_group, sketch_group): (Box<SketchGroup>, Box<SketchGroup>) = args.get_sketch_groups()?;
let new_sketch_group = inner_hole(hole_sketch_group, sketch_group, args).await?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Use a sketch to cut a hole in another sketch.
#[stdlib {
name = "hole",
}]
async fn inner_hole(
hole_sketch_group: Box<SketchGroup>,
sketch_group: Box<SketchGroup>,
args: Args,
) -> Result<Box<SketchGroup>, KclError> {
//TODO: batch these (once we have batch)
args.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid2DAddHole {
object_id: sketch_group.id,
hole_id: hole_sketch_group.id,
},
)
.await?;
//suggestion (mike)
//we also hide the source hole since its essentially "consumed" by this operation
args.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::ObjectVisible {
object_id: hole_sketch_group.id,
hidden: true,
},
)
.await?;
// TODO: should we modify the sketch group to include the hole data, probably?
Ok(sketch_group)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -6,6 +6,8 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::SemanticTokenType; use tower_lsp::lsp_types::SemanticTokenType;
use crate::{ast::types::VariableKind, executor::SourceRange};
mod tokeniser; mod tokeniser;
/// The types of tokens. /// The types of tokens.
@ -142,15 +144,39 @@ impl Token {
TokenType::Whitespace | TokenType::LineComment | TokenType::BlockComment TokenType::Whitespace | TokenType::LineComment | TokenType::BlockComment
) )
} }
pub fn as_source_range(&self) -> SourceRange {
SourceRange([self.start, self.end])
} }
impl From<Token> for crate::executor::SourceRange { pub fn as_source_ranges(&self) -> Vec<SourceRange> {
vec![self.as_source_range()]
}
/// Is this token the beginning of a variable/function declaration?
/// If so, what kind?
/// If not, returns None.
pub fn declaration_keyword(&self) -> Option<VariableKind> {
if !matches!(self.token_type, TokenType::Keyword) {
return None;
}
Some(match self.value.as_str() {
"var" => VariableKind::Var,
"let" => VariableKind::Let,
"fn" => VariableKind::Fn,
"const" => VariableKind::Const,
_ => return None,
})
}
}
impl From<Token> for SourceRange {
fn from(token: Token) -> Self { fn from(token: Token) -> Self {
Self([token.start, token.end]) Self([token.start, token.end])
} }
} }
impl From<&Token> for crate::executor::SourceRange { impl From<&Token> for SourceRange {
fn from(token: &Token) -> Self { fn from(token: &Token) -> Self {
Self([token.start, token.end]) Self([token.start, token.end])
} }

View File

@ -0,0 +1,12 @@
fn cube = (pos, scale) => {
const sg = startSketchAt(pos)
|> line([0, scale], %)
|> line([scale, 0], %)
|> line([0, -scale], %)
return sg
}
const b1 = cube([0,0], 10)
const pt1 = b1[0]
show(b1)

View File

@ -87,7 +87,7 @@ const fnBox = box(3, 6, 10)
show(fnBox)"#; show(fnBox)"#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -107,7 +107,11 @@ async fn serial_test_execute_with_function_sketch_with_position() {
show(box([0,0], 3, 6, 10))"#; show(box([0,0], 3, 6, 10))"#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/function_sketch_with_position.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/function_sketch_with_position.png",
&result,
0.999,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -125,7 +129,7 @@ async fn serial_test_execute_with_angled_line() {
show(part001)"#; show(part001)"#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -152,7 +156,7 @@ const bracket = startSketchOn('XY')
show(bracket)"#; show(bracket)"#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -169,14 +173,14 @@ const wallMountL = 8
const bracket = startSketchAt([0, 0]) const bracket = startSketchAt([0, 0])
|> line([0, wallMountL], %) |> line([0, wallMountL], %)
|> tangentalArc({ |> tangentialArc({
radius: filletR, radius: filletR,
offset: 90 offset: 90
}, %) }, %)
|> line([-shelfMountL, 0], %) |> line([-shelfMountL, 0], %)
|> line([0, -thickness], %) |> line([0, -thickness], %)
|> line([shelfMountL, 0], %) |> line([shelfMountL, 0], %)
|> tangentalArc({ |> tangentialArc({
radius: filletR - thickness, radius: filletR - thickness,
offset: -90 offset: -90
}, %) }, %)
@ -187,7 +191,7 @@ const bracket = startSketchAt([0, 0])
show(bracket)"#; show(bracket)"#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -215,7 +219,7 @@ async fn serial_test_execute_pipes_on_pipes() {
let code = include_str!("inputs/pipes_on_pipes.kcl"); let code = include_str!("inputs/pipes_on_pipes.kcl");
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -223,11 +227,11 @@ async fn serial_test_execute_kittycad_svg() {
let code = include_str!("inputs/kittycad_svg.kcl"); let code = include_str!("inputs/kittycad_svg.kcl");
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_member_expression_sketch_group() { async fn serial_test_member_expression_sketch_group() {
let code = r#"fn cube = (pos, scale) => { let code = r#"fn cube = (pos, scale) => {
const sg = startSketchOn('XY') const sg = startSketchOn('XY')
|> startProfileAt(pos, %) |> startProfileAt(pos, %)
@ -256,7 +260,7 @@ show(b2)"#;
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_close_arc() { async fn serial_test_close_arc() {
let code = r#"const center = [0,0] let code = r#"const center = [0,0]
const radius = 40 const radius = 40
const height = 3 const height = 3
@ -270,11 +274,11 @@ const body = startSketchOn('XY')
show(body)"#; show(body)"#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_negative_args() { async fn serial_test_negative_args() {
let code = r#"const width = 5 let code = r#"const width = 5
const height = 10 const height = 10
const length = 12 const length = 12
@ -296,50 +300,50 @@ let thing = box(-12, -15, 10)
box(-20, -5, 10)"#; box(-20, -5, 10)"#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_basic_tangental_arc() { async fn serial_test_basic_tangential_arc() {
let code = r#"const boxSketch = startSketchAt([0, 0]) let code = r#"const boxSketch = startSketchAt([0, 0])
|> line([0, 10], %) |> line([0, 10], %)
|> tangentalArc({radius: 5, offset: 90}, %) |> tangentialArc({radius: 5, offset: 90}, %)
|> line([5, -15], %) |> line([5, -15], %)
|> extrude(10, %) |> extrude(10, %)
"#; "#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/tangential_arc.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_basic_tangental_arc_with_point() { async fn serial_test_basic_tangential_arc_with_point() {
let code = r#"const boxSketch = startSketchAt([0, 0]) let code = r#"const boxSketch = startSketchAt([0, 0])
|> line([0, 10], %) |> line([0, 10], %)
|> tangentalArc([-5, 5], %) |> tangentialArc([-5, 5], %)
|> line([5, -15], %) |> line([5, -15], %)
|> extrude(10, %) |> extrude(10, %)
"#; "#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc_with_point.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_with_point.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_basic_tangental_arc_to() { async fn serial_test_basic_tangential_arc_to() {
let code = r#"const boxSketch = startSketchAt([0, 0]) let code = r#"const boxSketch = startSketchAt([0, 0])
|> line([0, 10], %) |> line([0, 10], %)
|> tangentalArcTo([-5, 15], %) |> tangentialArcTo([-5, 15], %)
|> line([5, -15], %) |> line([5, -15], %)
|> extrude(10, %) |> extrude(10, %)
"#; "#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc_to.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_to.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_different_planes_same_drawing() { async fn serial_test_different_planes_same_drawing() {
let code = r#"const width = 5 let code = r#"const width = 5
const height = 10 const height = 10
const length = 12 const length = 12
@ -362,11 +366,15 @@ let thing = box(-12, -15, 10, 'yz')
box(-20, -5, 10, 'xy')"#; box(-20, -5, 10, 'xy')"#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/different_planes_same_drawing.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/different_planes_same_drawing.png",
&result,
0.999,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_lots_of_planes() { async fn serial_test_lots_of_planes() {
let code = r#"const sigmaAllow = 15000 // psi let code = r#"const sigmaAllow = 15000 // psi
const width = 11 // inch const width = 11 // inch
const p = 150 // Force on shelf - lbs const p = 150 // Force on shelf - lbs
@ -380,11 +388,11 @@ const wallMountL = 8
const bracket = startSketchOn('XY') const bracket = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([0, wallMountL], %) |> line([0, wallMountL], %)
|> tangentalArc({ radius: filletR, offset: 90 }, %) |> tangentialArc({ radius: filletR, offset: 90 }, %)
|> line([-shelfMountL, 0], %) |> line([-shelfMountL, 0], %)
|> line([0, -thickness], %) |> line([0, -thickness], %)
|> line([shelfMountL, 0], %) |> line([shelfMountL, 0], %)
|> tangentalArc({ |> tangentialArc({
radius: filletR - thickness, radius: filletR - thickness,
offset: -90 offset: -90
}, %) }, %)
@ -421,5 +429,78 @@ const part004 = startSketchOn('YZ')
"#; "#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_holes() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt(pos, %)
|> arc({angle_end: 360, angle_start: 0, radius: radius}, %)
|> close(%)
return sg
}
const square = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> close(%)
|> hole(circle([2, 2], .5), %)
|> hole(circle([2, 8], .5), %)
|> extrude(2, %)
show(square)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/holes.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_rounded_with_holes() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
fn roundedRectangle = (pos, w, l, cornerRadius) => {
const rr = startSketchOn('XY')
|> startProfileAt([pos[0] - w/2, 0], %)
|> lineTo([pos[0] - w/2, pos[1] - l/2 + cornerRadius], %)
|> tangentialArcTo([pos[0] - w/2 + cornerRadius, pos[1] - l/2], %)
|> lineTo([pos[0] + w/2 - cornerRadius, pos[1] - l/2], %)
|> tangentialArcTo([pos[0] + w/2, pos[1] - l/2 + cornerRadius], %)
|> lineTo([pos[0] + w/2, pos[1] + l/2 - cornerRadius], %)
|> tangentialArcTo([pos[0] + w/2 - cornerRadius, pos[1] + l/2], %)
|> lineTo([pos[0] - w/2 + cornerRadius, pos[1] + l/2], %)
|> tangentialArcTo([pos[0] - w/2, pos[1] + l/2 - cornerRadius], %)
|> close(%)
return rr
}
const holeRadius = 1
const holeIndex = 6
const part = roundedRectangle([0, 0], 20, 20, 4)
|> hole(circle([-holeIndex, holeIndex], holeRadius), %)
|> hole(circle([holeIndex, holeIndex], holeRadius), %)
|> hole(circle([-holeIndex, -holeIndex], holeRadius), %)
|> hole(circle([holeIndex, -holeIndex], holeRadius), %)
|> extrude(2, %)
show(part)"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/rounded_with_holes.png", &result, 0.999);
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

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