Compare commits

...

52 Commits

Author SHA1 Message Date
caddac5059 Bump to v0.7.1 (#548) 2023-09-15 15:43:48 -04:00
54751aa7bb Fix rtc_freeze_count error (#544) 2023-09-15 12:05:46 -04:00
7b7d5e5f5e Fix extrude w/o pipe (#542) 2023-09-15 11:51:39 -04:00
f7971bddef Rename TooTip to ToolTip (#541) 2023-09-15 11:48:23 -04:00
e4f2e66029 inital rework of execution (#528)
* inital rework

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

* updates

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

* update the program memory as well

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

* cleanups

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

* code

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

* updates for typing code

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

* fixing

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

* some fixes

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

* more fixes

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

* Only unselect line or move tool on escape, don't exit sketch

* Make scrollbar on toolbar smaller

* Add escape to exit sketch mode

* tidy up usestore

* clear scene on empty file

* disable sketch mode and re-execute on sketch loop close

* disable all but xy plane

* fix entering back into edit mode

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2023-09-15 21:35:48 +10:00
663c396128 Add link to KCL docs to CodeMenu (#526)
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-15 00:07:11 +00:00
8db86a6783 Fix sketch mode planes visibility on enter, exit, enter (#527)
* Fix sketch mode planes visibility on enter, exit, enter

* Fix tsc

* Rename to something that makes more sense
2023-09-15 10:03:06 +10:00
d7ad7c749e Franknoirot/fix prod tauri auth (#531)
* Invoke tauri-based logout if in tauri

* Add kittycad Rust library

* Add logout and get_user Tauri commands

* Invoke get_user instead of fetching in Tauri

* @jessfraz review

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

* Remove unnecessary logout command

* Fix rushed last commit

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-09-14 19:31:16 -04:00
6e3c642d22 add expectations to modeling repo (#530) 2023-09-14 15:41:38 -07:00
4d7433ff3a Nicer geometry types (#522)
* Angle type

* Use Point2d

* use angle in more places

* Fix doctests

* Use angle in more places

* Import pi
2023-09-14 14:51:26 -07:00
4e93146559 Remove old deps (#529)
Updates chrono to remove time 0.1
Updates openapitor to remove clap 3
2023-09-14 15:05:07 -06:00
731a9bfbdb Enable devtools on Tauri builds (#525) 2023-09-14 10:25:17 -04:00
cdb4c36cf5 addNewSketchLn should close when latest point matches start (#479)
* addNewSketchLn should close when latest point matches start

* Fix types

* Include close in test case

* Add handling for continuing to sketch

* Fix types again

* close line edits (#523)

* add close to pipe

* undo some previous changes

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-14 09:34:37 -04:00
66ba60dc8e Bump to v0.7.0 (#524) 2023-09-14 07:35:08 -04:00
8fcc8cdd17 Change updater from .nsis to .msi, add EV Code Sign to it (#435)
* Change updater to msi over nsis, more common to sign

* Add sign-windows-msi from docs

* Add secrets and enable step

* Update ci.yml

* Add todo and fix download.json
2023-09-14 07:04:11 -04:00
bba9bdc563 Add logomark, update README (#412) 2023-09-14 05:16:24 +00:00
760a180f56 Convert to radians/degrees using Rust methods (#516)
use to_radians and to_degrees
2023-09-13 22:25:41 -06:00
0eeff8cb45 Add @replit/codemirror-interact (#500)
* Add @replit/codemirror-interact

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

* Add support for Alt + Meta + Shift for 0.01 increment

* Remove pixelsPerIncrement
This doesn't work because we have to rely on movementX, which means
only mouse movements of 3px or greater register while dragging. Although
I would eventually like to control the screen speed of incrementing,
I'd rather have consistent behavior for now.

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

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-14 04:03:51 +00:00
3c76721159 Fix id source range mapping with path_get_info (#517) 2023-09-14 13:49:59 +10:00
6ac79ae645 Move app.tsx effects (#513)
* move useSetupEngineManager

* move code eval

* add comment
2023-09-14 10:47:55 +10:00
90d7c33c92 add roadmap md file (#514) 2023-09-13 17:35:31 -07:00
e02bc76bdb add ast explorer (#460)
* add crude ast explorer

* tsc
2023-09-14 08:51:23 +10:00
0466f04d82 add sin cos tan to stdlib and make sure you cant redeclare a stdlib fn (#497)
more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 15:09:07 -07:00
f8ed830b60 Bump tauri from 1.3.0 to 1.4.1 in /src-tauri (#490)
Bumps [tauri](https://github.com/tauri-apps/tauri) from 1.3.0 to 1.4.1.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v1.3...tauri-v1.4.1)

---
updated-dependencies:
- dependency-name: tauri
  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-09-13 14:15:46 -07:00
b7ca91bf6d Bump oauth2 from 4.4.1 to 4.4.2 in /src-tauri (#489)
Bumps [oauth2](https://github.com/ramosbugs/oauth2-rs) from 4.4.1 to 4.4.2.
- [Release notes](https://github.com/ramosbugs/oauth2-rs/releases)
- [Commits](https://github.com/ramosbugs/oauth2-rs/compare/4.4.1...4.4.2)

---
updated-dependencies:
- dependency-name: oauth2
  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-09-13 13:46:17 -07:00
2261f92b0b Bump anyhow from 1.0.71 to 1.0.75 in /src-tauri (#488)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.71 to 1.0.75.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.71...1.0.75)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 13:46:07 -07:00
bbe9e621b1 Bump toml from 0.7.3 to 0.8.0 in /src-tauri (#493)
Bumps [toml](https://github.com/toml-rs/toml) from 0.7.3 to 0.8.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.7.3...toml-v0.8.0)

---
updated-dependencies:
- dependency-name: toml
  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-09-13 13:45:59 -07:00
bf087d760b Bump thiserror from 1.0.47 to 1.0.48 in /src/wasm-lib (#375)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.47 to 1.0.48.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.47...1.0.48)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 13:45:49 -07:00
a4353c63fd Bump tauri-plugin-fs-extra from 7e58dc8 to 5b814f5 in /src-tauri (#491)
Bumps [tauri-plugin-fs-extra](https://github.com/tauri-apps/plugins-workspace) from `7e58dc8` to `5b814f5`.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](7e58dc8502...5b814f56e6)

---
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-09-13 13:45:36 -07:00
c438d11c3d make .4 parse as number (#492)
updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 13:10:55 -07:00
43284e33c8 Bump toml from 0.6.0 to 0.7.3 in /src-tauri (#277)
Bumps [toml](https://github.com/toml-rs/toml) from 0.6.0 to 0.7.3.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.6.0...toml-v0.7.3)

---
updated-dependencies:
- dependency-name: toml
  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-09-13 12:50:52 -07:00
77dce7f0dd Bump syn from 2.0.32 to 2.0.33 in /src/wasm-lib (#485)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.32 to 2.0.33.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.32...2.0.33)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 12:50:39 -07:00
d559862051 Bump proc-macro2 from 1.0.66 to 1.0.67 in /src/wasm-lib (#486)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.66 to 1.0.67.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.66...1.0.67)

---
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>
2023-09-13 12:50:22 -07:00
7382ed87ba Bump tokio from 1.29.1 to 1.32.0 in /src-tauri (#280)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.29.1 to 1.32.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.29.1...tokio-1.32.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-09-13 12:23:56 -07:00
3324ed31de Bump serde_json from 1.0.96 to 1.0.106 in /src-tauri (#429)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.96 to 1.0.106.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.96...v1.0.106)

---
updated-dependencies:
- dependency-name: serde_json
  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-09-13 12:23:43 -07:00
ba9dbc2205 Bump rustls-webpki from 0.101.1 to 0.101.5 in /src-tauri (#449)
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.101.1 to 0.101.5.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.101.1...v/0.101.5)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 12:23:32 -07:00
b0028d4874 add basic docs for lang things (#484)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 11:59:21 -07:00
9e6be9651c Bump webpki from 0.22.0 to 0.22.1 in /src-tauri (#399)
Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.0 to 0.22.1.
- [Commits](https://github.com/briansmith/webpki/commits)

---
updated-dependencies:
- dependency-name: webpki
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 11:54:34 -07:00
b145ab0106 Bump clap from 4.4.2 to 4.4.3 in /src/wasm-lib (#459)
Bumps [clap](https://github.com/clap-rs/clap) from 4.4.2 to 4.4.3.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.4.2...v4.4.3)

---
updated-dependencies:
- dependency-name: clap
  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-09-13 11:54:13 -07:00
84e0fbb70f Bump serde from 1.0.186 to 1.0.188 in /src/wasm-lib (#330)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.186 to 1.0.188.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.186...v1.0.188)

---
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-09-13 11:54:02 -07:00
990605bbea fn required and can only be used for fn not var (#483)
* fixups

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

* fix javascript tests

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

* fix clippy

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 11:42:09 -07:00
d075c4ad13 Bump syn from 2.0.29 to 2.0.32 in /src/wasm-lib (#427)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.29 to 2.0.32.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.29...2.0.32)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 11:12:18 -07:00
a3f41f5519 Bump serde_json from 1.0.105 to 1.0.106 in /src/wasm-lib (#428)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.105 to 1.0.106.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.105...v1.0.106)

---
updated-dependencies:
- dependency-name: serde_json
  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-09-13 11:00:50 -07:00
cb173e2850 Error on keywords used in wrong way (#481)
* error if someone is using keyword where they should not

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

* updates

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

* cleanup

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 10:51:03 -07:00
87cd3b67f4 allow ranges in arrays to work (#476)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 10:03:28 -07:00
fe3ee3806e Fix array expressions (#473)
* fix array expressions

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

* fix-tests

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 09:23:24 -07:00
c9ed6c724c Fix path for starting second sketch (#474) 2023-09-13 11:49:19 -04:00
a5fa259d55 fixes obj.thing stuff and member expressions and bugs (#461)
* add failing tests for loops

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

* fix obj["thing"] and obj.thing complex cases

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

* fix clippy

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

* remove println

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

* fix test

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

* fixes more tests

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

* fixups

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

* fixups

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

* fix tests

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

* add more tests

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

* more test fixes

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

* fixes

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

* last test fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 07:23:14 -07:00
33822b5a19 Bump to v0.6.1 (#465) 2023-09-13 06:33:11 -04:00
a2a4daebe3 fix move cmd order (#462) 2023-09-13 07:42:42 +00:00
a17ede50bd Build endpoint for download page on website (#451) 2023-09-13 03:03:13 +00:00
2d452f80d1 ts-rs changes (#450)
* initial changes

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

* updates

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

* fix tests

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

* fixes

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

* updates

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

* bust cache

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

* add dumb shit

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

* Revert "add dumb shit"

This reverts commit 638e9cf72f75e1ad08fb6b22d2a7b143ab7e06e5.

* Revert "bust cache"

This reverts commit fd6f53ba0757d635190aa82d4b055a83755f3027.

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 11:10:27 +10:00
76 changed files with 4047 additions and 1412 deletions

View File

@ -1,6 +1,6 @@
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN=

View File

@ -147,8 +147,8 @@ jobs:
path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
publish-apps-release:
runs-on: ubuntu-20.04
sign-windows-msi:
runs-on: windows-latest
if: github.event_name == 'release'
needs: [build-test-web, build-apps]
env:
@ -157,23 +157,80 @@ jobs:
- uses: actions/download-artifact@v3
- name: Setup Certificate
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
shell: bash
- name: Set variables
id: variables
run: |
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup SSM KSP on windows latest
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Signing using Signtool
run: |
signtool.exe sign /sha1 ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "artifact\msi\*.msi"
signtool.exe verify /v /pa "artifact\msi\*.msi"
# TODO: for the updater, investigate if we need to also replace what's in the .zip, and what to do about the .sig file
- uses: actions/upload-artifact@v3
with:
path: artifact/*
publish-apps-release:
runs-on: ubuntu-20.04
if: github.event_name == 'release'
needs: [build-test-web, build-apps, sign-windows-msi]
env:
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
PUB_DATE: ${{ github.event.release.created_at }}
NOTES: ${{ github.event.release.body }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact/*/*itty*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
WINDOWS_SIG=`cat artifact/nsis/*.nsis.zip.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
jq --null-input \
--arg version "v${VERSION_NO_V}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
--arg linux_sig "$LINUX_SIG" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/nsis/KittyCAD%20Modeling_${VERSION_NO_V}_x64-setup.nsis.zip" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $darwin_sig,
@ -195,6 +252,34 @@ jobs:
}' > last_update.json
cat last_update.json
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
jq --null-input \
--arg version "v${VERSION_NO_V}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
},
"appimage-x86_64": {
"url": $linux_url
},
"msi-x86_64": {
"url": $windows_url
}
}
}' > last_download.json
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v1.1.1'
with:
@ -219,6 +304,12 @@ jobs:
path: last_update.json
destination: dl.kittycad.io/releases/modeling-app
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: last_download.json
destination: dl.kittycad.io/releases/modeling-app
- name: Upload release files to Github
uses: softprops/action-gh-release@v1
with:

100
README.md
View File

@ -1,48 +1,72 @@
## Kurt demo project
![KittyCAD Modeling App](/public/kcma-logomark.png)
## KittyCAD Modeling App
live at [app.kittycad.io](https://app.kittycad.io/)
Not sure what to call this, it's both a language/interpreter and a UI that uses the language as the source of truth model the user build with direct-manipulation with the UI.
A CAD application from the future, brought to you by the [KittyCAD team](https://kittycad.io).
It might make sense to split this repo up at some point, but not the lang and the UI are all togther in a react app
The KittyCAD modeling app is our take on what a modern modelling experience can be. It is applying several lessons learned in the decades since most major CAD tools came into existence:
Originally Presented on 10/01/2023
- All artifacts—including parts and assemblies—should be represented as human-readable code. At the end of the day, your CAD project should be "plain text"
- This makes version control—which is a solved problem in software engineering—trivial for CAD
- All GUI (or point-and-click) interactions should be actions performed on this code representation under the hood
- This unlocks a hybrid approach to modeling. Whether you point-and-click as you always have or you write your own KCL code, you are performing the same action in KittyCAD Modeling App
- Everything graphics _has_ to be built for the GPU
- Most CAD applications have had to retrofit support for GPUs, but our geometry engine is made for GPUs (primarily Nvidia's Vulkan), getting the order of magnitude rendering performance boost with it
- Make the resource-intensive pieces of an application auto-scaling
- One of the bottlenecks of today's hardware design tools is that they all rely on the local machine's resources to do the hardest parts, which include geometry rendering and analysis. Our geometry engine parallelizes rendering and just sends video frames back to the app (seriously, inspect source, it's just a `<video>` element), and our API will offload analysis as we build it in
[Video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
We are excited about what a small team of people could build in a short time with our API. We welcome you to try our API, build your own applications, or contribute to ours!
[demo-slides.pdf](https://github.com/KittyCAD/Eng/files/10398178/demo.pdf)
KittyCAD Modeling App is a _hybrid_ user interface for CAD modeling. You can point-and-click to design parts (and soon assemblies), but everything you make is really just [`kcl` code](https://github.com/KittyCAD/kcl-experiments) under the hood. All of your CAD models can be checked into source control such as GitHub and responsibly versioned, rolled back, and more.
## To run, there are a couple steps since we're compiling rust to WASM, you'll need to have rust stuff installed, then
The 3D view in KittyCAD Modeling App is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine.
## Tools
- UI
- [React](https://react.dev/)
- [Headless UI](https://headlessui.com/)
- [TailwindCSS](https://tailwindcss.com/)
- Networking
- WebSockets (via [KittyCAD TS client](https://github.com/KittyCAD/kittycad.ts))
- Code Editor
- [CodeMirror](https://codemirror.net/)
- Custom WASM LSP Server
- Modeling
- [KittyCAD TypeScript client](https://github.com/KittyCAD/kittycad.ts)
[Original demo video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
[Original demo slides](https://github.com/KittyCAD/Eng/files/10398178/demo.pdf)
## Get started
We recommend downloading the latest application binary from [our Releases page](https://github.com/KittyCAD/modeling-app/releases). If you don't see your platform or architecture supported there, please file an issue.
## Running a development build
First, [install Rust via `rustup`](https://www.rust-lang.org/tools/install). This project uses a lot of Rust compiled to [WASM](https://webassembly.org/) within it. Then, run:
```
yarn install
```
then
followed by:
```
yarn build:wasm
```
That will build the WASM binary and put in the `public` dir (though gitignored)
finally
finally, to run the web app only, run:
```
yarn start
```
and `yarn test` you would have need to have built the WASM previously. The tests need to download the binary from a server, so if you've already got `yarn start` running, that will work, otherwise running
```
yarn simpleserver
```
in one terminal
and
```
yarn test
```
in another.
If you want to edit the rust files, you can cd into `src/wasm-lib` and then use the usual rust commands, `cargo build`, `cargo test`, when you want to bring the changes back to the web-app, a fresh `yarn build:wasm` in the root will be needed.
Worth noting that the integration of the WASM into this project is very hacky because I'm really pushing create-react-app further than what's practical, but focusing on features atm rather than the setup.
## Developing in Chrome
Chrome is in the process of rolling out a new default which
@ -52,12 +76,26 @@ enable third-party cookies. You can enable third-party cookies by clicking on
the eye with a slash through it in the URL bar, and clicking on "Enable
Third-Party Cookies".
## Running tests
First, start the dev server following "Running a development build" above.
Then in another terminal tab, run:
```
yarn test
```
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
## Tauri
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
```
yarn tauri dev
```
Will spin up the web app before opening up the tauri dev desktop app. Note that it's probably a good idea to close the browser tab that gets opened since at the time of writting they can conflict.
The dev instance automatically opens up the browser devtools which can be disabled by [commenting it out](https://github.com/KittyCAD/modeling-app/blob/main/src-tauri/src/main.rs#L92.)
@ -67,11 +105,22 @@ To build, run `yarn tauri build`, or `yarn tauri build --debug` to keep access t
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
![image](https://github.com/KittyCAD/modeling-app/assets/29681384/a08762c5-8d16-42d8-a02f-a5efc9ae5551)
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
## Before submitting a PR
Before you submit a contribution PR to this repo, please ensure that:
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
- You have separated out refactoring commits from feature commits as much as possible
- You have run all of the following commands locally:
- `yarn fmt`
- `yarn tsc`
- `yarn test`
- Here they are all together: `yarn fmt && yarn tsc && yarn test`
## Release a new version
1. Bump the versions in the .json files by creating a `Bump to v{x}.{y}.{z}` PR, committing the changes from
@ -79,6 +128,7 @@ Note that these became separate apps on Macos, so make sure you open the right o
```bash
VERSION=x.y.z yarn run bump-jsons
```
The PR may serve as a place to discuss the human-readable changelog and extra QA.
2. Merge the PR

View File

@ -9322,6 +9322,34 @@
"unpublished": false,
"deprecated": false
},
{
"name": "cos",
"summary": "Computes the sine of a number (in radians).",
"description": "",
"tags": [],
"args": [
{
"name": "num",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
"unpublished": false,
"deprecated": false
},
{
"name": "extrude",
"summary": "Extrudes by a given amount.",
@ -11173,9 +11201,6 @@
},
"to": {
"description": "The to point.",
"anyOf": [
{
"description": "A point.",
"type": "array",
"items": {
"type": "number",
@ -11183,12 +11208,6 @@
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
}
}
},
@ -11201,10 +11220,6 @@
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
},
@ -13031,6 +13046,24 @@
"unpublished": false,
"deprecated": false
},
{
"name": "pi",
"summary": "Return the value of `pi`.",
"description": "",
"tags": [],
"args": [],
"returnValue": {
"name": "",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
"unpublished": false,
"deprecated": false
},
{
"name": "segAng",
"summary": "Returns the angle of the segment.",
@ -15315,6 +15348,34 @@
"unpublished": false,
"deprecated": false
},
{
"name": "sin",
"summary": "Computes the sine of a number (in radians).",
"description": "",
"tags": [],
"args": [
{
"name": "num",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
"unpublished": false,
"deprecated": false
},
{
"name": "startSketchAt",
"summary": "Start a sketch at a given point.",
@ -15341,9 +15402,6 @@
},
"to": {
"description": "The to point.",
"anyOf": [
{
"description": "A point.",
"type": "array",
"items": {
"type": "number",
@ -15351,12 +15409,6 @@
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
}
}
},
@ -15369,10 +15421,6 @@
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
},
@ -15815,6 +15863,34 @@
"unpublished": false,
"deprecated": false
},
{
"name": "tan",
"summary": "Computes the tangent of a number (in radians).",
"description": "",
"tags": [],
"args": [
{
"name": "num",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
"unpublished": false,
"deprecated": false
},
{
"name": "xLine",
"summary": "Draw a line on the x-axis.",

View File

@ -16,6 +16,7 @@
* [`arc`](#arc)
* [`bezierCurve`](#bezierCurve)
* [`close`](#close)
* [`cos`](#cos)
* [`extrude`](#extrude)
* [`getExtrudeWallTransform`](#getExtrudeWallTransform)
* [`lastSegX`](#lastSegX)
@ -26,12 +27,15 @@
* [`line`](#line)
* [`lineTo`](#lineTo)
* [`min`](#min)
* [`pi`](#pi)
* [`segAng`](#segAng)
* [`segEndX`](#segEndX)
* [`segEndY`](#segEndY)
* [`segLen`](#segLen)
* [`show`](#show)
* [`sin`](#sin)
* [`startSketchAt`](#startSketchAt)
* [`tan`](#tan)
* [`xLine`](#xLine)
* [`xLineTo`](#xLineTo)
* [`yLine`](#yLine)
@ -1637,6 +1641,26 @@ close(sketch_group: SketchGroup) -> SketchGroup
### cos
Computes the sine of a number (in radians).
```
cos(num: number) -> number
```
#### Arguments
* `num`: `number`
#### Returns
* `number`
### extrude
Extrudes by a given amount.
@ -2044,11 +2068,9 @@ line(data: LineData, sketch_group: SketchGroup) -> SketchGroup
// The tag.
tag: string,
// The to point.
to: [number] |
string,
to: [number],
} |
[number] |
string
[number]
```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
```
@ -2356,6 +2378,25 @@ min(args: [number]) -> number
### pi
Return the value of `pi`.
```
pi() -> number
```
#### Arguments
#### Returns
* `number`
### segAng
Returns the angle of the segment.
@ -2766,6 +2807,26 @@ show(sketch: SketchGroup)
### sin
Computes the sine of a number (in radians).
```
sin(num: number) -> number
```
#### Arguments
* `num`: `number`
#### Returns
* `number`
### startSketchAt
Start a sketch at a given point.
@ -2784,11 +2845,9 @@ startSketchAt(data: LineData) -> SketchGroup
// The tag.
tag: string,
// The to point.
to: [number] |
string,
to: [number],
} |
[number] |
string
[number]
```
#### Returns
@ -2859,6 +2918,26 @@ string
### tan
Computes the tangent of a number (in radians).
```
tan(num: number) -> number
```
#### Arguments
* `num`: `number`
#### Returns
* `number`
### xLine
Draw a line on the x-axis.

75
docs/kcl/types.md Normal file
View File

@ -0,0 +1,75 @@
# Types
`KCL` defines the following types and keywords the language.
All these types can be nested in various forms where nesting applies. Like
arrays can hold objects and vice versa.
## Boolean
`true` or `false` work when defining values.
## Variable declaration
Variables are defined with the `let` keyword like so:
```
let myBool = false
```
## Array
An array is defined with `[]` braces. What is inside the brackets can
be of any type. For example, the following is completely valid:
```
let myArray = ["thing", 2, false]
```
If you want to get a value from an array you can use the index like so:
`myArray[0]`.
## Object
An object is defined with `{}` braces. Here is an example object:
```
let myObj = {a: 0, b: "thing"}
```
We support two different ways of getting properties from objects, you can call
`myObj.a` or `myObj["a"]` both work.
## Functions
We also have support for defining your own functions. Functions can take in any
type of argument. Below is an example of the syntax:
```
fn myFn = (x) => {
return x
}
```
As you can see above `myFn` just returns whatever it is given.
## Binary expressions
You can also do math! Let's show an example below:
```
let myMathExpression = 3 + 1 * 2 / 3 - 7
```
You can nest expressions in parenthesis as well:
```
let myMathExpression = 3 + (1 * 2 / (3 - 7))
```
Please if you find any issues using any of the above expressions or syntax
please file an issue with the `ast` label on the [modeling-app
repo](https://github.com/KittyCAD/modeling-app/issues/new).

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.6.0",
"version": "0.7.1",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.9.0",
@ -14,6 +14,7 @@
"@lezer/javascript": "^1.4.7",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0",
"@sentry/react": "^7.65.0",
"@tauri-apps/api": "^1.3.0",
"@testing-library/jest-dom": "^5.14.1",

42
public/expectations.md Normal file
View File

@ -0,0 +1,42 @@
## Alpha Users Expectations
### Welcome
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
### KittyCAD Modeling App (KCMA)
What we are introducing to you is our KittyCAD Modeling App (KCMA). KCMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. KCMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
### Why Code?
Plenty of you have professional CAD experience, and may not understand why coding your model would be helpful. The "code-CAD" paradigm isnt as popular as traditional CAD programs (SolidWorks, NX, CREO, OnShape, etc.), but it certainly has its benefits. Some benefits include:
- Automation and parametric design
- Customization and flexibility
- Algorithmic and generative design
- Reproducibility
- Easier integration with other tools
### Before You Use KCMA
Before you dive straight into the app, we wanted to lay some expectations out for you.
- KCMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. KCMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure its not a duplicate or its feasible for the current state of the app.
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:
- **High:** The bug is blocking you from continuing.
- Example: Every time I click the extrude button with two faces selected, the app crashes.
- **Medium:** You can find a workaround to the problem, but it increases your time spent working or makes it unenjoyable.
- Example: When the app is full screen on Mac, the settings are not showing properly. It works if I have the app windowed.
- **Low:** The bug is annoying but doesnt affect workflow or block you from continuing (usually you can say “It would be nice if ___, but its not needed”)
- Example: It would be nice if the camera would orient normal to the sketching surface when I select a face/plane and click “sketch”.
- We want you all to be aware that we may reach out to you in regard to issues, bugs, problems, and satisfaction. This will typically be for further clarification so we can really nail things down.
### Discord
We will be using Discord a lot more now that the Alpha has been released to people outside of the company. Please feel free to discuss and talk with us in the **alpha users** section of the server. We highly encourage you to engage with us on Discord!
### Thank You!
Once again, from all of us to you, thank you for being a part of the closed Alpha. We are happy to chat with you all, hear your feedback, and see some of your projects!

BIN
public/kcma-logomark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

26
public/roadmap.md Normal file
View File

@ -0,0 +1,26 @@
## KittyCAD Modeling App Roadmap
This document ties into our [GH Discussions Feature List](https://github.com/KittyCAD/modeling-app/discussions). Please upvote any features that you want to see next, or add ones that are not listed and we will review.
### Current Priority List
1. [Sketch on Face](https://github.com/KittyCAD/modeling-app/discussions/477)
2. [Revolve](https://github.com/KittyCAD/modeling-app/discussions/496)
3. [Fillet](https://github.com/KittyCAD/modeling-app/discussions/501)
4. [Linear Pattern](https://github.com/KittyCAD/modeling-app/discussions/256)
5. [Circular Pattern](https://github.com/KittyCAD/modeling-app/discussions/257)
6. [Mirror-Sketch](https://github.com/KittyCAD/modeling-app/discussions/507)
7. [Chamfer](https://github.com/KittyCAD/modeling-app/discussions/502)
8. [Sweep](https://github.com/KittyCAD/modeling-app/discussions/498)
9. [Draft](https://github.com/KittyCAD/modeling-app/discussions/495)
10. [Shell](https://github.com/KittyCAD/modeling-app/discussions/503)
11. [Union](https://github.com/KittyCAD/modeling-app/discussions/509)
12. [Mirror-Model](https://github.com/KittyCAD/modeling-app/discussions/508)
13. [Subtract](https://github.com/KittyCAD/modeling-app/discussions/510)
14. [Intersect](https://github.com/KittyCAD/modeling-app/discussions/511)
15. [Offset](https://github.com/KittyCAD/modeling-app/discussions/512)
16. [Thicken](https://github.com/KittyCAD/modeling-app/discussions/499)
17. [Import](https://github.com/KittyCAD/modeling-app/discussions/478)
18. [Assemblies](https://github.com/KittyCAD/modeling-app/discussions/494)
19. [External Thread](https://github.com/KittyCAD/modeling-app/discussions/505)

945
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,13 +16,14 @@ tauri-build = { version = "1.4.0", features = [] }
[dependencies]
anyhow = "1"
oauth2 = "4.4.1"
kittycad = "0.2.25"
oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
tokio = { version = "1.29.1", features = ["time"] }
toml = "0.6.0"
tauri = { version = "1.4.1", features = ["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" }
tokio = { version = "1.32.0", features = ["time"] }
toml = "0.8.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.

View File

@ -85,6 +85,24 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
Ok(token)
}
///This command returns the KittyCAD user info given a token.
/// The string returned from this method is the user info as a json string.
#[tauri::command]
async fn get_user(token: Option<String>) -> Result<kittycad::types::User, InvokeError> {
println!("Getting user info...");
// use kittycad library to fetch the user info from /user/me
let client = kittycad::Client::new(token.unwrap());
let user_info: kittycad::types::User = client
.users()
.get_self()
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(user_info)
}
fn main() {
tauri::Builder::default()
.setup(|app| {
@ -97,7 +115,12 @@ fn main() {
}
Ok(())
})
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file])
.invoke_handler(tauri::generate_handler![
get_user,
login,
read_toml,
read_txt_file
])
.plugin(tauri_plugin_fs_extra::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

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

View File

@ -1,13 +1,6 @@
import {
useRef,
useEffect,
useLayoutEffect,
useCallback,
MouseEventHandler,
} from 'react'
import { useRef, useEffect, useCallback, MouseEventHandler } from 'react'
import { DebugPanel } from './components/DebugPanel'
import { v4 as uuidv4 } from 'uuid'
import { asyncParser } from './lang/abstractSyntaxTree'
import { _executor } from './lang/executor'
import { PaneType, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs'
@ -16,13 +9,9 @@ import { MemoryPanel } from './components/MemoryPanel'
import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import ModalContainer from 'react-modal-promise'
import {
EngineCommand,
EngineCommandManager,
} from './lang/std/engineConnection'
import { EngineCommand } from './lang/std/engineConnection'
import { throttle } from './lib/utils'
import { AppHeader } from './components/AppHeader'
import { KCLError } from './lang/errors'
import { Resizable } from 're-resizable'
import {
faCode,
@ -41,6 +30,8 @@ import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/mod
import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
@ -48,59 +39,25 @@ export function App() {
const streamRef = useRef<HTMLDivElement>(null)
useHotKeyListener()
const {
addLog,
addKCLError,
setCode,
setAst,
setError,
setProgramMemory,
resetLogs,
resetKCLErrors,
setArtifactMap,
engineCommandManager,
setEngineCommandManager,
highlightRange,
setHighlightRange,
setCursor2,
setMediaStream,
setIsStreamReady,
isStreamReady,
buttonDownInStream,
openPanes,
setOpenPanes,
didDragInStream,
setStreamDimensions,
streamDimensions,
setIsExecuting,
defferedCode,
guiMode,
setGuiMode,
} = useStore((s) => ({
guiMode: s.guiMode,
addLog: s.addLog,
defferedCode: s.defferedCode,
setGuiMode: s.setGuiMode,
setCode: s.setCode,
setAst: s.setAst,
setError: s.setError,
setProgramMemory: s.setProgramMemory,
resetLogs: s.resetLogs,
resetKCLErrors: s.resetKCLErrors,
setArtifactMap: s.setArtifactNSourceRangeMaps,
engineCommandManager: s.engineCommandManager,
setEngineCommandManager: s.setEngineCommandManager,
highlightRange: s.highlightRange,
setHighlightRange: s.setHighlightRange,
setCursor2: s.setCursor2,
setMediaStream: s.setMediaStream,
isStreamReady: s.isStreamReady,
setIsStreamReady: s.setIsStreamReady,
buttonDownInStream: s.buttonDownInStream,
addKCLError: s.addKCLError,
openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes,
didDragInStream: s.didDragInStream,
setStreamDimensions: s.setStreamDimensions,
streamDimensions: s.streamDimensions,
setIsExecuting: s.setIsExecuting,
}))
const {
@ -127,6 +84,38 @@ export function App() {
useHotkeys('shift + l', () => togglePane('logs'))
useHotkeys('shift + e', () => togglePane('kclErrors'))
useHotkeys('shift + d', () => togglePane('debug'))
useHotkeys('esc', () => {
if (guiMode.mode === 'sketch') {
if (guiMode.sketchMode === 'selectFace') return
if (guiMode.sketchMode === 'sketchEdit') {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
setGuiMode({ mode: 'default' })
} else {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_tool',
tool: 'select',
},
})
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
rotation: guiMode.rotation,
position: guiMode.position,
pathToNode: guiMode.pathToNode,
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
})
}
} else {
setGuiMode({ mode: 'default' })
}
})
const paneOpacity =
onboardingStatus === onboardingPaths.CAMERA
@ -149,142 +138,8 @@ export function App() {
}
}, [loadedCode, setCode])
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const width = streamWidth ? streamWidth : 0
const quadWidth = Math.round(width / 4) * 4
const height = streamHeight ? streamHeight : 0
const quadHeight = Math.round(height / 4) * 4
useLayoutEffect(() => {
setStreamDimensions({
streamWidth: quadWidth,
streamHeight: quadHeight,
})
if (!width || !height) return
const eng = new EngineCommandManager({
setMediaStream,
setIsStreamReady,
width: quadWidth,
height: quadHeight,
token,
})
setEngineCommandManager(eng)
return () => {
eng?.tearDown()
}
}, [quadWidth, quadHeight])
useEffect(() => {
if (!isStreamReady) return
if (!engineCommandManager) return
let unsubFn: any[] = []
const asyncWrap = async () => {
try {
if (!defferedCode) {
setAst({
start: 0,
end: 0,
body: [],
nonCodeMeta: {
noneCodeNodes: {},
start: null,
},
})
setProgramMemory({ root: {} })
engineCommandManager.endSession()
engineCommandManager.startNewSession()
return
}
const _ast = await asyncParser(defferedCode)
setAst(_ast)
resetLogs()
resetKCLErrors()
engineCommandManager.endSession()
engineCommandManager.startNewSession()
setIsExecuting(true)
const programMemory = await _executor(
_ast,
{
root: {
_0: {
type: 'userVal',
value: 0,
__meta: [],
},
_90: {
type: 'userVal',
value: 90,
__meta: [],
},
_180: {
type: 'userVal',
value: 180,
__meta: [],
},
_270: {
type: 'userVal',
value: 270,
__meta: [],
},
},
},
engineCommandManager
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands()
setIsExecuting(false)
if (programMemory !== undefined) {
setProgramMemory(programMemory)
}
setArtifactMap({ artifactMap, sourceRangeMap })
const unSubHover = engineCommandManager.subscribeToUnreliable({
event: 'highlight_set_entity',
callback: ({ data }) => {
if (data?.entity_id) {
const sourceRange = sourceRangeMap[data.entity_id]
setHighlightRange(sourceRange)
} else if (
!highlightRange ||
(highlightRange[0] !== 0 && highlightRange[1] !== 0)
) {
setHighlightRange([0, 0])
}
},
})
const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) {
setCursor2()
return
}
const sourceRange = sourceRangeMap[data.entity_id]
setCursor2({ range: sourceRange, type: 'default' })
},
})
unsubFn.push(unSubHover, unSubClick)
setError()
} catch (e: any) {
setIsExecuting(false)
if (e instanceof KCLError) {
addKCLError(e)
} else {
setError('problem')
console.log(e)
addLog(e)
}
}
}
asyncWrap()
return () => {
unsubFn.forEach((fn) => fn())
}
}, [defferedCode, isStreamReady, engineCommandManager])
useSetupEngineManager(streamRef, token)
useEngineConnectionSubscriptions()
const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message)
@ -313,18 +168,6 @@ export function App() {
window: { x, y },
},
})
} else if (
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('move' as any)
) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'handle_mouse_drag_move',
window: { x, y },
},
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
@ -336,6 +179,17 @@ export function App() {
})
}
} else {
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'handle_mouse_drag_move',
window: { x, y },
},
})
return
}
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type

View File

@ -47,6 +47,15 @@
@apply hover:bg-cool-20;
}
.smallScrollbar::-webkit-scrollbar {
@apply h-0.5;
}
.smallScrollbar {
@apply overflow-x-auto;
scrollbar-width: thin;
}
:global(.dark) .popoverToggle {
@apply hover:bg-cool-90;
}

View File

@ -27,6 +27,7 @@ export const Toolbar = () => {
updateAst,
programMemory,
engineCommandManager,
executeAst,
} = useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
@ -35,6 +36,7 @@ export const Toolbar = () => {
updateAst: s.updateAst,
programMemory: s.programMemory,
engineCommandManager: s.engineCommandManager,
executeAst: s.executeAst,
}))
useAppMode()
@ -44,7 +46,7 @@ export const Toolbar = () => {
function ToolbarButtons() {
return (
<span className="overflow-x-auto">
<span className={styles.smallScrollbar}>
{guiMode.mode === 'default' && (
<button
onClick={() => {
@ -70,7 +72,7 @@ export const Toolbar = () => {
pathToNode,
programMemory
)
updateAst(modifiedAst)
updateAst(modifiedAst, true)
}}
>
SketchOnFace
@ -113,7 +115,7 @@ export const Toolbar = () => {
ast,
pathToNode
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
}}
>
ExtrudeSketch
@ -130,7 +132,7 @@ export const Toolbar = () => {
pathToNode,
false
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
}}
>
ExtrudeSketch (w/o pipe)
@ -146,7 +148,14 @@ export const Toolbar = () => {
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
setGuiMode({ mode: 'default' })
executeAst()
}}
>
Exit sketch

View File

@ -0,0 +1,182 @@
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { useEffect, useRef, useState } from 'react'
import { useStore } from 'useStore'
export function AstExplorer() {
const { ast, setHighlightRange, selectionRanges } = useStore((s) => ({
ast: s.ast,
setHighlightRange: s.setHighlightRange,
selectionRanges: s.selectionRanges,
}))
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections?.[0]?.range
)
const node = getNodeFromPath(ast, pathToNode).node
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
return (
<div className="relative" style={{ width: '300px' }}>
<div className="">
filter out keys:<div className="w-2 inline-block"></div>
{['start', 'end', 'type'].map((key) => {
return (
<label key={key} className="inline-flex items-center">
<input
type="checkbox"
className="form-checkbox"
checked={filterKeys.includes(key)}
onChange={(e) => {
if (filterKeys.includes(key)) {
setFilterKeys(filterKeys.filter((k) => k !== key))
} else {
setFilterKeys([...filterKeys, key])
}
}}
/>
<span className="mr-2">{key}</span>
</label>
)
})}
</div>
<div
className="h-full relative"
onMouseLeave={(e) => {
setHighlightRange([0, 0])
}}
>
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
<DisplayObj obj={ast} filterKeys={filterKeys} node={node} />
</pre>
</div>
</div>
)
}
function DisplayBody({
body,
filterKeys,
node,
}: {
body: { start: number; end: number; [key: string]: any }[]
filterKeys: string[]
node: any
}) {
return (
<>
{body.map((b, index) => {
return (
<div className="my-2" key={index}>
<DisplayObj obj={b} filterKeys={filterKeys} node={node} />
</div>
)
})}
</>
)
}
function DisplayObj({
obj,
filterKeys,
node,
}: {
obj: { start: number; end: number; [key: string]: any }
filterKeys: string[]
node: any
}) {
const { setHighlightRange, setCursor2 } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
setCursor2: s.setCursor2,
}))
const ref = useRef<HTMLPreElement>(null)
const [hasCursor, setHasCursor] = useState(false)
const [isCollapsed, setIsCollapsed] = useState(false)
useEffect(() => {
if (
node?.start === obj?.start &&
node?.end === obj?.end &&
node.type === obj?.type
) {
ref?.current?.scrollIntoView?.({ behavior: 'smooth', block: 'center' })
setHasCursor(true)
} else {
setHasCursor(false)
}
}, [node.start, node.end, node.type])
return (
<pre
ref={ref}
className={`ml-2 border-l border-violet-600 pl-1 ${
hasCursor ? 'bg-violet-100/25' : ''
}`}
onMouseEnter={(e) => {
setHighlightRange([obj?.start || 0, obj.end])
e.stopPropagation()
}}
onMouseMove={(e) => {
e.stopPropagation()
setHighlightRange([obj?.start || 0, obj.end])
}}
onClick={(e) => {
setCursor2({ type: 'default', range: [obj?.start || 0, obj.end || 0] })
e.stopPropagation()
}}
>
{isCollapsed ? (
<button
className="m-0 p-0 border-0"
onClick={() => setIsCollapsed(false)}
>
{'>'}type: {obj.type}
</button>
) : (
<span className="flex">
{/* <button className="m-0 p-0 border-0 mb-auto" onClick={() => setIsCollapsed(true)}>{'⬇️'}</button> */}
<ul className="inline-block">
{Object.entries(obj).map(([key, value]) => {
if (filterKeys.includes(key)) {
return null
} else if (Array.isArray(value)) {
return (
<li key={key}>
{`${key}: [`}
<DisplayBody
body={value}
filterKeys={filterKeys}
node={node}
/>
{']'}
</li>
)
} else if (
typeof value === 'object' &&
value !== null &&
value?.end
) {
return (
<li key={key}>
{key}:
<DisplayObj
obj={value}
filterKeys={filterKeys}
node={node}
/>
</li>
)
} else if (
typeof value === 'string' ||
typeof value === 'number'
) {
return (
<li key={key}>
{key}: {value}
</li>
)
}
})}
</ul>
</span>
)}
</pre>
)
}

View File

@ -144,7 +144,7 @@ export function useCalc({
try {
const code = `const __result__ = ${value}\nshow(__result__)`
const ast = parser_wasm(code)
const _programMem: any = { root: {} }
const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})

View File

@ -1,11 +1,15 @@
import { Menu } from '@headlessui/react'
import { PropsWithChildren } from 'react'
import { faEllipsis } from '@fortawesome/free-solid-svg-icons'
import {
faArrowUpRightFromSquare,
faEllipsis,
} from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon'
import { useStore } from 'useStore'
import styles from './CodeMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
export const CodeMenu = ({ children }: PropsWithChildren) => {
const { formatCode } = useStore((s) => ({
@ -52,6 +56,24 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
</button>
</Menu.Item>
)}
<Menu.Item>
<a
className={styles.button}
href="https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md"
target="_blank"
rel="noopener noreferrer"
>
<span>Read the KCL docs</span>
<small>
On GitHub
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1 align-text-top"
width={12}
/>
</small>
</a>
</Menu.Item>
</Menu.Items>
</div>
</Menu>

View File

@ -6,6 +6,7 @@ import { useState } from 'react'
import { ActionButton } from '../components/ActionButton'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
import { isReducedMotion } from 'lang/util'
import { AstExplorer } from './AstExplorer'
type SketchModeCmd = Extract<
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
@ -94,6 +95,9 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
>
Send sketch mode command
</ActionButton>
<div style={{ height: '400px' }} className="overflow-y-auto">
<AstExplorer />
</div>
</section>
</CollapsiblePanel>
)

View File

@ -24,6 +24,9 @@ import {
StateFrom,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { invoke } from '@tauri-apps/api'
import { isTauri } from 'lib/isTauri'
import { VITE_KC_API_BASE_URL } from 'env'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -108,6 +111,7 @@ export const GlobalStateProvider = ({
actions: {
goToSignInPage: () => {
navigate(paths.SIGN_IN)
logout()
},
goToIndexPage: () => {
@ -149,10 +153,12 @@ export const GlobalStateProvider = ({
export default GlobalStateProvider
export function logout() {
const url = withBaseUrl('/logout')
localStorage.removeItem(TOKEN_PERSIST_KEY)
return fetch(url, {
return (
!isTauri() &&
fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
)
}

View File

@ -10,7 +10,7 @@ describe('processMemory', () => {
// Enable rotations #152
const code = `
const myVar = 5
const myFn = (a) => {
fn myFn = (a) => {
return a - 2
}
const otherVar = myFn(5)
@ -29,6 +29,7 @@ describe('processMemory', () => {
const ast = parser_wasm(code)
const programMemory = await enginelessExecutor(ast, {
root: {},
return: null,
})
const output = processMemory(programMemory)
expect(output.myVar).toEqual(5)

View File

@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { useStore } from '../useStore'
import { useMemo } from 'react'
import { ProgramMemory } from '../lang/executor'
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
import { Themes } from '../lib/theme'
interface MemoryPanelProps extends CollapsiblePanelProps {
@ -49,8 +49,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
Object.keys(programMemory.root).forEach((key) => {
const val = programMemory.root[key]
if (typeof val.value !== 'function') {
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if (val.type === 'ExtrudeGroup') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
} else {

View File

@ -14,7 +14,13 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { Models } from '@kittycad/lib'
import { addStartSketch } from 'lang/modifyAst'
import { addNewSketchLn } from 'lang/std/sketch'
import {
addCloseToPipe,
addNewSketchLn,
compareVec2Epsilon,
} from 'lang/std/sketch'
import { getNodeFromPath } from 'lang/queryAst'
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
export const Stream = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(true)
@ -204,8 +210,8 @@ export const Stream = ({ className = '' }) => {
window: { x, y },
}
}
engineCommandManager?.sendSceneCommand(command).then(async ({ data }) => {
if (command.cmd.type !== 'mouse_click' || !ast) return
engineCommandManager?.sendSceneCommand(command).then(async (resp) => {
if (command?.cmd?.type !== 'mouse_click' || !ast) return
if (
!(
guiMode.mode === 'sketch' &&
@ -214,13 +220,28 @@ export const Stream = ({ className = '' }) => {
)
return
if (data?.data?.entities_modified?.length && guiMode.waitingFirstClick) {
// Check if the sketch group already exists.
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
guiMode.pathToNode,
'VariableDeclarator'
).node
const variableName = varDec?.id?.name
const sketchGroup = programMemory.root[variableName]
const isEditingExistingSketch =
sketchGroup?.type === 'SketchGroup' && sketchGroup.value.length
if (
resp?.data?.data?.entities_modified?.length &&
guiMode.waitingFirstClick &&
!isEditingExistingSketch
) {
const curve = await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: data?.data?.entities_modified[0],
curve_id: resp?.data?.data?.entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
@ -241,29 +262,65 @@ export const Stream = ({ className = '' }) => {
pathToNode: _pathToNode,
waitingFirstClick: false,
})
updateAst(_modifiedAst)
updateAst(_modifiedAst, false)
} else if (
data?.data?.entities_modified?.length &&
!guiMode.waitingFirstClick
resp?.data?.data?.entities_modified?.length &&
(!guiMode.waitingFirstClick || isEditingExistingSketch)
) {
const curve = await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: data?.data?.entities_modified[0],
curve_id: resp?.data?.data?.entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
const _modifiedAst = addNewSketchLn({
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
ast,
guiMode.pathToNode,
'VariableDeclarator'
)
const variableName = varDec.id.name
const sketchGroup = programMemory.root[variableName]
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return
const initialCoords = sketchGroup.value[0].from
const isClose = compareVec2Epsilon(initialCoords, [
coords[1].x,
coords[1].y,
])
let _modifiedAst: Program
if (!isClose) {
_modifiedAst = addNewSketchLn({
node: ast,
programMemory,
to: [coords[1].x, coords[1].y],
fnName: 'line',
pathToNode: guiMode.pathToNode,
}).modifiedAst
updateAst(_modifiedAst)
updateAst(_modifiedAst, false)
} else {
_modifiedAst = addCloseToPipe({
node: ast,
programMemory,
pathToNode: guiMode.pathToNode,
})
setGuiMode({
mode: 'default',
})
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sketch_mode_disable',
},
})
updateAst(_modifiedAst, true)
}
}
})
setDidDragInStream(false)

View File

@ -26,9 +26,10 @@ import {
addLineHighlight,
lineHighlightField,
} from 'editor/highlightextension'
import { isOverlap } from 'lib/utils'
import { isOverlap, roundOff } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors'
import { CSSRuleObject } from 'tailwindcss/types/config'
import interact from '@replit/codemirror-interact'
export const editorShortcutMeta = {
formatCode: {
@ -49,7 +50,7 @@ export const TextEditor = ({
const pathParams = useParams()
const {
code,
defferedSetCode,
deferredSetCode,
editorView,
engineCommandManager,
formatCode,
@ -59,10 +60,9 @@ export const TextEditor = ({
setEditorView,
setIsLSPServerReady,
setSelectionRanges,
sourceRangeMap,
} = useStore((s) => ({
code: s.code,
defferedSetCode: s.defferedSetCode,
deferredSetCode: s.deferredSetCode,
editorView: s.editorView,
engineCommandManager: s.engineCommandManager,
formatCode: s.formatCode,
@ -72,7 +72,6 @@ export const TextEditor = ({
setEditorView: s.setEditorView,
setIsLSPServerReady: s.setIsLSPServerReady,
setSelectionRanges: s.setSelectionRanges,
sourceRangeMap: s.sourceRangeMap,
}))
const {
@ -125,7 +124,7 @@ export const TextEditor = ({
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => {
defferedSetCode(value)
deferredSetCode(value)
if (isTauri() && pathParams.id) {
// Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
@ -173,11 +172,11 @@ export const TextEditor = ({
)
const idBasedSelections = codeBasedSelections
.map(({ type, range }) => {
const hasOverlap = Object.entries(sourceRangeMap).filter(
([_, sourceRange]) => {
const hasOverlap = Object.entries(
engineCommandManager?.sourceRangeMap || {}
).filter(([_, sourceRange]) => {
return isOverlap(sourceRange, range)
}
)
})
if (hasOverlap.length) {
return {
type,
@ -237,6 +236,38 @@ export const TextEditor = ({
lintGutter(),
linter((_view) => {
return kclErrToDiagnostic(useStore.getState().kclErrors)
}),
interact({
rules: [
// a rule for a number dragger
{
// the regexp matching the value
regexp: /-?\b\d+\.?\d*\b/g,
// set cursor to "ew-resize" on hover
cursor: 'ew-resize',
// change number value based on mouse X movement on drag
onDrag: (text, setText, e) => {
const multiplier =
e.shiftKey && e.metaKey
? 0.01
: e.metaKey
? 0.1
: e.shiftKey
? 10
: 1
const delta = e.movementX * multiplier
const newVal = roundOff(
Number(text) + delta,
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
)
if (isNaN(newVal)) return
setText(newVal.toString())
},
},
],
})
)
if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)

View File

@ -82,7 +82,7 @@ export const EqualAngle = () => {
transformInfos,
programMemory,
})
updateAst(modifiedAst, {
updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}

View File

@ -82,7 +82,7 @@ export const EqualLength = () => {
transformInfos,
programMemory,
})
updateAst(modifiedAst, {
updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}

View File

@ -61,7 +61,7 @@ export const HorzVert = ({
programMemory,
referenceSegName: '',
})
updateAst(modifiedAst, {
updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}

View File

@ -154,7 +154,7 @@ export const Intersect = () => {
initialVariableName: 'offset',
} as any)
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
updateAst(modifiedAst, {
updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
} else {
@ -182,7 +182,7 @@ export const Intersect = () => {
)
_modifiedAst.body = newBody
}
updateAst(_modifiedAst, {
updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}

View File

@ -65,7 +65,7 @@ export const RemoveConstrainingValues = () => {
programMemory,
referenceSegName: '',
})
updateAst(modifiedAst, {
updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}

View File

@ -124,7 +124,7 @@ export const SetAbsDistance = ({
_modifiedAst.body = newBody
}
updateAst(_modifiedAst, {
updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
} catch (e) {

View File

@ -113,7 +113,7 @@ export const SetAngleBetween = () => {
initialVariableName: 'angle',
} as any)
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
updateAst(modifiedAst, {
updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
} else {
@ -141,7 +141,7 @@ export const SetAngleBetween = () => {
)
_modifiedAst.body = newBody
}
updateAst(_modifiedAst, {
updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}

View File

@ -137,7 +137,7 @@ export const SetHorzVertDistance = ({
constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
} as any))
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
updateAst(modifiedAst, {
updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
} else {
@ -163,7 +163,7 @@ export const SetHorzVertDistance = ({
)
_modifiedAst.body = newBody
}
updateAst(_modifiedAst, {
updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}

View File

@ -136,7 +136,7 @@ export const SetAngleLength = ({
_modifiedAst.body = newBody
}
updateAst(_modifiedAst, {
updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
} catch (e) {

View File

@ -11,8 +11,9 @@ import { isOverlap } from 'lib/utils'
interface DefaultPlanes {
xy: string
yz: string
xz: string
// TODO re-enable
// yz: string
// xz: string
}
export function useAppMode() {
@ -42,34 +43,26 @@ export function useAppMode() {
y_axis: { x: 0, y: 1, z: 0 },
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
})
const yz = createPlane(engineCommandManager, {
x_axis: { x: 0, y: 1, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
})
const xz = createPlane(engineCommandManager, {
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
})
setDefaultPlanes({ xy, yz, xz })
// TODO re-enable
// const yz = createPlane(engineCommandManager, {
// x_axis: { x: 0, y: 1, z: 0 },
// y_axis: { x: 0, y: 0, z: 1 },
// color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
// })
// const xz = createPlane(engineCommandManager, {
// x_axis: { x: 1, y: 0, z: 0 },
// y_axis: { x: 0, y: 0, z: 1 },
// color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
// })
setDefaultPlanes({ xy })
} else {
hideDefaultPlanes(engineCommandManager, defaultPlanes)
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, false)
}
}
if (guiMode.mode !== 'sketch' && defaultPlanes) {
Object.values(defaultPlanes).forEach((planeId) => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'object_visible',
object_id: planeId,
hidden: true,
},
})
})
} else if (guiMode.mode === 'default') {
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
}
if (guiMode.mode === 'default') {
const pathId =
engineCommandManager &&
isCursorInSketchCommandRange(
@ -128,7 +121,7 @@ export function useAppMode() {
},
}
)
hideDefaultPlanes(engineCommandManager, defaultPlanes)
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
const sketchUuid = uuidv4()
const proms: any[] = []
proms.push(
@ -204,9 +197,10 @@ function createPlane(
return planeId
}
function hideDefaultPlanes(
engineCommandManager: EngineCommandManager,
defaultPlanes: DefaultPlanes
function setDefaultPlanesHidden(
engineCommandManager: EngineCommandManager | undefined,
defaultPlanes: DefaultPlanes,
hidden: boolean
) {
Object.values(defaultPlanes).forEach((planeId) => {
engineCommandManager?.sendSceneCommand({
@ -215,7 +209,7 @@ function hideDefaultPlanes(
cmd: {
type: 'object_visible',
object_id: planeId,
hidden: true,
hidden: hidden,
},
})
})
@ -229,15 +223,17 @@ function isCursorInSketchCommandRange(
([id, artifact]) =>
selectionRanges.codeBasedSelections.some(
(selection) =>
Array.isArray(selection.range) &&
Array.isArray(artifact.range) &&
Array.isArray(selection?.range) &&
Array.isArray(artifact?.range) &&
isOverlap(selection.range, artifact.range) &&
(artifact.commandType === 'start_path' ||
artifact.commandType === 'extend_path' ||
'close_path')
artifact.commandType === 'close_path')
)
)
return overlapingEntries.length === 1 && overlapingEntries[0][1].parentId
return overlapingEntries.length && overlapingEntries[0][1].parentId
? overlapingEntries[0][1].parentId
: false
: overlapingEntries.find(
([, artifact]) => artifact.commandType === 'start_path'
)?.[0] || false
}

View File

@ -0,0 +1,50 @@
import { useEffect } from 'react'
import { useStore } from 'useStore'
export function useEngineConnectionSubscriptions() {
const {
engineCommandManager,
setCursor2,
setHighlightRange,
highlightRange,
} = useStore((s) => ({
engineCommandManager: s.engineCommandManager,
setCursor2: s.setCursor2,
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
useEffect(() => {
if (!engineCommandManager) return
const unSubHover = engineCommandManager.subscribeToUnreliable({
event: 'highlight_set_entity',
callback: ({ data }) => {
if (data?.entity_id) {
const sourceRange =
engineCommandManager.sourceRangeMap[data.entity_id]
setHighlightRange(sourceRange)
} else if (
!highlightRange ||
(highlightRange[0] !== 0 && highlightRange[1] !== 0)
) {
setHighlightRange([0, 0])
}
},
})
const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) {
setCursor2()
return
}
const sourceRange = engineCommandManager.sourceRangeMap[data.entity_id]
setCursor2({ range: sourceRange, type: 'default' })
},
})
return () => {
unSubHover()
unSubClick()
}
}, [engineCommandManager, setCursor2, setHighlightRange, highlightRange])
}

View File

@ -0,0 +1,53 @@
import { useLayoutEffect } from 'react'
import { _executor } from '../lang/executor'
import { useStore } from '../useStore'
import { EngineCommandManager } from '../lang/std/engineConnection'
export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>,
token?: string
) {
const {
setEngineCommandManager,
setMediaStream,
setIsStreamReady,
setStreamDimensions,
executeCode,
} = useStore((s) => ({
setEngineCommandManager: s.setEngineCommandManager,
setMediaStream: s.setMediaStream,
setIsStreamReady: s.setIsStreamReady,
setStreamDimensions: s.setStreamDimensions,
executeCode: s.executeCode,
}))
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const width = streamWidth ? streamWidth : 0
const quadWidth = Math.round(width / 4) * 4
const height = streamHeight ? streamHeight : 0
const quadHeight = Math.round(height / 4) * 4
useLayoutEffect(() => {
setStreamDimensions({
streamWidth: quadWidth,
streamHeight: quadHeight,
})
if (!width || !height) return
const eng = new EngineCommandManager({
setMediaStream,
setIsStreamReady,
width: quadWidth,
height: quadHeight,
token,
})
setEngineCommandManager(eng)
eng.waitForReady.then(() => {
executeCode()
})
return () => {
eng?.tearDown()
}
}, [quadWidth, quadHeight])
}

View File

@ -46,7 +46,7 @@ export function useConvertToVariable() {
variableName
)
updateAst(_modifiedAst)
updateAst(_modifiedAst, true)
} catch (e) {
console.log('e', e)
}

View File

@ -1,5 +1,5 @@
import { parser_wasm } from './abstractSyntaxTree'
import { KCLUnexpectedError } from './errors'
import { KCLError } from './errors'
import { initPromise } from './rust'
beforeAll(() => initPromise)
@ -1744,6 +1744,12 @@ describe('parsing errors', () => {
_theError = e
}
const theError = _theError as any
expect(theError).toEqual(new KCLUnexpectedError('Brace', [[29, 30]]))
expect(theError).toEqual(
new KCLError(
'unexpected',
'Unexpected token Token { token_type: Brace, start: 29, end: 30, value: "}" }',
[[29, 30]]
)
)
})
})

View File

@ -21,7 +21,7 @@ show(mySketch001)`
)
expect(shown).toEqual([
{
type: 'sketchGroup',
type: 'SketchGroup',
start: {
to: [0, 0],
from: [0, 0],
@ -77,7 +77,7 @@ show(mySketch001)`
)
expect(shown).toEqual([
{
type: 'extrudeGroup',
type: 'ExtrudeGroup',
id: expect.any(String),
value: [],
height: 2,
@ -117,7 +117,7 @@ show(theExtrude, sk2)`
)
expect(geos).toEqual([
{
type: 'extrudeGroup',
type: 'ExtrudeGroup',
id: expect.any(String),
value: [],
height: 2,
@ -126,7 +126,7 @@ show(theExtrude, sk2)`
__meta: [{ sourceRange: [13, 34] }],
},
{
type: 'extrudeGroup',
type: 'ExtrudeGroup',
id: expect.any(String),
value: [],
height: 2,

View File

@ -1,7 +1,7 @@
import fs from 'node:fs'
import { parser_wasm } from './abstractSyntaxTree'
import { ProgramMemory } from './executor'
import { ProgramMemory, SketchGroup } from './executor'
import { initPromise } from './rust'
import { enginelessExecutor } from '../lib/testHelpers'
import { vi } from 'vitest'
@ -117,10 +117,10 @@ show(mySketch)
// ].join('\n')
// const { root } = await exe(code)
// expect(root.mySk1.value).toHaveLength(3)
// expect(root?.rotated?.type).toBe('sketchGroup')
// expect(root?.rotated?.type).toBe('SketchGroup')
// if (
// root?.mySk1?.type !== 'sketchGroup' ||
// root?.rotated?.type !== 'sketchGroup'
// root?.mySk1?.type !== 'SketchGroup' ||
// root?.rotated?.type !== 'SketchGroup'
// )
// throw new Error('not a sketch group')
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
@ -143,7 +143,7 @@ show(mySketch)
].join('\n')
const { root } = await exe(code)
expect(root.mySk1).toEqual({
type: 'sketchGroup',
type: 'SketchGroup',
start: {
to: [0, 0],
from: [0, 0],
@ -199,7 +199,7 @@ show(mySketch)
// TODO path to node is probably wrong here, zero indexes are not correct
expect(root).toEqual({
three: {
type: 'userVal',
type: 'UserVal',
value: 3,
__meta: [
{
@ -208,7 +208,7 @@ show(mySketch)
],
},
yo: {
type: 'userVal',
type: 'UserVal',
value: [1, '2', 3, 9],
__meta: [
{
@ -225,7 +225,7 @@ show(mySketch)
].join('\n')
const { root } = await exe(code)
expect(root.yo).toEqual({
type: 'userVal',
type: 'UserVal',
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
__meta: [
{
@ -240,7 +240,7 @@ show(mySketch)
)
const { root } = await exe(code)
expect(root.myVar).toEqual({
type: 'userVal',
type: 'UserVal',
value: '123',
__meta: [
{
@ -338,7 +338,7 @@ describe('testing math operators', () => {
const { root } = await exe(code)
const sketch = root.part001
// result of `-legLen(5, min(3, 999))` should be -4
const yVal = sketch.value?.[0]?.to?.[1]
const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1]
expect(yVal).toBe(-4)
})
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
@ -356,8 +356,8 @@ describe('testing math operators', () => {
const { root } = await exe(code)
const sketch = root.part001
// expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0
expect(sketch.value?.[1]?.from).toEqual([3, 4])
expect(sketch.value?.[1]?.to).toEqual([6, 0])
expect((sketch as SketchGroup).value?.[1]?.from).toEqual([3, 4])
expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0])
const removedUnaryExp = code.replace(
`-legLen(segLen('seg01', %), myVar)`,
`legLen(segLen('seg01', %), myVar)`
@ -366,7 +366,9 @@ describe('testing math operators', () => {
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
// without the minus sign, the y value should be 8
expect(removedUnaryExpRootSketch.value?.[1]?.to).toEqual([6, 8])
expect((removedUnaryExpRootSketch as SketchGroup).value?.[1]?.to).toEqual([
6, 8,
])
})
it('with nested callExpression and binaryExpression', async () => {
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
@ -397,7 +399,10 @@ show(theExtrude)`
// helpers
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
async function exe(
code: string,
programMemory: ProgramMemory = { root: {}, return: null }
) {
const ast = parser_wasm(code)
const result = await enginelessExecutor(ast, programMemory)

View File

@ -5,96 +5,21 @@ import {
SourceRangeMap,
} from './std/engineConnection'
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { rangeTypeFix } from './abstractSyntaxTree'
export type SourceRange = [number, number]
export type PathToNode = [string | number, string][] // [pathKey, nodeType][]
export type Metadata = {
sourceRange: SourceRange
}
export type Position = [number, number, number]
export type Rotation = [number, number, number, number]
export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
export type { Position } from '../wasm-lib/kcl/bindings/Position'
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
export type { Path } from '../wasm-lib/kcl/bindings/Path'
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
interface BasePath {
from: [number, number]
to: [number, number]
name?: string
__geoMeta: {
id: string
sourceRange: SourceRange
}
}
export interface ToPoint extends BasePath {
type: 'toPoint'
}
export interface Base extends BasePath {
type: 'base'
}
export interface HorizontalLineTo extends BasePath {
type: 'horizontalLineTo'
x: number
}
export interface AngledLineTo extends BasePath {
type: 'angledLineTo'
angle: number
x?: number
y?: number
}
interface GeoMeta {
__geoMeta: {
id: string
sourceRange: SourceRange
}
}
export type Path = ToPoint | HorizontalLineTo | AngledLineTo | Base
export interface SketchGroup {
type: 'sketchGroup'
id: string
value: Path[]
start?: Base
position: Position
rotation: Rotation
__meta: Metadata[]
}
interface ExtrudePlane {
type: 'extrudePlane'
position: Position
rotation: Rotation
name?: string
}
export type ExtrudeSurface = GeoMeta &
ExtrudePlane /* | ExtrudeRadius | ExtrudeSpline */
export interface ExtrudeGroup {
type: 'extrudeGroup'
id: string
value: ExtrudeSurface[]
height: number
position: Position
rotation: Rotation
__meta: Metadata[]
}
/** UserVal not produced by one of our internal functions */
export interface UserVal {
type: 'userVal'
value: any
__meta: Metadata[]
}
type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
export type PathToNode = [string | number, string][]
interface Memory {
[key: string]: MemoryItem
@ -102,12 +27,12 @@ interface Memory {
export interface ProgramMemory {
root: Memory
return?: ProgramReturn
return: ProgramReturn | null
}
export const executor = async (
node: Program,
programMemory: ProgramMemory = { root: {} },
programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager,
// work around while the gemotry is still be stored on the frontend
// will be removed when the stream UI is added.
@ -123,7 +48,7 @@ export const executor = async (
engineCommandManager
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands()
await engineCommandManager.waitForAllCommands(node, _programMemory)
tempMapCallback({ artifactMap, sourceRangeMap })
engineCommandManager.endSession()
@ -132,7 +57,7 @@ export const executor = async (
export const _executor = async (
node: Program,
programMemory: ProgramMemory = { root: {} },
programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager
): Promise<ProgramMemory> => {
try {

View File

@ -176,7 +176,7 @@ show(part001)`
})
describe('Testing moveValueIntoNewVariable', () => {
const fn = (fnName: string) => `const ${fnName} = (x) => {
const fn = (fnName: string) => `fn ${fnName} = (x) => {
return x
}
`

View File

@ -1,4 +1,4 @@
import { Selection, TooTip } from '../useStore'
import { Selection, ToolTip } from '../useStore'
import {
Program,
CallExpression,
@ -27,6 +27,7 @@ import {
getFirstArg,
createFirstArg,
} from './std/sketch'
import { isLiteralArrayOrStatic } from './std/sketchcombos'
export function addStartSketch(
node: Program,
@ -51,11 +52,12 @@ export function addStartSketch(
createPipeExpression(pipeBody)
)
const newIndex = node.body.length
_node.body = [...node.body, variableDeclaration]
let pathToNode: PathToNode = [
['body', ''],
['0', 'index'],
[newIndex.toString(10), 'index'],
['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'],
@ -191,7 +193,7 @@ export function mutateArrExp(
): boolean {
if (node.type === 'ArrayExpression') {
node.elements.forEach((element, i) => {
if (element.type === 'Literal') {
if (isLiteralArrayOrStatic(element)) {
node.elements[i] = updateWith.elements[i]
}
})
@ -209,8 +211,8 @@ export function mutateObjExpProp(
const keyIndex = node.properties.findIndex((a) => a.key.name === key)
if (keyIndex !== -1) {
if (
updateWith.type === 'Literal' &&
node.properties[keyIndex].value.type === 'Literal'
isLiteralArrayOrStatic(updateWith) &&
isLiteralArrayOrStatic(node.properties[keyIndex].value)
) {
node.properties[keyIndex].value = updateWith
return true
@ -220,7 +222,7 @@ export function mutateObjExpProp(
) {
const arrExp = node.properties[keyIndex].value as ArrayExpression
arrExp.elements.forEach((element, i) => {
if (element.type === 'Literal') {
if (isLiteralArrayOrStatic(element)) {
arrExp.elements[i] = updateWith.elements[i]
}
})
@ -303,7 +305,11 @@ export function extrudeSketch(
}
const name = findUniqueName(node, 'part')
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
const showCallIndex = getShowIndex(_node)
let showCallIndex = getShowIndex(_node)
if (showCallIndex == -1) {
// We didn't find a show, so let's just append everything
showCallIndex = _node.body.length
}
_node.body.splice(showCallIndex, 0, VariableDeclaration)
const pathToExtrudeArg: PathToNode = [
['body', ''],
@ -633,7 +639,7 @@ export function giveSketchFnCallTag(
createLiteral(tag || findUniqueName(ast, 'seg', 2))) as Literal
const tagStr = String(tagValue.value)
const newFirstArg = createFirstArg(
primaryCallExp.callee.name as TooTip,
primaryCallExp.callee.name as ToolTip,
firstArg.val,
tagValue
)

View File

@ -1,5 +1,5 @@
import { PathToNode, ProgramMemory, SketchGroup, SourceRange } from './executor'
import { Selection, TooTip } from '../useStore'
import { Selection, ToolTip } from '../useStore'
import {
BinaryExpression,
Program,
@ -457,7 +457,7 @@ export function isLinesParallelAndConstrained(
const secondaryFirstArg = getFirstArg(secondaryNode)
const constraintType = getConstraintType(
secondaryFirstArg.val,
secondaryNode.callee.name as TooTip
secondaryNode.callee.name as ToolTip
)
const constraintLevel = getConstraintLevelFromSourceRange(
secondaryLine.range,

View File

@ -224,7 +224,7 @@ const key = 'c'
expect(recasted).toBe(code)
})
it('comments in a fn block', () => {
const code = `const myFn = () => {
const code = `fn myFn = () => {
// this is a comment
const yo = { a: { b: { c: '123' } } }

View File

@ -1,10 +1,12 @@
import { SourceRange } from 'lang/executor'
import { ProgramMemory, SourceRange } from 'lang/executor'
import { Selections } from 'useStore'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
let lastMessage = ''
@ -393,8 +395,6 @@ export class EngineConnection {
let videoTrack = mediaStream.getVideoTracks()[0]
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
// TODO(paultag): this needs type information from the KittyCAD typescript
// library once it's updated
let client_metrics: ClientMetrics = {
rtc_frames_decoded: 0,
rtc_frames_dropped: 0,
@ -422,12 +422,13 @@ export class EngineConnection {
videoTrackReport.framesReceived
client_metrics.rtc_frames_per_second =
videoTrackReport.framesPerSecond || 0
client_metrics.rtc_freeze_count = videoTrackReport.freezeCount
client_metrics.rtc_freeze_count =
videoTrackReport.freezeCount || 0
client_metrics.rtc_jitter_sec = videoTrackReport.jitter
client_metrics.rtc_keyframes_decoded =
videoTrackReport.keyFramesDecoded
client_metrics.rtc_total_freezes_duration_sec =
videoTrackReport.totalFreezesDuration
videoTrackReport.totalFreezesDuration || 0
} else if (videoTrackReport.type === 'transport') {
// videoTrackReport.bytesReceived,
// videoTrackReport.bytesSent,
@ -770,7 +771,8 @@ export class EngineCommandManager {
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
const cmd = command.cmd
if (
cmd.type === 'camera_drag_move' &&
(cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move') &&
this.engineConnection?.unreliableDataChannel
) {
cmd.sequence = this.outSequence
@ -882,7 +884,10 @@ export class EngineCommandManager {
}
return command.promise
}
async waitForAllCommands(): Promise<{
async waitForAllCommands(
ast?: Program,
programMemory?: ProgramMemory
): Promise<{
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
}> {
@ -891,9 +896,94 @@ export class EngineCommandManager {
) as PendingCommand[]
const proms = pendingCommands.map(({ promise }) => promise)
await Promise.all(proms)
if (ast && programMemory) {
await this.fixIdMappings(ast, programMemory)
}
return {
artifactMap: this.artifactMap,
sourceRangeMap: this.sourceRangeMap,
}
}
private async fixIdMappings(ast: Program, programMemory: ProgramMemory) {
/* 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') {
console.log('problem')
return
}
const pipeExpPath = getNodePathFromSourceRange(
ast,
originalArtifact.range
)
const pipeExp = getNodeFromPath<VariableDeclarator>(
ast,
pipeExpPath,
'VariableDeclarator'
).node
if (pipeExp.type !== 'VariableDeclarator') {
console.log('problem', pipeExp, pipeExpPath, ast)
return
}
const variableName = pipeExp.id.name
const memoryItem = programMemory.root[variableName]
if (!memoryItem) {
console.log('problem', variableName, programMemory)
return
} else if (memoryItem.type !== 'SketchGroup') {
console.log('problem', memoryItem, programMemory)
return
}
const relevantSegments = segments.filter(
({ command_id }: { command_id: string | null }) => command_id
)
if (memoryItem.value.length !== relevantSegments.length) {
console.log('problem', memoryItem.value, relevantSegments)
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]
this.artifactMap[engineSegment.command_id] = artifact
this.sourceRangeMap[engineSegment.command_id] = artifact.range
}
})
}
}

View File

@ -4,6 +4,7 @@ import {
addNewSketchLn,
getYComponent,
getXComponent,
addCloseToPipe,
} from './sketch'
import { parser_wasm } from '../abstractSyntaxTree'
import { getNodePathFromSourceRange } from '../queryAst'
@ -146,7 +147,7 @@ show(mySketch001)`
const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange)
expect(sourceStart).toBe(66)
const { modifiedAst } = addNewSketchLn({
let { modifiedAst } = addNewSketchLn({
node: ast,
programMemory,
to: [2, 3],
@ -160,12 +161,33 @@ show(mySketch001)`
],
})
// Enable rotations #152
const expectedCode = `const mySketch001 = startSketchAt([0, 0])
let expectedCode = `const mySketch001 = startSketchAt([0, 0])
// |> rx(45, %)
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> lineTo([2, 3], %)
show(mySketch001)
`
expect(recast(modifiedAst)).toBe(expectedCode)
modifiedAst = addCloseToPipe({
node: ast,
programMemory,
pathToNode: [
['body', ''],
[0, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'],
],
})
expectedCode = `const mySketch001 = startSketchAt([0, 0])
// |> rx(45, %)
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> close(%)
show(mySketch001)
`
expect(recast(modifiedAst)).toBe(expectedCode)
})

View File

@ -4,6 +4,7 @@ import {
SketchGroup,
SourceRange,
PathToNode,
MemoryItem,
} from '../executor'
import {
Program,
@ -19,7 +20,8 @@ import {
getNodeFromPathCurry,
getNodePathFromSourceRange,
} from '../queryAst'
import { GuiModes, toolTips, TooTip } from '../../useStore'
import { isLiteralArrayOrStatic } from './sketchcombos'
import { GuiModes, toolTips, ToolTip } from '../../useStore'
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
import { generateUuidFromHashSeed } from '../../lib/uuid'
@ -55,7 +57,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
}
export function createFirstArg(
sketchFn: TooTip,
sketchFn: ToolTip,
val: Value | [Value, Value] | [Value, Value, Value],
tag?: Value
): Value {
@ -197,7 +199,7 @@ export const line: SketchLineHelper = {
)
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
@ -293,7 +295,7 @@ export const xLineTo: SketchLineHelper = {
pathToNode
)
const newX = createLiteral(roundOff(to[0], 2))
if (callExpression.arguments?.[0]?.type === 'Literal') {
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newX
} else {
mutateObjExpProp(callExpression.arguments?.[0], newX, 'to')
@ -341,7 +343,7 @@ export const yLineTo: SketchLineHelper = {
pathToNode
)
const newY = createLiteral(roundOff(to[1], 2))
if (callExpression.arguments?.[0]?.type === 'Literal') {
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newY
} else {
mutateObjExpProp(callExpression.arguments?.[0], newY, 'to')
@ -391,7 +393,7 @@ export const xLine: SketchLineHelper = {
pathToNode
)
const newX = createLiteral(roundOff(to[0] - from[0], 2))
if (callExpression.arguments?.[0]?.type === 'Literal') {
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newX
} else {
mutateObjExpProp(callExpression.arguments?.[0], newX, 'length')
@ -435,7 +437,7 @@ export const yLine: SketchLineHelper = {
pathToNode
)
const newY = createLiteral(roundOff(to[1] - from[1], 2))
if (callExpression.arguments?.[0]?.type === 'Literal') {
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newY
} else {
mutateObjExpProp(callExpression.arguments?.[0], newY, 'length')
@ -538,7 +540,7 @@ export const angledLineOfXLength: SketchLineHelper = {
)
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const angle = createLiteral(roundOff(getAngle(from, to), 0))
const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1)
const newLine = createCallback
@ -611,7 +613,7 @@ export const angledLineOfYLength: SketchLineHelper = {
)
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const angle = createLiteral(roundOff(getAngle(from, to), 0))
const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1)
@ -868,7 +870,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
const varName = varDec.declarations[0].id.name
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup
const intersectPath = sketchGroup.value.find(
({ name }) => name === intersectTagName
({ name }: Path) => name === intersectTagName
)
let offset = 0
if (intersectPath) {
@ -941,17 +943,29 @@ interface CreateLineFnCallArgs {
programMemory: ProgramMemory
to: [number, number]
from: [number, number]
fnName: TooTip
fnName: ToolTip
pathToNode: PathToNode
}
export function compareVec2Epsilon(
vec1: [number, number],
vec2: [number, number]
) {
const compareEpsilon = 0.015625 // or 2^-6
const xDifference = Math.abs(vec1[0] - vec2[0])
const yDifference = Math.abs(vec1[0] - vec2[0])
return xDifference < compareEpsilon && yDifference < compareEpsilon
}
export function addNewSketchLn({
node: _node,
programMemory: previousProgramMemory,
to,
fnName,
pathToNode,
}: Omit<CreateLineFnCallArgs, 'from'>): { modifiedAst: Program } {
}: Omit<CreateLineFnCallArgs, 'from'>): {
modifiedAst: Program
} {
const node = JSON.parse(JSON.stringify(_node))
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
if (!add || !updateArgs) throw new Error('not a sketch line helper')
@ -965,11 +979,10 @@ export function addNewSketchLn({
>(node, pathToNode, 'PipeExpression')
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
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({
node,
previousProgramMemory,
@ -980,6 +993,29 @@ export function addNewSketchLn({
})
}
export function addCloseToPipe({
node,
pathToNode,
}: {
node: Program
programMemory: ProgramMemory
pathToNode: PathToNode
}) {
const _node = { ...node }
const closeExpression = createCallExpression('close', [
createPipeSubstitution(),
])
const pipeExpression = getNodeFromPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
).node
if (pipeExpression.type !== 'PipeExpression')
throw new Error('not a pipe expression')
pipeExpression.body = [...pipeExpression.body, closeExpression]
return _node
}
export function replaceSketchLine({
node,
programMemory,
@ -993,7 +1029,7 @@ export function replaceSketchLine({
node: Program
programMemory: ProgramMemory
sourceRange: SourceRange
fnName: TooTip
fnName: ToolTip
to: [number, number]
from: [number, number]
createCallback: TransformCallback
@ -1035,10 +1071,11 @@ export function addTagForSketchOnFace(
function isAngleLiteral(lineArugement: Value): boolean {
return lineArugement?.type === 'ArrayExpression'
? lineArugement.elements[0].type === 'Literal'
? isLiteralArrayOrStatic(lineArugement.elements[0])
: lineArugement?.type === 'ObjectExpression'
? lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
.type === 'Literal'
? isLiteralArrayOrStatic(
lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
)
: false
}
@ -1171,7 +1208,7 @@ function getFirstArgValuesForAngleFns(callExpression: CallExpression): {
const tag = firstArg.properties.find((p) => p.key.name === 'tag')?.value
const angle = firstArg.properties.find((p) => p.key.name === 'angle')?.value
const secondArgName = ['angledLineToX', 'angledLineToY'].includes(
callExpression?.callee?.name as TooTip
callExpression?.callee?.name as ToolTip
)
? 'to'
: 'length'

View File

@ -401,6 +401,11 @@ show(part001)`
programMemory.root['part001'] as SketchGroup,
[index, index]
).segment
expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' })
expect(segment).toEqual({
to: [0, 0.04],
from: [0, 0.04],
name: '',
type: 'base',
})
})
})

View File

@ -1,10 +1,10 @@
import { TooTip, toolTips } from '../../useStore'
import { ToolTip, toolTips } from '../../useStore'
import {
Program,
VariableDeclarator,
CallExpression,
} from '../abstractSyntaxTreeTypes'
import { SketchGroup, SourceRange } from '../executor'
import { SketchGroup, SourceRange, Path } from '../executor'
export function getSketchSegmentFromSourceRange(
sketchGroup: SketchGroup,
@ -20,10 +20,10 @@ export function getSketchSegmentFromSourceRange(
startSourceRange[1] >= rangeEnd &&
sketchGroup.start
)
return { segment: sketchGroup.start, index: -1 }
return { segment: { ...sketchGroup.start, type: 'base' }, index: -1 }
const lineIndex = sketchGroup.value.findIndex(
({ __geoMeta: { sourceRange } }) =>
({ __geoMeta: { sourceRange } }: Path) =>
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
)
const line = sketchGroup.value[lineIndex]
@ -67,7 +67,10 @@ export function isSketchVariablesLinked(
return false
const firstCallExp = // first in pipe expression or just the call expression
init?.type === 'CallExpression' ? init : (init?.body[0] as CallExpression)
if (!firstCallExp || !toolTips.includes(firstCallExp?.callee?.name as TooTip))
if (
!firstCallExp ||
!toolTips.includes(firstCallExp?.callee?.name as ToolTip)
)
return false
// convention for sketch fns is that the second argument is the sketch group
const secondArg = firstCallExp?.arguments[1]

View File

@ -9,7 +9,7 @@ import {
getConstraintLevelFromSourceRange,
} from './sketchcombos'
import { initPromise } from '../rust'
import { Selections, TooTip } from '../../useStore'
import { Selections, ToolTip } from '../../useStore'
import { enginelessExecutor } from '../../lib/testHelpers'
import { recast } from '../../lang/recast'
@ -68,7 +68,7 @@ function getConstraintTypeFromSourceHelper(
Value,
Value
]
const fnName = (ast.body[0] as any).expression.callee.name as TooTip
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
return getConstraintType(args, fnName)
}
function getConstraintTypeFromSourceHelper2(
@ -76,7 +76,7 @@ function getConstraintTypeFromSourceHelper2(
): ReturnType<typeof getConstraintType> {
const ast = parser_wasm(code)
const arg = (ast.body[0] as any).expression.arguments[0] as Value
const fnName = (ast.body[0] as any).expression.callee.name as TooTip
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
return getConstraintType(arg, fnName)
}

View File

@ -1,5 +1,5 @@
import { TransformCallback } from './stdTypes'
import { Selections, toolTips, TooTip, Selection } from '../../useStore'
import { Selections, toolTips, ToolTip, Selection } from '../../useStore'
import {
CallExpression,
Program,
@ -28,6 +28,7 @@ import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
import { PathToNode, ProgramMemory } from '../executor'
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
import { MemoryItem } from 'wasm-lib/kcl/bindings/MemoryItem'
type LineInputsType =
| 'xAbsolute'
@ -53,7 +54,7 @@ export type ConstraintType =
| 'setAngleBetween'
function createCallWrapper(
a: TooTip,
a: ToolTip,
val: [Value, Value] | Value,
tag?: Value,
valueUsedInTransform?: number
@ -100,7 +101,7 @@ function intersectCallWrapper({
}
export type TransformInfo = {
tooltip: TooTip
tooltip: ToolTip
createNode: (a: {
varValA: Value // x / angle
varValB: Value // y / length or x y for angledLineOfXlength etc
@ -111,7 +112,7 @@ export type TransformInfo = {
}
type TransformMap = {
[key in TooTip]?: {
[key in ToolTip]?: {
[key in LineInputsType | 'free']?: {
[key in ConstraintType]?: TransformInfo
}
@ -1094,12 +1095,12 @@ export function getRemoveConstraintsTransform(
sketchFnExp: CallExpression,
constraintType: ConstraintType
): TransformInfo | false {
let name = sketchFnExp.callee.name as TooTip
let name = sketchFnExp.callee.name as ToolTip
if (!toolTips.includes(name)) {
return false
}
const xyLineMap: {
[key in TooTip]?: TooTip
[key in ToolTip]?: ToolTip
} = {
xLine: 'line',
yLine: 'line',
@ -1136,27 +1137,18 @@ export function getRemoveConstraintsTransform(
// check if the function is locked down and so can't be transformed
const firstArg = getFirstArg(sketchFnExp)
if (Array.isArray(firstArg.val)) {
const [a, b] = firstArg.val
if (a?.type !== 'Literal' || b?.type !== 'Literal') {
if (isNotLiteralArrayOrStatic(firstArg.val)) {
return transformInfo
}
} else {
if (firstArg.val?.type !== 'Literal') {
return transformInfo
}
}
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) &&
firstArg.val?.[0]?.type === 'Literal' &&
firstArg.val?.[1]?.type === 'Literal'
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) {
return false
}
const isOneValFree =
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isOneValFree) {
return transformInfo
}
@ -1175,37 +1167,24 @@ function getTransformMapPath(
constraintType: ConstraintType
):
| {
toolTip: TooTip
toolTip: ToolTip
lineInputType: LineInputsType | 'free'
constraintType: ConstraintType
}
| false {
const name = sketchFnExp.callee.name as TooTip
const name = sketchFnExp.callee.name as ToolTip
if (!toolTips.includes(name)) {
return false
}
// check if the function is locked down and so can't be transformed
const firstArg = getFirstArg(sketchFnExp)
if (Array.isArray(firstArg.val)) {
const [a, b] = firstArg.val
if (a?.type !== 'Literal' && b?.type !== 'Literal') {
if (isNotLiteralArrayOrStatic(firstArg.val)) {
return false
}
} else {
if (firstArg.val?.type !== 'Literal') {
return false
}
}
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) &&
firstArg.val?.[0]?.type === 'Literal' &&
firstArg.val?.[1]?.type === 'Literal'
const isOneValFree =
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
if (isTwoValFree || isOneValFree) {
if (isLiteralArrayOrStatic(firstArg.val)) {
const info = transformMap?.[name]?.free?.[constraintType]
if (info)
return {
@ -1246,7 +1225,7 @@ export function getTransformInfo(
export function getConstraintType(
val: Value | [Value, Value] | [Value, Value, Value],
fnName: TooTip
fnName: ToolTip
): LineInputsType | null {
// this function assumes that for two val sketch functions that one arg is locked down not both
// and for one val sketch functions that the arg is NOT locked down
@ -1259,7 +1238,7 @@ export function getConstraintType(
if (fnName === 'xLineTo') return 'yAbsolute'
if (fnName === 'yLineTo') return 'xAbsolute'
} else {
const isFirstArgLockedDown = val?.[0]?.type !== 'Literal'
const isFirstArgLockedDown = isNotLiteralArrayOrStatic(val[0])
if (fnName === 'line')
return isFirstArgLockedDown ? 'xRelative' : 'yRelative'
if (fnName === 'lineTo')
@ -1452,7 +1431,7 @@ export function transformAstSketchLines({
const varName = varDec.id.name
const sketchGroup = programMemory.root?.[varName]
if (!sketchGroup || sketchGroup.type !== 'sketchGroup')
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')
throw new Error('not a sketch group')
const seg = getSketchSegmentFromSourceRange(sketchGroup, range).segment
const referencedSegment = referencedSegmentRange
@ -1466,7 +1445,7 @@ export function transformAstSketchLines({
programMemory,
sourceRange: range,
referencedSegment,
fnName: transformTo || (callExp.callee.name as TooTip),
fnName: transformTo || (callExp.callee.name as ToolTip),
to,
from,
createCallback: callBack({
@ -1532,29 +1511,52 @@ export function getConstraintLevelFromSourceRange(
getNodePathFromSourceRange(ast, cursorRange),
'CallExpression'
)
const name = sketchFnExp?.callee?.name as TooTip
const name = sketchFnExp?.callee?.name as ToolTip
if (!toolTips.includes(name)) return 'free'
const firstArg = getFirstArg(sketchFnExp)
// check if the function is fully constrained
if (Array.isArray(firstArg.val)) {
const [a, b] = firstArg.val
if (a?.type !== 'Literal' && b?.type !== 'Literal') return 'full'
} else {
if (firstArg.val?.type !== 'Literal') return 'full'
if (isNotLiteralArrayOrStatic(firstArg.val)) {
return 'full'
}
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) &&
firstArg.val?.[0]?.type === 'Literal' &&
firstArg.val?.[1]?.type === 'Literal'
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
const isOneValFree =
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) return 'free'
if (isOneValFree) return 'partial'
return 'partial'
}
export function isLiteralArrayOrStatic(
val: Value | [Value, Value] | [Value, Value, Value] | undefined
): boolean {
if (!val) return false
if (Array.isArray(val)) {
const [a, b] = val
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
}
return (
val.type === 'Literal' ||
(val.type === 'UnaryExpression' && val.argument.type === 'Literal')
)
}
export function isNotLiteralArrayOrStatic(
val: Value | [Value, Value] | [Value, Value, Value]
): boolean {
if (Array.isArray(val)) {
const [a, b] = val
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)
}
return (
(val.type !== 'Literal' && val.type !== 'UnaryExpression') ||
(val.type === 'UnaryExpression' && val.argument.type !== 'Literal')
)
}

View File

@ -1,6 +1,6 @@
import { ProgramMemory, Path, SourceRange } from '../executor'
import { Program, Value } from '../abstractSyntaxTreeTypes'
import { TooTip } from '../../useStore'
import { ToolTip } from '../../useStore'
import { PathToNode } from '../executor'
import { EngineCommandManager } from './engineConnection'
@ -45,7 +45,7 @@ export type TransformCallback = (
}
export type SketchCallTransfromMap = {
[key in TooTip]: TransformCallback
[key in ToolTip]: TransformCallback
}
export interface SketchLineHelper {

View File

@ -131,10 +131,12 @@ const yi=45`
})
it('test negative and decimal numbers', () => {
expect(stringSummaryLexer('-1')).toEqual([
"number '-1' from 0 to 2",
"operator '-' from 0 to 1",
"number '1' from 1 to 2",
])
expect(stringSummaryLexer('-1.5')).toEqual([
"number '-1.5' from 0 to 4",
"operator '-' from 0 to 1",
"number '1.5' from 1 to 4",
])
expect(stringSummaryLexer('1.5')).toEqual([
"number '1.5' from 0 to 3",
@ -158,10 +160,12 @@ const yi=45`
"whitespace ' ' from 3 to 4",
"operator '+' from 4 to 5",
"whitespace ' ' from 5 to 6",
"number '-2.5' from 6 to 10",
"operator '-' from 6 to 7",
"number '2.5' from 7 to 10",
])
expect(stringSummaryLexer('-1.5 + 2.5')).toEqual([
"number '-1.5' from 0 to 4",
"operator '-' from 0 to 1",
"number '1.5' from 1 to 4",
"whitespace ' ' from 4 to 5",
"operator '+' from 5 to 6",
"whitespace ' ' from 6 to 7",

View File

@ -49,7 +49,7 @@ class MockEngineCommandManager {
export async function enginelessExecutor(
ast: Program,
pm: ProgramMemory = { root: {} }
pm: ProgramMemory = { root: {}, return: null }
): Promise<ProgramMemory> {
const mockEngineCommandManager = new MockEngineCommandManager({
setIsStreamReady: () => {},
@ -64,7 +64,7 @@ export async function enginelessExecutor(
export async function executor(
ast: Program,
pm: ProgramMemory = { root: {} }
pm: ProgramMemory = { root: {}, return: null }
): Promise<ProgramMemory> {
const engineCommandManager = new EngineCommandManager({
setIsStreamReady: () => {},
@ -75,6 +75,6 @@ export async function executor(
await engineCommandManager.waitForReady
engineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, engineCommandManager)
await engineCommandManager.waitForAllCommands()
await engineCommandManager.waitForAllCommands(ast, programMemory)
return programMemory
}

View File

@ -57,7 +57,7 @@ export function throttle<T>(
}
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
export function defferExecution<T>(func: (args: T) => any, wait: number) {
export function deferExecution<T>(func: (args: T) => any, wait: number) {
let timeout: ReturnType<typeof setTimeout> | null
let latestArgs: T
@ -66,7 +66,7 @@ export function defferExecution<T>(func: (args: T) => any, wait: number) {
func(latestArgs)
}
function deffered(args: T) {
function deferred(args: T) {
latestArgs = args
if (timeout) {
clearTimeout(timeout)
@ -74,7 +74,7 @@ export function defferExecution<T>(func: (args: T) => any, wait: number) {
timeout = setTimeout(later, wait)
}
return deffered
return deferred
}
export function getNormalisedCoordinates({

View File

@ -2,6 +2,8 @@ import { createMachine, assign } from 'xstate'
import { Models } from '@kittycad/lib'
import withBaseURL from '../lib/withBaseURL'
import { CommandBarMeta } from '../lib/commands'
import { isTauri } from 'lib/isTauri'
import { invoke } from '@tauri-apps/api'
const SKIP_AUTH =
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
@ -115,16 +117,25 @@ async function getUser(context: UserContext) {
const headers: { [key: string]: string } = {
'Content-Type': 'application/json',
}
if (!context.token && '__TAURI__' in window) throw 'not log in'
if (!context.token && isTauri()) throw new Error('No token found')
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
if (SKIP_AUTH) return LOCAL_USER
const response = await fetch(url, {
const userPromise = !isTauri()
? fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
.then((res) => res.json())
.catch((err) => console.error('error from Browser getUser', err))
: invoke<Models['User_type'] | Record<'error_code', unknown>>('get_user', {
token: context.token,
}).catch((err) => console.error('error from Tauri getUser', err))
const user = await userPromise
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user

View File

@ -4,6 +4,7 @@ import { addLineHighlight, EditorView } from './editor/highlightextension'
import { parser_wasm } from './lang/abstractSyntaxTree'
import { Program } from './lang/abstractSyntaxTreeTypes'
import { getNodeFromPath } from './lang/queryAst'
import { enginelessExecutor } from './lib/testHelpers'
import {
ProgramMemory,
Position,
@ -13,13 +14,10 @@ import {
} from './lang/executor'
import { recast } from './lang/recast'
import { EditorSelection } from '@codemirror/state'
import {
ArtifactMap,
SourceRangeMap,
EngineCommandManager,
} from './lang/std/engineConnection'
import { EngineCommandManager } from './lang/std/engineConnection'
import { KCLError } from './lang/errors'
import { defferExecution } from 'lib/utils'
import { deferExecution } from 'lib/utils'
import { _executor } from './lang/executor'
export type Selection = {
type: 'default' | 'line-end' | 'line-mid'
@ -29,7 +27,7 @@ export type Selections = {
otherSelections: ('y-axis' | 'x-axis' | 'z-axis')[]
codeBasedSelections: Selection[]
}
export type TooTip =
export type ToolTip =
| 'lineTo'
| 'line'
| 'angledLine'
@ -59,7 +57,7 @@ export const toolTips = [
'xLineTo',
'yLineTo',
'angledLineThatIntersects',
] as any as TooTip[]
] as any as ToolTip[]
export type GuiModes =
| {
@ -67,7 +65,7 @@ export type GuiModes =
}
| {
mode: 'sketch'
sketchMode: TooTip
sketchMode: ToolTip
isTooltip: true
waitingFirstClick: boolean
rotation: Rotation
@ -123,40 +121,37 @@ export interface StoreState {
setGuiMode: (guiMode: GuiModes) => void
logs: string[]
addLog: (log: string) => void
resetLogs: () => void
setLogs: (logs: string[]) => void
kclErrors: KCLError[]
addKCLError: (err: KCLError) => void
setErrors: (errors: KCLError[]) => void
resetKCLErrors: () => void
ast: Program
setAst: (ast: Program) => void
executeAst: (ast?: Program) => void
executeAstMock: (ast?: Program) => void
updateAst: (
ast: Program,
execute: boolean,
optionalParams?: {
focusPath?: PathToNode
callBack?: (ast: Program) => void
}
) => void
updateAstAsync: (ast: Program, focusPath?: PathToNode) => void
updateAstAsync: (
ast: Program,
reexecute: boolean,
focusPath?: PathToNode
) => void
code: string
defferedCode: string
setCode: (code: string) => void
defferedSetCode: (code: string) => void
deferredSetCode: (code: string) => void
executeCode: (code?: string) => void
formatCode: () => void
errorState: {
isError: boolean
error: string
}
setError: (error?: string) => void
programMemory: ProgramMemory
setProgramMemory: (programMemory: ProgramMemory) => void
isShiftDown: boolean
setIsShiftDown: (isShiftDown: boolean) => void
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
setArtifactNSourceRangeMaps: (a: {
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
}) => void
engineCommandManager?: EngineCommandManager
setEngineCommandManager: (engineCommandManager: EngineCommandManager) => void
mediaStream?: MediaStream
@ -197,10 +192,13 @@ let pendingAstUpdates: number[] = []
export const useStore = create<StoreState>()(
persist(
(set, get) => {
const setDefferedCode = defferExecution(
(code: string) => set({ defferedCode: code }),
600
)
// We defer this so that likely our ast has caught up to the code.
// If we are making changes that are not reflected in the ast, we
// should not be updating the ast.
const setDeferredCode = deferExecution((code: string) => {
set({ code })
get().executeCode(code)
}, 600)
return {
editorView: null,
setEditorView: (editorView) => {
@ -214,6 +212,22 @@ export const useStore = create<StoreState>()(
editorView.dispatch({ effects: addLineHighlight.of(selection) })
}
},
executeCode: async (code) => {
const result = await executeCode({
code: code || get().code,
lastAst: get().ast,
engineCommandManager: get().engineCommandManager,
})
if (!result.isChange) {
return
}
set({
ast: result.ast,
logs: result.logs,
kclErrors: result.errors,
programMemory: result.programMemory,
})
},
setCursor: (selections) => {
const { editorView } = get()
if (!editorView) return
@ -243,7 +257,10 @@ export const useStore = create<StoreState>()(
get().setCursor({
otherSelections: currestSelections.otherSelections,
codeBasedSelections: [
{ range: [0, code.length - 1], type: 'default' },
{
range: [0, code.length ? code.length - 1 : 0],
type: 'default',
},
],
})
return
@ -277,8 +294,8 @@ export const useStore = create<StoreState>()(
set((state) => ({ logs: [...state.logs, log] }))
}
},
resetLogs: () => {
set({ logs: [] })
setLogs: (logs) => {
set({ logs })
},
kclErrors: [],
addKCLError: (e) => {
@ -287,6 +304,9 @@ export const useStore = create<StoreState>()(
resetKCLErrors: () => {
set({ kclErrors: [] })
},
setErrors: (errors) => {
set({ kclErrors: errors })
},
ast: {
start: 0,
end: 0,
@ -299,7 +319,47 @@ export const useStore = create<StoreState>()(
setAst: (ast) => {
set({ ast })
},
updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
executeAst: async (ast) => {
const _ast = ast || get().ast
if (!get().isStreamReady) return
const engineCommandManager = get().engineCommandManager!
if (!engineCommandManager) return
set({ isExecuting: true })
const { logs, errors, programMemory } = await executeAst({
ast: _ast,
engineCommandManager,
})
set({
programMemory,
logs,
kclErrors: errors,
isExecuting: false,
})
},
executeAstMock: async (ast) => {
const _ast = ast || get().ast
if (!get().isStreamReady) return
const engineCommandManager = get().engineCommandManager!
if (!engineCommandManager) return
const { logs, errors, programMemory } = await executeAst({
ast: _ast,
engineCommandManager,
useFakeExecutor: true,
})
set({
programMemory,
logs,
kclErrors: errors,
isExecuting: false,
})
},
updateAst: async (
ast,
reexecute,
{ focusPath, callBack = () => {} } = {}
) => {
const newCode = recast(ast)
const astWithUpdatedSource = parser_wasm(newCode)
callBack(astWithUpdatedSource)
@ -307,7 +367,6 @@ export const useStore = create<StoreState>()(
set({
ast: astWithUpdatedSource,
code: newCode,
defferedCode: newCode,
})
if (focusPath) {
const { node } = getNodeFromPath<any>(
@ -328,24 +387,33 @@ export const useStore = create<StoreState>()(
})
})
}
if (reexecute) {
// Call execute on the set ast.
get().executeAst(astWithUpdatedSource)
} else {
// When we don't re-execute, we still want to update the program
// memory with the new ast. So we will hit the mock executor
// instead.
get().executeAstMock(astWithUpdatedSource)
}
},
updateAstAsync: async (ast, focusPath) => {
updateAstAsync: async (ast, reexecute, focusPath) => {
// clear any pending updates
pendingAstUpdates.forEach((id) => clearTimeout(id))
pendingAstUpdates = []
// setup a new update
pendingAstUpdates.push(
setTimeout(() => {
get().updateAst(ast, { focusPath })
get().updateAst(ast, reexecute, { focusPath })
}, 100) as unknown as number
)
},
code: '',
defferedCode: '',
setCode: (code) => set({ code, defferedCode: code }),
defferedSetCode: (code) => {
setCode: (code) => set({ code }),
deferredSetCode: (code) => {
set({ code })
setDefferedCode(code)
setDeferredCode(code)
},
formatCode: async () => {
const code = get().code
@ -353,20 +421,10 @@ export const useStore = create<StoreState>()(
const newCode = recast(ast)
set({ code: newCode, ast })
},
errorState: {
isError: false,
error: '',
},
setError: (error = '') => {
set({ errorState: { isError: !!error, error } })
},
programMemory: { root: {}, pendingMemory: {} },
programMemory: { root: {}, return: null },
setProgramMemory: (programMemory) => set({ programMemory }),
isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
artifactMap: {},
sourceRangeMap: {},
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
setEngineCommandManager: (engineCommandManager) =>
set({ engineCommandManager }),
setMediaStream: (mediaStream) => set({ mediaStream }),
@ -409,9 +467,165 @@ export const useStore = create<StoreState>()(
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) =>
['code', 'defferedCode', 'openPanes'].includes(key)
['code', 'openPanes'].includes(key)
)
),
}
)
)
const defaultProgramMemory: ProgramMemory['root'] = {
_0: {
type: 'UserVal',
value: 0,
__meta: [],
},
_90: {
type: 'UserVal',
value: 90,
__meta: [],
},
_180: {
type: 'UserVal',
value: 180,
__meta: [],
},
_270: {
type: 'UserVal',
value: 270,
__meta: [],
},
PI: {
type: 'UserVal',
value: Math.PI,
__meta: [],
},
}
async function executeCode({
engineCommandManager,
code,
lastAst,
}: {
code: string
lastAst: Program
engineCommandManager?: EngineCommandManager
}): Promise<
| {
logs: string[]
errors: KCLError[]
programMemory: ProgramMemory
ast: Program
isChange: true
}
| { isChange: false }
> {
let ast: Program
try {
ast = parser_wasm(code)
} catch (e) {
let errors: KCLError[] = []
let logs: string[] = [JSON.stringify(e)]
if (e instanceof KCLError) {
errors = [e]
logs = []
if (e.msg === 'file is empty') engineCommandManager?.endSession()
}
return {
isChange: true,
logs,
errors,
programMemory: {
root: {},
return: null,
},
ast: {
start: 0,
end: 0,
body: [],
nonCodeMeta: {
noneCodeNodes: {},
start: null,
},
},
}
}
// Check if the ast we have is equal to the ast in the storage.
// If it is, we don't need to update the ast.
if (!engineCommandManager || JSON.stringify(ast) === JSON.stringify(lastAst))
return { isChange: false }
const { logs, errors, programMemory } = await executeAst({
ast,
engineCommandManager,
})
return {
ast,
logs,
errors,
programMemory,
isChange: true,
}
}
async function executeAst({
ast,
engineCommandManager,
useFakeExecutor = false,
}: {
ast: Program
engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean
}): Promise<{
logs: string[]
errors: KCLError[]
programMemory: ProgramMemory
}> {
try {
if (!useFakeExecutor) {
engineCommandManager.endSession()
engineCommandManager.startNewSession()
}
const programMemory = await (useFakeExecutor
? enginelessExecutor(ast, {
root: defaultProgramMemory,
return: null,
})
: _executor(
ast,
{
root: defaultProgramMemory,
return: null,
},
engineCommandManager
))
await engineCommandManager.waitForAllCommands(ast, programMemory)
return {
logs: [],
errors: [],
programMemory,
}
} catch (e: any) {
if (e instanceof KCLError) {
return {
errors: [e],
logs: [],
programMemory: {
root: {},
return: null,
},
}
} else {
console.log(e)
return {
logs: [e],
errors: [],
programMemory: {
root: {},
return: null,
},
}
}
}
}

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

@ -150,7 +150,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -306,7 +306,7 @@ dependencies = [
"serde",
"serde_bytes",
"serde_json",
"time 0.3.27",
"time",
"uuid",
]
@ -363,18 +363,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.26"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"time 0.1.45",
"wasm-bindgen",
"winapi",
"windows-targets 0.48.5",
]
[[package]]
@ -390,30 +387,12 @@ dependencies = [
[[package]]
name = "clap"
version = "3.2.25"
version = "4.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"atty",
"bitflags 1.3.2",
"clap_derive 3.2.25",
"clap_lex 0.2.4",
"indexmap 1.9.3",
"once_cell",
"strsim",
"termcolor",
"textwrap",
"unicase",
]
[[package]]
name = "clap"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6"
checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6"
dependencies = [
"clap_builder",
"clap_derive 4.4.2",
"clap_derive",
]
[[package]]
@ -424,25 +403,13 @@ checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
dependencies = [
"anstream",
"anstyle",
"clap_lex 0.5.1",
"clap_lex",
"strsim",
"terminal_size",
"unicase",
"unicode-width",
]
[[package]]
name = "clap_derive"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "clap_derive"
version = "4.4.2"
@ -452,16 +419,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
"syn 2.0.33",
]
[[package]]
@ -645,7 +603,7 @@ dependencies = [
"quote",
"serde",
"serde_tokenstream",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -659,7 +617,7 @@ dependencies = [
"quote",
"serde",
"serde_tokenstream",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -933,7 +891,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -985,7 +943,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"wasm-bindgen",
]
@ -1244,7 +1202,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
@ -1255,6 +1212,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
"serde",
]
[[package]]
@ -1269,6 +1227,17 @@ dependencies = [
"web-sys",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi 0.3.2",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "ipnet"
version = "2.8.0"
@ -1282,7 +1251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi 0.3.2",
"rustix",
"rustix 0.38.9",
"windows-sys 0.48.0",
]
@ -1345,7 +1314,7 @@ version = "0.1.26"
dependencies = [
"anyhow",
"bson",
"clap 4.4.2",
"clap",
"dashmap",
"derive-docs 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"expectorate",
@ -1393,7 +1362,7 @@ dependencies = [
"rand",
"reqwest",
"reqwest-conditional-middleware",
"reqwest-middleware 0.2.3",
"reqwest-middleware",
"reqwest-retry",
"reqwest-tracing",
"schemars",
@ -1447,6 +1416,12 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "linux-raw-sys"
version = "0.4.5"
@ -1554,7 +1529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"windows-sys 0.48.0",
]
@ -1675,22 +1650,22 @@ checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
[[package]]
name = "openapitor"
version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120168eae5b6485690af708bd1030547df62cca10a643763d416ab0e6831decb"
version = "0.0.9"
source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#3d74c1dfb41146a268a644e9fde2c19a8cd66895"
dependencies = [
"Inflector",
"anyhow",
"chrono",
"clap 3.2.25",
"clap",
"data-encoding",
"format_serde_error",
"futures-util",
"http",
"indexmap 1.9.3",
"indexmap 2.0.0",
"json-patch",
"log",
"numeral",
"once_cell",
"openapiv3",
"phonenumber",
"proc-macro2",
@ -1698,7 +1673,7 @@ dependencies = [
"rand",
"regex",
"reqwest",
"reqwest-middleware 0.1.6",
"reqwest-middleware",
"rustfmt-wrapper",
"schemars",
"serde",
@ -1711,18 +1686,17 @@ dependencies = [
"slog-stdlog",
"slog-term",
"thiserror",
"tokio",
"url",
"uuid",
]
[[package]]
name = "openapiv3"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1a9f106eb0a780abd17ba9fca8e0843e3461630bcbe2af0ad4d5d3ba4e9aa4"
checksum = "75e56d5c441965b6425165b7e3223cc933ca469834f4a8b4786817a1f9dc4f13"
dependencies = [
"indexmap 1.9.3",
"indexmap 2.0.0",
"serde",
"serde_json",
]
@ -1752,12 +1726,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "os_str_bytes"
version = "6.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
[[package]]
name = "parking_lot"
version = "0.11.2"
@ -1829,7 +1797,7 @@ dependencies = [
"regex",
"regex-syntax 0.7.4",
"structmeta",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -1891,7 +1859,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -1967,9 +1935,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.66"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [
"unicode-ident",
]
@ -2183,26 +2151,10 @@ checksum = "59e50a2e70970896c99d1b8f20ddc30a70b30d3ac6e619a03a8353b64a49b277"
dependencies = [
"async-trait",
"reqwest",
"reqwest-middleware 0.2.3",
"reqwest-middleware",
"task-local-extensions",
]
[[package]]
name = "reqwest-middleware"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69539cea4148dce683bec9dc95be3f0397a9bb2c248a49c8296a9d21659a8cdd"
dependencies = [
"anyhow",
"async-trait",
"futures",
"http",
"reqwest",
"serde",
"task-local-extensions",
"thiserror",
]
[[package]]
name = "reqwest-middleware"
version = "0.2.3"
@ -2233,7 +2185,7 @@ dependencies = [
"hyper",
"parking_lot 0.11.2",
"reqwest",
"reqwest-middleware 0.2.3",
"reqwest-middleware",
"retry-policies",
"task-local-extensions",
"tokio",
@ -2253,7 +2205,7 @@ dependencies = [
"matchit",
"opentelemetry",
"reqwest",
"reqwest-middleware 0.2.3",
"reqwest-middleware",
"task-local-extensions",
"tracing",
"tracing-opentelemetry",
@ -2310,6 +2262,20 @@ dependencies = [
"toolchain_find",
]
[[package]]
name = "rustix"
version = "0.37.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
dependencies = [
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys 0.3.8",
"windows-sys 0.48.0",
]
[[package]]
name = "rustix"
version = "0.38.9"
@ -2319,7 +2285,7 @@ dependencies = [
"bitflags 2.4.0",
"errno",
"libc",
"linux-raw-sys",
"linux-raw-sys 0.4.5",
"windows-sys 0.48.0",
]
@ -2484,9 +2450,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.187"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7fe14252655bd1e578af19f5fa00fe02fd0013b100ca6b49fde31c41bae4c"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
@ -2502,13 +2468,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.187"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e46b2a6ca578b3f1d4501b12f78ed4692006d79d82a1a7c561c12dbc3d625eb8"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -2524,9 +2490,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.105"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
dependencies = [
"indexmap 2.0.0",
"itoa",
@ -2542,7 +2508,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -2554,7 +2520,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -2677,7 +2643,7 @@ dependencies = [
"serde",
"serde_json",
"slog",
"time 0.3.27",
"time",
]
[[package]]
@ -2712,7 +2678,7 @@ dependencies = [
"slog",
"term",
"thread_local",
"time 0.3.27",
"time",
]
[[package]]
@ -2771,7 +2737,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -2782,7 +2748,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -2798,9 +2764,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.29"
version = "2.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
dependencies = [
"proc-macro2",
"quote",
@ -2837,7 +2803,7 @@ dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"rustix",
"rustix 0.38.9",
"windows-sys 0.48.0",
]
@ -2862,32 +2828,33 @@ dependencies = [
]
[[package]]
name = "textwrap"
version = "0.16.0"
name = "terminal_size"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
dependencies = [
"unicode-width",
"rustix 0.37.23",
"windows-sys 0.48.0",
]
[[package]]
name = "thiserror"
version = "1.0.47"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.47"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -2911,17 +2878,6 @@ dependencies = [
"weezl",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "time"
version = "0.3.27"
@ -2994,7 +2950,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -3110,7 +3066,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -3139,7 +3095,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
]
[[package]]
@ -3225,7 +3181,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
"termcolor",
]
@ -3400,12 +3356,6 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -3433,7 +3383,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
"wasm-bindgen-shared",
]
@ -3468,7 +3418,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"syn 2.0.33",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]

View File

@ -12,7 +12,7 @@ bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
serde_json = "1.0.93"
serde_json = "1.0.106"
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"

View File

@ -14,11 +14,11 @@ proc-macro = true
convert_case = "0.6.0"
proc-macro2 = "1"
quote = "1"
serde = { version = "1.0.186", features = ["derive"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.29", features = ["full"] }
syn = { version = "2.0.33", features = ["full"] }
[dev-dependencies]
expectorate = "1.0.7"
openapitor = "0.0.5"
openapitor = { git = "https://github.com/KittyCAD/kittycad.rs", branch = "main" }
pretty_assertions = "1.4.0"

View File

@ -9,7 +9,7 @@ license = "MIT"
[dependencies]
anyhow = { version = "1.0.75", features = ["backtrace"] }
clap = { version = "4.4.2", features = ["cargo", "derive", "env", "unicode"] }
clap = { version = "4.4.3", features = ["cargo", "derive", "env", "unicode"] }
dashmap = "5.5.3"
derive-docs = { version = "0.1.3" }
#derive-docs = { path = "../derive-docs" }
@ -18,9 +18,9 @@ lazy_static = "1.4.0"
parse-display = "0.8.2"
regex = "1.7.1"
schemars = { version = "0.8", features = ["impl_json_schema", "url", "uuid1"] }
serde = {version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
thiserror = "1.0.47"
serde = {version = "1.0.188", features = ["derive"] }
serde_json = "1.0.106"
thiserror = "1.0.48"
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }

View File

@ -12,7 +12,7 @@ use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, DocumentSymbol, R
use crate::{
engine::EngineConnection,
errors::{KclError, KclErrorDetails},
executor::{MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange},
executor::{MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange, UserVal},
parser::PIPE_OPERATOR,
};
@ -449,6 +449,7 @@ pub enum BinaryPart {
BinaryExpression(Box<BinaryExpression>),
CallExpression(Box<CallExpression>),
UnaryExpression(Box<UnaryExpression>),
MemberExpression(Box<MemberExpression>),
}
impl From<BinaryPart> for crate::executor::SourceRange {
@ -471,6 +472,7 @@ impl BinaryPart {
BinaryPart::BinaryExpression(binary_expression) => binary_expression.recast(options),
BinaryPart::CallExpression(call_expression) => call_expression.recast(options, indentation_level, false),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options),
BinaryPart::MemberExpression(member_expression) => member_expression.recast(),
}
}
@ -481,6 +483,7 @@ impl BinaryPart {
BinaryPart::BinaryExpression(binary_expression) => binary_expression.start(),
BinaryPart::CallExpression(call_expression) => call_expression.start(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.start(),
BinaryPart::MemberExpression(member_expression) => member_expression.start(),
}
}
@ -491,6 +494,7 @@ impl BinaryPart {
BinaryPart::BinaryExpression(binary_expression) => binary_expression.end(),
BinaryPart::CallExpression(call_expression) => call_expression.end(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.end(),
BinaryPart::MemberExpression(member_expression) => member_expression.end(),
}
}
@ -523,6 +527,7 @@ impl BinaryPart {
source_ranges: vec![unary_expression.into()],
}))
}
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(memory),
}
}
@ -536,6 +541,9 @@ impl BinaryPart {
}
BinaryPart::CallExpression(call_expression) => call_expression.get_hover_value_for_position(pos, code),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_hover_value_for_position(pos, code),
BinaryPart::MemberExpression(member_expression) => {
member_expression.get_hover_value_for_position(pos, code)
}
}
}
@ -553,6 +561,9 @@ impl BinaryPart {
BinaryPart::UnaryExpression(ref mut unary_expression) => {
unary_expression.rename_identifiers(old_name, new_name)
}
BinaryPart::MemberExpression(ref mut member_expression) => {
member_expression.rename_identifiers(old_name, new_name)
}
}
}
}
@ -751,12 +762,7 @@ impl CallExpression {
})
})?
.clone(),
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::MemberExpression(member_expression) => member_expression.get_result(memory)?,
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
@ -1082,23 +1088,23 @@ impl Literal {
impl From<Literal> for MemoryItem {
fn from(literal: Literal) -> Self {
MemoryItem::UserVal {
MemoryItem::UserVal(UserVal {
value: literal.value.clone(),
meta: vec![Metadata {
source_range: literal.into(),
}],
}
})
}
}
impl From<&Box<Literal>> for MemoryItem {
fn from(literal: &Box<Literal>) -> Self {
MemoryItem::UserVal {
MemoryItem::UserVal(UserVal {
value: literal.value.clone(),
meta: vec![Metadata {
source_range: literal.into(),
}],
}
})
}
}
@ -1227,12 +1233,7 @@ impl ArrayExpression {
source_ranges: vec![pipe_substitution.into()],
}));
}
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::MemberExpression(member_expression) => member_expression.get_result(memory)?,
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
@ -1245,12 +1246,12 @@ impl ArrayExpression {
results.push(result);
}
Ok(MemoryItem::UserVal {
Ok(MemoryItem::UserVal(UserVal {
value: results.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
})
}))
}
/// Rename all identifiers that have the old name to the new given name.
@ -1370,12 +1371,12 @@ impl ObjectExpression {
object.insert(property.key.name.clone(), result.get_json_value()?);
}
Ok(MemoryItem::UserVal {
Ok(MemoryItem::UserVal(UserVal {
value: object.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
})
}))
}
/// Rename all identifiers that have the old name to the new given name.
@ -1554,6 +1555,38 @@ impl MemberExpression {
None
}
pub fn get_result_array(&self, memory: &mut ProgramMemory, index: usize) -> Result<MemoryItem, KclError> {
let array = match &self.object {
MemberObject::MemberExpression(member_expr) => member_expr.get_result(memory)?,
MemberObject::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
value.clone()
}
}
.get_json_value()?;
if let serde_json::Value::Array(array) = array {
if let Some(value) = array.get(index) {
Ok(MemoryItem::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("index {} not found in array", index),
source_ranges: vec![self.clone().into()],
}))
}
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression array is not an array: {:?}", array),
source_ranges: vec![self.clone().into()],
}))
}
}
pub fn get_result(&self, memory: &mut ProgramMemory) -> Result<MemoryItem, KclError> {
let property_name = match &self.property {
LiteralIdentifier::Identifier(identifier) => identifier.name.to_string(),
@ -1562,9 +1595,12 @@ impl MemberExpression {
// Parse this as a string.
if let serde_json::Value::String(string) = value {
string
} else if let serde_json::Value::Number(_) = &value {
// It can also be a number if we are getting a member of an array.
return self.get_result_array(memory, parse_json_number_as_usize(&value, self.into())?);
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Expected string literal for property name, found {:?}", value),
message: format!("Expected string literal or number for property name, found {:?}", value),
source_ranges: vec![literal.into()],
}));
}
@ -1582,12 +1618,12 @@ impl MemberExpression {
if let serde_json::Value::Object(map) = object {
if let Some(value) = map.get(&property_name) {
Ok(MemoryItem::UserVal {
Ok(MemoryItem::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
})
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("Property {} not found in object", property_name),
@ -1715,12 +1751,12 @@ impl BinaryExpression {
parse_json_value_as_string(&right_json_value),
) {
let value = serde_json::Value::String(format!("{}{}", left, right));
return Ok(MemoryItem::UserVal {
return Ok(MemoryItem::UserVal(UserVal {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
});
}));
}
}
@ -1735,12 +1771,12 @@ impl BinaryExpression {
BinaryOperator::Mod => (left % right).into(),
};
Ok(MemoryItem::UserVal {
Ok(MemoryItem::UserVal(UserVal {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
})
}))
}
/// Rename all identifiers that have the old name to the new given name.
@ -1766,6 +1802,22 @@ pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange
}
}
pub fn parse_json_number_as_usize(j: &serde_json::Value, source_range: SourceRange) -> Result<usize, KclError> {
if let serde_json::Value::Number(n) = &j {
Ok(n.as_i64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid index: {}", j),
})
})? as usize)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid index: {}", j),
}))
}
}
pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
if let serde_json::Value::String(n) = &j {
Some(n.clone())
@ -1845,12 +1897,12 @@ impl UnaryExpression {
.get_json_value()?,
self.into(),
)?;
Ok(MemoryItem::UserVal {
Ok(MemoryItem::UserVal(UserVal {
value: (-(num)).into(),
meta: vec![Metadata {
source_range: self.into(),
}],
})
}))
}
/// Returns a hover value that includes the given character position.
@ -2231,7 +2283,7 @@ show(part001)
#[test]
fn test_recast_comment_in_a_fn_block() {
let some_program_string = r#"const myFn = () => {
let some_program_string = r#"fn myFn = () => {
// this is a comment
const yo = { a: { b: { c: '123' } } } /* block
comment */
@ -2247,7 +2299,7 @@ show(part001)
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"const myFn = () => {
r#"fn myFn = () => {
// this is a comment
const yo = { a: { b: { c: '123' } } }
/* block
@ -2542,4 +2594,15 @@ show(firstExtrude)
"#
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_recast_math_start_negative() {
let some_program_string = r#"const myVar = -5 + 6"#;
let tokens = crate::tokeniser::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted.trim(), some_program_string);
}
}

View File

@ -98,16 +98,14 @@ impl ProgramReturn {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum MemoryItem {
UserVal {
value: serde_json::Value,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
UserVal(UserVal),
SketchGroup(SketchGroup),
ExtrudeGroup(ExtrudeGroup),
#[ts(skip)]
ExtrudeTransform(ExtrudeTransform),
#[ts(skip)]
Function {
#[serde(skip)]
func: Option<MemoryFunction>,
@ -119,7 +117,16 @@ pub enum MemoryItem {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct UserVal {
pub value: serde_json::Value,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct ExtrudeTransform {
pub position: Position,
pub rotation: Rotation,
@ -138,7 +145,7 @@ pub type MemoryFunction = fn(
impl From<MemoryItem> for Vec<SourceRange> {
fn from(item: MemoryItem) -> Self {
match item {
MemoryItem::UserVal { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
MemoryItem::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::SketchGroup(s) => s.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeGroup(e) => e.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
@ -149,8 +156,8 @@ impl From<MemoryItem> for Vec<SourceRange> {
impl MemoryItem {
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
if let MemoryItem::UserVal { value, .. } = self {
Ok(value.clone())
if let MemoryItem::UserVal(user_val) = self {
Ok(user_val.value.clone())
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("Not a user value: {:?}", self),
@ -186,7 +193,7 @@ impl MemoryItem {
/// A sketch group is a collection of paths.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct SketchGroup {
/// The id of the sketch group.
pub id: uuid::Uuid,
@ -238,7 +245,7 @@ impl SketchGroup {
/// An extrude group is a collection of extrude surfaces.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct ExtrudeGroup {
/// The id of the extrude group.
pub id: uuid::Uuid,
@ -276,15 +283,15 @@ pub enum BodyType {
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Position(pub [f64; 3]);
pub struct Position(#[ts(type = "[number, number, number]")] pub [f64; 3]);
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Rotation(pub [f64; 4]);
pub struct Rotation(#[ts(type = "[number, number, number, number]")] pub [f64; 4]);
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
#[ts(export)]
pub struct SourceRange(pub [usize; 2]);
pub struct SourceRange(#[ts(type = "[number, number]")] pub [usize; 2]);
impl SourceRange {
/// Create a new source range.
@ -341,7 +348,7 @@ impl SourceRange {
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Point2d {
pub x: f64,
@ -372,6 +379,16 @@ impl From<Point2d> for kittycad::types::Point2D {
}
}
impl Point2d {
pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
pub fn scale(self, scalar: f64) -> Self {
Self {
x: self.x * scalar,
y: self.y * scalar,
}
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Point3d {
@ -401,8 +418,10 @@ impl From<SourceRange> for Metadata {
#[serde(rename_all = "camelCase")]
pub struct BasePath {
/// The from point.
#[ts(type = "[number, number]")]
pub from: [f64; 2],
/// The to point.
#[ts(type = "[number, number]")]
pub to: [f64; 2],
/// The name of the path.
pub name: String,
@ -804,16 +823,16 @@ show(part001)"#,
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_fn_definitions() {
let ast = r#"const def = (x) => {
let ast = r#"fn def = (x) => {
return x
}
const ghi = (x) => {
fn ghi = (x) => {
return x
}
const jkl = (x) => {
fn jkl = (x) => {
return x
}
const hmm = (x) => {
fn hmm = (x) => {
return x
}
@ -981,7 +1000,7 @@ show(firstExtrude)"#;
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() {
let ast = r#"const box = (h, l, w) => {
let ast = r#"fn box = (h, l, w) => {
const myBox = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
@ -998,4 +1017,169 @@ show(fnBox)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_object_with_function_period() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchAt(obj.start)
|> line([0, obj.l], %)
|> line([obj.w, 0], %)
|> line([0, -obj.l], %)
|> close(%)
|> extrude(obj.h, %)
return myBox
}
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
show(thisBox)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_object_with_function_brace() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchAt(obj["start"])
|> line([0, obj["l"]], %)
|> line([obj["w"], 0], %)
|> line([0, -obj["l"]], %)
|> close(%)
|> extrude(obj["h"], %)
return myBox
}
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
show(thisBox)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_object_with_function_mix_period_brace() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchAt(obj["start"])
|> line([0, obj["l"]], %)
|> line([obj["w"], 0], %)
|> line([10 - obj["w"], -obj.l], %)
|> close(%)
|> extrude(obj["h"], %)
return myBox
}
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
show(thisBox)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] // ignore til we get loops
async fn test_execute_with_function_sketch_loop_objects() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchAt(obj.start)
|> line([0, obj.l], %)
|> line([obj.w, 0], %)
|> line([0, -obj.l], %)
|> close(%)
|> extrude(obj.h, %)
return myBox
}
for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
const thisBox = box(var)
show(thisBox)
}"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] // ignore til we get loops
async fn test_execute_with_function_sketch_loop_array() {
let ast = r#"fn box = (h, l, w, start) => {
const myBox = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close(%)
|> extrude(h, %)
return myBox
}
for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
const thisBox = box(var[0], var[1], var[2], var[3])
show(thisBox)
}"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_array_with_function() {
let ast = r#"fn box = (array) => {
let myBox = startSketchAt(array[0])
|> line([0, array[1]], %)
|> line([array[2], 0], %)
|> line([0, -array[1]], %)
|> close(%)
|> extrude(array[3], %)
return myBox
}
const thisBox = box([[0,0], 6, 10, 3])
show(thisBox)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5.0),
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute() {
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(7.4),
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_start_negative() {
let ast = r#"const myVar = -5 + 6"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(1.0),
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_define_decimal_without_leading_zero() {
let ast = r#"let thing = .4 + 7"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(7.4),
memory.root.get("thing").unwrap().get_json_value().unwrap()
);
}
}

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use crate::{
abstract_syntax_tree_types::{
BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Identifier, Literal, ValueMeta,
BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Identifier, Literal, MemberExpression, ValueMeta,
},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
@ -81,6 +81,7 @@ pub enum MathExpression {
BinaryExpression(Box<BinaryExpression>),
ExtendedBinaryExpression(Box<ExtendedBinaryExpression>),
ParenthesisToken(Box<ParenthesisToken>),
MemberExpression(Box<MemberExpression>),
}
impl MathExpression {
@ -92,6 +93,7 @@ impl MathExpression {
MathExpression::BinaryExpression(binary_expression) => binary_expression.start(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.start(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.start(),
MathExpression::MemberExpression(member_expression) => member_expression.start(),
}
}
@ -103,6 +105,7 @@ impl MathExpression {
MathExpression::BinaryExpression(binary_expression) => binary_expression.end(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.end(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.end(),
MathExpression::MemberExpression(member_expression) => member_expression.end(),
}
}
}
@ -133,7 +136,7 @@ impl ReversePolishNotation {
}
let current_token = self.parser.get_token(0)?;
if current_token.token_type == TokenType::Word || current_token.token_type == TokenType::Keyword {
if current_token.token_type == TokenType::Word {
if let Ok(next) = self.parser.get_token(1) {
if next.token_type == TokenType::Brace && next.value == "(" {
let closing_brace = self.parser.find_closing_brace(1, 0, "")?;
@ -149,6 +152,24 @@ impl ReversePolishNotation {
);
return rpn.parse();
}
if (current_token.token_type == TokenType::Word)
&& (next.token_type == TokenType::Period
|| (next.token_type == TokenType::Brace && next.value == "["))
{
// Find the end of the binary expression, ie the member expression.
let end = self.parser.make_member_expression(0)?.last_index;
let rpn = ReversePolishNotation::new(
&self.parser.tokens[end + 1..],
&self
.previous_postfix
.iter()
.cloned()
.chain(self.parser.tokens[0..end + 1].iter().cloned())
.collect::<Vec<Token>>(),
&self.operators,
);
return rpn.parse();
}
}
let rpn = ReversePolishNotation::new(
@ -164,7 +185,6 @@ impl ReversePolishNotation {
return rpn.parse();
} else if current_token.token_type == TokenType::Number
|| current_token.token_type == TokenType::Word
|| current_token.token_type == TokenType::Keyword
|| current_token.token_type == TokenType::String
{
let rpn = ReversePolishNotation::new(
@ -180,6 +200,35 @@ impl ReversePolishNotation {
return rpn.parse();
} else if let Ok(binop) = BinaryOperator::from_str(current_token.value.as_str()) {
if !self.operators.is_empty() {
if binop == BinaryOperator::Sub {
// We need to check if we have a "sub" and if the previous token is a word or
// number or string, then we need to treat it as a negative number.
// This oddity only applies to the "-" operator.
if let Some(prevtoken) = self.previous_postfix.last() {
if prevtoken.token_type == TokenType::Operator {
// Get the next token and see if it is a number.
if let Ok(nexttoken) = self.parser.get_token(1) {
if nexttoken.token_type == TokenType::Number {
// We have a negative number/ word or string.
// Change the value of the token to be the negative number/ word or string.
let mut new_token = nexttoken.clone();
new_token.value = format!("-{}", nexttoken.value);
let rpn = ReversePolishNotation::new(
&self.parser.tokens[2..],
&self
.previous_postfix
.iter()
.cloned()
.chain(vec![new_token.clone()])
.collect::<Vec<Token>>(),
&self.operators,
);
return rpn.parse();
}
}
}
}
}
if let Ok(prevbinop) = BinaryOperator::from_str(self.operators[self.operators.len() - 1].value.as_str())
{
if prevbinop.precedence() >= binop.precedence() {
@ -196,6 +245,29 @@ impl ReversePolishNotation {
return rpn.parse();
}
}
} else if self.previous_postfix.is_empty()
&& current_token.token_type == TokenType::Operator
&& current_token.value == "-"
{
if let Ok(nexttoken) = self.parser.get_token(1) {
if nexttoken.token_type == TokenType::Number {
// We have a negative number/ word or string.
// Change the value of the token to be the negative number/ word or string.
let mut new_token = nexttoken.clone();
new_token.value = format!("-{}", nexttoken.value);
let rpn = ReversePolishNotation::new(
&self.parser.tokens[2..],
&self
.previous_postfix
.iter()
.cloned()
.chain(vec![new_token.clone()])
.collect::<Vec<Token>>(),
&self.operators,
);
return rpn.parse();
}
}
}
let rpn = ReversePolishNotation::new(
@ -299,7 +371,7 @@ impl ReversePolishNotation {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![SourceRange([a.start(), a.end()])],
message: format!("{:?}", a),
}))
}));
}
};
}
@ -338,7 +410,7 @@ impl ReversePolishNotation {
start_extended: None,
})));
return self.build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Word || current_token.token_type == TokenType::Keyword {
} else if current_token.token_type == TokenType::Word {
if reverse_polish_notation_tokens.len() > 1 {
if reverse_polish_notation_tokens[1].token_type == TokenType::Brace
&& reverse_polish_notation_tokens[1].value == "("
@ -350,6 +422,18 @@ impl ReversePolishNotation {
)));
return self.build_tree(&reverse_polish_notation_tokens[closing_brace + 1..], new_stack);
}
if reverse_polish_notation_tokens[1].token_type == TokenType::Period
|| (reverse_polish_notation_tokens[1].token_type == TokenType::Brace
&& reverse_polish_notation_tokens[1].value == "[")
{
let mut new_stack = stack;
let member_expression = self.parser.make_member_expression(0)?;
new_stack.push(MathExpression::MemberExpression(Box::new(member_expression.expression)));
return self.build_tree(
&reverse_polish_notation_tokens[member_expression.last_index + 1..],
new_stack,
);
}
let mut new_stack = stack;
new_stack.push(MathExpression::Identifier(Box::new(Identifier {
name: current_token.value.clone(),
@ -396,7 +480,7 @@ impl ReversePolishNotation {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}));
}
};
let paran = match &stack[stack.len() - 2] {
@ -445,7 +529,7 @@ impl ReversePolishNotation {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}));
}
};
let mut new_stack = stack[0..stack.len() - 2].to_vec();
@ -483,6 +567,10 @@ impl ReversePolishNotation {
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.start),
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.start),
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.start),
MathExpression::MemberExpression(member_expression) => (
BinaryPart::MemberExpression(member_expression.clone()),
member_expression.start,
),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
@ -513,6 +601,10 @@ impl ReversePolishNotation {
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.end),
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.end),
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.end),
MathExpression::MemberExpression(member_expression) => (
BinaryPart::MemberExpression(member_expression.clone()),
member_expression.end,
),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
@ -521,13 +613,7 @@ impl ReversePolishNotation {
}
};
let right_end = match right.0.clone() {
BinaryPart::BinaryExpression(_bin_exp) => right.1,
BinaryPart::Literal(lit) => lit.end,
BinaryPart::Identifier(ident) => ident.end,
BinaryPart::CallExpression(call) => call.end,
BinaryPart::UnaryExpression(unary_exp) => unary_exp.end,
};
let right_end = right.0.clone().end();
let tree = BinaryExpression {
operator: BinaryOperator::from_str(&current_token.value.clone()).map_err(|err| {
@ -562,25 +648,13 @@ impl MathParser {
pub fn parse(&mut self) -> Result<BinaryExpression, KclError> {
let rpn = self.rpn.parse()?;
let tree_with_maybe_bad_top_level_start_end = self.rpn.build_tree(&rpn, vec![])?;
let left_start = match tree_with_maybe_bad_top_level_start_end.clone().left {
BinaryPart::BinaryExpression(bin_exp) => bin_exp.start,
BinaryPart::Literal(lit) => lit.start,
BinaryPart::Identifier(ident) => ident.start,
BinaryPart::CallExpression(call) => call.start,
BinaryPart::UnaryExpression(unary_exp) => unary_exp.start,
};
let left_start = tree_with_maybe_bad_top_level_start_end.clone().left.start();
let min_start = if left_start < tree_with_maybe_bad_top_level_start_end.start {
left_start
} else {
tree_with_maybe_bad_top_level_start_end.start
};
let right_end = match tree_with_maybe_bad_top_level_start_end.clone().right {
BinaryPart::BinaryExpression(bin_exp) => bin_exp.end,
BinaryPart::Literal(lit) => lit.end,
BinaryPart::Identifier(ident) => ident.end,
BinaryPart::CallExpression(call) => call.end,
BinaryPart::UnaryExpression(unary_exp) => unary_exp.end,
};
let right_end = tree_with_maybe_bad_top_level_start_end.clone().right.end();
let max_end = if right_end > tree_with_maybe_bad_top_level_start_end.end {
right_end
} else {
@ -629,6 +703,60 @@ mod test {
);
}
#[test]
fn test_parse_expression_add_no_spaces() {
let tokens = crate::tokeniser::lexer("1+2");
let mut parser = MathParser::new(&tokens);
let result = parser.parse().unwrap();
assert_eq!(
result,
BinaryExpression {
operator: BinaryOperator::Add,
start: 0,
end: 3,
left: BinaryPart::Literal(Box::new(Literal {
value: serde_json::Value::Number(serde_json::Number::from(1)),
raw: "1".to_string(),
start: 0,
end: 1,
})),
right: BinaryPart::Literal(Box::new(Literal {
value: serde_json::Value::Number(serde_json::Number::from(2)),
raw: "2".to_string(),
start: 2,
end: 3,
})),
}
);
}
#[test]
fn test_parse_expression_sub_no_spaces() {
let tokens = crate::tokeniser::lexer("1 -2");
let mut parser = MathParser::new(&tokens);
let result = parser.parse().unwrap();
assert_eq!(
result,
BinaryExpression {
operator: BinaryOperator::Sub,
start: 0,
end: 4,
left: BinaryPart::Literal(Box::new(Literal {
value: serde_json::Value::Number(serde_json::Number::from(1)),
raw: "1".to_string(),
start: 0,
end: 1,
})),
right: BinaryPart::Literal(Box::new(Literal {
value: serde_json::Value::Number(serde_json::Number::from(2)),
raw: "2".to_string(),
start: 3,
end: 4,
})),
}
);
}
#[test]
fn test_parse_expression_plus_followed_by_star() {
let tokens = crate::tokeniser::lexer("1 + 2 * 3");

View File

@ -427,7 +427,7 @@ impl Parser {
}
let current_token = self.get_token(index)?;
let very_next_token = self.get_token(index + 1)?;
if (current_token.token_type == TokenType::Word || current_token.token_type == TokenType::Keyword)
if (current_token.token_type == TokenType::Word)
&& very_next_token.token_type == TokenType::Brace
&& very_next_token.value == "("
{
@ -550,22 +550,41 @@ impl Parser {
&self,
index: usize,
_previous_keys: Option<Vec<ObjectKeyInfo>>,
has_opening_brace: bool,
) -> Result<Vec<ObjectKeyInfo>, KclError> {
let previous_keys = _previous_keys.unwrap_or(vec![]);
let next_token = self.next_meaningful_token(index, None)?;
let _next_token = next_token.clone();
if _next_token.index == self.tokens.len() - 1 {
if next_token.index == self.tokens.len() - 1 {
return Ok(previous_keys);
}
let period_or_opening_bracket = match next_token.token {
let mut has_opening_brace = match &next_token.token {
Some(next_token_val) => {
if next_token_val.token_type == TokenType::Brace && next_token_val.value == "]" {
self.next_meaningful_token(next_token.index, None)?
if next_token_val.token_type == TokenType::Brace && next_token_val.value == "[" {
true
} else {
_next_token
has_opening_brace
}
}
None => _next_token,
None => has_opening_brace,
};
let period_or_opening_bracket = match &next_token.token {
Some(next_token_val) => {
if has_opening_brace && next_token_val.token_type == TokenType::Brace && next_token_val.value == "]" {
// We need to reset our has_opening_brace flag, since we've closed it.
has_opening_brace = false;
let next_next_token = self.next_meaningful_token(next_token.index, None)?;
if let Some(next_next_token_val) = &next_next_token.token {
if next_next_token_val.token_type == TokenType::Brace && next_next_token_val.value == "[" {
// Set the opening brace flag again, since we've opened it again.
has_opening_brace = true;
}
}
next_next_token.clone()
} else {
next_token.clone()
}
}
None => next_token.clone(),
};
if let Some(period_or_opening_bracket_token) = period_or_opening_bracket.token {
if period_or_opening_bracket_token.token_type != TokenType::Period
@ -573,11 +592,26 @@ impl Parser {
{
return Ok(previous_keys);
}
// We don't care if we never opened the brace.
if !has_opening_brace && period_or_opening_bracket_token.token_type == TokenType::Brace {
return Ok(previous_keys);
}
// Make sure its the right kind of brace, we don't care about ().
if period_or_opening_bracket_token.token_type == TokenType::Brace
&& period_or_opening_bracket_token.value != "["
&& period_or_opening_bracket_token.value != "]"
{
return Ok(previous_keys);
}
let key_token = self.next_meaningful_token(period_or_opening_bracket.index, None)?;
let next_period_or_opening_bracket = self.next_meaningful_token(key_token.index, None)?;
let is_braced = match next_period_or_opening_bracket.token {
Some(next_period_or_opening_bracket_val) => {
next_period_or_opening_bracket_val.token_type == TokenType::Brace
has_opening_brace
&& next_period_or_opening_bracket_val.token_type == TokenType::Brace
&& next_period_or_opening_bracket_val.value == "]"
}
None => false,
@ -588,23 +622,19 @@ impl Parser {
key_token.index
};
if let Some(key_token_token) = key_token.token {
let key = if key_token_token.token_type == TokenType::Word
|| key_token_token.token_type == TokenType::Keyword
{
let key = if key_token_token.token_type == TokenType::Word {
LiteralIdentifier::Identifier(Box::new(self.make_identifier(key_token.index)?))
} else {
LiteralIdentifier::Literal(Box::new(self.make_literal(key_token.index)?))
};
let computed = is_braced
&& (key_token_token.token_type == TokenType::Word
|| key_token_token.token_type == TokenType::Keyword);
let computed = is_braced && key_token_token.token_type == TokenType::Word;
let mut new_previous_keys = previous_keys;
new_previous_keys.push(ObjectKeyInfo {
key,
index: end_index,
computed,
});
self.collect_object_keys(key_token.index, Some(new_previous_keys))
self.collect_object_keys(key_token.index, Some(new_previous_keys), has_opening_brace)
} else {
Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![period_or_opening_bracket_token.clone().into()],
@ -616,9 +646,9 @@ impl Parser {
}
}
fn make_member_expression(&self, index: usize) -> Result<MemberExpressionReturn, KclError> {
pub fn make_member_expression(&self, index: usize) -> Result<MemberExpressionReturn, KclError> {
let current_token = self.get_token(index)?;
let mut keys_info = self.collect_object_keys(index, None)?;
let mut keys_info = self.collect_object_keys(index, None, false)?;
if keys_info.is_empty() {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
@ -653,6 +683,7 @@ impl Parser {
fn find_end_of_binary_expression(&self, index: usize) -> Result<usize, KclError> {
let current_token = self.get_token(index)?;
if current_token.token_type == TokenType::Brace && current_token.value == "(" {
let closing_parenthesis = self.find_closing_brace(index, 0, "")?;
let maybe_another_operator = self.next_meaningful_token(closing_parenthesis, None)?;
@ -669,10 +700,17 @@ impl Parser {
Ok(closing_parenthesis)
};
}
if (current_token.token_type == TokenType::Keyword || current_token.token_type == TokenType::Word)
&& self.get_token(index + 1)?.token_type == TokenType::Brace
&& self.get_token(index + 1)?.value == "("
if current_token.token_type == TokenType::Word {
if let Ok(next_token) = self.get_token(index + 1) {
if next_token.token_type == TokenType::Period
|| (next_token.token_type == TokenType::Brace && next_token.value == "[")
{
let member_expression = self.make_member_expression(index)?;
return self.find_end_of_binary_expression(member_expression.last_index);
}
if next_token.token_type == TokenType::Brace && next_token.value == "(" {
let closing_parenthesis = self.find_closing_brace(index + 1, 0, "")?;
let maybe_another_operator = self.next_meaningful_token(closing_parenthesis, None)?;
return if let Some(maybe_another_operator_token) = maybe_another_operator.token {
@ -688,9 +726,16 @@ impl Parser {
Ok(closing_parenthesis)
};
}
}
}
let maybe_operator = self.next_meaningful_token(index, None)?;
if let Some(maybe_operator_token) = maybe_operator.token {
if maybe_operator_token.token_type != TokenType::Operator || maybe_operator_token.value == PIPE_OPERATOR {
if maybe_operator_token.token_type == TokenType::Number {
return self.find_end_of_binary_expression(maybe_operator.index);
} else if maybe_operator_token.token_type != TokenType::Operator
|| maybe_operator_token.value == PIPE_OPERATOR
{
return Ok(index);
}
let next_right = self.next_meaningful_token(maybe_operator.index, None)?;
@ -731,7 +776,6 @@ impl Parser {
}
}
if current_token.token_type == TokenType::Word
|| current_token.token_type == TokenType::Keyword
|| current_token.token_type == TokenType::Number
|| current_token.token_type == TokenType::String
{
@ -745,6 +789,29 @@ impl Parser {
}
}
}
// Account for negative numbers.
if current_token.token_type == TokenType::Operator || current_token.value == "-" {
if let Some(next_token) = &next.token {
if next_token.token_type == TokenType::Word
|| next_token.token_type == TokenType::Number
|| next_token.token_type == TokenType::String
{
// See if the next token is an operator.
let next_right = self.next_meaningful_token(next.index, None)?;
if let Some(next_right_token) = next_right.token {
if next_right_token.token_type == TokenType::Operator {
let binary_expression = self.make_binary_expression(index)?;
return Ok(ValueReturn {
value: Value::BinaryExpression(Box::new(binary_expression.expression)),
last_index: binary_expression.last_index,
});
}
}
}
}
}
if current_token.token_type == TokenType::Brace && current_token.value == "{" {
let object_expression = self.make_object_expression(index)?;
return Ok(ValueReturn {
@ -761,11 +828,25 @@ impl Parser {
}
if let Some(next_token) = next.token {
if (current_token.token_type == TokenType::Keyword || current_token.token_type == TokenType::Word)
if (current_token.token_type == TokenType::Word)
&& (next_token.token_type == TokenType::Period
|| (next_token.token_type == TokenType::Brace && next_token.value == "["))
{
let member_expression = self.make_member_expression(index)?;
// If the next token is an operator, we need to make a binary expression.
let next_right = self.next_meaningful_token(member_expression.last_index, None)?;
if let Some(next_right_token) = next_right.token {
if next_right_token.token_type == TokenType::Operator
|| next_right_token.token_type == TokenType::Number
{
let binary_expression = self.make_binary_expression(index)?;
return Ok(ValueReturn {
value: Value::BinaryExpression(Box::new(binary_expression.expression)),
last_index: binary_expression.last_index,
});
}
}
return Ok(ValueReturn {
value: Value::MemberExpression(Box::new(member_expression.expression)),
last_index: member_expression.last_index,
@ -820,7 +901,7 @@ impl Parser {
Err(KclError::Unexpected(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", current_token.token_type),
message: format!("Unexpected token {:?}", current_token),
}))
}
@ -841,10 +922,12 @@ impl Parser {
if let Some(next_token_token) = next_token.token {
let is_closing_brace = next_token_token.token_type == TokenType::Brace && next_token_token.value == "]";
let is_comma = next_token_token.token_type == TokenType::Comma;
if !is_closing_brace && !is_comma {
// Check if we have a double period, which would act as an expansion operator.
let is_double_period = next_token_token.token_type == TokenType::DoublePeriod;
if !is_closing_brace && !is_comma && !is_double_period {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![next_token_token.clone().into()],
message: format!("Expected a comma or closing brace, found {:?}", next_token_token.value),
message: format!("Expected a `,`, `]`, or `..`, found {:?}", next_token_token.value),
}));
}
let next_call_index = if is_closing_brace {
@ -852,9 +935,60 @@ impl Parser {
} else {
self.next_meaningful_token(next_token.index, None)?.index
};
let mut _previous_elements = previous_elements;
_previous_elements.push(current_element.value);
self.make_array_elements(next_call_index, _previous_elements)
if is_double_period {
// We want to expand the array.
// Make sure the previous element is a number literal.
if first_element_token.token_type != TokenType::Number {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![first_element_token.into()],
message: "`..` expansion operator requires a number literal on both sides".to_string(),
}));
}
// Make sure the next element is a number literal.
let last_element_token = self.get_token(next_call_index)?;
if last_element_token.token_type != TokenType::Number {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![last_element_token.into()],
message: "`..` expansion operator requires a number literal on both sides".to_string(),
}));
}
// Expand the array.
let mut previous_elements = previous_elements.clone();
let first_element = first_element_token.value.parse::<i64>().map_err(|_| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![first_element_token.into()],
message: "expected a number literal".to_string(),
})
})?;
let last_element = last_element_token.value.parse::<i64>().map_err(|_| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![last_element_token.into()],
message: "expected a number literal".to_string(),
})
})?;
if first_element > last_element {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![first_element_token.into(), last_element_token.into()],
message: "first element must be less than or equal to the last element".to_string(),
}));
}
for i in first_element..=last_element {
previous_elements.push(Value::Literal(Box::new(Literal {
start: first_element_token.start,
end: first_element_token.end,
value: i.into(),
raw: i.to_string(),
})));
}
return self.make_array_elements(next_call_index + 1, previous_elements);
}
let mut previous_elements = previous_elements.clone();
previous_elements.push(current_element.value);
self.make_array_elements(next_call_index, previous_elements)
} else {
Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![first_element_token.into()],
@ -867,12 +1001,18 @@ impl Parser {
let opening_brace_token = self.get_token(index)?;
let first_element_token = self.next_meaningful_token(index, None)?;
// Make sure there is a closing brace.
let _closing_brace = self.find_closing_brace(index, 0, "")?;
let closing_brace_index = self.find_closing_brace(index, 0, "").map_err(|_| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![opening_brace_token.into()],
message: "missing a closing brace for the array".to_string(),
})
})?;
let closing_brace_token = self.get_token(closing_brace_index)?;
let array_elements = self.make_array_elements(first_element_token.index, Vec::new())?;
Ok(ArrayReturn {
expression: ArrayExpression {
start: opening_brace_token.start,
end: self.get_token(array_elements.last_index)?.end,
end: closing_brace_token.end,
elements: array_elements.elements,
},
last_index: array_elements.last_index,
@ -949,9 +1089,23 @@ impl Parser {
});
}
let argument_token = self.next_meaningful_token(index, None)?;
if let Some(argument_token_token) = argument_token.token {
let next_brace_or_comma = self.next_meaningful_token(argument_token.index, None)?;
if let Some(next_brace_or_comma_token) = next_brace_or_comma.token {
if (argument_token_token.token_type == TokenType::Word)
&& (next_brace_or_comma_token.token_type == TokenType::Period
|| (next_brace_or_comma_token.token_type == TokenType::Brace
&& next_brace_or_comma_token.value == "["))
{
let member_expression = self.make_member_expression(argument_token.index)?;
let mut _previous_args = previous_args;
_previous_args.push(Value::MemberExpression(Box::new(member_expression.expression)));
let next_comma_or_brace_token_index =
self.next_meaningful_token(member_expression.last_index, None)?.index;
return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
}
let is_identifier_or_literal = next_brace_or_comma_token.token_type == TokenType::Comma
|| next_brace_or_comma_token.token_type == TokenType::Brace;
if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "[" {
@ -962,12 +1116,12 @@ impl Parser {
_previous_args.push(Value::ArrayExpression(Box::new(array_expression.expression)));
return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
}
if argument_token_token.token_type == TokenType::Operator && argument_token_token.value == "-" {
let unary_expression = self.make_unary_expression(argument_token.index)?;
let next_comma_or_brace_token_index =
self.next_meaningful_token(unary_expression.last_index, None)?.index;
let value = self.make_value(argument_token.index)?;
let next_comma_or_brace_token_index = self.next_meaningful_token(value.last_index, None)?.index;
let mut _previous_args = previous_args;
_previous_args.push(Value::UnaryExpression(Box::new(unary_expression.expression)));
_previous_args.push(value.value);
return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
}
if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "{" {
@ -978,8 +1132,7 @@ impl Parser {
_previous_args.push(Value::ObjectExpression(Box::new(object_expression.expression)));
return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
}
if (argument_token_token.token_type == TokenType::Keyword
|| argument_token_token.token_type == TokenType::Word
if (argument_token_token.token_type == TokenType::Word
|| argument_token_token.token_type == TokenType::Number
|| argument_token_token.token_type == TokenType::String)
&& next_brace_or_comma_token.token_type == TokenType::Operator
@ -998,6 +1151,7 @@ impl Parser {
_previous_args.push(Value::BinaryExpression(Box::new(binary_expression.expression)));
return self.make_arguments(binary_expression.last_index, _previous_args);
}
if argument_token_token.token_type == TokenType::Operator
&& argument_token_token.value == PIPE_SUBSTITUTION_OPERATOR
{
@ -1010,8 +1164,7 @@ impl Parser {
_previous_args.push(value);
return self.make_arguments(next_brace_or_comma.index, _previous_args);
}
if (argument_token_token.token_type == TokenType::Keyword
|| argument_token_token.token_type == TokenType::Word)
if argument_token_token.token_type == TokenType::Word
&& next_brace_or_comma_token.token_type == TokenType::Brace
&& next_brace_or_comma_token.value == "("
{
@ -1085,9 +1238,14 @@ impl Parser {
let brace_token = self.next_meaningful_token(index, None)?;
let callee = self.make_identifier(index)?;
// Make sure there is a closing brace.
let _closing_brace_token = self.find_closing_brace(brace_token.index, 0, "")?;
let closing_brace_index = self.find_closing_brace(brace_token.index, 0, "").map_err(|_| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: "missing a closing brace for the function call".to_string(),
})
})?;
let closing_brace_token = self.get_token(closing_brace_index)?;
let args = self.make_arguments(brace_token.index, vec![])?;
let closing_brace_token = self.get_token(args.last_index)?;
let function = if let Some(stdlib_fn) = self.stdlib.get(&callee.name) {
crate::abstract_syntax_tree_types::Function::StdLib { func: stdlib_fn }
} else {
@ -1127,6 +1285,19 @@ impl Parser {
previous_declarators: Vec<VariableDeclarator>,
) -> Result<VariableDeclaratorsReturn, KclError> {
let current_token = self.get_token(index)?;
// Make sure they are not assigning a variable to a reserved keyword.
// Or a stdlib function.
if current_token.token_type == TokenType::Keyword || self.stdlib.fns.contains_key(&current_token.value) {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!(
"Cannot assign a variable to a reserved keyword: {}",
current_token.value
),
}));
}
let assignment = self.next_meaningful_token(index, None)?;
let Some(assignment_token) = assignment.token else {
return Err(KclError::Unimplemented(KclErrorDetails {
@ -1169,17 +1340,40 @@ impl Parser {
fn make_variable_declaration(&self, index: usize) -> Result<VariableDeclarationResult, KclError> {
let current_token = self.get_token(index)?;
let declaration_start_token = self.next_meaningful_token(index, None)?;
let kind = VariableKind::from_str(&current_token.value).map_err(|_| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Unexpected token: {}", current_token.value),
})
})?;
let variable_declarators_result = self.make_variable_declarators(declaration_start_token.index, vec![])?;
// Check if we have a fn variable kind but are not assigning a function.
if !variable_declarators_result.declarations.is_empty() {
if let Some(declarator) = variable_declarators_result.declarations.get(0) {
if let Value::FunctionExpression(_) = declarator.init {
if kind != VariableKind::Fn {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Expected a `fn` variable kind, found: `{}`", current_token.value),
}));
}
} else {
// If we have anything other than a function, make sure we are not using the `fn` variable kind.
if kind == VariableKind::Fn {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Expected a `let` variable kind, found: `{}`", current_token.value),
}));
}
}
}
}
Ok(VariableDeclarationResult {
declaration: VariableDeclaration {
start: current_token.start,
end: variable_declarators_result.declarations[variable_declarators_result.declarations.len() - 1].end,
kind: VariableKind::from_str(&current_token.value).map_err(|_| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: "Unexpected token".to_string(),
})
})?,
kind,
declarations: variable_declarators_result.declarations,
},
last_index: variable_declarators_result.last_index,
@ -1189,9 +1383,14 @@ impl Parser {
fn make_params(&self, index: usize, previous_params: Vec<Identifier>) -> Result<ParamsResult, KclError> {
let brace_or_comma_token = self.get_token(index)?;
let argument = self.next_meaningful_token(index, None)?;
if let Some(argument_token) = argument.token {
let should_finish_recursion = (argument_token.token_type == TokenType::Brace
&& argument_token.value == ")")
let Some(argument_token) = argument.token else {
return Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![brace_or_comma_token.into()],
message: format!("expected a function parameter, found: {}", brace_or_comma_token.value),
}));
};
let should_finish_recursion = (argument_token.token_type == TokenType::Brace && argument_token.value == ")")
|| (brace_or_comma_token.token_type == TokenType::Brace && brace_or_comma_token.value == ")");
if should_finish_recursion {
return Ok(ParamsResult {
@ -1199,17 +1398,24 @@ impl Parser {
last_index: index,
});
}
// Make sure they are not assigning a variable to a reserved keyword.
// Or a stdlib function.
if argument_token.token_type == TokenType::Keyword || self.stdlib.fns.contains_key(&argument_token.value) {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![argument_token.clone().into()],
message: format!(
"Cannot assign a variable to a reserved keyword: {}",
argument_token.value
),
}));
}
let next_brace_or_comma_token = self.next_meaningful_token(argument.index, None)?;
let identifier = self.make_identifier(argument.index)?;
let mut _previous_params = previous_params;
_previous_params.push(identifier);
self.make_params(next_brace_or_comma_token.index, _previous_params)
} else {
Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![brace_or_comma_token.into()],
message: format!("Unexpected token {}", brace_or_comma_token.value),
}))
}
}
fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
@ -1239,10 +1445,11 @@ impl Parser {
Value::Literal(literal) => BinaryPart::Literal(literal),
Value::UnaryExpression(unary_expression) => BinaryPart::UnaryExpression(unary_expression),
Value::CallExpression(call_expression) => BinaryPart::CallExpression(call_expression),
Value::MemberExpression(member_expression) => BinaryPart::MemberExpression(member_expression),
_ => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: "Invalid argument for unary expression".to_string(),
message: format!("Invalid argument for unary expression: {:?}", argument.value),
}));
}
},
@ -1462,7 +1669,7 @@ impl Parser {
}
if let Some(next_token) = next.token {
if (token.token_type == TokenType::Keyword || token.token_type == TokenType::Word)
if token.token_type == TokenType::Word
&& next_token.token_type == TokenType::Brace
&& next_token.value == "("
{
@ -1488,9 +1695,7 @@ impl Parser {
let next_thing = self.next_meaningful_token(token_index, None)?;
if let Some(next_thing_token) = next_thing.token {
if (token.token_type == TokenType::Number
|| token.token_type == TokenType::Word
|| token.token_type == TokenType::Keyword)
if (token.token_type == TokenType::Number || token.token_type == TokenType::Word)
&& next_thing_token.token_type == TokenType::Operator
{
if let Some(node) = &next_thing.non_code_node {
@ -1513,7 +1718,7 @@ impl Parser {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![token.into()],
message: "unexpected token".to_string(),
message: format!("unexpected token {}", token.value),
}))
}
@ -1730,7 +1935,7 @@ const key = 'c'"#,
fn test_collect_object_keys() {
let tokens = crate::tokeniser::lexer("const prop = yo.one[\"two\"]");
let parser = Parser::new(tokens);
let keys_info = parser.collect_object_keys(6, None).unwrap();
let keys_info = parser.collect_object_keys(6, None, false).unwrap();
assert_eq!(keys_info.len(), 2);
let first_key = match keys_info[0].key.clone() {
LiteralIdentifier::Identifier(identifier) => format!("identifier-{}", identifier.name),
@ -2759,6 +2964,73 @@ show(mySk1)"#;
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
}
#[test]
fn test_parse_member_expression_double_nested_braces() {
let tokens = crate::tokeniser::lexer(r#"const prop = yo["one"][two]"#);
let parser = Parser::new(tokens);
parser.ast().unwrap();
}
#[test]
fn test_parse_member_expression_binary_expression_period_number_first() {
let tokens = crate::tokeniser::lexer(
r#"const obj = { a: 1, b: 2 }
const height = 1 - obj.a"#,
);
let parser = Parser::new(tokens);
parser.ast().unwrap();
}
#[test]
fn test_parse_member_expression_binary_expression_brace_number_first() {
let tokens = crate::tokeniser::lexer(
r#"const obj = { a: 1, b: 2 }
const height = 1 - obj["a"]"#,
);
let parser = Parser::new(tokens);
parser.ast().unwrap();
}
#[test]
fn test_parse_member_expression_binary_expression_brace_number_second() {
let tokens = crate::tokeniser::lexer(
r#"const obj = { a: 1, b: 2 }
const height = obj["a"] - 1"#,
);
let parser = Parser::new(tokens);
parser.ast().unwrap();
}
#[test]
fn test_parse_member_expression_binary_expression_in_array_number_first() {
let tokens = crate::tokeniser::lexer(
r#"const obj = { a: 1, b: 2 }
const height = [1 - obj["a"], 0]"#,
);
let parser = Parser::new(tokens);
parser.ast().unwrap();
}
#[test]
fn test_parse_member_expression_binary_expression_in_array_number_second() {
let tokens = crate::tokeniser::lexer(
r#"const obj = { a: 1, b: 2 }
const height = [obj["a"] - 1, 0]"#,
);
let parser = Parser::new(tokens);
parser.ast().unwrap();
}
#[test]
fn test_parse_member_expression_binary_expression_in_array_number_second_missing_space() {
let tokens = crate::tokeniser::lexer(
r#"const obj = { a: 1, b: 2 }
const height = [obj["a"] -1, 0]"#,
);
let parser = Parser::new(tokens);
parser.ast().unwrap();
}
#[test]
fn test_parse_half_pipe() {
let tokens = crate::tokeniser::lexer(
@ -2816,7 +3088,10 @@ z(-[["#,
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("unexpected end"));
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]
@ -2828,7 +3103,10 @@ z(-[["#,
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("unexpected end"));
assert_eq!(
result.err().unwrap().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 1])], message: "missing a closing brace for the function call" }"#
);
}
#[test]
@ -2881,4 +3159,229 @@ e
.to_string()
.contains("unexpected end of expression"));
}
#[test]
fn test_parse_expand_array() {
let code = "const myArray = [0..10]";
let parser = Parser::new(crate::tokeniser::lexer(code));
let result = parser.ast().unwrap();
let expected_result = Program {
start: 0,
end: 23,
body: vec![BodyItem::VariableDeclaration(VariableDeclaration {
start: 0,
end: 23,
declarations: vec![VariableDeclarator {
start: 6,
end: 23,
id: Identifier {
start: 6,
end: 13,
name: "myArray".to_string(),
},
init: Value::ArrayExpression(Box::new(ArrayExpression {
start: 16,
end: 23,
elements: vec![
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 0.into(),
raw: "0".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 1.into(),
raw: "1".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 2.into(),
raw: "2".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 3.into(),
raw: "3".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 4.into(),
raw: "4".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 5.into(),
raw: "5".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 6.into(),
raw: "6".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 7.into(),
raw: "7".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 8.into(),
raw: "8".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 9.into(),
raw: "9".to_string(),
})),
Value::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 10.into(),
raw: "10".to_string(),
})),
],
})),
}],
kind: VariableKind::Const,
})],
non_code_meta: NoneCodeMeta {
none_code_nodes: Default::default(),
start: None,
},
};
assert_eq!(result, expected_result);
}
#[test]
fn test_error_keyword_in_variable() {
let some_program_string = r#"const let = "thing""#;
let tokens = crate::tokeniser::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([6, 9])], message: "Cannot assign a variable to a reserved keyword: let" }"#
);
}
#[test]
fn test_error_keyword_in_fn_name() {
let some_program_string = r#"fn let = () {}"#;
let tokens = crate::tokeniser::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([3, 6])], message: "Cannot assign a variable to a reserved keyword: let" }"#
);
}
#[test]
fn test_error_stdlib_in_fn_name() {
let some_program_string = r#"fn cos = () {}"#;
let tokens = crate::tokeniser::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([3, 6])], message: "Cannot assign a variable to a reserved keyword: cos" }"#
);
}
#[test]
fn test_error_keyword_in_fn_args() {
let some_program_string = r#"fn thing = (let) => {
return 1
}"#;
let tokens = crate::tokeniser::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([12, 15])], message: "Cannot assign a variable to a reserved keyword: let" }"#
);
}
#[test]
fn test_error_stdlib_in_fn_args() {
let some_program_string = r#"fn thing = (cos) => {
return 1
}"#;
let tokens = crate::tokeniser::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([12, 15])], message: "Cannot assign a variable to a reserved keyword: cos" }"#
);
}
#[test]
fn test_keyword_ok_in_fn_args_return() {
let some_program_string = r#"fn thing = (param) => {
return true
}
thing(false)
"#;
let tokens = crate::tokeniser::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
parser.ast().unwrap();
}
#[test]
fn test_error_define_function_as_var() {
for name in ["var", "let", "const"] {
let some_program_string = format!(
r#"{} thing = (param) => {{
return true
}}
thing(false)
"#,
name
);
let tokens = crate::tokeniser::lexer(&some_program_string);
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
format!(
r#"syntax: KclErrorDetails {{ source_ranges: [SourceRange([0, {}])], message: "Expected a `fn` variable kind, found: `{}`" }}"#,
name.len(),
name
)
);
}
}
#[test]
fn test_error_define_var_as_function() {
let some_program_string = r#"fn thing = "thing""#;
let tokens = crate::tokeniser::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 2])], message: "Expected a `let` variable kind, found: `fn`" }"#
);
}
}

View File

@ -0,0 +1,70 @@
//! Functions related to mathematics.
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{errors::KclError, executor::MemoryItem, std::Args};
/// Computes the cosine of a number (in radians).
pub fn cos(args: &mut Args) -> Result<MemoryItem, KclError> {
let num = args.get_number()?;
let result = inner_cos(num)?;
args.make_user_val_from_f64(result)
}
/// Computes the sine of a number (in radians).
#[stdlib {
name = "cos",
}]
fn inner_cos(num: f64) -> Result<f64, KclError> {
Ok(num.cos())
}
/// Computes the sine of a number (in radians).
pub fn sin(args: &mut Args) -> Result<MemoryItem, KclError> {
let num = args.get_number()?;
let result = inner_sin(num)?;
args.make_user_val_from_f64(result)
}
/// Computes the sine of a number (in radians).
#[stdlib {
name = "sin",
}]
fn inner_sin(num: f64) -> Result<f64, KclError> {
Ok(num.sin())
}
/// Computes the tangent of a number (in radians).
pub fn tan(args: &mut Args) -> Result<MemoryItem, KclError> {
let num = args.get_number()?;
let result = inner_tan(num)?;
args.make_user_val_from_f64(result)
}
/// Computes the tangent of a number (in radians).
#[stdlib {
name = "tan",
}]
fn inner_tan(num: f64) -> Result<f64, KclError> {
Ok(num.tan())
}
/// Return the value of `pi`.
pub fn pi(args: &mut Args) -> Result<MemoryItem, KclError> {
let result = inner_pi()?;
args.make_user_val_from_f64(result)
}
/// Return the value of `pi`.
#[stdlib {
name = "pi",
}]
fn inner_pi() -> Result<f64, KclError> {
Ok(std::f64::consts::PI)
}

View File

@ -1,6 +1,7 @@
//! Functions implemented for language execution.
pub mod extrude;
pub mod math;
pub mod segment;
pub mod sketch;
pub mod utils;
@ -61,6 +62,10 @@ impl StdLib {
Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin),
Box::new(crate::std::math::Tan),
Box::new(crate::std::math::Pi),
];
let mut fns = HashMap::new();
@ -103,12 +108,12 @@ impl<'a> Args<'a> {
}
fn make_user_val_from_json(&self, j: serde_json::Value) -> Result<MemoryItem, KclError> {
Ok(MemoryItem::UserVal {
Ok(MemoryItem::UserVal(crate::executor::UserVal {
value: j,
meta: vec![Metadata {
source_range: self.source_range,
}],
})
}))
}
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> {
@ -122,6 +127,21 @@ impl<'a> Args<'a> {
)?))
}
fn get_number(&self) -> Result<f64, KclError> {
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a number as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
parse_json_number_as_f64(&first_value, self.source_range)
}
fn get_number_array(&self) -> Result<Vec<f64>, KclError> {
let mut numbers: Vec<f64> = Vec::new();
for arg in &self.args {
@ -471,7 +491,7 @@ pub fn leg_angle_x(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "legAngX",
}]
fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).acos() * 180.0 / std::f64::consts::PI
(leg.min(hypotenuse) / hypotenuse).acos().to_degrees()
}
/// Returns the angle of the given leg for y.
@ -486,7 +506,7 @@ pub fn leg_angle_y(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "legAngY",
}]
fn inner_leg_angle_y(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).asin() * 180.0 / std::f64::consts::PI
(leg.min(hypotenuse) / hypotenuse).asin().to_degrees()
}
/// The primitive types that can be used in a KCL file.
@ -591,7 +611,7 @@ mod tests {
buf.push_str(&fn_docs);
}
expectorate::assert_contents("../../../docs/kcl.md", &buf);
expectorate::assert_contents("../../../docs/kcl/std.md", &buf);
}
#[test]
@ -606,7 +626,7 @@ mod tests {
}
expectorate::assert_contents(
"../../../docs/kcl.json",
"../../../docs/kcl/std.json",
&serde_json::to_string_pretty(&json_data).unwrap(),
);
}

View File

@ -7,7 +7,7 @@ use schemars::JsonSchema;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{MemoryItem, SketchGroup},
std::{utils::get_angle, Args},
std::{utils::Angle, Args},
};
/// Returns the segment end of x.
@ -174,9 +174,9 @@ fn inner_segment_angle(segment_name: &str, sketch_group: SketchGroup, args: &mut
})?;
let line = path.get_base();
let result = get_angle(&line.from, &line.to);
let result = Angle::between(line.from.into(), line.to.into());
Ok(result)
Ok(result.degrees())
}
/// Returns the angle to match the given length for x.
@ -230,7 +230,7 @@ fn inner_angle_to_match_length_x(
if diff > length {
Ok(0.0)
} else {
Ok(angle_r * 180.0 / std::f64::consts::PI)
Ok(angle_r.to_degrees())
}
}
@ -285,6 +285,6 @@ fn inner_angle_to_match_length_y(
if diff > length {
Ok(0.0)
} else {
Ok(angle_r * 180.0 / std::f64::consts::PI)
Ok(angle_r.to_degrees())
}
}

View File

@ -15,6 +15,8 @@ use crate::{
},
};
use super::utils::Angle;
/// Data to draw a line to a point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -161,34 +163,12 @@ pub enum LineData {
/// A point with a tag.
PointWithTag {
/// The to point.
to: PointOrDefault,
to: [f64; 2],
/// The tag.
tag: String,
},
/// A point.
Point([f64; 2]),
/// A string like `default`.
Default(String),
}
/// A point or a default value.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum PointOrDefault {
/// A point.
Point([f64; 2]),
/// A string like `default`.
Default(String),
}
impl PointOrDefault {
fn get_point_with_default(&self, default: [f64; 2]) -> [f64; 2] {
match self {
PointOrDefault::Point(point) => *point,
PointOrDefault::Default(_) => default,
}
}
}
/// Draw a line.
@ -205,12 +185,9 @@ pub fn line(args: &mut Args) -> Result<MemoryItem, KclError> {
}]
fn inner_line(data: LineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let default = [0.2, 1.0];
let inner_args = match &data {
LineData::PointWithTag { to, .. } => to.get_point_with_default(default),
LineData::PointWithTag { to, .. } => *to,
LineData::Point(to) => *to,
LineData::Default(_) => default,
};
let to = [from.x + inner_args[0], from.y + inner_args[1]];
@ -283,10 +260,7 @@ pub fn x_line(args: &mut Args) -> Result<MemoryItem, KclError> {
}]
fn inner_x_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([length, 0.0]),
tag,
},
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { to: [length, 0.0], tag },
AxisLineData::Length(length) => LineData::Point([length, 0.0]),
};
@ -308,10 +282,7 @@ pub fn y_line(args: &mut Args) -> Result<MemoryItem, KclError> {
}]
fn inner_y_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([0.0, length]),
tag,
},
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { to: [0.0, length], tag },
AxisLineData::Length(length) => LineData::Point([0.0, length]),
};
@ -360,8 +331,8 @@ fn inner_angled_line(
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to: [f64; 2] = [
from.x + length * f64::cos(angle * std::f64::consts::PI / 180.0),
from.y + length * f64::sin(angle * std::f64::consts::PI / 180.0),
from.x + length * f64::cos(angle.to_radians()),
from.y + length * f64::sin(angle.to_radians()),
];
let id = uuid::Uuid::new_v4();
@ -423,16 +394,13 @@ fn inner_angled_line_of_x_length(
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to = get_y_component(angle, length);
let to = get_y_component(Angle::from_degrees(angle), length);
let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag {
to: PointOrDefault::Point(to),
tag,
}
LineData::PointWithTag { to: to.into(), tag }
} else {
LineData::Point(to)
LineData::Point(to.into())
},
sketch_group,
args,
@ -483,7 +451,7 @@ fn inner_angled_line_to_x(
};
let x_component = x_to - from.x;
let y_component = x_component * f64::tan(angle * std::f64::consts::PI / 180.0);
let y_component = x_component * f64::tan(angle.to_radians());
let y_to = from.y + y_component;
let new_sketch_group = inner_line_to(
@ -521,16 +489,13 @@ fn inner_angled_line_of_y_length(
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to = get_x_component(angle, length);
let to = get_x_component(Angle::from_degrees(angle), length);
let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag {
to: PointOrDefault::Point(to),
tag,
}
LineData::PointWithTag { to: to.into(), tag }
} else {
LineData::Point(to)
LineData::Point(to.into())
},
sketch_group,
args,
@ -563,7 +528,7 @@ fn inner_angled_line_to_y(
};
let y_component = y_to - from.y;
let x_component = y_component / f64::tan(angle * std::f64::consts::PI / 180.0);
let x_component = y_component / f64::tan(angle.to_radians());
let x_to = from.x + x_component;
let new_sketch_group = inner_line_to(
@ -625,16 +590,16 @@ fn inner_angled_line_that_intersects(
let from = sketch_group.get_coords_from_paths()?;
let to = intersection_with_parallel_line(
&[intersect_path.from, intersect_path.to],
&[intersect_path.from.into(), intersect_path.to.into()],
data.offset.unwrap_or_default(),
data.angle,
from.into(),
from,
);
let line_to_data = if let Some(tag) = data.tag {
LineToData::PointWithTag { to, tag }
LineToData::PointWithTag { to: to.into(), tag }
} else {
LineToData::Point(to)
LineToData::Point(to.into())
};
let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?;
@ -654,11 +619,9 @@ pub fn start_sketch_at(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "startSketchAt",
}]
fn inner_start_sketch_at(data: LineData, args: &mut Args) -> Result<SketchGroup, KclError> {
let default = [0.0, 0.0];
let to = match &data {
LineData::PointWithTag { to, .. } => to.get_point_with_default(default),
LineData::PointWithTag { to, .. } => *to,
LineData::Point(to) => *to,
LineData::Default(_) => default,
};
let id = uuid::Uuid::new_v4();
@ -805,7 +768,7 @@ pub fn arc(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "arc",
}]
fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let from: Point2d = sketch_group.get_coords_from_paths()?;
let (center, angle_start, angle_end, radius, end) = match &data {
ArcData::AnglesAndRadiusWithTag {
@ -814,23 +777,27 @@ fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Resul
radius,
..
} => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius);
(center, *angle_start, *angle_end, *radius, end)
let a_start = Angle::from_degrees(*angle_start);
let a_end = Angle::from_degrees(*angle_end);
let (center, end) = arc_center_and_end(from, a_start, a_end, *radius);
(center, a_start, a_end, *radius, end)
}
ArcData::AnglesAndRadius {
angle_start,
angle_end,
radius,
} => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius);
(center, *angle_start, *angle_end, *radius, end)
let a_start = Angle::from_degrees(*angle_start);
let a_end = Angle::from_degrees(*angle_end);
let (center, end) = arc_center_and_end(from, a_start, a_end, *radius);
(center, a_start, a_end, *radius, end)
}
ArcData::CenterToRadiusWithTag { center, to, radius, .. } => {
let (angle_start, angle_end) = arc_angles(&from, &center.into(), &to.into(), *radius, args.source_range)?;
let (angle_start, angle_end) = arc_angles(from, center.into(), to.into(), *radius, args.source_range)?;
(center.into(), angle_start, angle_end, *radius, to.into())
}
ArcData::CenterToRadius { center, to, radius } => {
let (angle_start, angle_end) = arc_angles(&from, &center.into(), &to.into(), *radius, args.source_range)?;
let (angle_start, angle_end) = arc_angles(from, center.into(), to.into(), *radius, args.source_range)?;
(center.into(), angle_start, angle_end, *radius, to.into())
}
};
@ -842,8 +809,8 @@ fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Resul
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Arc {
angle_start,
angle_end,
angle_start: angle_start.degrees(),
angle_end: angle_end.degrees(),
center: center.into(),
radius,
},
@ -992,16 +959,12 @@ mod tests {
use pretty_assertions::assert_eq;
use crate::std::sketch::{LineData, PointOrDefault};
use crate::std::sketch::LineData;
#[test]
fn test_deserialize_line_data() {
let mut str_json = "\"default\"".to_string();
let data: LineData = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, LineData::Default("default".to_string()));
let data = LineData::Point([0.0, 1.0]);
str_json = serde_json::to_string(&data).unwrap();
let mut str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, "[0.0,1.0]");
str_json = "[0, 1]".to_string();
@ -1013,7 +976,7 @@ mod tests {
assert_eq!(
data,
LineData::PointWithTag {
to: PointOrDefault::Point([0.0, 1.0]),
to: [0.0, 1.0],
tag: "thing".to_string()
}
);

View File

@ -1,30 +1,85 @@
use std::f64::consts::PI;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{Point2d, SourceRange},
};
pub fn get_angle(a: &[f64; 2], b: &[f64; 2]) -> f64 {
let x = b[0] - a[0];
let y = b[1] - a[1];
normalise_angle(y.atan2(x) * 180.0 / std::f64::consts::PI)
#[derive(Clone, Copy, Default, PartialOrd, PartialEq, Debug)]
pub struct Angle {
degrees: f64,
}
pub fn normalise_angle(angle: f64) -> f64 {
impl Angle {
const ZERO: Self = Self { degrees: 0.0 };
/// Make an angle of the given degrees.
pub fn from_degrees(degrees: f64) -> Self {
Self { degrees }
}
/// Make an angle of the given radians.
pub fn from_radians(radians: f64) -> Self {
Self::from_degrees(radians.to_degrees())
}
/// Get the angle in degrees
pub fn degrees(&self) -> f64 {
self.degrees
}
/// Get the angle in radians
pub fn radians(&self) -> f64 {
self.degrees.to_radians()
}
/// Get the angle between these points
pub fn between(a: Point2d, b: Point2d) -> Self {
let x = b.x - a.x;
let y = b.y - a.y;
Self::from_radians(y.atan2(x)).normalize()
}
/// Normalize the angle
pub fn normalize(self) -> Self {
let angle = self.degrees();
let result = ((angle % 360.0) + 360.0) % 360.0;
if result > 180.0 {
result - 360.0
} else {
result
Self::from_degrees(if result > 180.0 { result - 360.0 } else { result })
}
/// Gives the ▲-angle between from and to angles (shortest path), use radians.
///
/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
/// # Examples
///
/// ```
/// use std::f64::consts::PI;
/// use kcl_lib::std::utils::Angle;
///
/// assert_eq!(
/// Angle::delta(Angle::from_radians(PI / 8.0), Angle::from_radians(PI / 4.0)),
/// Angle::from_radians(PI / 8.0)
/// );
/// ```
#[allow(dead_code)]
pub fn delta(from_angle: Self, to_angle: Self) -> Self {
let norm_from_angle = normalize_rad(from_angle.radians());
let norm_to_angle = normalize_rad(to_angle.radians());
let provisional = norm_to_angle - norm_from_angle;
if provisional > -PI && provisional <= PI {
return Angle::from_radians(provisional);
}
if provisional > PI {
return Angle::from_radians(provisional - 2.0 * PI);
}
if provisional < -PI {
return Angle::from_radians(provisional + 2.0 * PI);
}
Angle::ZERO
}
}
#[allow(dead_code)]
pub fn clockwise_sign(points: &[[f64; 2]]) -> i32 {
pub fn clockwise_sign(points: &[Point2d]) -> i32 {
let mut sum = 0.0;
for i in 0..points.len() {
let current_point = points[i];
let next_point = points[(i + 1) % points.len()];
sum += (next_point[0] - current_point[0]) * (next_point[1] + current_point[1]);
sum += (next_point.x - current_point.x) * (next_point.y + current_point.y);
}
if sum >= 0.0 {
1
@ -35,139 +90,145 @@ pub fn clockwise_sign(points: &[[f64; 2]]) -> i32 {
#[allow(dead_code)]
pub fn normalize_rad(angle: f64) -> f64 {
let draft = angle % (2.0 * std::f64::consts::PI);
let draft = angle % (2.0 * PI);
if draft < 0.0 {
draft + 2.0 * std::f64::consts::PI
draft + 2.0 * PI
} else {
draft
}
}
/// Gives the ▲-angle between from and to angles (shortest path), use radians.
///
/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
/// # Examples
///
/// ```
/// assert_eq!(
/// kcl_lib::std::utils::delta_angle(std::f64::consts::PI / 8.0, std::f64::consts::PI / 4.0),
/// std::f64::consts::PI / 8.0
/// );
/// ```
#[allow(dead_code)]
pub fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
let norm_from_angle = normalize_rad(from_angle);
let norm_to_angle = normalize_rad(to_angle);
let provisional = norm_to_angle - norm_from_angle;
if provisional > -std::f64::consts::PI && provisional <= std::f64::consts::PI {
return provisional;
}
if provisional > std::f64::consts::PI {
return provisional - 2.0 * std::f64::consts::PI;
}
if provisional < -std::f64::consts::PI {
return provisional + 2.0 * std::f64::consts::PI;
}
0.0
}
/// Calculates the distance between two points.
///
/// # Examples
///
/// ```
/// use kcl_lib::executor::Point2d;
///
/// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[0.0, 5.0]),
/// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d{x: 0.0, y: 5.0}),
/// 5.0
/// );
/// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[3.0, 4.0]),
/// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d{x: 3.0, y: 4.0}),
/// 5.0
/// );
/// ```
#[allow(dead_code)]
pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 {
let x1 = point_a[0];
let y1 = point_a[1];
let x2 = point_b[0];
let y2 = point_b[1];
pub fn distance_between_points(point_a: Point2d, point_b: Point2d) -> f64 {
let x1 = point_a.x;
let y1 = point_a.y;
let x2 = point_b.x;
let y2 = point_b.y;
((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt()
}
pub fn calculate_intersection_of_two_lines(line1: &[[f64; 2]; 2], line2_angle: f64, line2_point: [f64; 2]) -> [f64; 2] {
let line2_point_b = [
line2_point[0] + f64::cos(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
line2_point[1] + f64::sin(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
];
pub fn calculate_intersection_of_two_lines(line1: &[Point2d; 2], line2_angle: f64, line2_point: Point2d) -> Point2d {
let line2_point_b = Point2d {
x: line2_point.x + f64::cos(line2_angle.to_radians()) * 10.0,
y: line2_point.y + f64::sin(line2_angle.to_radians()) * 10.0,
};
intersect(line1[0], line1[1], line2_point, line2_point_b)
}
pub fn intersect(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2], p4: [f64; 2]) -> [f64; 2] {
let slope = |p1: [f64; 2], p2: [f64; 2]| (p1[1] - p2[1]) / (p1[0] - p2[0]);
let constant = |p1: [f64; 2], p2: [f64; 2]| p1[1] - slope(p1, p2) * p1[0];
let get_y = |for_x: f64, p1: [f64; 2], p2: [f64; 2]| slope(p1, p2) * for_x + constant(p1, p2);
pub fn intersect(p1: Point2d, p2: Point2d, p3: Point2d, p4: Point2d) -> Point2d {
let slope = |p1: Point2d, p2: Point2d| (p1.y - p2.y) / (p1.x - p2.x);
let constant = |p1: Point2d, p2: Point2d| p1.y - slope(p1, p2) * p1.x;
let get_y = |for_x: f64, p1: Point2d, p2: Point2d| slope(p1, p2) * for_x + constant(p1, p2);
if p1[0] == p2[0] {
return [p1[0], get_y(p1[0], p3, p4)];
if p1.x == p2.x {
return Point2d {
x: p1.x,
y: get_y(p1.x, p3, p4),
};
}
if p3[0] == p4[0] {
return [p3[0], get_y(p3[0], p1, p2)];
if p3.x == p4.x {
return Point2d {
x: p3.x,
y: get_y(p3.x, p1, p2),
};
}
let x = (constant(p3, p4) - constant(p1, p2)) / (slope(p1, p2) - slope(p3, p4));
let y = get_y(x, p1, p2);
[x, y]
Point2d { x, y }
}
pub fn intersection_with_parallel_line(
line1: &[[f64; 2]; 2],
line1: &[Point2d; 2],
line1_offset: f64,
line2_angle: f64,
line2_point: [f64; 2],
) -> [f64; 2] {
line2_point: Point2d,
) -> Point2d {
calculate_intersection_of_two_lines(&offset_line(line1_offset, line1[0], line1[1]), line2_angle, line2_point)
}
fn offset_line(offset: f64, p1: [f64; 2], p2: [f64; 2]) -> [[f64; 2]; 2] {
if p1[0] == p2[0] {
let direction = (p1[1] - p2[1]).signum();
return [[p1[0] + offset * direction, p1[1]], [p2[0] + offset * direction, p2[1]]];
fn offset_line(offset: f64, p1: Point2d, p2: Point2d) -> [Point2d; 2] {
if p1.x == p2.x {
let direction = (p1.y - p2.y).signum();
return [
Point2d {
x: p1.x + offset * direction,
y: p1.y,
},
Point2d {
x: p2.x + offset * direction,
y: p2.y,
},
];
}
if p1[1] == p2[1] {
let direction = (p2[0] - p1[0]).signum();
return [[p1[0], p1[1] + offset * direction], [p2[0], p2[1] + offset * direction]];
if p1.y == p2.y {
let direction = (p2.x - p1.x).signum();
return [
Point2d {
x: p1.x,
y: p1.y + offset * direction,
},
Point2d {
x: p2.x,
y: p2.y + offset * direction,
},
];
}
let x_offset = offset / f64::sin(f64::atan2(p1[1] - p2[1], p1[0] - p2[0]));
[[p1[0] + x_offset, p1[1]], [p2[0] + x_offset, p2[1]]]
let x_offset = offset / f64::sin(f64::atan2(p1.y - p2.y, p1.x - p2.x));
[
Point2d {
x: p1.x + x_offset,
y: p1.y,
},
Point2d {
x: p2.x + x_offset,
y: p2.y,
},
]
}
pub fn get_y_component(angle_degree: f64, x_component: f64) -> [f64; 2] {
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360
let y_component = x_component * f64::tan(normalised_angle * std::f64::consts::PI / 180.0);
pub fn get_y_component(angle: Angle, x: f64) -> Point2d {
let normalised_angle = ((angle.degrees() % 360.0) + 360.0) % 360.0; // between 0 and 360
let y = x * f64::tan(normalised_angle.to_radians());
let sign = if normalised_angle > 90.0 && normalised_angle <= 270.0 {
-1.0
} else {
1.0
};
[sign * x_component, sign * y_component]
Point2d { x, y }.scale(sign)
}
pub fn get_x_component(angle_degree: f64, y_component: f64) -> [f64; 2] {
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360
let x_component = y_component / f64::tan(normalised_angle * std::f64::consts::PI / 180.0);
pub fn get_x_component(angle: Angle, y: f64) -> Point2d {
let normalised_angle = ((angle.degrees() % 360.0) + 360.0) % 360.0; // between 0 and 360
let x = y / f64::tan(normalised_angle.to_radians());
let sign = if normalised_angle > 180.0 && normalised_angle <= 360.0 {
-1.0
} else {
1.0
};
[sign * x_component, sign * y_component]
Point2d { x, y }.scale(sign)
}
pub fn arc_center_and_end(from: &Point2d, start_angle_deg: f64, end_angle_deg: f64, radius: f64) -> (Point2d, Point2d) {
let start_angle = start_angle_deg * (std::f64::consts::PI / 180.0);
let end_angle = end_angle_deg * (std::f64::consts::PI / 180.0);
pub fn arc_center_and_end(from: Point2d, start_angle: Angle, end_angle: Angle, radius: f64) -> (Point2d, Point2d) {
let start_angle = start_angle.radians();
let end_angle = end_angle.radians();
let center = Point2d {
x: -1.0 * (radius * start_angle.cos() - from.x),
@ -183,12 +244,12 @@ pub fn arc_center_and_end(from: &Point2d, start_angle_deg: f64, end_angle_deg: f
}
pub fn arc_angles(
from: &Point2d,
to: &Point2d,
center: &Point2d,
from: Point2d,
to: Point2d,
center: Point2d,
radius: f64,
source_range: SourceRange,
) -> Result<(f64, f64), KclError> {
) -> Result<(Angle, Angle), KclError> {
// First make sure that the points are on the circumference of the circle.
// If not, we'll return an error.
if !is_on_circumference(center, from, radius) {
@ -214,13 +275,10 @@ pub fn arc_angles(
let start_angle = (from.y - center.y).atan2(from.x - center.x);
let end_angle = (to.y - center.y).atan2(to.x - center.x);
let start_angle_deg = start_angle * (180.0 / std::f64::consts::PI);
let end_angle_deg = end_angle * (180.0 / std::f64::consts::PI);
Ok((start_angle_deg, end_angle_deg))
Ok((Angle::from_radians(start_angle), Angle::from_radians(end_angle)))
}
pub fn is_on_circumference(center: &Point2d, point: &Point2d, radius: f64) -> bool {
pub fn is_on_circumference(center: Point2d, point: Point2d, radius: f64) -> bool {
let dx = point.x - center.x;
let dy = point.y - center.y;
@ -237,7 +295,7 @@ mod tests {
// Here you can bring your functions into scope
use pretty_assertions::assert_eq;
use super::{get_x_component, get_y_component};
use super::{get_x_component, get_y_component, Angle};
use crate::executor::SourceRange;
static EACH_QUAD: [(i32, [i32; 2]); 12] = [
@ -261,28 +319,28 @@ mod tests {
let mut results = Vec::new();
for &(angle, expected_result) in EACH_QUAD.iter() {
let res = get_y_component(angle as f64, 1.0);
results.push([res[0].round() as i32, res[1].round() as i32]);
let res = get_y_component(Angle::from_degrees(angle as f64), 1.0);
results.push([res.x.round() as i32, res.y.round() as i32]);
expected.push(expected_result);
}
assert_eq!(results, expected);
let result = get_y_component(0.0, 1.0);
assert_eq!(result[0] as i32, 1);
assert_eq!(result[1] as i32, 0);
let result = get_y_component(Angle::ZERO, 1.0);
assert_eq!(result.x as i32, 1);
assert_eq!(result.y as i32, 0);
let result = get_y_component(90.0, 1.0);
assert_eq!(result[0] as i32, 1);
assert!(result[1] > 100000.0);
let result = get_y_component(Angle::from_degrees(90.0), 1.0);
assert_eq!(result.x as i32, 1);
assert!(result.y > 100000.0);
let result = get_y_component(180.0, 1.0);
assert_eq!(result[0] as i32, -1);
assert!((result[1] - 0.0).abs() < f64::EPSILON);
let result = get_y_component(Angle::from_degrees(180.0), 1.0);
assert_eq!(result.x as i32, -1);
assert!((result.y - 0.0).abs() < f64::EPSILON);
let result = get_y_component(270.0, 1.0);
assert_eq!(result[0] as i32, -1);
assert!(result[1] < -100000.0);
let result = get_y_component(Angle::from_degrees(270.0), 1.0);
assert_eq!(result.x as i32, -1);
assert!(result.y < -100000.0);
}
#[test]
@ -291,45 +349,60 @@ mod tests {
let mut results = Vec::new();
for &(angle, expected_result) in EACH_QUAD.iter() {
let res = get_x_component(angle as f64, 1.0);
results.push([res[0].round() as i32, res[1].round() as i32]);
let res = get_x_component(Angle::from_degrees(angle as f64), 1.0);
results.push([res.x.round() as i32, res.y.round() as i32]);
expected.push(expected_result);
}
assert_eq!(results, expected);
let result = get_x_component(0.0, 1.0);
assert!(result[0] > 100000.0);
assert_eq!(result[1] as i32, 1);
let result = get_x_component(Angle::ZERO, 1.0);
assert!(result.x > 100000.0);
assert_eq!(result.y as i32, 1);
let result = get_x_component(90.0, 1.0);
assert!((result[0] - 0.0).abs() < f64::EPSILON);
assert_eq!(result[1] as i32, 1);
let result = get_x_component(Angle::from_degrees(90.0), 1.0);
assert!((result.x - 0.0).abs() < f64::EPSILON);
assert_eq!(result.y as i32, 1);
let result = get_x_component(180.0, 1.0);
assert!(result[0] < -100000.0);
assert_eq!(result[1] as i32, 1);
let result = get_x_component(Angle::from_degrees(180.0), 1.0);
assert!(result.x < -100000.0);
assert_eq!(result.y as i32, 1);
let result = get_x_component(270.0, 1.0);
assert!((result[0] - 0.0).abs() < f64::EPSILON);
assert_eq!(result[1] as i32, -1);
let result = get_x_component(Angle::from_degrees(270.0), 1.0);
assert!((result.x - 0.0).abs() < f64::EPSILON);
assert_eq!(result.y as i32, -1);
}
#[test]
fn test_arc_center_and_end() {
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 90.0, 1.0);
let (center, end) = super::arc_center_and_end(
super::Point2d { x: 0.0, y: 0.0 },
Angle::ZERO,
Angle::from_degrees(90.0),
1.0,
);
assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -1.0);
assert_eq!(end.y, 1.0);
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 180.0, 1.0);
let (center, end) = super::arc_center_and_end(
super::Point2d { x: 0.0, y: 0.0 },
Angle::ZERO,
Angle::from_degrees(180.0),
1.0,
);
assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -2.0);
assert_eq!(end.y.round(), 0.0);
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 180.0, 10.0);
let (center, end) = super::arc_center_and_end(
super::Point2d { x: 0.0, y: 0.0 },
Angle::ZERO,
Angle::from_degrees(180.0),
10.0,
);
assert_eq!(center.x.round(), -10.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -20.0);
@ -339,42 +412,42 @@ mod tests {
#[test]
fn test_arc_angles() {
let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -1.0, y: 1.0 },
&super::Point2d { x: -1.0, y: 0.0 },
super::Point2d { x: 0.0, y: 0.0 },
super::Point2d { x: -1.0, y: 1.0 },
super::Point2d { x: -1.0, y: 0.0 },
1.0,
SourceRange(Default::default()),
)
.unwrap();
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 90.0);
assert_eq!(angle_start.degrees().round(), 0.0);
assert_eq!(angle_end.degrees().round(), 90.0);
let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -2.0, y: 0.0 },
&super::Point2d { x: -1.0, y: 0.0 },
super::Point2d { x: 0.0, y: 0.0 },
super::Point2d { x: -2.0, y: 0.0 },
super::Point2d { x: -1.0, y: 0.0 },
1.0,
SourceRange(Default::default()),
)
.unwrap();
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 180.0);
assert_eq!(angle_start.degrees().round(), 0.0);
assert_eq!(angle_end.degrees().round(), 180.0);
let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -20.0, y: 0.0 },
&super::Point2d { x: -10.0, y: 0.0 },
super::Point2d { x: 0.0, y: 0.0 },
super::Point2d { x: -20.0, y: 0.0 },
super::Point2d { x: -10.0, y: 0.0 },
10.0,
SourceRange(Default::default()),
)
.unwrap();
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 180.0);
assert_eq!(angle_start.degrees().round(), 0.0);
assert_eq!(angle_end.degrees().round(), 180.0);
let result = super::arc_angles(
&super::Point2d { x: 0.0, y: 5.0 },
&super::Point2d { x: 5.0, y: 5.0 },
&super::Point2d { x: 10.0, y: -10.0 },
super::Point2d { x: 0.0, y: 5.0 },
super::Point2d { x: 5.0, y: 5.0 },
super::Point2d { x: 10.0, y: -10.0 },
10.0,
SourceRange(Default::default()),
);
@ -384,7 +457,7 @@ mod tests {
} else {
panic!("Expected error");
}
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 180.0);
assert_eq!(angle_start.degrees().round(), 0.0);
assert_eq!(angle_end.degrees().round(), 180.0);
}
}

View File

@ -34,6 +34,8 @@ pub enum TokenType {
Colon,
/// A period.
Period,
/// A double period: `..`.
DoublePeriod,
/// A line comment.
LineComment,
/// A block comment.
@ -54,7 +56,12 @@ impl TryFrom<TokenType> for SemanticTokenType {
TokenType::LineComment => Self::COMMENT,
TokenType::BlockComment => Self::COMMENT,
TokenType::Function => Self::FUNCTION,
TokenType::Whitespace | TokenType::Brace | TokenType::Comma | TokenType::Colon | TokenType::Period => {
TokenType::Whitespace
| TokenType::Brace
| TokenType::Comma
| TokenType::Colon
| TokenType::Period
| TokenType::DoublePeriod => {
anyhow::bail!("unsupported token type: {:?}", token_type)
}
})
@ -130,12 +137,12 @@ impl From<&Token> for crate::executor::SourceRange {
}
lazy_static! {
static ref NUMBER: Regex = Regex::new(r"^-?\d+(\.\d+)?").unwrap();
static ref NUMBER: Regex = Regex::new(r"^(\d+(\.\d*)?|\.\d+)\b").unwrap();
static ref WHITESPACE: Regex = Regex::new(r"\s+").unwrap();
static ref WORD: Regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*").unwrap();
// TODO: these should be generated using our struct types for these.
static ref KEYWORD: Regex =
Regex::new(r"^(if|else|for|while|return|break|continue|fn|let|true|false|nil|and|or|not|var|const)\b").unwrap();
Regex::new(r"^(if|else|for|while|return|break|continue|fn|let|mut|loop|true|false|nil|and|or|not|var|const)\b").unwrap();
static ref OPERATOR: Regex = Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
static ref BLOCK_START: Regex = Regex::new(r"^\{").unwrap();
@ -147,6 +154,7 @@ lazy_static! {
static ref COMMA: Regex = Regex::new(r"^,").unwrap();
static ref COLON: Regex = Regex::new(r"^:").unwrap();
static ref PERIOD: Regex = Regex::new(r"^\.").unwrap();
static ref DOUBLE_PERIOD: Regex = Regex::new(r"^\.\.").unwrap();
static ref LINECOMMENT: Regex = Regex::new(r"^//.*").unwrap();
static ref BLOCKCOMMENT: Regex = Regex::new(r"^/\*[\s\S]*?\*/").unwrap();
}
@ -196,6 +204,9 @@ fn is_comma(character: &str) -> bool {
fn is_colon(character: &str) -> bool {
COLON.is_match(character)
}
fn is_double_period(character: &str) -> bool {
DOUBLE_PERIOD.is_match(character)
}
fn is_period(character: &str) -> bool {
PERIOD.is_match(character)
}
@ -296,13 +307,6 @@ fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
start_index,
));
}
if is_number(str_from_index) {
return Some(make_token(
TokenType::Number,
&match_first(str_from_index, &NUMBER)?,
start_index,
));
}
if is_operator(str_from_index) {
return Some(make_token(
TokenType::Operator,
@ -310,6 +314,13 @@ fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
start_index,
));
}
if is_number(str_from_index) {
return Some(make_token(
TokenType::Number,
&match_first(str_from_index, &NUMBER)?,
start_index,
));
}
if is_keyword(str_from_index) {
return Some(make_token(
TokenType::Keyword,
@ -331,6 +342,13 @@ fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
start_index,
));
}
if is_double_period(str_from_index) {
return Some(make_token(
TokenType::DoublePeriod,
&match_first(str_from_index, &DOUBLE_PERIOD)?,
start_index,
));
}
if is_period(str_from_index) {
return Some(make_token(
TokenType::Period,
@ -376,19 +394,18 @@ mod tests {
fn is_number_test() {
assert!(is_number("1"));
assert!(is_number("1 abc"));
assert!(is_number("1abc"));
assert!(is_number("1.1"));
assert!(is_number("1.1 abc"));
assert!(!is_number("a"));
assert!(is_number("1"));
assert!(is_number(".1"));
assert!(is_number("5?"));
assert!(is_number("5 + 6"));
assert!(is_number("5 + a"));
assert!(is_number("-5"));
assert!(is_number("5.5"));
assert!(is_number("-5.5"));
assert!(!is_number("1abc"));
assert!(!is_number("a"));
assert!(!is_number("?"));
assert!(!is_number("?5"));

View File

@ -61,7 +61,7 @@ async fn execute_and_snapshot(code: &str) -> Result<image::DynamicImage> {
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() {
let code = r#"const box = (h, l, w) => {
let code = r#"fn box = (h, l, w) => {
const myBox = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)

View File

@ -1207,7 +1207,7 @@
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0":
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0", "@codemirror/state@^6.2.1":
version "6.2.1"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.1.tgz#6dc8d8e5abb26b875e3164191872d69a5e85bd73"
integrity sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==
@ -1629,6 +1629,13 @@
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8"
integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==
"@replit/codemirror-interact@^6.3.0":
version "6.3.0"
resolved "https://registry.yarnpkg.com/@replit/codemirror-interact/-/codemirror-interact-6.3.0.tgz#977432c0b8f1a2995b93b1d5acaac27dbbb30c37"
integrity sha512-kB7ukZaZZkeKEiN5KLFOq9snxnFZRBjICLkKu5bqfPrPJXYDBmirzzpZE1dPX6qtNH5jTE3m3I1lJ+ltxk08fA==
dependencies:
"@codemirror/state" "^6.2.1"
"@rollup/pluginutils@^4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"