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_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN= 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/*/*' }} path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
publish-apps-release: sign-windows-msi:
runs-on: ubuntu-20.04 runs-on: windows-latest
if: github.event_name == 'release' if: github.event_name == 'release'
needs: [build-test-web, build-apps] needs: [build-test-web, build-apps]
env: env:
@ -157,23 +157,80 @@ jobs:
- uses: actions/download-artifact@v3 - 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 - name: Generate the update static endpoint
run: | run: |
ls -l artifact/*/*itty* ls -l artifact/*/*itty*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig` DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig` LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
WINDOWS_SIG=`cat artifact/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} RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
jq --null-input \ jq --null-input \
--arg version "v${VERSION_NO_V}" \ --arg version "v${VERSION_NO_V}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \ --arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \ --arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
--arg linux_sig "$LINUX_SIG" \ --arg linux_sig "$LINUX_SIG" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \ --arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \ --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, "version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": { "platforms": {
"darwin-x86_64": { "darwin-x86_64": {
"signature": $darwin_sig, "signature": $darwin_sig,
@ -195,6 +252,34 @@ jobs:
}' > last_update.json }' > last_update.json
cat 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 - name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v1.1.1' uses: 'google-github-actions/auth@v1.1.1'
with: with:
@ -219,6 +304,12 @@ jobs:
path: last_update.json path: last_update.json
destination: dl.kittycad.io/releases/modeling-app 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 - name: Upload release files to Github
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:

102
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/) 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 yarn install
``` ```
then
followed by:
``` ```
yarn build:wasm yarn build:wasm
``` ```
That will build the WASM binary and put in the `public` dir (though gitignored) That will build the WASM binary and put in the `public` dir (though gitignored)
finally finally, to run the web app only, run:
``` ```
yarn start 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 ## Developing in Chrome
Chrome is in the process of rolling out a new default which 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 the eye with a slash through it in the URL bar, and clicking on "Enable
Third-Party Cookies". 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 ## Tauri
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
``` ```
yarn tauri dev 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. 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.) 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 😉 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) ![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" 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"> <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 ## 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 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 ```bash
VERSION=x.y.z yarn run bump-jsons VERSION=x.y.z yarn run bump-jsons
``` ```
The PR may serve as a place to discuss the human-readable changelog and extra QA. The PR may serve as a place to discuss the human-readable changelog and extra QA.
2. Merge the PR 2. Merge the PR
@ -105,5 +155,5 @@ $ cargo fuzz list
$ cargo +nightly fuzz run parser $ cargo +nightly fuzz run parser
``` ```
For more information on fuzzing you can check out For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html). [this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).

View File

@ -9322,6 +9322,34 @@
"unpublished": false, "unpublished": false,
"deprecated": 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", "name": "extrude",
"summary": "Extrudes by a given amount.", "summary": "Extrudes by a given amount.",
@ -11173,22 +11201,13 @@
}, },
"to": { "to": {
"description": "The to point.", "description": "The to point.",
"anyOf": [ "type": "array",
{ "items": {
"description": "A point.", "type": "number",
"type": "array", "format": "double"
"items": { },
"type": "number", "maxItems": 2,
"format": "double" "minItems": 2
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
} }
} }
}, },
@ -11201,10 +11220,6 @@
}, },
"maxItems": 2, "maxItems": 2,
"minItems": 2 "minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
} }
] ]
}, },
@ -13031,6 +13046,24 @@
"unpublished": false, "unpublished": false,
"deprecated": 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", "name": "segAng",
"summary": "Returns the angle of the segment.", "summary": "Returns the angle of the segment.",
@ -15315,6 +15348,34 @@
"unpublished": false, "unpublished": false,
"deprecated": 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", "name": "startSketchAt",
"summary": "Start a sketch at a given point.", "summary": "Start a sketch at a given point.",
@ -15341,22 +15402,13 @@
}, },
"to": { "to": {
"description": "The to point.", "description": "The to point.",
"anyOf": [ "type": "array",
{ "items": {
"description": "A point.", "type": "number",
"type": "array", "format": "double"
"items": { },
"type": "number", "maxItems": 2,
"format": "double" "minItems": 2
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
} }
} }
}, },
@ -15369,10 +15421,6 @@
}, },
"maxItems": 2, "maxItems": 2,
"minItems": 2 "minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
} }
] ]
}, },
@ -15815,6 +15863,34 @@
"unpublished": false, "unpublished": false,
"deprecated": 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", "name": "xLine",
"summary": "Draw a line on the x-axis.", "summary": "Draw a line on the x-axis.",

View File

@ -16,6 +16,7 @@
* [`arc`](#arc) * [`arc`](#arc)
* [`bezierCurve`](#bezierCurve) * [`bezierCurve`](#bezierCurve)
* [`close`](#close) * [`close`](#close)
* [`cos`](#cos)
* [`extrude`](#extrude) * [`extrude`](#extrude)
* [`getExtrudeWallTransform`](#getExtrudeWallTransform) * [`getExtrudeWallTransform`](#getExtrudeWallTransform)
* [`lastSegX`](#lastSegX) * [`lastSegX`](#lastSegX)
@ -26,12 +27,15 @@
* [`line`](#line) * [`line`](#line)
* [`lineTo`](#lineTo) * [`lineTo`](#lineTo)
* [`min`](#min) * [`min`](#min)
* [`pi`](#pi)
* [`segAng`](#segAng) * [`segAng`](#segAng)
* [`segEndX`](#segEndX) * [`segEndX`](#segEndX)
* [`segEndY`](#segEndY) * [`segEndY`](#segEndY)
* [`segLen`](#segLen) * [`segLen`](#segLen)
* [`show`](#show) * [`show`](#show)
* [`sin`](#sin)
* [`startSketchAt`](#startSketchAt) * [`startSketchAt`](#startSketchAt)
* [`tan`](#tan)
* [`xLine`](#xLine) * [`xLine`](#xLine)
* [`xLineTo`](#xLineTo) * [`xLineTo`](#xLineTo)
* [`yLine`](#yLine) * [`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 ### extrude
Extrudes by a given amount. Extrudes by a given amount.
@ -2044,11 +2068,9 @@ line(data: LineData, sketch_group: SketchGroup) -> SketchGroup
// The tag. // The tag.
tag: string, tag: string,
// The to point. // The to point.
to: [number] | to: [number],
string,
} | } |
[number] | [number]
string
``` ```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. * `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 ### segAng
Returns the angle of the segment. 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 ### startSketchAt
Start a sketch at a given point. Start a sketch at a given point.
@ -2784,11 +2845,9 @@ startSketchAt(data: LineData) -> SketchGroup
// The tag. // The tag.
tag: string, tag: string,
// The to point. // The to point.
to: [number] | to: [number],
string,
} | } |
[number] | [number]
string
``` ```
#### Returns #### 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 ### xLine
Draw a line on the x-axis. 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", "name": "untitled-app",
"version": "0.6.0", "version": "0.7.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.9.0", "@codemirror/autocomplete": "^6.9.0",
@ -14,6 +14,7 @@
"@lezer/javascript": "^1.4.7", "@lezer/javascript": "^1.4.7",
"@open-rpc/client-js": "^1.8.1", "@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0",
"@sentry/react": "^7.65.0", "@sentry/react": "^7.65.0",
"@tauri-apps/api": "^1.3.0", "@tauri-apps/api": "^1.3.0",
"@testing-library/jest-dom": "^5.14.1", "@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] [dependencies]
anyhow = "1" anyhow = "1"
oauth2 = "4.4.1" kittycad = "0.2.25"
oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] } tauri = { version = "1.4.1", features = ["dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
tokio = { version = "1.29.1", features = ["time"] }
toml = "0.6.0"
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.32.0", features = ["time"] }
toml = "0.8.0"
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. # 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) 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() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.setup(|app| { .setup(|app| {
@ -97,7 +115,12 @@ fn main() {
} }
Ok(()) 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()) .plugin(tauri_plugin_fs_extra::init())
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

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

View File

@ -1,13 +1,6 @@
import { import { useRef, useEffect, useCallback, MouseEventHandler } from 'react'
useRef,
useEffect,
useLayoutEffect,
useCallback,
MouseEventHandler,
} from 'react'
import { DebugPanel } from './components/DebugPanel' import { DebugPanel } from './components/DebugPanel'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { asyncParser } from './lang/abstractSyntaxTree'
import { _executor } from './lang/executor' import { _executor } from './lang/executor'
import { PaneType, useStore } from './useStore' import { PaneType, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs' import { Logs, KCLErrors } from './components/Logs'
@ -16,13 +9,9 @@ import { MemoryPanel } from './components/MemoryPanel'
import { useHotKeyListener } from './hooks/useHotKeyListener' import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream' import { Stream } from './components/Stream'
import ModalContainer from 'react-modal-promise' import ModalContainer from 'react-modal-promise'
import { import { EngineCommand } from './lang/std/engineConnection'
EngineCommand,
EngineCommandManager,
} from './lang/std/engineConnection'
import { throttle } from './lib/utils' import { throttle } from './lib/utils'
import { AppHeader } from './components/AppHeader' import { AppHeader } from './components/AppHeader'
import { KCLError } from './lang/errors'
import { Resizable } from 're-resizable' import { Resizable } from 're-resizable'
import { import {
faCode, faCode,
@ -41,6 +30,8 @@ import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/mod
import { CodeMenu } from 'components/CodeMenu' import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor' import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
export function App() { export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
@ -48,59 +39,25 @@ export function App() {
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
useHotKeyListener() useHotKeyListener()
const { const {
addLog,
addKCLError,
setCode, setCode,
setAst,
setError,
setProgramMemory,
resetLogs,
resetKCLErrors,
setArtifactMap,
engineCommandManager, engineCommandManager,
setEngineCommandManager,
highlightRange,
setHighlightRange,
setCursor2,
setMediaStream,
setIsStreamReady,
isStreamReady,
buttonDownInStream, buttonDownInStream,
openPanes, openPanes,
setOpenPanes, setOpenPanes,
didDragInStream, didDragInStream,
setStreamDimensions,
streamDimensions, streamDimensions,
setIsExecuting,
defferedCode,
guiMode, guiMode,
setGuiMode,
} = useStore((s) => ({ } = useStore((s) => ({
guiMode: s.guiMode, guiMode: s.guiMode,
addLog: s.addLog, setGuiMode: s.setGuiMode,
defferedCode: s.defferedCode,
setCode: s.setCode, setCode: s.setCode,
setAst: s.setAst,
setError: s.setError,
setProgramMemory: s.setProgramMemory,
resetLogs: s.resetLogs,
resetKCLErrors: s.resetKCLErrors,
setArtifactMap: s.setArtifactNSourceRangeMaps,
engineCommandManager: s.engineCommandManager, 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, buttonDownInStream: s.buttonDownInStream,
addKCLError: s.addKCLError,
openPanes: s.openPanes, openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes, setOpenPanes: s.setOpenPanes,
didDragInStream: s.didDragInStream, didDragInStream: s.didDragInStream,
setStreamDimensions: s.setStreamDimensions,
streamDimensions: s.streamDimensions, streamDimensions: s.streamDimensions,
setIsExecuting: s.setIsExecuting,
})) }))
const { const {
@ -127,6 +84,38 @@ export function App() {
useHotkeys('shift + l', () => togglePane('logs')) useHotkeys('shift + l', () => togglePane('logs'))
useHotkeys('shift + e', () => togglePane('kclErrors')) useHotkeys('shift + e', () => togglePane('kclErrors'))
useHotkeys('shift + d', () => togglePane('debug')) 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 = const paneOpacity =
onboardingStatus === onboardingPaths.CAMERA onboardingStatus === onboardingPaths.CAMERA
@ -149,142 +138,8 @@ export function App() {
} }
}, [loadedCode, setCode]) }, [loadedCode, setCode])
const streamWidth = streamRef?.current?.offsetWidth useSetupEngineManager(streamRef, token)
const streamHeight = streamRef?.current?.offsetHeight useEngineConnectionSubscriptions()
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])
const debounceSocketSend = throttle<EngineCommand>((message) => { const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message) engineCommandManager?.sendSceneCommand(message)
@ -313,18 +168,6 @@ export function App() {
window: { x, y }, 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 { } else {
debounceSocketSend({ debounceSocketSend({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -336,6 +179,17 @@ export function App() {
}) })
} }
} else { } 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] const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type let interaction: CameraDragInteractionType_type

View File

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

View File

@ -27,6 +27,7 @@ export const Toolbar = () => {
updateAst, updateAst,
programMemory, programMemory,
engineCommandManager, engineCommandManager,
executeAst,
} = useStore((s) => ({ } = useStore((s) => ({
guiMode: s.guiMode, guiMode: s.guiMode,
setGuiMode: s.setGuiMode, setGuiMode: s.setGuiMode,
@ -35,6 +36,7 @@ export const Toolbar = () => {
updateAst: s.updateAst, updateAst: s.updateAst,
programMemory: s.programMemory, programMemory: s.programMemory,
engineCommandManager: s.engineCommandManager, engineCommandManager: s.engineCommandManager,
executeAst: s.executeAst,
})) }))
useAppMode() useAppMode()
@ -44,7 +46,7 @@ export const Toolbar = () => {
function ToolbarButtons() { function ToolbarButtons() {
return ( return (
<span className="overflow-x-auto"> <span className={styles.smallScrollbar}>
{guiMode.mode === 'default' && ( {guiMode.mode === 'default' && (
<button <button
onClick={() => { onClick={() => {
@ -70,7 +72,7 @@ export const Toolbar = () => {
pathToNode, pathToNode,
programMemory programMemory
) )
updateAst(modifiedAst) updateAst(modifiedAst, true)
}} }}
> >
SketchOnFace SketchOnFace
@ -113,7 +115,7 @@ export const Toolbar = () => {
ast, ast,
pathToNode pathToNode
) )
updateAst(modifiedAst, { focusPath: pathToExtrudeArg }) updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
}} }}
> >
ExtrudeSketch ExtrudeSketch
@ -130,7 +132,7 @@ export const Toolbar = () => {
pathToNode, pathToNode,
false false
) )
updateAst(modifiedAst, { focusPath: pathToExtrudeArg }) updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
}} }}
> >
ExtrudeSketch (w/o pipe) ExtrudeSketch (w/o pipe)
@ -146,7 +148,14 @@ export const Toolbar = () => {
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' }, cmd: { type: 'edit_mode_exit' },
}) })
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
setGuiMode({ mode: 'default' }) setGuiMode({ mode: 'default' })
executeAst()
}} }}
> >
Exit sketch 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 { try {
const code = `const __result__ = ${value}\nshow(__result__)` const code = `const __result__ = ${value}\nshow(__result__)`
const ast = parser_wasm(code) const ast = parser_wasm(code)
const _programMem: any = { root: {} } const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => { availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] } _programMem.root[key] = { type: 'userVal', value, __meta: [] }
}) })

View File

@ -1,11 +1,15 @@
import { Menu } from '@headlessui/react' import { Menu } from '@headlessui/react'
import { PropsWithChildren } from '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 { ActionIcon } from './ActionIcon'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import styles from './CodeMenu.module.css' import styles from './CodeMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor' import { editorShortcutMeta } from './TextEditor'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
export const CodeMenu = ({ children }: PropsWithChildren) => { export const CodeMenu = ({ children }: PropsWithChildren) => {
const { formatCode } = useStore((s) => ({ const { formatCode } = useStore((s) => ({
@ -52,6 +56,24 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
</button> </button>
</Menu.Item> </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> </Menu.Items>
</div> </div>
</Menu> </Menu>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,9 +26,10 @@ import {
addLineHighlight, addLineHighlight,
lineHighlightField, lineHighlightField,
} from 'editor/highlightextension' } from 'editor/highlightextension'
import { isOverlap } from 'lib/utils' import { isOverlap, roundOff } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors' import { kclErrToDiagnostic } from 'lang/errors'
import { CSSRuleObject } from 'tailwindcss/types/config' import { CSSRuleObject } from 'tailwindcss/types/config'
import interact from '@replit/codemirror-interact'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -49,7 +50,7 @@ export const TextEditor = ({
const pathParams = useParams() const pathParams = useParams()
const { const {
code, code,
defferedSetCode, deferredSetCode,
editorView, editorView,
engineCommandManager, engineCommandManager,
formatCode, formatCode,
@ -59,10 +60,9 @@ export const TextEditor = ({
setEditorView, setEditorView,
setIsLSPServerReady, setIsLSPServerReady,
setSelectionRanges, setSelectionRanges,
sourceRangeMap,
} = useStore((s) => ({ } = useStore((s) => ({
code: s.code, code: s.code,
defferedSetCode: s.defferedSetCode, deferredSetCode: s.deferredSetCode,
editorView: s.editorView, editorView: s.editorView,
engineCommandManager: s.engineCommandManager, engineCommandManager: s.engineCommandManager,
formatCode: s.formatCode, formatCode: s.formatCode,
@ -72,7 +72,6 @@ export const TextEditor = ({
setEditorView: s.setEditorView, setEditorView: s.setEditorView,
setIsLSPServerReady: s.setIsLSPServerReady, setIsLSPServerReady: s.setIsLSPServerReady,
setSelectionRanges: s.setSelectionRanges, setSelectionRanges: s.setSelectionRanges,
sourceRangeMap: s.sourceRangeMap,
})) }))
const { const {
@ -125,7 +124,7 @@ export const TextEditor = ({
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => { // const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => { const onChange = (value: string, viewUpdate: ViewUpdate) => {
defferedSetCode(value) deferredSetCode(value)
if (isTauri() && pathParams.id) { if (isTauri() && pathParams.id) {
// Save the file to disk // Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
@ -173,11 +172,11 @@ export const TextEditor = ({
) )
const idBasedSelections = codeBasedSelections const idBasedSelections = codeBasedSelections
.map(({ type, range }) => { .map(({ type, range }) => {
const hasOverlap = Object.entries(sourceRangeMap).filter( const hasOverlap = Object.entries(
([_, sourceRange]) => { engineCommandManager?.sourceRangeMap || {}
return isOverlap(sourceRange, range) ).filter(([_, sourceRange]) => {
} return isOverlap(sourceRange, range)
) })
if (hasOverlap.length) { if (hasOverlap.length) {
return { return {
type, type,
@ -237,6 +236,38 @@ export const TextEditor = ({
lintGutter(), lintGutter(),
linter((_view) => { linter((_view) => {
return kclErrToDiagnostic(useStore.getState().kclErrors) 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) if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { parser_wasm } from './abstractSyntaxTree' import { parser_wasm } from './abstractSyntaxTree'
import { KCLUnexpectedError } from './errors' import { KCLError } from './errors'
import { initPromise } from './rust' import { initPromise } from './rust'
beforeAll(() => initPromise) beforeAll(() => initPromise)
@ -1744,6 +1744,12 @@ describe('parsing errors', () => {
_theError = e _theError = e
} }
const theError = _theError as any 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([ expect(shown).toEqual([
{ {
type: 'sketchGroup', type: 'SketchGroup',
start: { start: {
to: [0, 0], to: [0, 0],
from: [0, 0], from: [0, 0],
@ -77,7 +77,7 @@ show(mySketch001)`
) )
expect(shown).toEqual([ expect(shown).toEqual([
{ {
type: 'extrudeGroup', type: 'ExtrudeGroup',
id: expect.any(String), id: expect.any(String),
value: [], value: [],
height: 2, height: 2,
@ -117,7 +117,7 @@ show(theExtrude, sk2)`
) )
expect(geos).toEqual([ expect(geos).toEqual([
{ {
type: 'extrudeGroup', type: 'ExtrudeGroup',
id: expect.any(String), id: expect.any(String),
value: [], value: [],
height: 2, height: 2,
@ -126,7 +126,7 @@ show(theExtrude, sk2)`
__meta: [{ sourceRange: [13, 34] }], __meta: [{ sourceRange: [13, 34] }],
}, },
{ {
type: 'extrudeGroup', type: 'ExtrudeGroup',
id: expect.any(String), id: expect.any(String),
value: [], value: [],
height: 2, height: 2,

View File

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

View File

@ -5,96 +5,21 @@ import {
SourceRangeMap, SourceRangeMap,
} from './std/engineConnection' } from './std/engineConnection'
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn' import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib' import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { rangeTypeFix } from './abstractSyntaxTree' import { rangeTypeFix } from './abstractSyntaxTree'
export type SourceRange = [number, number] export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
export type PathToNode = [string | number, string][] // [pathKey, nodeType][] export type { Position } from '../wasm-lib/kcl/bindings/Position'
export type Metadata = { export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
sourceRange: SourceRange export type { Path } from '../wasm-lib/kcl/bindings/Path'
} export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
export type Position = [number, number, number] export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
export type Rotation = [number, number, number, number] export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
interface BasePath { export type PathToNode = [string | number, string][]
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
interface Memory { interface Memory {
[key: string]: MemoryItem [key: string]: MemoryItem
@ -102,12 +27,12 @@ interface Memory {
export interface ProgramMemory { export interface ProgramMemory {
root: Memory root: Memory
return?: ProgramReturn return: ProgramReturn | null
} }
export const executor = async ( export const executor = async (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {} }, programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
// work around while the gemotry is still be stored on the frontend // work around while the gemotry is still be stored on the frontend
// will be removed when the stream UI is added. // will be removed when the stream UI is added.
@ -123,7 +48,7 @@ export const executor = async (
engineCommandManager engineCommandManager
) )
const { artifactMap, sourceRangeMap } = const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands(node, _programMemory)
tempMapCallback({ artifactMap, sourceRangeMap }) tempMapCallback({ artifactMap, sourceRangeMap })
engineCommandManager.endSession() engineCommandManager.endSession()
@ -132,7 +57,7 @@ export const executor = async (
export const _executor = async ( export const _executor = async (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {} }, programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
try { try {

View File

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

View File

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

View File

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

View File

@ -224,7 +224,7 @@ const key = 'c'
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('comments in a fn block', () => { it('comments in a fn block', () => {
const code = `const myFn = () => { const code = `fn myFn = () => {
// this is a comment // this is a comment
const yo = { a: { b: { c: '123' } } } 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 { Selections } from 'useStore'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
let lastMessage = '' let lastMessage = ''
@ -393,8 +395,6 @@ export class EngineConnection {
let videoTrack = mediaStream.getVideoTracks()[0] let videoTrack = mediaStream.getVideoTracks()[0]
this.pc?.getStats(videoTrack).then((videoTrackStats) => { 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 = { let client_metrics: ClientMetrics = {
rtc_frames_decoded: 0, rtc_frames_decoded: 0,
rtc_frames_dropped: 0, rtc_frames_dropped: 0,
@ -422,12 +422,13 @@ export class EngineConnection {
videoTrackReport.framesReceived videoTrackReport.framesReceived
client_metrics.rtc_frames_per_second = client_metrics.rtc_frames_per_second =
videoTrackReport.framesPerSecond || 0 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_jitter_sec = videoTrackReport.jitter
client_metrics.rtc_keyframes_decoded = client_metrics.rtc_keyframes_decoded =
videoTrackReport.keyFramesDecoded videoTrackReport.keyFramesDecoded
client_metrics.rtc_total_freezes_duration_sec = client_metrics.rtc_total_freezes_duration_sec =
videoTrackReport.totalFreezesDuration videoTrackReport.totalFreezesDuration || 0
} else if (videoTrackReport.type === 'transport') { } else if (videoTrackReport.type === 'transport') {
// videoTrackReport.bytesReceived, // videoTrackReport.bytesReceived,
// videoTrackReport.bytesSent, // videoTrackReport.bytesSent,
@ -770,7 +771,8 @@ export class EngineCommandManager {
if (command.type !== 'modeling_cmd_req') return Promise.resolve() if (command.type !== 'modeling_cmd_req') return Promise.resolve()
const cmd = command.cmd const cmd = command.cmd
if ( if (
cmd.type === 'camera_drag_move' && (cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move') &&
this.engineConnection?.unreliableDataChannel this.engineConnection?.unreliableDataChannel
) { ) {
cmd.sequence = this.outSequence cmd.sequence = this.outSequence
@ -882,7 +884,10 @@ export class EngineCommandManager {
} }
return command.promise return command.promise
} }
async waitForAllCommands(): Promise<{ async waitForAllCommands(
ast?: Program,
programMemory?: ProgramMemory
): Promise<{
artifactMap: ArtifactMap artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap sourceRangeMap: SourceRangeMap
}> { }> {
@ -891,9 +896,94 @@ export class EngineCommandManager {
) as PendingCommand[] ) as PendingCommand[]
const proms = pendingCommands.map(({ promise }) => promise) const proms = pendingCommands.map(({ promise }) => promise)
await Promise.all(proms) await Promise.all(proms)
if (ast && programMemory) {
await this.fixIdMappings(ast, programMemory)
}
return { return {
artifactMap: this.artifactMap, artifactMap: this.artifactMap,
sourceRangeMap: this.sourceRangeMap, 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, addNewSketchLn,
getYComponent, getYComponent,
getXComponent, getXComponent,
addCloseToPipe,
} from './sketch' } from './sketch'
import { parser_wasm } from '../abstractSyntaxTree' import { parser_wasm } from '../abstractSyntaxTree'
import { getNodePathFromSourceRange } from '../queryAst' import { getNodePathFromSourceRange } from '../queryAst'
@ -146,7 +147,7 @@ show(mySketch001)`
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
expect(sourceStart).toBe(66) expect(sourceStart).toBe(66)
const { modifiedAst } = addNewSketchLn({ let { modifiedAst } = addNewSketchLn({
node: ast, node: ast,
programMemory, programMemory,
to: [2, 3], to: [2, 3],
@ -160,12 +161,33 @@ show(mySketch001)`
], ],
}) })
// Enable rotations #152 // Enable rotations #152
const expectedCode = `const mySketch001 = startSketchAt([0, 0]) let expectedCode = `const mySketch001 = startSketchAt([0, 0])
// |> rx(45, %) // |> rx(45, %)
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
|> lineTo([2, 3], %) |> lineTo([2, 3], %)
show(mySketch001) 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) expect(recast(modifiedAst)).toBe(expectedCode)
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,7 +49,7 @@ class MockEngineCommandManager {
export async function enginelessExecutor( export async function enginelessExecutor(
ast: Program, ast: Program,
pm: ProgramMemory = { root: {} } pm: ProgramMemory = { root: {}, return: null }
): Promise<ProgramMemory> { ): Promise<ProgramMemory> {
const mockEngineCommandManager = new MockEngineCommandManager({ const mockEngineCommandManager = new MockEngineCommandManager({
setIsStreamReady: () => {}, setIsStreamReady: () => {},
@ -64,7 +64,7 @@ export async function enginelessExecutor(
export async function executor( export async function executor(
ast: Program, ast: Program,
pm: ProgramMemory = { root: {} } pm: ProgramMemory = { root: {}, return: null }
): Promise<ProgramMemory> { ): Promise<ProgramMemory> {
const engineCommandManager = new EngineCommandManager({ const engineCommandManager = new EngineCommandManager({
setIsStreamReady: () => {}, setIsStreamReady: () => {},
@ -75,6 +75,6 @@ export async function executor(
await engineCommandManager.waitForReady await engineCommandManager.waitForReady
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, engineCommandManager) const programMemory = await _executor(ast, pm, engineCommandManager)
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands(ast, programMemory)
return 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 // 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 timeout: ReturnType<typeof setTimeout> | null
let latestArgs: T let latestArgs: T
@ -66,7 +66,7 @@ export function defferExecution<T>(func: (args: T) => any, wait: number) {
func(latestArgs) func(latestArgs)
} }
function deffered(args: T) { function deferred(args: T) {
latestArgs = args latestArgs = args
if (timeout) { if (timeout) {
clearTimeout(timeout) clearTimeout(timeout)
@ -74,7 +74,7 @@ export function defferExecution<T>(func: (args: T) => any, wait: number) {
timeout = setTimeout(later, wait) timeout = setTimeout(later, wait)
} }
return deffered return deferred
} }
export function getNormalisedCoordinates({ export function getNormalisedCoordinates({

View File

@ -2,6 +2,8 @@ import { createMachine, assign } from 'xstate'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import withBaseURL from '../lib/withBaseURL' import withBaseURL from '../lib/withBaseURL'
import { CommandBarMeta } from '../lib/commands' import { CommandBarMeta } from '../lib/commands'
import { isTauri } from 'lib/isTauri'
import { invoke } from '@tauri-apps/api'
const SKIP_AUTH = const SKIP_AUTH =
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV 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 } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', '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 (context.token) headers['Authorization'] = `Bearer ${context.token}`
if (SKIP_AUTH) return LOCAL_USER if (SKIP_AUTH) return LOCAL_USER
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json() 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
if ('error_code' in user) throw new Error(user.message) if ('error_code' in user) throw new Error(user.message)
return user return user

View File

@ -4,6 +4,7 @@ import { addLineHighlight, EditorView } from './editor/highlightextension'
import { parser_wasm } from './lang/abstractSyntaxTree' import { parser_wasm } from './lang/abstractSyntaxTree'
import { Program } from './lang/abstractSyntaxTreeTypes' import { Program } from './lang/abstractSyntaxTreeTypes'
import { getNodeFromPath } from './lang/queryAst' import { getNodeFromPath } from './lang/queryAst'
import { enginelessExecutor } from './lib/testHelpers'
import { import {
ProgramMemory, ProgramMemory,
Position, Position,
@ -13,13 +14,10 @@ import {
} from './lang/executor' } from './lang/executor'
import { recast } from './lang/recast' import { recast } from './lang/recast'
import { EditorSelection } from '@codemirror/state' import { EditorSelection } from '@codemirror/state'
import { import { EngineCommandManager } from './lang/std/engineConnection'
ArtifactMap,
SourceRangeMap,
EngineCommandManager,
} from './lang/std/engineConnection'
import { KCLError } from './lang/errors' import { KCLError } from './lang/errors'
import { defferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { _executor } from './lang/executor'
export type Selection = { export type Selection = {
type: 'default' | 'line-end' | 'line-mid' type: 'default' | 'line-end' | 'line-mid'
@ -29,7 +27,7 @@ export type Selections = {
otherSelections: ('y-axis' | 'x-axis' | 'z-axis')[] otherSelections: ('y-axis' | 'x-axis' | 'z-axis')[]
codeBasedSelections: Selection[] codeBasedSelections: Selection[]
} }
export type TooTip = export type ToolTip =
| 'lineTo' | 'lineTo'
| 'line' | 'line'
| 'angledLine' | 'angledLine'
@ -59,7 +57,7 @@ export const toolTips = [
'xLineTo', 'xLineTo',
'yLineTo', 'yLineTo',
'angledLineThatIntersects', 'angledLineThatIntersects',
] as any as TooTip[] ] as any as ToolTip[]
export type GuiModes = export type GuiModes =
| { | {
@ -67,7 +65,7 @@ export type GuiModes =
} }
| { | {
mode: 'sketch' mode: 'sketch'
sketchMode: TooTip sketchMode: ToolTip
isTooltip: true isTooltip: true
waitingFirstClick: boolean waitingFirstClick: boolean
rotation: Rotation rotation: Rotation
@ -123,40 +121,37 @@ export interface StoreState {
setGuiMode: (guiMode: GuiModes) => void setGuiMode: (guiMode: GuiModes) => void
logs: string[] logs: string[]
addLog: (log: string) => void addLog: (log: string) => void
resetLogs: () => void setLogs: (logs: string[]) => void
kclErrors: KCLError[] kclErrors: KCLError[]
addKCLError: (err: KCLError) => void addKCLError: (err: KCLError) => void
setErrors: (errors: KCLError[]) => void
resetKCLErrors: () => void resetKCLErrors: () => void
ast: Program ast: Program
setAst: (ast: Program) => void setAst: (ast: Program) => void
executeAst: (ast?: Program) => void
executeAstMock: (ast?: Program) => void
updateAst: ( updateAst: (
ast: Program, ast: Program,
execute: boolean,
optionalParams?: { optionalParams?: {
focusPath?: PathToNode focusPath?: PathToNode
callBack?: (ast: Program) => void callBack?: (ast: Program) => void
} }
) => void ) => void
updateAstAsync: (ast: Program, focusPath?: PathToNode) => void updateAstAsync: (
ast: Program,
reexecute: boolean,
focusPath?: PathToNode
) => void
code: string code: string
defferedCode: string
setCode: (code: string) => void setCode: (code: string) => void
defferedSetCode: (code: string) => void deferredSetCode: (code: string) => void
executeCode: (code?: string) => void
formatCode: () => void formatCode: () => void
errorState: {
isError: boolean
error: string
}
setError: (error?: string) => void
programMemory: ProgramMemory programMemory: ProgramMemory
setProgramMemory: (programMemory: ProgramMemory) => void setProgramMemory: (programMemory: ProgramMemory) => void
isShiftDown: boolean isShiftDown: boolean
setIsShiftDown: (isShiftDown: boolean) => void setIsShiftDown: (isShiftDown: boolean) => void
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
setArtifactNSourceRangeMaps: (a: {
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
}) => void
engineCommandManager?: EngineCommandManager engineCommandManager?: EngineCommandManager
setEngineCommandManager: (engineCommandManager: EngineCommandManager) => void setEngineCommandManager: (engineCommandManager: EngineCommandManager) => void
mediaStream?: MediaStream mediaStream?: MediaStream
@ -197,10 +192,13 @@ let pendingAstUpdates: number[] = []
export const useStore = create<StoreState>()( export const useStore = create<StoreState>()(
persist( persist(
(set, get) => { (set, get) => {
const setDefferedCode = defferExecution( // We defer this so that likely our ast has caught up to the code.
(code: string) => set({ defferedCode: code }), // If we are making changes that are not reflected in the ast, we
600 // should not be updating the ast.
) const setDeferredCode = deferExecution((code: string) => {
set({ code })
get().executeCode(code)
}, 600)
return { return {
editorView: null, editorView: null,
setEditorView: (editorView) => { setEditorView: (editorView) => {
@ -214,6 +212,22 @@ export const useStore = create<StoreState>()(
editorView.dispatch({ effects: addLineHighlight.of(selection) }) 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) => { setCursor: (selections) => {
const { editorView } = get() const { editorView } = get()
if (!editorView) return if (!editorView) return
@ -243,7 +257,10 @@ export const useStore = create<StoreState>()(
get().setCursor({ get().setCursor({
otherSelections: currestSelections.otherSelections, otherSelections: currestSelections.otherSelections,
codeBasedSelections: [ codeBasedSelections: [
{ range: [0, code.length - 1], type: 'default' }, {
range: [0, code.length ? code.length - 1 : 0],
type: 'default',
},
], ],
}) })
return return
@ -277,8 +294,8 @@ export const useStore = create<StoreState>()(
set((state) => ({ logs: [...state.logs, log] })) set((state) => ({ logs: [...state.logs, log] }))
} }
}, },
resetLogs: () => { setLogs: (logs) => {
set({ logs: [] }) set({ logs })
}, },
kclErrors: [], kclErrors: [],
addKCLError: (e) => { addKCLError: (e) => {
@ -287,6 +304,9 @@ export const useStore = create<StoreState>()(
resetKCLErrors: () => { resetKCLErrors: () => {
set({ kclErrors: [] }) set({ kclErrors: [] })
}, },
setErrors: (errors) => {
set({ kclErrors: errors })
},
ast: { ast: {
start: 0, start: 0,
end: 0, end: 0,
@ -299,7 +319,47 @@ export const useStore = create<StoreState>()(
setAst: (ast) => { setAst: (ast) => {
set({ 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 newCode = recast(ast)
const astWithUpdatedSource = parser_wasm(newCode) const astWithUpdatedSource = parser_wasm(newCode)
callBack(astWithUpdatedSource) callBack(astWithUpdatedSource)
@ -307,7 +367,6 @@ export const useStore = create<StoreState>()(
set({ set({
ast: astWithUpdatedSource, ast: astWithUpdatedSource,
code: newCode, code: newCode,
defferedCode: newCode,
}) })
if (focusPath) { if (focusPath) {
const { node } = getNodeFromPath<any>( 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 // clear any pending updates
pendingAstUpdates.forEach((id) => clearTimeout(id)) pendingAstUpdates.forEach((id) => clearTimeout(id))
pendingAstUpdates = [] pendingAstUpdates = []
// setup a new update // setup a new update
pendingAstUpdates.push( pendingAstUpdates.push(
setTimeout(() => { setTimeout(() => {
get().updateAst(ast, { focusPath }) get().updateAst(ast, reexecute, { focusPath })
}, 100) as unknown as number }, 100) as unknown as number
) )
}, },
code: '', code: '',
defferedCode: '', setCode: (code) => set({ code }),
setCode: (code) => set({ code, defferedCode: code }), deferredSetCode: (code) => {
defferedSetCode: (code) => {
set({ code }) set({ code })
setDefferedCode(code) setDeferredCode(code)
}, },
formatCode: async () => { formatCode: async () => {
const code = get().code const code = get().code
@ -353,20 +421,10 @@ export const useStore = create<StoreState>()(
const newCode = recast(ast) const newCode = recast(ast)
set({ code: newCode, ast }) set({ code: newCode, ast })
}, },
errorState: { programMemory: { root: {}, return: null },
isError: false,
error: '',
},
setError: (error = '') => {
set({ errorState: { isError: !!error, error } })
},
programMemory: { root: {}, pendingMemory: {} },
setProgramMemory: (programMemory) => set({ programMemory }), setProgramMemory: (programMemory) => set({ programMemory }),
isShiftDown: false, isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }), setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
artifactMap: {},
sourceRangeMap: {},
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
setEngineCommandManager: (engineCommandManager) => setEngineCommandManager: (engineCommandManager) =>
set({ engineCommandManager }), set({ engineCommandManager }),
setMediaStream: (mediaStream) => set({ mediaStream }), setMediaStream: (mediaStream) => set({ mediaStream }),
@ -409,9 +467,165 @@ export const useStore = create<StoreState>()(
partialize: (state) => partialize: (state) =>
Object.fromEntries( Object.fromEntries(
Object.entries(state).filter(([key]) => 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 = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -306,7 +306,7 @@ dependencies = [
"serde", "serde",
"serde_bytes", "serde_bytes",
"serde_json", "serde_json",
"time 0.3.27", "time",
"uuid", "uuid",
] ]
@ -363,18 +363,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.26" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"time 0.1.45", "windows-targets 0.48.5",
"wasm-bindgen",
"winapi",
] ]
[[package]] [[package]]
@ -390,30 +387,12 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.2.25" version = "4.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6"
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"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive 4.4.2", "clap_derive",
] ]
[[package]] [[package]]
@ -424,25 +403,13 @@ checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
"clap_lex 0.5.1", "clap_lex",
"strsim", "strsim",
"terminal_size",
"unicase", "unicase",
"unicode-width", "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]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.4.2" version = "4.4.2"
@ -452,16 +419,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
] ]
[[package]] [[package]]
@ -645,7 +603,7 @@ dependencies = [
"quote", "quote",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -659,7 +617,7 @@ dependencies = [
"quote", "quote",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -933,7 +891,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -985,7 +943,7 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1244,7 +1202,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown 0.12.3", "hashbrown 0.12.3",
"serde",
] ]
[[package]] [[package]]
@ -1255,6 +1212,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.0", "hashbrown 0.14.0",
"serde",
] ]
[[package]] [[package]]
@ -1269,6 +1227,17 @@ dependencies = [
"web-sys", "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]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.8.0" version = "2.8.0"
@ -1282,7 +1251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [ dependencies = [
"hermit-abi 0.3.2", "hermit-abi 0.3.2",
"rustix", "rustix 0.38.9",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -1345,7 +1314,7 @@ version = "0.1.26"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bson", "bson",
"clap 4.4.2", "clap",
"dashmap", "dashmap",
"derive-docs 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "derive-docs 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"expectorate", "expectorate",
@ -1393,7 +1362,7 @@ dependencies = [
"rand", "rand",
"reqwest", "reqwest",
"reqwest-conditional-middleware", "reqwest-conditional-middleware",
"reqwest-middleware 0.2.3", "reqwest-middleware",
"reqwest-retry", "reqwest-retry",
"reqwest-tracing", "reqwest-tracing",
"schemars", "schemars",
@ -1447,6 +1416,12 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.5" version = "0.4.5"
@ -1554,7 +1529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [ dependencies = [
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -1675,22 +1650,22 @@ checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
[[package]] [[package]]
name = "openapitor" name = "openapitor"
version = "0.0.5" version = "0.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#3d74c1dfb41146a268a644e9fde2c19a8cd66895"
checksum = "120168eae5b6485690af708bd1030547df62cca10a643763d416ab0e6831decb"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
"chrono", "chrono",
"clap 3.2.25", "clap",
"data-encoding", "data-encoding",
"format_serde_error", "format_serde_error",
"futures-util", "futures-util",
"http", "http",
"indexmap 1.9.3", "indexmap 2.0.0",
"json-patch", "json-patch",
"log", "log",
"numeral", "numeral",
"once_cell",
"openapiv3", "openapiv3",
"phonenumber", "phonenumber",
"proc-macro2", "proc-macro2",
@ -1698,7 +1673,7 @@ dependencies = [
"rand", "rand",
"regex", "regex",
"reqwest", "reqwest",
"reqwest-middleware 0.1.6", "reqwest-middleware",
"rustfmt-wrapper", "rustfmt-wrapper",
"schemars", "schemars",
"serde", "serde",
@ -1711,18 +1686,17 @@ dependencies = [
"slog-stdlog", "slog-stdlog",
"slog-term", "slog-term",
"thiserror", "thiserror",
"tokio",
"url", "url",
"uuid", "uuid",
] ]
[[package]] [[package]]
name = "openapiv3" name = "openapiv3"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1a9f106eb0a780abd17ba9fca8e0843e3461630bcbe2af0ad4d5d3ba4e9aa4" checksum = "75e56d5c441965b6425165b7e3223cc933ca469834f4a8b4786817a1f9dc4f13"
dependencies = [ dependencies = [
"indexmap 1.9.3", "indexmap 2.0.0",
"serde", "serde",
"serde_json", "serde_json",
] ]
@ -1752,12 +1726,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "os_str_bytes"
version = "6.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.2" version = "0.11.2"
@ -1829,7 +1797,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.7.4", "regex-syntax 0.7.4",
"structmeta", "structmeta",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -1891,7 +1859,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -1967,9 +1935,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.66" version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2183,26 +2151,10 @@ checksum = "59e50a2e70970896c99d1b8f20ddc30a70b30d3ac6e619a03a8353b64a49b277"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"reqwest", "reqwest",
"reqwest-middleware 0.2.3", "reqwest-middleware",
"task-local-extensions", "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]] [[package]]
name = "reqwest-middleware" name = "reqwest-middleware"
version = "0.2.3" version = "0.2.3"
@ -2233,7 +2185,7 @@ dependencies = [
"hyper", "hyper",
"parking_lot 0.11.2", "parking_lot 0.11.2",
"reqwest", "reqwest",
"reqwest-middleware 0.2.3", "reqwest-middleware",
"retry-policies", "retry-policies",
"task-local-extensions", "task-local-extensions",
"tokio", "tokio",
@ -2253,7 +2205,7 @@ dependencies = [
"matchit", "matchit",
"opentelemetry", "opentelemetry",
"reqwest", "reqwest",
"reqwest-middleware 0.2.3", "reqwest-middleware",
"task-local-extensions", "task-local-extensions",
"tracing", "tracing",
"tracing-opentelemetry", "tracing-opentelemetry",
@ -2310,6 +2262,20 @@ dependencies = [
"toolchain_find", "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]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.9" version = "0.38.9"
@ -2319,7 +2285,7 @@ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys 0.4.5",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -2484,9 +2450,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.187" version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7fe14252655bd1e578af19f5fa00fe02fd0013b100ca6b49fde31c41bae4c" checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -2502,13 +2468,13 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.187" version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e46b2a6ca578b3f1d4501b12f78ed4692006d79d82a1a7c561c12dbc3d625eb8" checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2524,9 +2490,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.105" version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
dependencies = [ dependencies = [
"indexmap 2.0.0", "indexmap 2.0.0",
"itoa", "itoa",
@ -2542,7 +2508,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2554,7 +2520,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2677,7 +2643,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"slog", "slog",
"time 0.3.27", "time",
] ]
[[package]] [[package]]
@ -2712,7 +2678,7 @@ dependencies = [
"slog", "slog",
"term", "term",
"thread_local", "thread_local",
"time 0.3.27", "time",
] ]
[[package]] [[package]]
@ -2771,7 +2737,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2782,7 +2748,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2798,9 +2764,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.29" version = "2.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2837,7 +2803,7 @@ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"rustix", "rustix 0.38.9",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -2862,32 +2828,33 @@ dependencies = [
] ]
[[package]] [[package]]
name = "textwrap" name = "terminal_size"
version = "0.16.0" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
dependencies = [ dependencies = [
"unicode-width", "rustix 0.37.23",
"windows-sys 0.48.0",
] ]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.47" version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.47" version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2911,17 +2878,6 @@ dependencies = [
"weezl", "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]] [[package]]
name = "time" name = "time"
version = "0.3.27" version = "0.3.27"
@ -2994,7 +2950,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -3110,7 +3066,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -3139,7 +3095,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -3225,7 +3181,7 @@ dependencies = [
"Inflector", "Inflector",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
"termcolor", "termcolor",
] ]
@ -3400,12 +3356,6 @@ dependencies = [
"try-lock", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -3433,7 +3383,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3468,7 +3418,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.29", "syn 2.0.33",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]

View File

@ -12,7 +12,7 @@ bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" } kcl-lib = { path = "kcl" }
kittycad = { version = "0.2.25", default-features = false, features = ["js"] } 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 = "0.2.87"
wasm-bindgen-futures = "0.4.37" wasm-bindgen-futures = "0.4.37"

View File

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

View File

@ -9,7 +9,7 @@ license = "MIT"
[dependencies] [dependencies]
anyhow = { version = "1.0.75", features = ["backtrace"] } 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" dashmap = "5.5.3"
derive-docs = { version = "0.1.3" } derive-docs = { version = "0.1.3" }
#derive-docs = { path = "../derive-docs" } #derive-docs = { path = "../derive-docs" }
@ -18,9 +18,9 @@ lazy_static = "1.4.0"
parse-display = "0.8.2" parse-display = "0.8.2"
regex = "1.7.1" regex = "1.7.1"
schemars = { version = "0.8", features = ["impl_json_schema", "url", "uuid1"] } schemars = { version = "0.8", features = ["impl_json_schema", "url", "uuid1"] }
serde = {version = "1.0.152", features = ["derive"] } serde = {version = "1.0.188", features = ["derive"] }
serde_json = "1.0.93" serde_json = "1.0.106"
thiserror = "1.0.47" thiserror = "1.0.48"
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] } ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] } 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::{ use crate::{
engine::EngineConnection, engine::EngineConnection,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange}, executor::{MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange, UserVal},
parser::PIPE_OPERATOR, parser::PIPE_OPERATOR,
}; };
@ -449,6 +449,7 @@ pub enum BinaryPart {
BinaryExpression(Box<BinaryExpression>), BinaryExpression(Box<BinaryExpression>),
CallExpression(Box<CallExpression>), CallExpression(Box<CallExpression>),
UnaryExpression(Box<UnaryExpression>), UnaryExpression(Box<UnaryExpression>),
MemberExpression(Box<MemberExpression>),
} }
impl From<BinaryPart> for crate::executor::SourceRange { impl From<BinaryPart> for crate::executor::SourceRange {
@ -471,6 +472,7 @@ impl BinaryPart {
BinaryPart::BinaryExpression(binary_expression) => binary_expression.recast(options), BinaryPart::BinaryExpression(binary_expression) => binary_expression.recast(options),
BinaryPart::CallExpression(call_expression) => call_expression.recast(options, indentation_level, false), BinaryPart::CallExpression(call_expression) => call_expression.recast(options, indentation_level, false),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options), 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::BinaryExpression(binary_expression) => binary_expression.start(),
BinaryPart::CallExpression(call_expression) => call_expression.start(), BinaryPart::CallExpression(call_expression) => call_expression.start(),
BinaryPart::UnaryExpression(unary_expression) => unary_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::BinaryExpression(binary_expression) => binary_expression.end(),
BinaryPart::CallExpression(call_expression) => call_expression.end(), BinaryPart::CallExpression(call_expression) => call_expression.end(),
BinaryPart::UnaryExpression(unary_expression) => unary_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()], 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::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::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) => { BinaryPart::UnaryExpression(ref mut unary_expression) => {
unary_expression.rename_identifiers(old_name, new_name) 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(), .clone(),
Value::MemberExpression(member_expression) => { Value::MemberExpression(member_expression) => member_expression.get_result(memory)?,
return Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => { Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!("FunctionExpression not implemented here: {:?}", function_expression), message: format!("FunctionExpression not implemented here: {:?}", function_expression),
@ -1082,23 +1088,23 @@ impl Literal {
impl From<Literal> for MemoryItem { impl From<Literal> for MemoryItem {
fn from(literal: Literal) -> Self { fn from(literal: Literal) -> Self {
MemoryItem::UserVal { MemoryItem::UserVal(UserVal {
value: literal.value.clone(), value: literal.value.clone(),
meta: vec![Metadata { meta: vec![Metadata {
source_range: literal.into(), source_range: literal.into(),
}], }],
} })
} }
} }
impl From<&Box<Literal>> for MemoryItem { impl From<&Box<Literal>> for MemoryItem {
fn from(literal: &Box<Literal>) -> Self { fn from(literal: &Box<Literal>) -> Self {
MemoryItem::UserVal { MemoryItem::UserVal(UserVal {
value: literal.value.clone(), value: literal.value.clone(),
meta: vec![Metadata { meta: vec![Metadata {
source_range: literal.into(), source_range: literal.into(),
}], }],
} })
} }
} }
@ -1227,12 +1233,7 @@ impl ArrayExpression {
source_ranges: vec![pipe_substitution.into()], source_ranges: vec![pipe_substitution.into()],
})); }));
} }
Value::MemberExpression(member_expression) => { Value::MemberExpression(member_expression) => member_expression.get_result(memory)?,
return Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => { Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!("FunctionExpression not implemented here: {:?}", function_expression), message: format!("FunctionExpression not implemented here: {:?}", function_expression),
@ -1245,12 +1246,12 @@ impl ArrayExpression {
results.push(result); results.push(result);
} }
Ok(MemoryItem::UserVal { Ok(MemoryItem::UserVal(UserVal {
value: results.into(), value: results.into(),
meta: vec![Metadata { meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }],
}) }))
} }
/// Rename all identifiers that have the old name to the new given name. /// 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()?); object.insert(property.key.name.clone(), result.get_json_value()?);
} }
Ok(MemoryItem::UserVal { Ok(MemoryItem::UserVal(UserVal {
value: object.into(), value: object.into(),
meta: vec![Metadata { meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }],
}) }))
} }
/// Rename all identifiers that have the old name to the new given name. /// Rename all identifiers that have the old name to the new given name.
@ -1554,6 +1555,38 @@ impl MemberExpression {
None 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> { pub fn get_result(&self, memory: &mut ProgramMemory) -> Result<MemoryItem, KclError> {
let property_name = match &self.property { let property_name = match &self.property {
LiteralIdentifier::Identifier(identifier) => identifier.name.to_string(), LiteralIdentifier::Identifier(identifier) => identifier.name.to_string(),
@ -1562,9 +1595,12 @@ impl MemberExpression {
// Parse this as a string. // Parse this as a string.
if let serde_json::Value::String(string) = value { if let serde_json::Value::String(string) = value {
string 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 { } else {
return Err(KclError::Semantic(KclErrorDetails { 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()], source_ranges: vec![literal.into()],
})); }));
} }
@ -1582,12 +1618,12 @@ impl MemberExpression {
if let serde_json::Value::Object(map) = object { if let serde_json::Value::Object(map) = object {
if let Some(value) = map.get(&property_name) { if let Some(value) = map.get(&property_name) {
Ok(MemoryItem::UserVal { Ok(MemoryItem::UserVal(UserVal {
value: value.clone(), value: value.clone(),
meta: vec![Metadata { meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }],
}) }))
} else { } else {
Err(KclError::UndefinedValue(KclErrorDetails { Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("Property {} not found in object", property_name), message: format!("Property {} not found in object", property_name),
@ -1715,12 +1751,12 @@ impl BinaryExpression {
parse_json_value_as_string(&right_json_value), parse_json_value_as_string(&right_json_value),
) { ) {
let value = serde_json::Value::String(format!("{}{}", left, right)); let value = serde_json::Value::String(format!("{}{}", left, right));
return Ok(MemoryItem::UserVal { return Ok(MemoryItem::UserVal(UserVal {
value, value,
meta: vec![Metadata { meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }],
}); }));
} }
} }
@ -1735,12 +1771,12 @@ impl BinaryExpression {
BinaryOperator::Mod => (left % right).into(), BinaryOperator::Mod => (left % right).into(),
}; };
Ok(MemoryItem::UserVal { Ok(MemoryItem::UserVal(UserVal {
value, value,
meta: vec![Metadata { meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }],
}) }))
} }
/// Rename all identifiers that have the old name to the new given name. /// 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> { pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
if let serde_json::Value::String(n) = &j { if let serde_json::Value::String(n) = &j {
Some(n.clone()) Some(n.clone())
@ -1845,12 +1897,12 @@ impl UnaryExpression {
.get_json_value()?, .get_json_value()?,
self.into(), self.into(),
)?; )?;
Ok(MemoryItem::UserVal { Ok(MemoryItem::UserVal(UserVal {
value: (-(num)).into(), value: (-(num)).into(),
meta: vec![Metadata { meta: vec![Metadata {
source_range: self.into(), source_range: self.into(),
}], }],
}) }))
} }
/// Returns a hover value that includes the given character position. /// Returns a hover value that includes the given character position.
@ -2231,7 +2283,7 @@ show(part001)
#[test] #[test]
fn test_recast_comment_in_a_fn_block() { 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 // this is a comment
const yo = { a: { b: { c: '123' } } } /* block const yo = { a: { b: { c: '123' } } } /* block
comment */ comment */
@ -2247,7 +2299,7 @@ show(part001)
let recasted = program.recast(&Default::default(), 0); let recasted = program.recast(&Default::default(), 0);
assert_eq!( assert_eq!(
recasted, recasted,
r#"const myFn = () => { r#"fn myFn = () => {
// this is a comment // this is a comment
const yo = { a: { b: { c: '123' } } } const yo = { a: { b: { c: '123' } } }
/* block /* block
@ -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)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type")]
pub enum MemoryItem { pub enum MemoryItem {
UserVal { UserVal(UserVal),
value: serde_json::Value,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
SketchGroup(SketchGroup), SketchGroup(SketchGroup),
ExtrudeGroup(ExtrudeGroup), ExtrudeGroup(ExtrudeGroup),
#[ts(skip)]
ExtrudeTransform(ExtrudeTransform), ExtrudeTransform(ExtrudeTransform),
#[ts(skip)]
Function { Function {
#[serde(skip)] #[serde(skip)]
func: Option<MemoryFunction>, func: Option<MemoryFunction>,
@ -119,7 +117,16 @@ pub enum MemoryItem {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(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 struct ExtrudeTransform {
pub position: Position, pub position: Position,
pub rotation: Rotation, pub rotation: Rotation,
@ -138,7 +145,7 @@ pub type MemoryFunction = fn(
impl From<MemoryItem> for Vec<SourceRange> { impl From<MemoryItem> for Vec<SourceRange> {
fn from(item: MemoryItem) -> Self { fn from(item: MemoryItem) -> Self {
match item { 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::SketchGroup(s) => s.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeGroup(e) => e.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(), MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
@ -149,8 +156,8 @@ impl From<MemoryItem> for Vec<SourceRange> {
impl MemoryItem { impl MemoryItem {
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> { pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
if let MemoryItem::UserVal { value, .. } = self { if let MemoryItem::UserVal(user_val) = self {
Ok(value.clone()) Ok(user_val.value.clone())
} else { } else {
Err(KclError::Semantic(KclErrorDetails { Err(KclError::Semantic(KclErrorDetails {
message: format!("Not a user value: {:?}", self), message: format!("Not a user value: {:?}", self),
@ -186,7 +193,7 @@ impl MemoryItem {
/// A sketch group is a collection of paths. /// A sketch group is a collection of paths.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub struct SketchGroup { pub struct SketchGroup {
/// The id of the sketch group. /// The id of the sketch group.
pub id: uuid::Uuid, pub id: uuid::Uuid,
@ -238,7 +245,7 @@ impl SketchGroup {
/// An extrude group is a collection of extrude surfaces. /// An extrude group is a collection of extrude surfaces.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub struct ExtrudeGroup { pub struct ExtrudeGroup {
/// The id of the extrude group. /// The id of the extrude group.
pub id: uuid::Uuid, pub id: uuid::Uuid,
@ -276,15 +283,15 @@ pub enum BodyType {
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)] #[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)] #[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)] #[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)] #[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)] #[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
#[ts(export)] #[ts(export)]
pub struct SourceRange(pub [usize; 2]); pub struct SourceRange(#[ts(type = "[number, number]")] pub [usize; 2]);
impl SourceRange { impl SourceRange {
/// Create a new source range. /// 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)] #[ts(export)]
pub struct Point2d { pub struct Point2d {
pub x: f64, 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)] #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
pub struct Point3d { pub struct Point3d {
@ -401,8 +418,10 @@ impl From<SourceRange> for Metadata {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BasePath { pub struct BasePath {
/// The from point. /// The from point.
#[ts(type = "[number, number]")]
pub from: [f64; 2], pub from: [f64; 2],
/// The to point. /// The to point.
#[ts(type = "[number, number]")]
pub to: [f64; 2], pub to: [f64; 2],
/// The name of the path. /// The name of the path.
pub name: String, pub name: String,
@ -804,16 +823,16 @@ show(part001)"#,
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_execute_fn_definitions() { async fn test_execute_fn_definitions() {
let ast = r#"const def = (x) => { let ast = r#"fn def = (x) => {
return x return x
} }
const ghi = (x) => { fn ghi = (x) => {
return x return x
} }
const jkl = (x) => { fn jkl = (x) => {
return x return x
} }
const hmm = (x) => { fn hmm = (x) => {
return x return x
} }
@ -981,7 +1000,7 @@ show(firstExtrude)"#;
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() { 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]) const myBox = startSketchAt([0,0])
|> line([0, l], %) |> line([0, l], %)
|> line([w, 0], %) |> line([w, 0], %)
@ -998,4 +1017,169 @@ show(fnBox)"#;
parse_execute(ast).await.unwrap(); 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::{ use crate::{
abstract_syntax_tree_types::{ abstract_syntax_tree_types::{
BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Identifier, Literal, ValueMeta, BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Identifier, Literal, MemberExpression, ValueMeta,
}, },
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::SourceRange, executor::SourceRange,
@ -81,6 +81,7 @@ pub enum MathExpression {
BinaryExpression(Box<BinaryExpression>), BinaryExpression(Box<BinaryExpression>),
ExtendedBinaryExpression(Box<ExtendedBinaryExpression>), ExtendedBinaryExpression(Box<ExtendedBinaryExpression>),
ParenthesisToken(Box<ParenthesisToken>), ParenthesisToken(Box<ParenthesisToken>),
MemberExpression(Box<MemberExpression>),
} }
impl MathExpression { impl MathExpression {
@ -92,6 +93,7 @@ impl MathExpression {
MathExpression::BinaryExpression(binary_expression) => binary_expression.start(), MathExpression::BinaryExpression(binary_expression) => binary_expression.start(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.start(), MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.start(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.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::BinaryExpression(binary_expression) => binary_expression.end(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.end(), MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.end(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.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)?; 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 let Ok(next) = self.parser.get_token(1) {
if next.token_type == TokenType::Brace && next.value == "(" { if next.token_type == TokenType::Brace && next.value == "(" {
let closing_brace = self.parser.find_closing_brace(1, 0, "")?; let closing_brace = self.parser.find_closing_brace(1, 0, "")?;
@ -149,6 +152,24 @@ impl ReversePolishNotation {
); );
return rpn.parse(); 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( let rpn = ReversePolishNotation::new(
@ -164,7 +185,6 @@ impl ReversePolishNotation {
return rpn.parse(); return rpn.parse();
} else if current_token.token_type == TokenType::Number } else if current_token.token_type == TokenType::Number
|| current_token.token_type == TokenType::Word || current_token.token_type == TokenType::Word
|| current_token.token_type == TokenType::Keyword
|| current_token.token_type == TokenType::String || current_token.token_type == TokenType::String
{ {
let rpn = ReversePolishNotation::new( let rpn = ReversePolishNotation::new(
@ -180,6 +200,35 @@ impl ReversePolishNotation {
return rpn.parse(); return rpn.parse();
} else if let Ok(binop) = BinaryOperator::from_str(current_token.value.as_str()) { } else if let Ok(binop) = BinaryOperator::from_str(current_token.value.as_str()) {
if !self.operators.is_empty() { 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 let Ok(prevbinop) = BinaryOperator::from_str(self.operators[self.operators.len() - 1].value.as_str())
{ {
if prevbinop.precedence() >= binop.precedence() { if prevbinop.precedence() >= binop.precedence() {
@ -196,6 +245,29 @@ impl ReversePolishNotation {
return rpn.parse(); 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( let rpn = ReversePolishNotation::new(
@ -299,7 +371,7 @@ impl ReversePolishNotation {
return Err(KclError::InvalidExpression(KclErrorDetails { return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![SourceRange([a.start(), a.end()])], source_ranges: vec![SourceRange([a.start(), a.end()])],
message: format!("{:?}", a), message: format!("{:?}", a),
})) }));
} }
}; };
} }
@ -338,7 +410,7 @@ impl ReversePolishNotation {
start_extended: None, start_extended: None,
}))); })));
return self.build_tree(&reverse_polish_notation_tokens[1..], new_stack); 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.len() > 1 {
if reverse_polish_notation_tokens[1].token_type == TokenType::Brace if reverse_polish_notation_tokens[1].token_type == TokenType::Brace
&& reverse_polish_notation_tokens[1].value == "(" && 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); 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; let mut new_stack = stack;
new_stack.push(MathExpression::Identifier(Box::new(Identifier { new_stack.push(MathExpression::Identifier(Box::new(Identifier {
name: current_token.value.clone(), name: current_token.value.clone(),
@ -396,7 +480,7 @@ impl ReversePolishNotation {
return Err(KclError::InvalidExpression(KclErrorDetails { return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()], source_ranges: vec![current_token.into()],
message: format!("{:?}", a), message: format!("{:?}", a),
})) }));
} }
}; };
let paran = match &stack[stack.len() - 2] { let paran = match &stack[stack.len() - 2] {
@ -445,7 +529,7 @@ impl ReversePolishNotation {
return Err(KclError::InvalidExpression(KclErrorDetails { return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()], source_ranges: vec![current_token.into()],
message: format!("{:?}", a), message: format!("{:?}", a),
})) }));
} }
}; };
let mut new_stack = stack[0..stack.len() - 2].to_vec(); 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::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.start),
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.start), MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.start),
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.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 => { a => {
return Err(KclError::InvalidExpression(KclErrorDetails { return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()], source_ranges: vec![current_token.into()],
@ -513,6 +601,10 @@ impl ReversePolishNotation {
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.end), MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.end),
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.end), MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.end),
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.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 => { a => {
return Err(KclError::InvalidExpression(KclErrorDetails { return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()], source_ranges: vec![current_token.into()],
@ -521,13 +613,7 @@ impl ReversePolishNotation {
} }
}; };
let right_end = match right.0.clone() { let right_end = right.0.clone().end();
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 tree = BinaryExpression { let tree = BinaryExpression {
operator: BinaryOperator::from_str(&current_token.value.clone()).map_err(|err| { 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> { pub fn parse(&mut self) -> Result<BinaryExpression, KclError> {
let rpn = self.rpn.parse()?; let rpn = self.rpn.parse()?;
let tree_with_maybe_bad_top_level_start_end = self.rpn.build_tree(&rpn, vec![])?; 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 { let left_start = tree_with_maybe_bad_top_level_start_end.clone().left.start();
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 min_start = if left_start < tree_with_maybe_bad_top_level_start_end.start { let min_start = if left_start < tree_with_maybe_bad_top_level_start_end.start {
left_start left_start
} else { } else {
tree_with_maybe_bad_top_level_start_end.start tree_with_maybe_bad_top_level_start_end.start
}; };
let right_end = match tree_with_maybe_bad_top_level_start_end.clone().right { let right_end = tree_with_maybe_bad_top_level_start_end.clone().right.end();
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 max_end = if right_end > tree_with_maybe_bad_top_level_start_end.end { let max_end = if right_end > tree_with_maybe_bad_top_level_start_end.end {
right_end right_end
} else { } 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] #[test]
fn test_parse_expression_plus_followed_by_star() { fn test_parse_expression_plus_followed_by_star() {
let tokens = crate::tokeniser::lexer("1 + 2 * 3"); let tokens = crate::tokeniser::lexer("1 + 2 * 3");

View File

@ -427,7 +427,7 @@ impl Parser {
} }
let current_token = self.get_token(index)?; let current_token = self.get_token(index)?;
let very_next_token = self.get_token(index + 1)?; 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.token_type == TokenType::Brace
&& very_next_token.value == "(" && very_next_token.value == "("
{ {
@ -550,22 +550,41 @@ impl Parser {
&self, &self,
index: usize, index: usize,
_previous_keys: Option<Vec<ObjectKeyInfo>>, _previous_keys: Option<Vec<ObjectKeyInfo>>,
has_opening_brace: bool,
) -> Result<Vec<ObjectKeyInfo>, KclError> { ) -> Result<Vec<ObjectKeyInfo>, KclError> {
let previous_keys = _previous_keys.unwrap_or(vec![]); let previous_keys = _previous_keys.unwrap_or(vec![]);
let next_token = self.next_meaningful_token(index, None)?; 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); 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) => { Some(next_token_val) => {
if next_token_val.token_type == TokenType::Brace && next_token_val.value == "]" { if next_token_val.token_type == TokenType::Brace && next_token_val.value == "[" {
self.next_meaningful_token(next_token.index, None)? true
} else { } 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 let Some(period_or_opening_bracket_token) = period_or_opening_bracket.token {
if period_or_opening_bracket_token.token_type != TokenType::Period if period_or_opening_bracket_token.token_type != TokenType::Period
@ -573,11 +592,26 @@ impl Parser {
{ {
return Ok(previous_keys); 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 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 next_period_or_opening_bracket = self.next_meaningful_token(key_token.index, None)?;
let is_braced = match next_period_or_opening_bracket.token { let is_braced = match next_period_or_opening_bracket.token {
Some(next_period_or_opening_bracket_val) => { 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 == "]" && next_period_or_opening_bracket_val.value == "]"
} }
None => false, None => false,
@ -588,23 +622,19 @@ impl Parser {
key_token.index key_token.index
}; };
if let Some(key_token_token) = key_token.token { if let Some(key_token_token) = key_token.token {
let key = if key_token_token.token_type == TokenType::Word let key = if key_token_token.token_type == TokenType::Word {
|| key_token_token.token_type == TokenType::Keyword
{
LiteralIdentifier::Identifier(Box::new(self.make_identifier(key_token.index)?)) LiteralIdentifier::Identifier(Box::new(self.make_identifier(key_token.index)?))
} else { } else {
LiteralIdentifier::Literal(Box::new(self.make_literal(key_token.index)?)) LiteralIdentifier::Literal(Box::new(self.make_literal(key_token.index)?))
}; };
let computed = is_braced let computed = is_braced && key_token_token.token_type == TokenType::Word;
&& (key_token_token.token_type == TokenType::Word
|| key_token_token.token_type == TokenType::Keyword);
let mut new_previous_keys = previous_keys; let mut new_previous_keys = previous_keys;
new_previous_keys.push(ObjectKeyInfo { new_previous_keys.push(ObjectKeyInfo {
key, key,
index: end_index, index: end_index,
computed, 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 { } else {
Err(KclError::Unimplemented(KclErrorDetails { Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![period_or_opening_bracket_token.clone().into()], 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 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() { if keys_info.is_empty() {
return Err(KclError::Syntax(KclErrorDetails { return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()], source_ranges: vec![current_token.into()],
@ -653,6 +683,7 @@ impl Parser {
fn find_end_of_binary_expression(&self, index: usize) -> Result<usize, KclError> { fn find_end_of_binary_expression(&self, index: usize) -> Result<usize, KclError> {
let current_token = self.get_token(index)?; let current_token = self.get_token(index)?;
if current_token.token_type == TokenType::Brace && current_token.value == "(" { if current_token.token_type == TokenType::Brace && current_token.value == "(" {
let closing_parenthesis = self.find_closing_brace(index, 0, "")?; let closing_parenthesis = self.find_closing_brace(index, 0, "")?;
let maybe_another_operator = self.next_meaningful_token(closing_parenthesis, None)?; let maybe_another_operator = self.next_meaningful_token(closing_parenthesis, None)?;
@ -669,28 +700,42 @@ impl Parser {
Ok(closing_parenthesis) 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 if current_token.token_type == TokenType::Word {
&& self.get_token(index + 1)?.value == "(" if let Ok(next_token) = self.get_token(index + 1) {
{ if next_token.token_type == TokenType::Period
let closing_parenthesis = self.find_closing_brace(index + 1, 0, "")?; || (next_token.token_type == TokenType::Brace && next_token.value == "[")
let maybe_another_operator = self.next_meaningful_token(closing_parenthesis, None)?;
return if let Some(maybe_another_operator_token) = maybe_another_operator.token {
if maybe_another_operator_token.token_type != TokenType::Operator
|| maybe_another_operator_token.value == PIPE_OPERATOR
{ {
Ok(closing_parenthesis) let member_expression = self.make_member_expression(index)?;
} else { return self.find_end_of_binary_expression(member_expression.last_index);
let next_right = self.next_meaningful_token(maybe_another_operator.index, None)?;
self.find_end_of_binary_expression(next_right.index)
} }
} else {
Ok(closing_parenthesis) 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 {
if maybe_another_operator_token.token_type != TokenType::Operator
|| maybe_another_operator_token.value == PIPE_OPERATOR
{
Ok(closing_parenthesis)
} else {
let next_right = self.next_meaningful_token(maybe_another_operator.index, None)?;
self.find_end_of_binary_expression(next_right.index)
}
} else {
Ok(closing_parenthesis)
};
}
}
} }
let maybe_operator = self.next_meaningful_token(index, None)?; let maybe_operator = self.next_meaningful_token(index, None)?;
if let Some(maybe_operator_token) = maybe_operator.token { 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); return Ok(index);
} }
let next_right = self.next_meaningful_token(maybe_operator.index, None)?; let next_right = self.next_meaningful_token(maybe_operator.index, None)?;
@ -731,7 +776,6 @@ impl Parser {
} }
} }
if current_token.token_type == TokenType::Word if current_token.token_type == TokenType::Word
|| current_token.token_type == TokenType::Keyword
|| current_token.token_type == TokenType::Number || current_token.token_type == TokenType::Number
|| current_token.token_type == TokenType::String || 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 == "{" { if current_token.token_type == TokenType::Brace && current_token.value == "{" {
let object_expression = self.make_object_expression(index)?; let object_expression = self.make_object_expression(index)?;
return Ok(ValueReturn { return Ok(ValueReturn {
@ -761,11 +828,25 @@ impl Parser {
} }
if let Some(next_token) = next.token { 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::Period
|| (next_token.token_type == TokenType::Brace && next_token.value == "[")) || (next_token.token_type == TokenType::Brace && next_token.value == "["))
{ {
let member_expression = self.make_member_expression(index)?; 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 { return Ok(ValueReturn {
value: Value::MemberExpression(Box::new(member_expression.expression)), value: Value::MemberExpression(Box::new(member_expression.expression)),
last_index: member_expression.last_index, last_index: member_expression.last_index,
@ -820,7 +901,7 @@ impl Parser {
Err(KclError::Unexpected(KclErrorDetails { Err(KclError::Unexpected(KclErrorDetails {
source_ranges: vec![current_token.into()], 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 { 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_closing_brace = next_token_token.token_type == TokenType::Brace && next_token_token.value == "]";
let is_comma = next_token_token.token_type == TokenType::Comma; 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 { return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![next_token_token.clone().into()], 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 { let next_call_index = if is_closing_brace {
@ -852,9 +935,60 @@ impl Parser {
} else { } else {
self.next_meaningful_token(next_token.index, None)?.index self.next_meaningful_token(next_token.index, None)?.index
}; };
let mut _previous_elements = previous_elements;
_previous_elements.push(current_element.value); if is_double_period {
self.make_array_elements(next_call_index, _previous_elements) // 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 { } else {
Err(KclError::Unimplemented(KclErrorDetails { Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![first_element_token.into()], source_ranges: vec![first_element_token.into()],
@ -867,12 +1001,18 @@ impl Parser {
let opening_brace_token = self.get_token(index)?; let opening_brace_token = self.get_token(index)?;
let first_element_token = self.next_meaningful_token(index, None)?; let first_element_token = self.next_meaningful_token(index, None)?;
// Make sure there is a closing brace. // 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())?; let array_elements = self.make_array_elements(first_element_token.index, Vec::new())?;
Ok(ArrayReturn { Ok(ArrayReturn {
expression: ArrayExpression { expression: ArrayExpression {
start: opening_brace_token.start, start: opening_brace_token.start,
end: self.get_token(array_elements.last_index)?.end, end: closing_brace_token.end,
elements: array_elements.elements, elements: array_elements.elements,
}, },
last_index: array_elements.last_index, last_index: array_elements.last_index,
@ -949,9 +1089,23 @@ impl Parser {
}); });
} }
let argument_token = self.next_meaningful_token(index, None)?; let argument_token = self.next_meaningful_token(index, None)?;
if let Some(argument_token_token) = argument_token.token { if let Some(argument_token_token) = argument_token.token {
let next_brace_or_comma = self.next_meaningful_token(argument_token.index, None)?; 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 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 let is_identifier_or_literal = next_brace_or_comma_token.token_type == TokenType::Comma
|| next_brace_or_comma_token.token_type == TokenType::Brace; || next_brace_or_comma_token.token_type == TokenType::Brace;
if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "[" { 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))); _previous_args.push(Value::ArrayExpression(Box::new(array_expression.expression)));
return self.make_arguments(next_comma_or_brace_token_index, _previous_args); return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
} }
if argument_token_token.token_type == TokenType::Operator && argument_token_token.value == "-" { if argument_token_token.token_type == TokenType::Operator && argument_token_token.value == "-" {
let unary_expression = self.make_unary_expression(argument_token.index)?; let value = self.make_value(argument_token.index)?;
let next_comma_or_brace_token_index = let next_comma_or_brace_token_index = self.next_meaningful_token(value.last_index, None)?.index;
self.next_meaningful_token(unary_expression.last_index, None)?.index;
let mut _previous_args = previous_args; 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); return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
} }
if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "{" { 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))); _previous_args.push(Value::ObjectExpression(Box::new(object_expression.expression)));
return self.make_arguments(next_comma_or_brace_token_index, _previous_args); return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
} }
if (argument_token_token.token_type == TokenType::Keyword if (argument_token_token.token_type == TokenType::Word
|| argument_token_token.token_type == TokenType::Word
|| argument_token_token.token_type == TokenType::Number || argument_token_token.token_type == TokenType::Number
|| argument_token_token.token_type == TokenType::String) || argument_token_token.token_type == TokenType::String)
&& next_brace_or_comma_token.token_type == TokenType::Operator && 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))); _previous_args.push(Value::BinaryExpression(Box::new(binary_expression.expression)));
return self.make_arguments(binary_expression.last_index, _previous_args); return self.make_arguments(binary_expression.last_index, _previous_args);
} }
if argument_token_token.token_type == TokenType::Operator if argument_token_token.token_type == TokenType::Operator
&& argument_token_token.value == PIPE_SUBSTITUTION_OPERATOR && argument_token_token.value == PIPE_SUBSTITUTION_OPERATOR
{ {
@ -1010,8 +1164,7 @@ impl Parser {
_previous_args.push(value); _previous_args.push(value);
return self.make_arguments(next_brace_or_comma.index, _previous_args); return self.make_arguments(next_brace_or_comma.index, _previous_args);
} }
if (argument_token_token.token_type == TokenType::Keyword if argument_token_token.token_type == TokenType::Word
|| argument_token_token.token_type == TokenType::Word)
&& next_brace_or_comma_token.token_type == TokenType::Brace && next_brace_or_comma_token.token_type == TokenType::Brace
&& next_brace_or_comma_token.value == "(" && next_brace_or_comma_token.value == "("
{ {
@ -1085,9 +1238,14 @@ impl Parser {
let brace_token = self.next_meaningful_token(index, None)?; let brace_token = self.next_meaningful_token(index, None)?;
let callee = self.make_identifier(index)?; let callee = self.make_identifier(index)?;
// Make sure there is a closing brace. // 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 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) { let function = if let Some(stdlib_fn) = self.stdlib.get(&callee.name) {
crate::abstract_syntax_tree_types::Function::StdLib { func: stdlib_fn } crate::abstract_syntax_tree_types::Function::StdLib { func: stdlib_fn }
} else { } else {
@ -1127,6 +1285,19 @@ impl Parser {
previous_declarators: Vec<VariableDeclarator>, previous_declarators: Vec<VariableDeclarator>,
) -> Result<VariableDeclaratorsReturn, KclError> { ) -> Result<VariableDeclaratorsReturn, KclError> {
let current_token = self.get_token(index)?; 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 assignment = self.next_meaningful_token(index, None)?;
let Some(assignment_token) = assignment.token else { let Some(assignment_token) = assignment.token else {
return Err(KclError::Unimplemented(KclErrorDetails { return Err(KclError::Unimplemented(KclErrorDetails {
@ -1169,17 +1340,40 @@ impl Parser {
fn make_variable_declaration(&self, index: usize) -> Result<VariableDeclarationResult, KclError> { fn make_variable_declaration(&self, index: usize) -> Result<VariableDeclarationResult, KclError> {
let current_token = self.get_token(index)?; let current_token = self.get_token(index)?;
let declaration_start_token = self.next_meaningful_token(index, None)?; 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![])?; 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 { Ok(VariableDeclarationResult {
declaration: VariableDeclaration { declaration: VariableDeclaration {
start: current_token.start, start: current_token.start,
end: variable_declarators_result.declarations[variable_declarators_result.declarations.len() - 1].end, end: variable_declarators_result.declarations[variable_declarators_result.declarations.len() - 1].end,
kind: VariableKind::from_str(&current_token.value).map_err(|_| { kind,
KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: "Unexpected token".to_string(),
})
})?,
declarations: variable_declarators_result.declarations, declarations: variable_declarators_result.declarations,
}, },
last_index: variable_declarators_result.last_index, last_index: variable_declarators_result.last_index,
@ -1189,27 +1383,39 @@ impl Parser {
fn make_params(&self, index: usize, previous_params: Vec<Identifier>) -> Result<ParamsResult, KclError> { fn make_params(&self, index: usize, previous_params: Vec<Identifier>) -> Result<ParamsResult, KclError> {
let brace_or_comma_token = self.get_token(index)?; let brace_or_comma_token = self.get_token(index)?;
let argument = self.next_meaningful_token(index, None)?; let argument = self.next_meaningful_token(index, None)?;
if let Some(argument_token) = argument.token { let Some(argument_token) = argument.token else {
let should_finish_recursion = (argument_token.token_type == TokenType::Brace return Err(KclError::Unimplemented(KclErrorDetails {
&& argument_token.value == ")")
|| (brace_or_comma_token.token_type == TokenType::Brace && brace_or_comma_token.value == ")");
if should_finish_recursion {
return Ok(ParamsResult {
params: previous_params,
last_index: index,
});
}
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()], source_ranges: vec![brace_or_comma_token.into()],
message: format!("Unexpected token {}", brace_or_comma_token.value), 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 {
params: previous_params,
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)
} }
fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> { fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
@ -1239,10 +1445,11 @@ impl Parser {
Value::Literal(literal) => BinaryPart::Literal(literal), Value::Literal(literal) => BinaryPart::Literal(literal),
Value::UnaryExpression(unary_expression) => BinaryPart::UnaryExpression(unary_expression), Value::UnaryExpression(unary_expression) => BinaryPart::UnaryExpression(unary_expression),
Value::CallExpression(call_expression) => BinaryPart::CallExpression(call_expression), Value::CallExpression(call_expression) => BinaryPart::CallExpression(call_expression),
Value::MemberExpression(member_expression) => BinaryPart::MemberExpression(member_expression),
_ => { _ => {
return Err(KclError::Syntax(KclErrorDetails { return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()], 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 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.token_type == TokenType::Brace
&& next_token.value == "(" && next_token.value == "("
{ {
@ -1488,9 +1695,7 @@ impl Parser {
let next_thing = self.next_meaningful_token(token_index, None)?; let next_thing = self.next_meaningful_token(token_index, None)?;
if let Some(next_thing_token) = next_thing.token { if let Some(next_thing_token) = next_thing.token {
if (token.token_type == TokenType::Number if (token.token_type == TokenType::Number || token.token_type == TokenType::Word)
|| token.token_type == TokenType::Word
|| token.token_type == TokenType::Keyword)
&& next_thing_token.token_type == TokenType::Operator && next_thing_token.token_type == TokenType::Operator
{ {
if let Some(node) = &next_thing.non_code_node { if let Some(node) = &next_thing.non_code_node {
@ -1513,7 +1718,7 @@ impl Parser {
Err(KclError::Syntax(KclErrorDetails { Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![token.into()], 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() { fn test_collect_object_keys() {
let tokens = crate::tokeniser::lexer("const prop = yo.one[\"two\"]"); let tokens = crate::tokeniser::lexer("const prop = yo.one[\"two\"]");
let parser = Parser::new(tokens); 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); assert_eq!(keys_info.len(), 2);
let first_key = match keys_info[0].key.clone() { let first_key = match keys_info[0].key.clone() {
LiteralIdentifier::Identifier(identifier) => format!("identifier-{}", identifier.name), LiteralIdentifier::Identifier(identifier) => format!("identifier-{}", identifier.name),
@ -2759,6 +2964,73 @@ show(mySk1)"#;
assert!(result.err().unwrap().to_string().contains("Unexpected token")); 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] #[test]
fn test_parse_half_pipe() { fn test_parse_half_pipe() {
let tokens = crate::tokeniser::lexer( let tokens = crate::tokeniser::lexer(
@ -2816,7 +3088,10 @@ z(-[["#,
let parser = Parser::new(tokens); let parser = Parser::new(tokens);
let result = parser.ast(); let result = parser.ast();
assert!(result.is_err()); assert!(result.is_err());
assert!(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] #[test]
@ -2828,7 +3103,10 @@ z(-[["#,
let parser = Parser::new(tokens); let parser = Parser::new(tokens);
let result = parser.ast(); let result = parser.ast();
assert!(result.is_err()); assert!(result.is_err());
assert!(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] #[test]
@ -2881,4 +3159,229 @@ e
.to_string() .to_string()
.contains("unexpected end of expression")); .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. //! Functions implemented for language execution.
pub mod extrude; pub mod extrude;
pub mod math;
pub mod segment; pub mod segment;
pub mod sketch; pub mod sketch;
pub mod utils; pub mod utils;
@ -61,6 +62,10 @@ impl StdLib {
Box::new(crate::std::sketch::Close), Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc), Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::BezierCurve), 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(); 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> { 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, value: j,
meta: vec![Metadata { meta: vec![Metadata {
source_range: self.source_range, source_range: self.source_range,
}], }],
}) }))
} }
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> { 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> { fn get_number_array(&self) -> Result<Vec<f64>, KclError> {
let mut numbers: Vec<f64> = Vec::new(); let mut numbers: Vec<f64> = Vec::new();
for arg in &self.args { for arg in &self.args {
@ -471,7 +491,7 @@ pub fn leg_angle_x(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "legAngX", name = "legAngX",
}] }]
fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 { 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. /// 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", name = "legAngY",
}] }]
fn inner_leg_angle_y(hypotenuse: f64, leg: f64) -> f64 { 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. /// The primitive types that can be used in a KCL file.
@ -591,7 +611,7 @@ mod tests {
buf.push_str(&fn_docs); buf.push_str(&fn_docs);
} }
expectorate::assert_contents("../../../docs/kcl.md", &buf); expectorate::assert_contents("../../../docs/kcl/std.md", &buf);
} }
#[test] #[test]
@ -606,7 +626,7 @@ mod tests {
} }
expectorate::assert_contents( expectorate::assert_contents(
"../../../docs/kcl.json", "../../../docs/kcl/std.json",
&serde_json::to_string_pretty(&json_data).unwrap(), &serde_json::to_string_pretty(&json_data).unwrap(),
); );
} }

View File

@ -7,7 +7,7 @@ use schemars::JsonSchema;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{MemoryItem, SketchGroup}, executor::{MemoryItem, SketchGroup},
std::{utils::get_angle, Args}, std::{utils::Angle, Args},
}; };
/// Returns the segment end of x. /// 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 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. /// Returns the angle to match the given length for x.
@ -230,7 +230,7 @@ fn inner_angle_to_match_length_x(
if diff > length { if diff > length {
Ok(0.0) Ok(0.0)
} else { } 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 { if diff > length {
Ok(0.0) Ok(0.0)
} else { } 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. /// Data to draw a line to a point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
@ -161,34 +163,12 @@ pub enum LineData {
/// A point with a tag. /// A point with a tag.
PointWithTag { PointWithTag {
/// The to point. /// The to point.
to: PointOrDefault, to: [f64; 2],
/// The tag. /// The tag.
tag: String, tag: String,
}, },
/// A point. /// A point.
Point([f64; 2]), 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. /// 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> { fn inner_line(data: LineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.get_coords_from_paths()?;
let default = [0.2, 1.0];
let inner_args = match &data { let inner_args = match &data {
LineData::PointWithTag { to, .. } => to.get_point_with_default(default), LineData::PointWithTag { to, .. } => *to,
LineData::Point(to) => *to, LineData::Point(to) => *to,
LineData::Default(_) => default,
}; };
let to = [from.x + inner_args[0], from.y + inner_args[1]]; 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> { fn inner_x_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data { let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { to: [length, 0.0], tag },
to: PointOrDefault::Point([length, 0.0]),
tag,
},
AxisLineData::Length(length) => LineData::Point([length, 0.0]), 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> { fn inner_y_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data { let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { to: [0.0, length], tag },
to: PointOrDefault::Point([0.0, length]),
tag,
},
AxisLineData::Length(length) => LineData::Point([0.0, length]), 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]), AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
}; };
let to: [f64; 2] = [ let to: [f64; 2] = [
from.x + length * f64::cos(angle * std::f64::consts::PI / 180.0), from.x + length * f64::cos(angle.to_radians()),
from.y + length * f64::sin(angle * std::f64::consts::PI / 180.0), from.y + length * f64::sin(angle.to_radians()),
]; ];
let id = uuid::Uuid::new_v4(); 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]), 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( let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data { if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag { LineData::PointWithTag { to: to.into(), tag }
to: PointOrDefault::Point(to),
tag,
}
} else { } else {
LineData::Point(to) LineData::Point(to.into())
}, },
sketch_group, sketch_group,
args, args,
@ -483,7 +451,7 @@ fn inner_angled_line_to_x(
}; };
let x_component = x_to - from.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 y_to = from.y + y_component;
let new_sketch_group = inner_line_to( 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]), 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( let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data { if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag { LineData::PointWithTag { to: to.into(), tag }
to: PointOrDefault::Point(to),
tag,
}
} else { } else {
LineData::Point(to) LineData::Point(to.into())
}, },
sketch_group, sketch_group,
args, args,
@ -563,7 +528,7 @@ fn inner_angled_line_to_y(
}; };
let y_component = y_to - from.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 x_to = from.x + x_component;
let new_sketch_group = inner_line_to( 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 from = sketch_group.get_coords_from_paths()?;
let to = intersection_with_parallel_line( 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.offset.unwrap_or_default(),
data.angle, data.angle,
from.into(), from,
); );
let line_to_data = if let Some(tag) = data.tag { let line_to_data = if let Some(tag) = data.tag {
LineToData::PointWithTag { to, tag } LineToData::PointWithTag { to: to.into(), tag }
} else { } else {
LineToData::Point(to) LineToData::Point(to.into())
}; };
let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?; 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", name = "startSketchAt",
}] }]
fn inner_start_sketch_at(data: LineData, args: &mut Args) -> Result<SketchGroup, KclError> { fn inner_start_sketch_at(data: LineData, args: &mut Args) -> Result<SketchGroup, KclError> {
let default = [0.0, 0.0];
let to = match &data { let to = match &data {
LineData::PointWithTag { to, .. } => to.get_point_with_default(default), LineData::PointWithTag { to, .. } => *to,
LineData::Point(to) => *to, LineData::Point(to) => *to,
LineData::Default(_) => default,
}; };
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
@ -805,7 +768,7 @@ pub fn arc(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "arc", name = "arc",
}] }]
fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> { 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 { let (center, angle_start, angle_end, radius, end) = match &data {
ArcData::AnglesAndRadiusWithTag { ArcData::AnglesAndRadiusWithTag {
@ -814,23 +777,27 @@ fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Resul
radius, radius,
.. ..
} => { } => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius); let a_start = Angle::from_degrees(*angle_start);
(center, *angle_start, *angle_end, *radius, end) 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 { ArcData::AnglesAndRadius {
angle_start, angle_start,
angle_end, angle_end,
radius, radius,
} => { } => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius); let a_start = Angle::from_degrees(*angle_start);
(center, *angle_start, *angle_end, *radius, end) 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, .. } => { 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()) (center.into(), angle_start, angle_end, *radius, to.into())
} }
ArcData::CenterToRadius { center, to, radius } => { 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()) (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 { ModelingCmd::ExtendPath {
path: sketch_group.id, path: sketch_group.id,
segment: kittycad::types::PathSegment::Arc { segment: kittycad::types::PathSegment::Arc {
angle_start, angle_start: angle_start.degrees(),
angle_end, angle_end: angle_end.degrees(),
center: center.into(), center: center.into(),
radius, radius,
}, },
@ -992,16 +959,12 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::std::sketch::{LineData, PointOrDefault}; use crate::std::sketch::LineData;
#[test] #[test]
fn test_deserialize_line_data() { 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]); 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]"); assert_eq!(str_json, "[0.0,1.0]");
str_json = "[0, 1]".to_string(); str_json = "[0, 1]".to_string();
@ -1013,7 +976,7 @@ mod tests {
assert_eq!( assert_eq!(
data, data,
LineData::PointWithTag { LineData::PointWithTag {
to: PointOrDefault::Point([0.0, 1.0]), to: [0.0, 1.0],
tag: "thing".to_string() tag: "thing".to_string()
} }
); );

View File

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

View File

@ -34,6 +34,8 @@ pub enum TokenType {
Colon, Colon,
/// A period. /// A period.
Period, Period,
/// A double period: `..`.
DoublePeriod,
/// A line comment. /// A line comment.
LineComment, LineComment,
/// A block comment. /// A block comment.
@ -54,7 +56,12 @@ impl TryFrom<TokenType> for SemanticTokenType {
TokenType::LineComment => Self::COMMENT, TokenType::LineComment => Self::COMMENT,
TokenType::BlockComment => Self::COMMENT, TokenType::BlockComment => Self::COMMENT,
TokenType::Function => Self::FUNCTION, 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) anyhow::bail!("unsupported token type: {:?}", token_type)
} }
}) })
@ -130,12 +137,12 @@ impl From<&Token> for crate::executor::SourceRange {
} }
lazy_static! { 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 WHITESPACE: Regex = Regex::new(r"\s+").unwrap();
static ref WORD: Regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*").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. // TODO: these should be generated using our struct types for these.
static ref KEYWORD: Regex = 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 OPERATOR: Regex = Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap(); static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
static ref BLOCK_START: 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 COMMA: Regex = Regex::new(r"^,").unwrap();
static ref COLON: Regex = Regex::new(r"^:").unwrap(); static ref COLON: Regex = Regex::new(r"^:").unwrap();
static ref PERIOD: 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 LINECOMMENT: Regex = Regex::new(r"^//.*").unwrap();
static ref BLOCKCOMMENT: Regex = Regex::new(r"^/\*[\s\S]*?\*/").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 { fn is_colon(character: &str) -> bool {
COLON.is_match(character) COLON.is_match(character)
} }
fn is_double_period(character: &str) -> bool {
DOUBLE_PERIOD.is_match(character)
}
fn is_period(character: &str) -> bool { fn is_period(character: &str) -> bool {
PERIOD.is_match(character) PERIOD.is_match(character)
} }
@ -296,13 +307,6 @@ fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
start_index, 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) { if is_operator(str_from_index) {
return Some(make_token( return Some(make_token(
TokenType::Operator, TokenType::Operator,
@ -310,6 +314,13 @@ fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
start_index, 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) { if is_keyword(str_from_index) {
return Some(make_token( return Some(make_token(
TokenType::Keyword, TokenType::Keyword,
@ -331,6 +342,13 @@ fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
start_index, 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) { if is_period(str_from_index) {
return Some(make_token( return Some(make_token(
TokenType::Period, TokenType::Period,
@ -376,19 +394,18 @@ mod tests {
fn is_number_test() { fn is_number_test() {
assert!(is_number("1")); assert!(is_number("1"));
assert!(is_number("1 abc")); assert!(is_number("1 abc"));
assert!(is_number("1abc"));
assert!(is_number("1.1")); assert!(is_number("1.1"));
assert!(is_number("1.1 abc")); assert!(is_number("1.1 abc"));
assert!(!is_number("a")); assert!(!is_number("a"));
assert!(is_number("1")); assert!(is_number("1"));
assert!(is_number(".1"));
assert!(is_number("5?")); assert!(is_number("5?"));
assert!(is_number("5 + 6")); assert!(is_number("5 + 6"));
assert!(is_number("5 + a")); assert!(is_number("5 + a"));
assert!(is_number("-5"));
assert!(is_number("5.5")); assert!(is_number("5.5"));
assert!(is_number("-5.5"));
assert!(!is_number("1abc"));
assert!(!is_number("a")); assert!(!is_number("a"));
assert!(!is_number("?")); assert!(!is_number("?"));
assert!(!is_number("?5")); 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")] #[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() { 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]) const myBox = startSketchAt([0,0])
|> line([0, l], %) |> line([0, l], %)
|> line([w, 0], %) |> line([w, 0], %)

View File

@ -1207,7 +1207,7 @@
"@codemirror/view" "^6.0.0" "@codemirror/view" "^6.0.0"
crelt "^1.0.5" 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" version "6.2.1"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.1.tgz#6dc8d8e5abb26b875e3164191872d69a5e85bd73" resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.1.tgz#6dc8d8e5abb26b875e3164191872d69a5e85bd73"
integrity sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw== integrity sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==
@ -1629,6 +1629,13 @@
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8"
integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A== 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": "@rollup/pluginutils@^4.2.1":
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"