Compare commits

..

29 Commits

Author SHA1 Message Date
1fbd0ad675 Bump to v0.2.0 (#352) 2023-08-29 22:19:09 -04:00
743ea1af4d Kcl updates for snapshot stuff for cli (#350)
* updates for snapshot file

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-29 19:13:30 -07:00
2b1a556b81 Wrap await in try/catch to fix sign-in in tauri builds (#349)
Fixes #342
2023-08-29 21:45:40 -04:00
853389ba22 bump kittycad.rs (#348)
fixups

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-29 16:31:19 -07:00
023af60781 fmt and move error stuff locally (#347)
* fmt

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

* some fixups for errors

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

* some fixups for errors

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

* bump version

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

* fix tsc

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-29 14:12:48 -07:00
18db6f2dc1 change name for initial publish to cargo (#340)
* change name for initial publish to cargo

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

* add license

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

* updates

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

* updates

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

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-29 12:35:45 -07:00
4afec15323 bugfix: don't show a toast when onboarding changes (#346) 2023-08-29 15:27:02 -04:00
152108f7a5 Refactor to just CommandBar and GlobalState (#337)
* Refactor to just CommandBar and GlobalState

* @Irev-Dev review: consolidate uses of useContext
2023-08-29 10:48:55 -04:00
32d928ae0c Franknoirot/cmd bar (#328)
* Add XState and naive ActionBar

* Add basic dialog and combobox

* Selectable commands in command bar

* Add a few (broken) file actions

* Home commands

* Add subcommand descriptions, cleanup on navigate

* Refactor: move command creation and types to lib

* Refactor to allow any machine to add commands

* Add auth to command bar, add ability to hide cmds

* Refactor: consolidate theme utilities

* Add settings as machine and command set

* Fix: type tweaks

* Fix: only allow auth to navigate from signin

* Remove zustand-powered settings

* Fix: remove zustand settings from App

* Fix: browser infinite redirect

* Feature: allow commands to be hidden per-platform

* Fix: tsc errors

* Fix: hide default project directory from cmd bar

* Polish: transitions, css tweaks

* Feature: label current value in options settings

* Fix broken debug panel UI

* Refactor: move settings toasts to actions

* Tweak: css rounding

* Fix: set default directory recursion and reload 🐞

* Refactor: move machines to their own directory

* Fix formatting

* @Irev-Dev clean-up catches, import cleanup
2023-08-29 10:31:49 +10:00
6f0fae625f quick fik (#339) 2023-08-29 10:28:27 +10:00
9bc47cf14a fix export and prepare for cli lib (#325)
* add a test

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

* fixes

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

* fix

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

* fixup and make cleaner cfg options

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

* updates

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

* fixes

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

* redo

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

* rearrange

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>

* updates

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

* bincode error

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

* updates

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

* working

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

* switch to bson

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

* remove all bincode

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-08-28 14:58:24 -07:00
eea47aae1e Bump to v0.1.0 (#335) 2023-08-28 17:23:37 -04:00
25b9b4cf98 add isReducedMotion util (#333) 2023-08-28 18:48:31 +10:00
0f3f0b3b68 Docs macros (#318)
* initial port

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

* updates

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

* start of macro

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

* updates

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

* updates

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

* updates

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

* updates

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

* more macros

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

* updates

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

* new

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

* fixes

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

* fix clippy

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

* updates

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

* start of generated docs

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

* updates

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

* updates

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

* fixups

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

* fix

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

* fix

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates for objects

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

* fixiups

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>

* updates

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

* updates

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

* descriptions

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

* descriptions

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

* updates

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

* fixes

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

* remove vecs

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-08-25 13:41:04 -07:00
33eb6126d4 bump rust types (#321)
* get rid of noisy log

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

* fix types in rust

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-25 11:29:18 -07:00
dccb83f614 add plausible (#322)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-25 11:09:36 -07:00
b56a3398ad Fix up message structure to match the new Engine messages (#316)
* Fix up message structure to match the new Engine messages

The types are still jacked up, I reckon we need to bump the node
@KittyCAD dep.

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>

* update types

* fmt

* export tsc

* fmt again

---------

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2023-08-25 14:16:37 +10:00
11658e2ff5 cleanup code we are no longer using (#319)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-25 10:28:59 +10:00
de255acc59 Port executor (#287)
* parent

initial types

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

updates

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

updates

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

more port

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

updates

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

updates

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

fixups

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

updates

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

updates

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

updates

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

fixups

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

updates

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

updates

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

fixes

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>

updates

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

updates

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

fixups

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

fixes

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

use the function

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

ipdates

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

fixes

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

fixups

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>

updates

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

updates

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

updates

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

updates

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>

updates

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

updates

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

updates

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

pipe sjhit

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

updates

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

cleanup and pipes

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

updates

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

fixups

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

updates

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

attempt to call the function

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

updates

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

updates

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

fix tests

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

better

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

add first function

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

start of stdlib

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

updates

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

organize better

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

fixes

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

cleanup

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

fixes

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

boilerplace

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

boilerplace

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

more functions

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

more stuff

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

more path segment functions

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

reorganize files

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

extrude boilerplate

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

extrude

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

updates

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

sketch boilerplate

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

updates

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

comment out extrude for now

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

more executor test passing

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

rename meta

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

updates

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

cleanup unneeded deps

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

generate executor typoes

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

updates

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

remove path to node

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

updates for tests js

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

updates

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

ignore wasm file

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

fixes

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

start of websocket connection

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

boilerplate for engine connection

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

fix

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

updates

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

send the modeling cmd

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

implement close

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

updates

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

updates

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

updates

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

updates

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

updates

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

updates

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

updates

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

remove refid

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

remove refid

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

do sketch start

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

almost done w sketch port

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

updates

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

add more tests

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

fix deserialize and tests

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

fix tests remove logging

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

fix the return type

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

make compile

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

more tests pass

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

updates

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

expect any string

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

add failing test

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

fix the tests

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

fix tests

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

fix more tests

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

replace wasm_execute

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

fix more tests

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

updates

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

add more tests

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

make all tests pass

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

fixes

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

fix remaining tests

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

add a warpper

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

start of server side ws/webrtc

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

more nonweb working

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

updates

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

updates

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

updates

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

fixes

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

updates

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

fixes

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

add test mock

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

fixes

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

mutable engine

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

blocking snd engine cmd

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

updates

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

tmp

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

* tmp

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

* updates

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

* fix clippy

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

* fixups

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

* build wasm only

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

* fix cargo builds

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>

* more logging

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

* push

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-24 15:34:51 -07:00
d33ddb2f1b fix typo in function name, cleanup unused args (#317)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-24 11:41:38 -07:00
a0730ded4e Remove EventTarget from EngineConnection (#315)
Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2023-08-24 10:46:02 -04:00
afd2b507ef refactor engine connection to use callbacks (#314)
Less type maintenence
2023-08-24 09:46:45 -04:00
8983a8231b update for types only kittycad.rs (#312)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-24 07:08:15 +10:00
9dd708db5d Make the timeout for a WebRTC/WebSocket connection pair configurable (#310)
Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
2023-08-24 06:49:04 +10:00
c25dd1800c Track the connection time in the console (#311)
In the future, this should be sent as telemetry, but at least this would
give us a bit of info about the latency sitatuion locally if we start
seeing the refresh happen a lot.

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
2023-08-23 15:29:03 -04:00
e56e7ba0fa Detect when a video stream fails from the server (#307)
It's not clear /what/ is breaking the connection, but this will add
retry when it does. The term 'muted' here is a bit hard to grok, it's a
read-only indication of the *peer*'s state

https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/muted

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2023-08-22 20:07:39 -04:00
dbe4e7faa6 Clean up after a closed EngineConnection (#304)
Because we gate a lot of things off this.foo?, leaving the closed
websocket/pc/etc in-place was causing a noisy log, and mucking with
reestablishing a broken connection.

While I was in here, I fixed a style nit from yarn.

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2023-08-22 18:18:22 -04:00
148e125dd7 test parse errors are thrown (#294) 2023-08-22 13:28:02 +10:00
75bb91c7e1 Build out EngineConnection's retry and timeout logic (#299)
* Build out EngineConnection's retry and timeout logic

 * Migrate the EngineConnection to be an EventTarget for other parts of the
   code (mostly the EngineManager, but maybe others?) to listen to, rather
   than having a boolean 'done' promise, and remove callbacks in favor of
   the eventListeners.

 * When a WebRTC connection is online, send a 'ping' command every 10 seconds.
   The UDP stream likely needs something similar, but the connection is
   maintained by the WebRTC video stream for now.

 * Begin to migrate code to use a more generic object "send" helper
   which can handle the JSON encoding, as well as connection retry logic
   in the future.

 * Add a watchdog to trigger 5 seconds after a connection is initiated
   to cancel and retry the connection if it's not become ready by the
   time it wakes up. This won't watch an established connection yet.

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2023-08-21 16:53:31 -04:00
105 changed files with 29044 additions and 3759 deletions

View File

@ -1,3 +1,4 @@
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
VITE_KC_CONNECTION_TIMEOUT_MS=5000

View File

@ -1,3 +1,4 @@
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_CONNECTION_TIMEOUT_MS=15000

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
src/wasm-lib/pkg/wasm_lib.js

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
dir: ['src/wasm-lib', 'src-tauri']
dir: ['src/wasm-lib']
steps:
- uses: actions/checkout@v3

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
dir: ['src/wasm-lib', 'src-tauri']
dir: ['src/wasm-lib']
steps:
- uses: actions/checkout@v3
- name: Install latest rust

View File

@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest-8-cores
strategy:
matrix:
dir: ['src/wasm-lib', 'src-tauri']
dir: ['src/wasm-lib']
steps:
- uses: actions/checkout@v3
- name: Install latest rust
@ -45,4 +45,7 @@ jobs:
shell: bash
run: |-
cd "${{ matrix.dir }}"
cargo llvm-cov nextest --lcov --output-path lcov.info --test-threads=1 --no-fail-fast
cargo test --all
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}

1
.gitignore vendored
View File

@ -25,5 +25,6 @@ yarn-error.log*
# rust
src/wasm-lib/target
src/wasm-lib/bindings
src/wasm-lib/kcl/bindings
public/wasm_lib_bg.wasm
src/wasm-lib/lcov.info

17021
docs/kcl.json Normal file

File diff suppressed because it is too large Load Diff

3048
docs/kcl.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
<title>KittyCAD Modeling App</title>
</head>
<body class="body-bg">

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.0.4",
"version": "0.2.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.2",
@ -8,7 +8,8 @@
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.13",
"@kittycad/lib": "^0.0.29",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.34",
"@react-hook/resize-observer": "^1.2.6",
"@tauri-apps/api": "^1.3.0",
"@testing-library/jest-dom": "^5.14.1",
@ -22,6 +23,7 @@
"@xstate/react": "^3.2.2",
"crypto-js": "^4.1.1",
"formik": "^2.4.3",
"fuse.js": "^6.6.2",
"http-server": "^14.1.1",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
@ -62,7 +64,7 @@
"fmt-check": "prettier --check ./src",
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test --all) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/bindings",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
},

21
src-tauri/Cargo.lock generated
View File

@ -1628,6 +1628,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
[[package]]
name = "miniz_oxide"
version = "0.6.2"
@ -3022,6 +3028,7 @@ checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
dependencies = [
"anyhow",
"attohttpc",
"base64 0.21.2",
"cocoa",
"dirs-next",
"embed_plist",
@ -3034,6 +3041,7 @@ dependencies = [
"heck 0.4.1",
"http",
"ignore",
"minisign-verify",
"objc",
"once_cell",
"open",
@ -3055,12 +3063,14 @@ dependencies = [
"tauri-utils",
"tempfile",
"thiserror",
"time",
"tokio",
"url",
"uuid",
"webkit2gtk",
"webview2-com",
"windows 0.39.0",
"zip",
]
[[package]]
@ -4228,3 +4238,14 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
dependencies = [
"libc",
]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"byteorder",
"crc32fast",
"crossbeam-utils",
]

View File

@ -19,7 +19,7 @@ anyhow = "1"
oauth2 = "4.4.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "1.3.0", features = [ "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
tokio = { version = "1.29.1", features = ["time"] }
toml = "0.6.0"
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }

View File

@ -8,7 +8,7 @@
},
"package": {
"productName": "kittycad-modeling-app",
"version": "0.0.4"
"version": "0.2.0"
},
"tauri": {
"allowlist": {

View File

@ -2,7 +2,8 @@ import { render, screen } from '@testing-library/react'
import { App } from './App'
import { describe, test, vi } from 'vitest'
import { BrowserRouter } from 'react-router-dom'
import { GlobalStateProvider } from './hooks/useAuthMachine'
import { GlobalStateProvider } from './components/GlobalStateProvider'
import CommandBarProvider from 'components/CommandBar'
let listener: ((rect: any) => void) | undefined = undefined
;(global as any).ResizeObserver = class ResizeObserver {
@ -43,7 +44,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context
return (
<BrowserRouter>
<GlobalStateProvider>{children}</GlobalStateProvider>
<CommandBarProvider>
<GlobalStateProvider>{children}</GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter>
)
}

View File

@ -18,7 +18,7 @@ import {
lineHighlightField,
addLineHighlight,
} from './editor/highlightextension'
import { PaneType, Selections, Themes, useStore } from './useStore'
import { PaneType, Selections, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel'
import { MemoryPanel } from './components/MemoryPanel'
@ -41,14 +41,14 @@ import {
import { useHotkeys } from 'react-hotkeys-hook'
import { TEST } from './env'
import { getNormalisedCoordinates } from './lib/utils'
import { getSystemTheme } from './lib/getSystemTheme'
import { Themes, getSystemTheme } from './lib/theme'
import { isTauri } from './lib/isTauri'
import { useLoaderData, useParams } from 'react-router-dom'
import { writeTextFile } from '@tauri-apps/api/fs'
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
import { IndexLoaderData } from './Router'
import { toast } from 'react-hot-toast'
import { useAuthMachine } from './hooks/useAuthMachine'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
@ -83,11 +83,8 @@ export function App() {
cmdId,
setCmdId,
formatCode,
debugPanel,
theme,
openPanes,
setOpenPanes,
onboardingStatus,
didDragInStream,
setDidDragInStream,
setStreamDimensions,
@ -122,18 +119,23 @@ export function App() {
cmdId: s.cmdId,
setCmdId: s.setCmdId,
formatCode: s.formatCode,
debugPanel: s.debugPanel,
addKCLError: s.addKCLError,
theme: s.theme,
openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes,
onboardingStatus: s.onboardingStatus,
didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream,
setStreamDimensions: s.setStreamDimensions,
streamDimensions: s.streamDimensions,
}))
const [token] = useAuthMachine((s) => s?.context?.token)
const {
auth: {
context: { token },
},
settings: {
context: { showDebugPanel, theme, onboardingStatus },
},
} = useGlobalStateContext()
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
@ -295,18 +297,6 @@ export function App() {
_ast,
{
root: {
log: {
type: 'userVal',
value: (a: any) => {
addLog(a)
},
__meta: [
{
pathToNode: [],
sourceRange: [0, 0],
},
],
},
_0: {
type: 'userVal',
value: 0,
@ -328,11 +318,8 @@ export function App() {
__meta: [],
},
},
pendingMemory: {},
},
engineCommandManager,
{ bodyType: 'root' },
[]
engineCommandManager
)
const { artifactMap, sourceRangeMap } =
@ -525,7 +512,7 @@ export function App() {
</div>
</Resizable>
<Stream className="absolute inset-0 z-0" />
{debugPanel && (
{showDebugPanel && (
<DebugPanel
title="Debug"
className={

View File

@ -1,9 +1,12 @@
import Loading from './components/Loading'
import { useAuthMachine } from './hooks/useAuthMachine'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
// Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => {
const [isLoggedIn] = useAuthMachine((s) => s.matches('checkIfLoggedIn'))
const {
auth: { state },
} = useGlobalStateContext()
const isLoggedIn = state.matches('checkIfLoggedIn')
return isLoggedIn ? (
<Loading>Loading KittyCAD Modeling App...</Loading>

View File

@ -24,7 +24,13 @@ import {
} from './lib/tauriFS'
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
import DownloadAppBanner from './components/DownloadAppBanner'
import { GlobalStateProvider } from './hooks/useAuthMachine'
import { GlobalStateProvider } from './components/GlobalStateProvider'
import {
SETTINGS_PERSIST_KEY,
settingsMachine,
} from './machines/settingsMachine'
import { ContextFrom } from 'xstate'
import CommandBarProvider from 'components/CommandBar'
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
@ -68,7 +74,11 @@ const addGlobalContextToElements = (
'element' in route
? {
...route,
element: <GlobalStateProvider>{route.element}</GlobalStateProvider>,
element: (
<CommandBarProvider>
<GlobalStateProvider>{route.element}</GlobalStateProvider>
</CommandBarProvider>
),
}
: route
)
@ -95,26 +105,23 @@ const router = createBrowserRouter(
request,
params,
}): Promise<IndexLoaderData | Response> => {
const store = localStorage.getItem('store')
if (store === null) {
return redirect(paths.ONBOARDING.INDEX)
} else {
const status = JSON.parse(store).state.onboardingStatus || ''
const notEnRouteToOnboarding =
!request.url.includes(paths.ONBOARDING.INDEX) &&
request.method === 'GET'
// '' is the initial state, 'done' and 'dismissed' are the final states
const hasValidOnboardingStatus =
(status !== undefined && status.length === 0) ||
!(status === 'done' || status === 'dismissed')
const shouldRedirectToOnboarding =
notEnRouteToOnboarding && hasValidOnboardingStatus
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
ContextFrom<typeof settingsMachine>
>
if (shouldRedirectToOnboarding) {
return redirect(
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status
)
}
const status = persistedSettings.onboardingStatus || ''
const notEnRouteToOnboarding = !request.url.includes(
paths.ONBOARDING.INDEX
)
// '' is the initial state, 'done' and 'dismissed' are the final states
const hasValidOnboardingStatus =
status.length === 0 || !(status === 'done' || status === 'dismissed')
const shouldRedirectToOnboarding =
notEnRouteToOnboarding && hasValidOnboardingStatus
if (shouldRedirectToOnboarding) {
return redirect(makeUrlPathRelative(paths.ONBOARDING.INDEX) + status)
}
if (params.id && params.id !== 'new') {
@ -164,9 +171,23 @@ const router = createBrowserRouter(
if (!isTauri()) {
return redirect(paths.FILE + '/new')
}
const projectDir = await initializeProjectDirectory()
const projectsNoMeta = (await readDir(projectDir.dir)).filter(
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
ContextFrom<typeof settingsMachine>
>
const projectDir = await initializeProjectDirectory(
persistedSettings.defaultDirectory || ''
)
if (projectDir !== persistedSettings.defaultDirectory) {
localStorage.setItem(
SETTINGS_PERSIST_KEY,
JSON.stringify({
...persistedSettings,
defaultDirectory: projectDir,
})
)
}
const projectsNoMeta = (await readDir(projectDir)).filter(
isProjectDirectory
)
const projects = await Promise.all(

View File

@ -8,11 +8,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
const iconSizes = {
sm: 12,
md: 14.4,
lg: 18,
lg: 20,
xl: 28,
}
export interface ActionIconProps extends React.PropsWithChildren {
icon?: SolidIconDefinition | BrandIconDefinition
className?: string
bgClassName?: string
iconClassName?: string
size?: keyof typeof iconSizes
@ -20,6 +22,7 @@ export interface ActionIconProps extends React.PropsWithChildren {
export const ActionIcon = ({
icon = faCircleExclamation,
className,
bgClassName,
iconClassName,
size = 'md',
@ -28,7 +31,9 @@ export const ActionIcon = ({
return (
<div
className={
'p-1 w-fit inline-grid place-content-center ' +
`p-${
size === 'xl' ? '2' : '1'
} w-fit inline-grid place-content-center ${className} ` +
(bgClassName ||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
}
@ -40,7 +45,7 @@ export const ActionIcon = ({
height={iconSizes[size]}
className={
iconClassName ||
'text-liquid-20 group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
'text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
}
/>
)}

View File

@ -2,7 +2,7 @@ import { Toolbar } from '../Toolbar'
import UserSidebarMenu from './UserSidebarMenu'
import { ProjectWithEntryPointMetadata } from '../Router'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useAuthMachine } from '../hooks/useAuthMachine'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
@ -18,7 +18,11 @@ export const AppHeader = ({
className = '',
enableMenu = false,
}: AppHeaderProps) => {
const [user] = useAuthMachine((s) => s?.context?.user)
const {
auth: {
context: { user },
},
} = useGlobalStateContext()
return (
<header

View File

@ -0,0 +1,290 @@
import { Combobox, Dialog, Transition } from '@headlessui/react'
import {
Dispatch,
Fragment,
SetStateAction,
createContext,
useState,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { ActionIcon } from './ActionIcon'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import Fuse from 'fuse.js'
import { Command, SubCommand } from '../lib/commands'
import { useCommandsContext } from 'hooks/useCommandsContext'
export type SortedCommand = {
item: Partial<Command | SubCommand> & { name: string }
}
export const CommandsContext = createContext(
{} as {
commands: Command[]
addCommands: (commands: Command[]) => void
removeCommands: (commands: Command[]) => void
commandBarOpen: boolean
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
}
)
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [commands, internalSetCommands] = useState([] as Command[])
const [commandBarOpen, setCommandBarOpen] = useState(false)
const addCommands = (newCommands: Command[]) => {
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
}
const removeCommands = (newCommands: Command[]) => {
internalSetCommands((prevCommands) =>
prevCommands.filter((command) => !newCommands.includes(command))
)
}
return (
<CommandsContext.Provider
value={{
commands,
addCommands,
removeCommands,
commandBarOpen,
setCommandBarOpen,
}}
>
{children}
<CommandBar />
</CommandsContext.Provider>
)
}
const CommandBar = () => {
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
useHotkeys('meta+k', () => {
if (commands.length === 0) return
setCommandBarOpen(!commandBarOpen)
})
const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>(
null
)
// keep track of the current subcommand index
const [subCommandIndex, setSubCommandIndex] = useState<number>()
const [subCommandData, setSubCommandData] = useState<{
[key: string]: string
}>({})
// if the subcommand index is null, we're not in a subcommand
const inSubCommand =
selectedCommand &&
'meta' in selectedCommand.item &&
selectedCommand.item.meta?.args !== undefined &&
subCommandIndex !== undefined
const currentSubCommand =
inSubCommand && 'meta' in selectedCommand.item
? selectedCommand.item.meta?.args[subCommandIndex]
: undefined
const [query, setQuery] = useState('')
const availableCommands =
inSubCommand && currentSubCommand
? currentSubCommand.type === 'string'
? query
? [{ name: query }]
: currentSubCommand.options
: currentSubCommand.options
: commands
const fuse = new Fuse(availableCommands || [], {
keys: ['name', 'description'],
})
const filteredCommands = query
? fuse.search(query)
: availableCommands?.map((c) => ({ item: c } as SortedCommand))
function clearState() {
setQuery('')
setCommandBarOpen(false)
setSelectedCommand(null)
setSubCommandIndex(undefined)
setSubCommandData({})
}
function handleCommandSelection(entry: SortedCommand) {
// If we have subcommands and have not yet gathered all the
// data required from them, set the selected command to the
// current command and increment the subcommand index
if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
setSelectedCommand(entry)
setSubCommandIndex(0)
setQuery('')
return
}
const { item } = entry
// If we have just selected a command with no subcommands, run it
const isCommandWithoutSubcommands =
'callback' in item && !('meta' in item && item.meta)
if (isCommandWithoutSubcommands) {
if (item.callback === undefined) return
item.callback()
setCommandBarOpen(false)
return
}
// If we have subcommands and have not yet gathered all the
// data required from them, set the selected command to the
// current command and increment the subcommand index
if (
selectedCommand &&
subCommandIndex !== undefined &&
'meta' in selectedCommand.item
) {
const subCommand = selectedCommand.item.meta?.args[subCommandIndex]
if (subCommand) {
const newSubCommandData = {
...subCommandData,
[subCommand.name]: item.name,
}
const newSubCommandIndex = subCommandIndex + 1
// If we have subcommands and have gathered all the data required
// from them, run the command with the gathered data
if (
selectedCommand.item.callback &&
selectedCommand.item.meta?.args.length === newSubCommandIndex
) {
selectedCommand.item.callback(newSubCommandData)
setCommandBarOpen(false)
} else {
// Otherwise, set the subcommand data and increment the subcommand index
setSubCommandData(newSubCommandData)
setSubCommandIndex(newSubCommandIndex)
setQuery('')
}
}
}
}
function getDisplayValue(command: Command) {
if (command.meta?.displayValue === undefined || !command.meta.args)
return command.name
return command.meta?.displayValue(
command.meta.args.map((c) =>
subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
)
)
}
return (
<Transition.Root
show={
commandBarOpen &&
availableCommands?.length !== undefined &&
availableCommands.length > 0
}
as={Fragment}
afterLeave={() => clearState()}
>
<Dialog
onClose={() => {
setCommandBarOpen(false)
clearState()
}}
className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
>
<Transition.Child
enter="duration-100 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-75 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
</Transition.Child>
<Transition.Child
enter="duration-100 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-75 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
as={Fragment}
>
<Combobox
value={selectedCommand}
onChange={handleCommandSelection}
className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
as="div"
>
<div className="flex gap-2 items-center">
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
<div>
{inSubCommand && (
<p className="text-liquid-70 dark:text-liquid-30">
{selectedCommand.item &&
getDisplayValue(selectedCommand.item as Command)}
</p>
)}
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
className="bg-transparent focus:outline-none w-full"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k')
setCommandBarOpen(false)
if (
inSubCommand &&
event.key === 'Backspace' &&
!event.currentTarget.value
) {
setSubCommandIndex(subCommandIndex - 1)
setSelectedCommand(null)
}
}}
displayValue={(command: SortedCommand) =>
command !== null ? command.item.name : ''
}
placeholder={
inSubCommand
? `Enter <${currentSubCommand?.name}>`
: 'Search for a command'
}
value={query}
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
/>
</div>
</div>
<Combobox.Options static className="max-h-96 overflow-y-auto">
{filteredCommands?.map((commandResult) => (
<Combobox.Option
key={commandResult.item.name}
value={commandResult}
className="my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90 py-1 px-2"
>
<p>{commandResult.item.name}</p>
{(commandResult.item as SubCommand).description && (
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
{(commandResult.item as SubCommand).description}
</p>
)}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
</Transition.Child>
</Dialog>
</Transition.Root>
)
}
export default CommandBarProvider

View File

@ -5,6 +5,7 @@ import { EngineCommand } from '../lang/std/engineConnection'
import { useState } from 'react'
import { ActionButton } from '../components/ActionButton'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
import { isReducedMotion } from 'lang/util'
type SketchModeCmd = Extract<
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
@ -22,7 +23,7 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
y_axis: { x: 0, y: 1, z: 0 },
distance_to_plane: 100,
ortho: true,
animated: true, // TODO #273 get prefers reduced motion from CSS
animated: !isReducedMotion(),
})
if (!sketchModeCmd) return null
return (

View File

@ -39,6 +39,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
const initialValues: OutputFormat = {
type: defaultType,
storage: 'embedded',
presentation: 'compact',
}
const formik = useFormik({
initialValues,
@ -82,6 +83,8 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
},
})
const yo = formik.values
return (
<>
<ActionButton
@ -127,7 +130,9 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
id="storage"
name="storage"
onChange={formik.handleChange}
value={formik.values.storage}
value={
'storage' in formik.values ? formik.values.storage : ''
}
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
>
{type === 'gltf' && (

View File

@ -0,0 +1,147 @@
import { useMachine } from '@xstate/react'
import { useNavigate } from 'react-router-dom'
import { paths } from '../Router'
import {
authCommandBarMeta,
authMachine,
TOKEN_PERSIST_KEY,
} from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useRef } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import {
SETTINGS_PERSIST_KEY,
settingsCommandBarMeta,
settingsMachine,
} from 'machines/settingsMachine'
import { toast } from 'react-hot-toast'
import { setThemeClass } from 'lib/theme'
import {
AnyStateMachine,
ContextFrom,
InterpreterFrom,
Prop,
StateFrom,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'>
}
type GlobalContext = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine>
}
export const GlobalStateContext = createContext({} as GlobalContext)
export const GlobalStateProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { commands } = useCommandsContext()
// Settings machine setup
const retrievedSettings = useRef(
localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
)
const persistedSettings = Object.assign(
settingsMachine.initialState.context,
JSON.parse(retrievedSettings.current) as Partial<
(typeof settingsMachine)['context']
>
)
const [settingsState, settingsSend] = useMachine(settingsMachine, {
context: persistedSettings,
actions: {
toastSuccess: (context, event) => {
const truncatedNewValue =
'data' in event && event.data instanceof Object
? (context[Object.keys(event.data)[0] as keyof typeof context]
.toString()
.substring(0, 28) as any)
: undefined
toast.success(
event.type +
(truncatedNewValue
? ` to "${truncatedNewValue}${
truncatedNewValue.length === 28 ? '...' : ''
}"`
: '')
)
},
},
})
useStateMachineCommands({
state: settingsState,
send: settingsSend,
commands,
owner: 'settings',
commandBarMeta: settingsCommandBarMeta,
})
useEffect(
() => setThemeClass(settingsState.context.theme),
[settingsState.context.theme]
)
// Auth machine setup
const [authState, authSend] = useMachine(authMachine, {
actions: {
goToSignInPage: () => {
navigate(paths.SIGN_IN)
logout()
},
goToIndexPage: () => {
if (window.location.pathname.includes(paths.SIGN_IN)) {
navigate(paths.INDEX)
}
},
},
})
useStateMachineCommands({
state: authState,
send: authSend,
commands,
commandBarMeta: authCommandBarMeta,
owner: 'auth',
})
return (
<GlobalStateContext.Provider
value={{
auth: {
state: authState,
context: authState.context,
send: authSend,
},
settings: {
state: settingsState,
context: settingsState.context,
send: settingsSend,
},
}}
>
{children}
</GlobalStateContext.Provider>
)
}
export default GlobalStateProvider
export function logout() {
const url = withBaseUrl('/logout')
localStorage.removeItem(TOKEN_PERSIST_KEY)
return fetch(url, {
method: 'POST',
credentials: 'include',
})
}

View File

@ -1,7 +1,8 @@
import ReactJson from 'react-json-view'
import { useEffect } from 'react'
import { Themes, useStore } from '../useStore'
import { useStore } from '../useStore'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes } from '../lib/theme'
const ReactJsonTypeHack = ReactJson as any

View File

@ -14,12 +14,12 @@ describe('processMemory', () => {
return a - 2
}
const otherVar = myFn(5)
const theExtrude = startSketchAt([0, 0])
const theExtrude = startSketchAt([0, 0])
|> lineTo([-2.4, myVar], %)
|> lineTo([-0.76, otherVar], %)
|> extrude(4, %)
const theSketch = startSketchAt([0, 0])
|> lineTo([-3.35, 0.17], %)
|> lineTo([0.98, 5.16], %)
@ -28,30 +28,20 @@ describe('processMemory', () => {
show(theExtrude, theSketch)`
const ast = parser_wasm(code)
const programMemory = await enginelessExecutor(ast, {
root: {
log: {
type: 'userVal',
value: (a: any) => {
console.log('raw log', a)
},
__meta: [],
},
},
pendingMemory: {},
root: {},
})
const output = processMemory(programMemory)
expect(output.myVar).toEqual(5)
expect(output.myFn).toEqual('__function__')
expect(output.otherVar).toEqual(3)
expect(output).toEqual({
myVar: 5,
myFn: '__function__',
myFn: undefined,
otherVar: 3,
theExtrude: [],
theSketch: [
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0] },
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17] },
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16] },
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' },
],
})
})

View File

@ -1,8 +1,9 @@
import ReactJson from 'react-json-view'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes, useStore } from '../useStore'
import { useStore } from '../useStore'
import { useMemo } from 'react'
import { ProgramMemory } from '../lang/executor'
import { Themes } from '../lib/theme'
interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System>

View File

@ -2,7 +2,8 @@ import { fireEvent, render, screen } from '@testing-library/react'
import UserSidebarMenu from './UserSidebarMenu'
import { BrowserRouter } from 'react-router-dom'
import { Models } from '@kittycad/lib'
import { GlobalStateProvider } from '../hooks/useAuthMachine'
import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar'
type User = Models['User_type']
@ -94,7 +95,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context
return (
<BrowserRouter>
<GlobalStateProvider>{children}</GlobalStateProvider>
<CommandBarProvider>
<GlobalStateProvider>{children}</GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter>
)
}

View File

@ -6,8 +6,8 @@ import { useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { paths } from '../Router'
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
import { useAuthMachine } from '../hooks/useAuthMachine'
import { Models } from '@kittycad/lib'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
type User = Models['User_type']
@ -15,7 +15,9 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate()
const [_, send] = useAuthMachine()
const {
auth: { send },
} = useGlobalStateContext()
// Fallback logic for displaying user's "name":
// 1. user.name
@ -120,7 +122,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
</ActionButton>
<ActionButton
Element="button"
onClick={() => send('logout')}
onClick={() => send('Log out')}
icon={{
icon: faSignOutAlt,
bgClassName: 'bg-destroy-80',

View File

@ -8,4 +8,6 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
.VITE_KC_API_WS_MODELING_URL
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
.VITE_KC_CONNECTION_TIMEOUT_MS
export const TEST = import.meta.env.TEST

View File

@ -1,54 +0,0 @@
import { createActorContext } from '@xstate/react'
import { useNavigate } from 'react-router-dom'
import { paths } from '../Router'
import { authMachine, TOKEN_PERSIST_KEY } from '../lib/authMachine'
import withBaseUrl from '../lib/withBaseURL'
export const AuthMachineContext = createActorContext(authMachine)
export const GlobalStateProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const navigate = useNavigate()
return (
<AuthMachineContext.Provider
machine={() =>
authMachine.withConfig({
actions: {
goToSignInPage: () => {
navigate(paths.SIGN_IN)
logout()
},
goToIndexPage: () => navigate(paths.INDEX),
},
})
}
>
{children}
</AuthMachineContext.Provider>
)
}
export function useAuthMachine<T>(
selector: (
state: Parameters<Parameters<typeof AuthMachineContext.useSelector>[0]>[0]
) => T = () => null as T
): [T, ReturnType<typeof AuthMachineContext.useActor>[1]] {
// useActor api normally `[state, send] = useActor`
// we're only interested in send because of the selector
const send = AuthMachineContext.useActor()[1]
const selection = AuthMachineContext.useSelector(selector)
return [selection, send]
}
export function logout() {
const url = withBaseUrl('/logout')
localStorage.removeItem(TOKEN_PERSIST_KEY)
return fetch(url, {
method: 'POST',
credentials: 'include',
})
}

View File

@ -0,0 +1,6 @@
import { CommandsContext } from 'components/CommandBar'
import { useContext } from 'react'
export const useCommandsContext = () => {
return useContext(CommandsContext)
}

View File

@ -0,0 +1,6 @@
import { GlobalStateContext } from 'components/GlobalStateProvider'
import { useContext } from 'react'
export const useGlobalStateContext = () => {
return useContext(GlobalStateContext)
}

View File

@ -0,0 +1,42 @@
import { useEffect } from 'react'
import { AnyStateMachine, StateFrom } from 'xstate'
import { Command, CommandBarMeta, createMachineCommand } from '../lib/commands'
import { useCommandsContext } from './useCommandsContext'
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
state: StateFrom<T>
send: Function
commandBarMeta?: CommandBarMeta
commands: Command[]
owner: string
}
export default function useStateMachineCommands<T extends AnyStateMachine>({
state,
send,
commandBarMeta,
owner,
}: UseStateMachineCommandsArgs<T>) {
const { addCommands, removeCommands } = useCommandsContext()
useEffect(() => {
const newCommands = state.nextEvents
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.map((type) =>
createMachineCommand<T>({
type,
state,
send,
commandBarMeta,
owner,
})
)
.filter((c) => c !== null) as Command[]
addCommands(newCommands)
return () => {
removeCommands(newCommands)
}
}, [state])
}

View File

@ -1,67 +0,0 @@
import { useEffect } from 'react'
import { useStore } from '../useStore'
import { parse } from 'toml'
import {
createDir,
BaseDirectory,
readDir,
readTextFile,
} from '@tauri-apps/api/fs'
export const useTauriBoot = () => {
const { defaultDir, setDefaultDir, setHomeMenuItems } = useStore((s) => ({
defaultDir: s.defaultDir,
setDefaultDir: s.setDefaultDir,
setHomeMenuItems: s.setHomeMenuItems,
}))
useEffect(() => {
const isTauri = (window as any).__TAURI__
if (!isTauri) return
const run = async () => {
if (!defaultDir.base) {
createDir('puffin-projects/example', {
dir: BaseDirectory.Home,
recursive: true,
})
setDefaultDir({
base: BaseDirectory.Home,
dir: 'puffin-projects',
})
} else {
const directoryResult = await readDir(defaultDir.dir, {
dir: defaultDir.base,
recursive: true,
})
const puffinProjects = directoryResult.filter(
(file) =>
!file?.name?.startsWith('.') &&
file?.children?.find((child) => child?.name === 'wax.toml')
)
const tomlFiles = await Promise.all(
puffinProjects.map(async (file) => {
const parsedToml = parse(
await readTextFile(`${file.path}/wax.toml`, {
dir: defaultDir.base,
})
)
const mainPath = parsedToml?.package?.main
const projectName = parsedToml?.package?.name
return {
file,
mainPath,
projectName,
}
})
)
setHomeMenuItems(
tomlFiles.map(({ file, mainPath, projectName }) => ({
name: projectName,
path: mainPath ? `${file.path}/${mainPath}` : file.path,
}))
)
}
}
run()
}, [])
}

View File

@ -2,23 +2,10 @@ import ReactDOM from 'react-dom/client'
import './index.css'
import reportWebVitals from './reportWebVitals'
import { Toaster } from 'react-hot-toast'
import { Themes, useStore } from './useStore'
import { Router } from './Router'
import { HotkeysProvider } from 'react-hotkeys-hook'
import { getSystemTheme } from './lib/getSystemTheme'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
function setThemeClass(state: Partial<{ theme: Themes }>) {
const systemTheme = state.theme === Themes.System && getSystemTheme()
if (state.theme === Themes.Dark || systemTheme === Themes.Dark) {
document.body.classList.add('dark')
} else {
document.body.classList.remove('dark')
}
}
const { theme } = useStore.getState()
setThemeClass({ theme })
useStore.subscribe(setThemeClass)
root.render(
<HotkeysProvider>

View File

@ -1,4 +1,5 @@
import { parser_wasm } from './abstractSyntaxTree'
import { KCLUnexpectedError } from './errors'
import { initPromise } from './rust'
beforeAll(() => initPromise)
@ -213,7 +214,6 @@ describe('testing function declaration', () => {
id: null,
params: [],
body: {
type: 'BlockStatement',
start: 17,
end: 19,
body: [],
@ -266,7 +266,6 @@ describe('testing function declaration', () => {
},
],
body: {
type: 'BlockStatement',
start: 21,
end: 39,
body: [
@ -343,7 +342,6 @@ const myVar = funcN(1, 2)`
},
],
body: {
type: 'BlockStatement',
start: 21,
end: 37,
body: [
@ -1570,8 +1568,8 @@ const key = 'c'`
it('comments nested within a block statement', () => {
const code = `const mySketch = startSketchAt([0,0])
|> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|> lineTo([1, 1], %) /* this is
a comment
|> lineTo([1, 1], %) /* this is
a comment
spanning a few lines */
|> lineTo({ to: [1,0], tag: "rightPath" }, %)
|> close(%)
@ -1584,9 +1582,8 @@ const key = 'c'`
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
type: 'NoneCodeNode',
start: 106,
end: 168,
value:
' /* this is \n a comment \n spanning a few lines */\n ',
end: 166,
value: ' /* this is\n a comment\n spanning a few lines */\n ',
})
})
it('comments in a pipe expression', () => {
@ -1706,3 +1703,19 @@ describe('should recognise callExpresions in binaryExpressions', () => {
])
})
})
describe('parsing errors', () => {
it('should return an error when there is a unexpected closed curly brace', async () => {
const code = `const myVar = startSketchAt([}], %)`
let _theError
try {
const result = expect(parser_wasm(code))
console.log('result', result)
} catch (e) {
_theError = e
}
const theError = _theError as any
expect(theError).toEqual(new KCLUnexpectedError('Brace', [[29, 30]]))
})
})

View File

@ -3,21 +3,21 @@ import { parse_js } from '../wasm-lib/pkg/wasm_lib'
import { initPromise } from './rust'
import { Token } from './tokeniser'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
export const rangeTypeFix = (ranges: number[][]): [number, number][] =>
ranges.map(([start, end]) => [start, end])
export const parser_wasm = (code: string): Program => {
try {
const program: Program = parse_js(code)
return program
} catch (e: any) {
const parsed: {
kind: string
msg: string
sourceRanges: [number, number][]
} = JSON.parse(e.toString())
const kclError: KCLError = new KCLError(
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.msg,
parsed.sourceRanges
rangeTypeFix(parsed.sourceRanges)
)
console.log(kclError)
@ -31,15 +31,11 @@ export async function asyncParser(code: string): Promise<Program> {
const program: Program = parse_js(code)
return program
} catch (e: any) {
const parsed: {
kind: string
msg: string
sourceRanges: [number, number][]
} = JSON.parse(e.toString())
const kclError: KCLError = new KCLError(
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.msg,
parsed.sourceRanges
rangeTypeFix(parsed.sourceRanges)
)
console.log(kclError)

View File

@ -1,20 +1,20 @@
export type { Program } from '../wasm-lib/bindings/Program'
export type { Value } from '../wasm-lib/bindings/Value'
export type { ObjectExpression } from '../wasm-lib/bindings/ObjectExpression'
export type { MemberExpression } from '../wasm-lib/bindings/MemberExpression'
export type { PipeExpression } from '../wasm-lib/bindings/PipeExpression'
export type { VariableDeclaration } from '../wasm-lib/bindings/VariableDeclaration'
export type { PipeSubstitution } from '../wasm-lib/bindings/PipeSubstitution'
export type { Identifier } from '../wasm-lib/bindings/Identifier'
export type { UnaryExpression } from '../wasm-lib/bindings/UnaryExpression'
export type { BinaryExpression } from '../wasm-lib/bindings/BinaryExpression'
export type { ReturnStatement } from '../wasm-lib/bindings/ReturnStatement'
export type { ExpressionStatement } from '../wasm-lib/bindings/ExpressionStatement'
export type { CallExpression } from '../wasm-lib/bindings/CallExpression'
export type { VariableDeclarator } from '../wasm-lib/bindings/VariableDeclarator'
export type { BinaryPart } from '../wasm-lib/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/bindings/Literal'
export type { ArrayExpression } from '../wasm-lib/bindings/ArrayExpression'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value'
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
export type { MemberExpression } from '../wasm-lib/kcl/bindings/MemberExpression'
export type { PipeExpression } from '../wasm-lib/kcl/bindings/PipeExpression'
export type { VariableDeclaration } from '../wasm-lib/kcl/bindings/VariableDeclaration'
export type { PipeSubstitution } from '../wasm-lib/kcl/bindings/PipeSubstitution'
export type { Identifier } from '../wasm-lib/kcl/bindings/Identifier'
export type { UnaryExpression } from '../wasm-lib/kcl/bindings/UnaryExpression'
export type { BinaryExpression } from '../wasm-lib/kcl/bindings/BinaryExpression'
export type { ReturnStatement } from '../wasm-lib/kcl/bindings/ReturnStatement'
export type { ExpressionStatement } from '../wasm-lib/kcl/bindings/ExpressionStatement'
export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression'
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type SyntaxType =
| 'Program'
@ -22,7 +22,6 @@ export type SyntaxType =
| 'BinaryExpression'
| 'CallExpression'
| 'Identifier'
| 'BlockStatement'
| 'ReturnStatement'
| 'VariableDeclaration'
| 'VariableDeclarator'

View File

@ -11,51 +11,52 @@ describe('testing artifacts', () => {
const mySketch001 = startSketchAt([0, 0])
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
// |> rx(45, %)
// |> rx(45, %)
show(mySketch001)`
const programMemory = await enginelessExecutor(parser_wasm(code))
// @ts-ignore
const shown = programMemory?.return?.map(
// @ts-ignore
(a) => programMemory?.root?.[a.name]
)
expect(shown).toEqual([
{
type: 'sketchGroup',
start: {
type: 'base',
to: [0, 0],
from: [0, 0],
name: '',
__geoMeta: {
id: '66366561-6465-4734-a463-366330356563',
id: expect.any(String),
sourceRange: [21, 42],
pathToNode: [],
},
},
value: [
{
type: 'toPoint',
name: '',
to: [-1.59, -1.54],
from: [0, 0],
__geoMeta: {
sourceRange: [48, 73],
id: '30366338-6462-4330-a364-303935626163',
pathToNode: [],
id: expect.any(String),
},
},
{
type: 'toPoint',
to: [0.46, -5.82],
from: [-1.59, -1.54],
name: '',
__geoMeta: {
sourceRange: [79, 103],
id: '32653334-6331-4231-b162-663334363535',
pathToNode: [],
id: expect.any(String),
},
},
],
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
id: '39643164-6130-4734-b432-623638393262',
__meta: [{ sourceRange: [21, 42], pathToNode: [] }],
id: expect.any(String),
__meta: [{ sourceRange: [21, 42] }],
},
])
})
@ -69,21 +70,20 @@ const mySketch001 = startSketchAt([0, 0])
|> extrude(2, %)
show(mySketch001)`
const programMemory = await enginelessExecutor(parser_wasm(code))
// @ts-ignore
const shown = programMemory?.return?.map(
// @ts-ignore
(a) => programMemory?.root?.[a.name]
)
expect(shown).toEqual([
{
type: 'extrudeGroup',
id: '65383433-3839-4333-b836-343263636638',
id: expect.any(String),
value: [],
height: 2,
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
__meta: [
{ sourceRange: [127, 140], pathToNode: [] },
{ sourceRange: [21, 42], pathToNode: [] },
],
__meta: [{ sourceRange: [21, 42] }],
},
])
})
@ -106,37 +106,33 @@ const sk2 = startSketchAt([0, 0])
|> lineTo([2.5, 0], %)
// |> transform(theTransf, %)
|> extrude(2, %)
show(theExtrude, sk2)`
const programMemory = await enginelessExecutor(parser_wasm(code))
// @ts-ignore
const geos = programMemory?.return?.map(
// @ts-ignore
({ name }) => programMemory?.root?.[name]
)
expect(geos).toEqual([
{
type: 'extrudeGroup',
id: '63333330-3631-4230-b664-623132643731',
id: expect.any(String),
value: [],
height: 2,
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
__meta: [
{ sourceRange: [212, 227], pathToNode: [] },
{ sourceRange: [13, 34], pathToNode: [] },
],
__meta: [{ sourceRange: [13, 34] }],
},
{
type: 'extrudeGroup',
id: '33316639-3438-4661-a334-663262383737',
id: expect.any(String),
value: [],
height: 2,
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
__meta: [
{ sourceRange: [453, 466], pathToNode: [] },
{ sourceRange: [302, 323], pathToNode: [] },
],
__meta: [{ sourceRange: [302, 323] }],
},
])
})

View File

@ -1,11 +1,13 @@
import { Diagnostic } from '@codemirror/lint'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
type ExtractKind<T> = T extends { kind: infer K } ? K : never
export class KCLError {
kind: string | undefined
kind: ExtractKind<RustKclError> | 'name'
sourceRanges: [number, number][]
msg: string
constructor(
kind: string | undefined,
kind: ExtractKind<RustKclError> | 'name',
msg: string,
sourceRanges: [number, number][]
) {
@ -39,11 +41,18 @@ export class KCLTypeError extends KCLError {
export class KCLUnimplementedError extends KCLError {
constructor(msg: string, sourceRanges: [number, number][]) {
super('unimplemented feature', msg, sourceRanges)
super('unimplemented', msg, sourceRanges)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
}
}
export class KCLUnexpectedError extends KCLError {
constructor(msg: string, sourceRanges: [number, number][]) {
super('unexpected', msg, sourceRanges)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
}
}
export class KCLValueAlreadyDefined extends KCLError {
constructor(key: string, sourceRanges: [number, number][]) {
super('name', `Key ${key} was already defined elsewhere`, sourceRanges)

View File

@ -5,7 +5,7 @@ import { ProgramMemory } from './executor'
import { initPromise } from './rust'
import { enginelessExecutor } from '../lib/testHelpers'
import { vi } from 'vitest'
import { KCLUndefinedValueError } from './errors'
import { KCLError } from './errors'
beforeAll(() => initPromise)
@ -30,29 +30,6 @@ const newVar = myVar + 1`
const { root } = await exe(code)
expect(root.myVar.value).toBe('a str another str')
})
it('test with function call', async () => {
const code = `
const myVar = "hello"
log(5, myVar)`
const programMemoryOverride: ProgramMemory['root'] = {
log: {
type: 'userVal',
value: vi.fn(),
__meta: [
{
sourceRange: [0, 0],
pathToNode: [],
},
],
},
}
const { root } = await enginelessExecutor(parser_wasm(code), {
root: programMemoryOverride,
pendingMemory: {},
})
expect(root.myVar.value).toBe('hello')
expect(programMemoryOverride.log.value).toHaveBeenCalledWith(5, 'hello')
})
it('fn funcN = () => {} execute', async () => {
const { root } = await exe(
[
@ -84,8 +61,7 @@ show(mySketch)
from: [0, 0],
__geoMeta: {
sourceRange: [43, 80],
id: '37333036-3033-4432-b530-643030303837',
pathToNode: [],
id: expect.any(String),
},
name: 'myPath',
},
@ -93,10 +69,10 @@ show(mySketch)
type: 'toPoint',
to: [2, 3],
from: [0, 2],
name: '',
__geoMeta: {
sourceRange: [86, 102],
id: '32343136-3330-4134-a462-376437386365',
pathToNode: [],
id: expect.any(String),
},
},
{
@ -105,8 +81,7 @@ show(mySketch)
from: [2, 3],
__geoMeta: {
sourceRange: [108, 151],
id: '32306132-6130-4138-b832-636363326330',
pathToNode: [],
id: expect.any(String),
},
name: 'rightPath',
},
@ -170,13 +145,12 @@ show(mySketch)
expect(root.mySk1).toEqual({
type: 'sketchGroup',
start: {
type: 'base',
to: [0, 0],
from: [0, 0],
name: '',
__geoMeta: {
id: '37663863-3664-4366-a637-623739336334',
id: expect.any(String),
sourceRange: [14, 34],
pathToNode: [],
},
},
value: [
@ -184,10 +158,10 @@ show(mySketch)
type: 'toPoint',
to: [1, 1],
from: [0, 0],
name: '',
__geoMeta: {
sourceRange: [40, 56],
id: '34356231-3362-4363-b935-393033353034',
pathToNode: [],
id: expect.any(String),
},
},
{
@ -196,8 +170,7 @@ show(mySketch)
from: [1, 1],
__geoMeta: {
sourceRange: [62, 100],
id: '39623339-3538-4366-b633-356630326639',
pathToNode: [],
id: expect.any(String),
},
name: 'myPath',
},
@ -205,17 +178,17 @@ show(mySketch)
type: 'toPoint',
to: [1, 1],
from: [0, 1],
name: '',
__geoMeta: {
sourceRange: [106, 122],
id: '30636135-6232-4335-b665-366562303161',
pathToNode: [],
id: expect.any(String),
},
},
],
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
id: '30376661-3039-4965-b532-653665313731',
__meta: [{ sourceRange: [14, 34], pathToNode: [] }],
id: expect.any(String),
__meta: [{ sourceRange: [14, 34] }],
})
})
it('execute array expression', async () => {
@ -230,13 +203,6 @@ show(mySketch)
value: 3,
__meta: [
{
pathToNode: [
['body', ''],
[0, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclaration'],
],
sourceRange: [14, 15],
},
],
@ -246,25 +212,8 @@ show(mySketch)
value: [1, '2', 3, 9],
__meta: [
{
pathToNode: [
['body', ''],
[1, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclaration'],
],
sourceRange: [27, 49],
},
{
pathToNode: [
['body', ''],
[0, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclaration'],
],
sourceRange: [14, 15],
},
],
},
})
@ -280,13 +229,6 @@ show(mySketch)
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
__meta: [
{
pathToNode: [
['body', ''],
[1, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclaration'],
],
sourceRange: [27, 83],
},
],
@ -302,13 +244,6 @@ show(mySketch)
value: '123',
__meta: [
{
pathToNode: [
['body', ''],
[1, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclaration'],
],
sourceRange: [41, 50],
},
],
@ -451,17 +386,18 @@ const theExtrude = startSketchAt([0, 0])
|> extrude(4, %)
show(theExtrude)`
await expect(exe(code)).rejects.toEqual(
new KCLUndefinedValueError('Memory item myVarZ not found', [[100, 106]])
new KCLError(
'undefined_value',
'memory item key `myVarZ` is not defined',
[[100, 106]]
)
)
})
})
// helpers
async function exe(
code: string,
programMemory: ProgramMemory = { root: {}, pendingMemory: {} }
) {
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
const ast = parser_wasm(code)
const result = await enginelessExecutor(ast, programMemory)

View File

@ -1,35 +1,19 @@
import {
Program,
BinaryPart,
BinaryExpression,
PipeExpression,
ObjectExpression,
MemberExpression,
Identifier,
CallExpression,
ArrayExpression,
UnaryExpression,
} from './abstractSyntaxTreeTypes'
import { InternalFnNames } from './std/stdTypes'
import { internalFns } from './std/std'
import {
KCLUndefinedValueError,
KCLValueAlreadyDefined,
KCLSyntaxError,
KCLSemanticError,
KCLTypeError,
} from './errors'
import { Program } from './abstractSyntaxTreeTypes'
import {
EngineCommandManager,
ArtifactMap,
SourceRangeMap,
} from './std/engineConnection'
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { rangeTypeFix } from './abstractSyntaxTree'
export type SourceRange = [number, number]
export type PathToNode = [string | number, string][] // [pathKey, nodeType][]
export type Metadata = {
sourceRange: SourceRange
pathToNode: PathToNode
}
export type Position = [number, number, number]
export type Rotation = [number, number, number, number]
@ -41,7 +25,6 @@ interface BasePath {
__geoMeta: {
id: string
sourceRange: SourceRange
pathToNode: PathToNode
}
}
@ -68,9 +51,7 @@ export interface AngledLineTo extends BasePath {
interface GeoMeta {
__geoMeta: {
id: string
refId?: string
sourceRange: SourceRange
pathToNode: PathToNode
}
}
@ -118,69 +99,16 @@ type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
interface Memory {
[key: string]: MemoryItem
}
interface PendingMemory {
[key: string]: Promise<MemoryItem>
}
export interface ProgramMemory {
root: Memory
pendingMemory: Partial<PendingMemory>
return?: Identifier[]
}
const addItemToMemory = (
programMemory: ProgramMemory,
key: string,
sourceRange: [[number, number]],
value: MemoryItem | Promise<MemoryItem>
) => {
const _programMemory = programMemory
if (_programMemory.root[key] || _programMemory.pendingMemory[key]) {
throw new KCLValueAlreadyDefined(key, sourceRange)
}
if (value instanceof Promise) {
_programMemory.pendingMemory[key] = value
value.then((resolvedValue) => {
_programMemory.root[key] = resolvedValue
delete _programMemory.pendingMemory[key]
})
} else {
_programMemory.root[key] = value
}
return _programMemory
}
const promisifyMemoryItem = async (obj: MemoryItem) => {
if (obj.value instanceof Promise) {
const resolvedGuy = await obj.value
return {
...obj,
value: resolvedGuy,
}
}
return obj
}
const getMemoryItem = async (
programMemory: ProgramMemory,
key: string,
sourceRanges: [number, number][]
): Promise<MemoryItem> => {
if (programMemory.root[key]) {
return programMemory.root[key]
}
if (programMemory.pendingMemory[key]) {
return programMemory.pendingMemory[key] as Promise<MemoryItem>
}
throw new KCLUndefinedValueError(`Memory item ${key} not found`, sourceRanges)
return?: ProgramReturn
}
export const executor = async (
node: Program,
programMemory: ProgramMemory = { root: {}, pendingMemory: {} },
programMemory: ProgramMemory = { root: {} },
engineCommandManager: EngineCommandManager,
options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' },
previousPathToNode: PathToNode = [],
// work around while the gemotry is still be stored on the frontend
// will be removed when the stream UI is added.
tempMapCallback: (a: {
@ -192,9 +120,7 @@ export const executor = async (
const _programMemory = await _executor(
node,
programMemory,
engineCommandManager,
options,
previousPathToNode
engineCommandManager
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands()
@ -206,840 +132,25 @@ export const executor = async (
export const _executor = async (
node: Program,
programMemory: ProgramMemory = { root: {}, pendingMemory: {} },
engineCommandManager: EngineCommandManager,
options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' },
previousPathToNode: PathToNode = []
programMemory: ProgramMemory = { root: {} },
engineCommandManager: EngineCommandManager
): Promise<ProgramMemory> => {
let _programMemory: ProgramMemory = {
root: {
...programMemory.root,
},
pendingMemory: {
...programMemory.pendingMemory,
},
return: programMemory.return,
}
const { body } = node
const proms: Promise<any>[] = []
for (let bodyIndex = 0; bodyIndex < body.length; bodyIndex++) {
const statement = body[bodyIndex]
if (statement.type === 'VariableDeclaration') {
for (let index = 0; index < statement.declarations.length; index++) {
const declaration = statement.declarations[index]
const variableName = declaration.id.name
const pathToNode: PathToNode = [
...previousPathToNode,
['body', ''],
[bodyIndex, 'index'],
['declarations', 'VariableDeclaration'],
[index, 'index'],
['init', 'VariableDeclaration'],
]
const sourceRange: SourceRange = [
declaration.init.start,
declaration.init.end,
]
const __meta: Metadata[] = [
{
pathToNode,
sourceRange,
},
]
if (declaration.init.type === 'PipeExpression') {
const prom = getPipeExpressionResult(
declaration.init,
_programMemory,
engineCommandManager,
pathToNode
)
proms.push(prom)
const value = await prom
if (value?.type === 'sketchGroup' || value?.type === 'extrudeGroup') {
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
value
)
} else {
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
{
type: 'userVal',
value,
__meta,
}
)
}
} else if (declaration.init.type === 'Identifier') {
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
{
type: 'userVal',
value: _programMemory.root[declaration.init.name].value,
__meta,
}
)
} else if (declaration.init.type === 'Literal') {
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
{
type: 'userVal',
value: declaration.init.value,
__meta,
}
)
} else if (declaration.init.type === 'BinaryExpression') {
const prom = getBinaryExpressionResult(
declaration.init,
_programMemory,
engineCommandManager
)
proms.push(prom)
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
promisifyMemoryItem({
type: 'userVal',
value: prom,
__meta,
})
)
} else if (declaration.init.type === 'UnaryExpression') {
const prom = getUnaryExpressionResult(
declaration.init,
_programMemory,
engineCommandManager
)
proms.push(prom)
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
promisifyMemoryItem({
type: 'userVal',
value: prom,
__meta,
})
)
} else if (declaration.init.type === 'ArrayExpression') {
const valueInfo: Promise<{ value: any; __meta?: Metadata }>[] =
declaration.init.elements.map(
async (element): Promise<{ value: any; __meta?: Metadata }> => {
if (element.type === 'Literal') {
return {
value: element.value,
}
} else if (element.type === 'BinaryExpression') {
const prom = getBinaryExpressionResult(
element,
_programMemory,
engineCommandManager
)
proms.push(prom)
return {
value: await prom,
}
} else if (element.type === 'PipeExpression') {
const prom = getPipeExpressionResult(
element,
_programMemory,
engineCommandManager,
pathToNode
)
proms.push(prom)
return {
value: await prom,
}
} else if (element.type === 'Identifier') {
const node = await getMemoryItem(
_programMemory,
element.name,
[[element.start, element.end]]
)
return {
value: node.value,
__meta: node.__meta[node.__meta.length - 1],
}
} else if (element.type === 'UnaryExpression') {
const prom = getUnaryExpressionResult(
element,
_programMemory,
engineCommandManager
)
proms.push(prom)
return {
value: await prom,
}
} else {
throw new KCLSyntaxError(
`Unexpected element type ${element.type} in array expression`,
// TODO: Refactor this whole block into a `switch` so that we have a specific
// type here and can put a sourceRange.
[]
)
}
}
)
const awaitedValueInfo = await Promise.all(valueInfo)
const meta = awaitedValueInfo
.filter(({ __meta }) => __meta)
.map(({ __meta }) => __meta) as Metadata[]
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
{
type: 'userVal',
value: awaitedValueInfo.map(({ value }) => value),
__meta: [...__meta, ...meta],
}
)
} else if (declaration.init.type === 'ObjectExpression') {
const prom = executeObjectExpression(
_programMemory,
declaration.init,
engineCommandManager
)
proms.push(prom)
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
promisifyMemoryItem({
type: 'userVal',
value: prom,
__meta,
})
)
} else if (declaration.init.type === 'FunctionExpression') {
const fnInit = declaration.init
_programMemory = addItemToMemory(
_programMemory,
declaration.id.name,
[sourceRange],
{
type: 'userVal',
value: async (...args: any[]) => {
let fnMemory: ProgramMemory = {
root: {
..._programMemory.root,
},
pendingMemory: {
..._programMemory.pendingMemory,
},
}
if (args.length > fnInit.params.length) {
throw new KCLSyntaxError(
`Too many arguments passed to function ${declaration.id.name}`,
[[declaration.start, declaration.end]]
)
} else if (args.length < fnInit.params.length) {
throw new KCLSyntaxError(
`Too few arguments passed to function ${declaration.id.name}`,
[[declaration.start, declaration.end]]
)
}
fnInit.params.forEach((param, index) => {
fnMemory = addItemToMemory(
fnMemory,
param.name,
[sourceRange],
{
type: 'userVal',
value: args[index],
__meta,
}
)
})
const prom = _executor(
fnInit.body,
fnMemory,
engineCommandManager,
{
bodyType: 'block',
}
)
proms.push(prom)
const result = (await prom).return
return result
},
__meta,
}
)
} else if (declaration.init.type === 'MemberExpression') {
await Promise.all([...proms]) // TODO wait for previous promises, does that makes sense?
const prom = getMemberExpressionResult(
declaration.init,
_programMemory
)
proms.push(prom)
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
promisifyMemoryItem({
type: 'userVal',
value: prom,
__meta,
})
)
} else if (declaration.init.type === 'CallExpression') {
const prom = executeCallExpression(
_programMemory,
declaration.init,
engineCommandManager,
previousPathToNode
)
proms.push(prom)
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
prom.then((a) => {
return a?.type === 'sketchGroup' || a?.type === 'extrudeGroup'
? a
: {
type: 'userVal',
value: a,
__meta,
}
})
)
} else {
throw new KCLSyntaxError(
'Unsupported declaration type: ' + declaration.init.type,
[[declaration.start, declaration.end]]
)
}
}
} else if (statement.type === 'ExpressionStatement') {
const expression = statement.expression
if (expression.type === 'CallExpression') {
const functionName = expression.callee.name
const args = expression.arguments.map((arg) => {
if (arg.type === 'Literal') {
return arg.value
} else if (arg.type === 'Identifier') {
return _programMemory.root[arg.name]?.value
}
})
if ('show' === functionName) {
if (options.bodyType !== 'root') {
throw new KCLSemanticError(
`Cannot call ${functionName} outside of a root`,
[[statement.start, statement.end]]
)
}
_programMemory.return = expression.arguments as any // todo memory redo
} else {
if (_programMemory.root[functionName] === undefined) {
throw new KCLSemanticError(`No such name ${functionName} defined`, [
[statement.start, statement.end],
])
}
_programMemory.root[functionName].value(...args)
}
}
} else if (statement.type === 'ReturnStatement') {
if (statement.argument.type === 'BinaryExpression') {
const prom = getBinaryExpressionResult(
statement.argument,
_programMemory,
engineCommandManager
)
proms.push(prom)
_programMemory.return = await prom
}
}
}
await Promise.all(proms)
return _programMemory
}
function getMemberExpressionResult(
expression: MemberExpression,
programMemory: ProgramMemory
) {
const propertyName = (
expression.property.type === 'Identifier'
? expression.property.name
: expression.property.value
) as any
const object: any =
expression.object.type === 'MemberExpression'
? getMemberExpressionResult(expression.object, programMemory)
: programMemory.root[expression.object.name]?.value
return object?.[propertyName]
}
async function getBinaryExpressionResult(
expression: BinaryExpression,
programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager,
pipeInfo: {
isInPipe: boolean
previousResults: any[]
expressionIndex: number
body: PipeExpression['body']
sourceRangeOverride?: SourceRange
} = {
isInPipe: false,
previousResults: [],
expressionIndex: 0,
body: [],
}
) {
const _pipeInfo = {
...pipeInfo,
isInPipe: false,
}
const left = await getBinaryPartResult(
expression.left,
programMemory,
engineCommandManager,
_pipeInfo
)
const right = await getBinaryPartResult(
expression.right,
programMemory,
engineCommandManager,
_pipeInfo
)
if (expression.operator === '+') return left + right
if (expression.operator === '-') return left - right
if (expression.operator === '*') return left * right
if (expression.operator === '/') return left / right
if (expression.operator === '%') return left % right
}
async function getBinaryPartResult(
part: BinaryPart,
programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager,
pipeInfo: {
isInPipe: boolean
previousResults: any[]
expressionIndex: number
body: PipeExpression['body']
sourceRangeOverride?: SourceRange
} = {
isInPipe: false,
previousResults: [],
expressionIndex: 0,
body: [],
}
): Promise<any> {
const _pipeInfo = {
...pipeInfo,
isInPipe: false,
}
if (part.type === 'Literal') {
return part.value
} else if (part.type === 'Identifier') {
return programMemory.root[part.name].value
} else if (part.type === 'BinaryExpression') {
const prom = getBinaryExpressionResult(
part,
programMemory,
engineCommandManager,
_pipeInfo
)
const result = await prom
return result
} else if (part.type === 'CallExpression') {
const result = await executeCallExpression(
programMemory,
part,
engineCommandManager,
[],
_pipeInfo
)
return result
}
}
async function getUnaryExpressionResult(
expression: UnaryExpression,
programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager,
pipeInfo: {
isInPipe: boolean
previousResults: any[]
expressionIndex: number
body: PipeExpression['body']
sourceRangeOverride?: SourceRange
} = {
isInPipe: false,
previousResults: [],
expressionIndex: 0,
body: [],
}
) {
return -(await getBinaryPartResult(
expression.argument,
programMemory,
engineCommandManager,
{
...pipeInfo,
isInPipe: false,
}
))
}
async function getPipeExpressionResult(
expression: PipeExpression,
programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager,
previousPathToNode: PathToNode = []
) {
const executedBody = await executePipeBody(
expression.body,
programMemory,
engineCommandManager,
previousPathToNode
)
const result = executedBody[executedBody.length - 1]
return result
}
async function executePipeBody(
body: PipeExpression['body'],
programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager,
previousPathToNode: PathToNode = [],
expressionIndex = 0,
previousResults: any[] = []
): Promise<any[]> {
if (expressionIndex === body.length) {
return previousResults
}
const expression = body[expressionIndex]
if (expression.type === 'BinaryExpression') {
const result = await getBinaryExpressionResult(
expression,
programMemory,
try {
const memory: ProgramMemory = await execute_wasm(
JSON.stringify(node),
JSON.stringify(programMemory),
engineCommandManager
)
return executePipeBody(
body,
programMemory,
engineCommandManager,
previousPathToNode,
expressionIndex + 1,
[...previousResults, result]
return memory
} catch (e: any) {
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
)
} else if (expression.type === 'CallExpression') {
return await executeCallExpression(
programMemory,
expression,
engineCommandManager,
previousPathToNode,
{
isInPipe: true,
previousResults,
expressionIndex,
body,
}
)
}
throw new KCLSyntaxError('Invalid pipe expression', [
[expression.start, expression.end],
])
}
async function executeObjectExpression(
_programMemory: ProgramMemory,
objExp: ObjectExpression,
engineCommandManager: EngineCommandManager,
pipeInfo: {
isInPipe: boolean
previousResults: any[]
expressionIndex: number
body: PipeExpression['body']
sourceRangeOverride?: SourceRange
} = {
isInPipe: false,
previousResults: [],
expressionIndex: 0,
body: [],
}
) {
const _pipeInfo = {
...pipeInfo,
isInPipe: false,
}
const obj: { [key: string]: any } = {}
const proms: Promise<any>[] = []
objExp.properties.forEach(async (property) => {
if (property.type === 'ObjectProperty') {
if (property.value.type === 'Literal') {
obj[property.key.name] = property.value.value
} else if (property.value.type === 'BinaryExpression') {
const prom = getBinaryExpressionResult(
property.value,
_programMemory,
engineCommandManager,
_pipeInfo
)
proms.push(prom)
obj[property.key.name] = await prom
} else if (property.value.type === 'PipeExpression') {
const prom = getPipeExpressionResult(
property.value,
_programMemory,
engineCommandManager
)
proms.push(prom)
obj[property.key.name] = await prom
} else if (property.value.type === 'Identifier') {
obj[property.key.name] = (
await getMemoryItem(_programMemory, property.value.name, [
[property.value.start, property.value.end],
])
).value
} else if (property.value.type === 'ObjectExpression') {
const prom = executeObjectExpression(
_programMemory,
property.value,
engineCommandManager
)
proms.push(prom)
obj[property.key.name] = await prom
} else if (property.value.type === 'ArrayExpression') {
const prom = executeArrayExpression(
_programMemory,
property.value,
engineCommandManager
)
proms.push(prom)
obj[property.key.name] = await prom
} else if (property.value.type === 'CallExpression') {
const prom = executeCallExpression(
_programMemory,
property.value,
engineCommandManager,
[],
_pipeInfo
)
proms.push(prom)
const result = await prom
obj[property.key.name] = result
} else if (property.value.type === 'UnaryExpression') {
const prom = getUnaryExpressionResult(
property.value,
_programMemory,
engineCommandManager
)
proms.push(prom)
obj[property.key.name] = await prom
} else {
throw new KCLSyntaxError(
`Unexpected property type ${property.value.type} in object expression`,
[[property.value.start, property.value.end]]
)
}
} else {
throw new KCLSyntaxError(
`Unexpected property type ${property.type} in object expression`,
[[property.value.start, property.value.end]]
)
}
})
await Promise.all(proms)
return obj
}
async function executeArrayExpression(
_programMemory: ProgramMemory,
arrExp: ArrayExpression,
engineCommandManager: EngineCommandManager,
pipeInfo: {
isInPipe: boolean
previousResults: any[]
expressionIndex: number
body: PipeExpression['body']
sourceRangeOverride?: SourceRange
} = {
isInPipe: false,
previousResults: [],
expressionIndex: 0,
body: [],
}
) {
const _pipeInfo = {
...pipeInfo,
isInPipe: false,
}
return await Promise.all(
arrExp.elements.map((el) => {
if (el.type === 'Literal') {
return el.value
} else if (el.type === 'Identifier') {
return _programMemory.root?.[el.name]?.value
} else if (el.type === 'BinaryExpression') {
return getBinaryExpressionResult(
el,
_programMemory,
engineCommandManager,
_pipeInfo
)
} else if (el.type === 'ObjectExpression') {
return executeObjectExpression(_programMemory, el, engineCommandManager)
} else if (el.type === 'CallExpression') {
const result: any = executeCallExpression(
_programMemory,
el,
engineCommandManager,
[],
_pipeInfo
)
return result
} else if (el.type === 'UnaryExpression') {
return getUnaryExpressionResult(
el,
_programMemory,
engineCommandManager,
{
...pipeInfo,
isInPipe: false,
}
)
}
throw new KCLTypeError('Invalid argument type', [[el.start, el.end]])
})
)
}
async function executeCallExpression(
programMemory: ProgramMemory,
expression: CallExpression,
engineCommandManager: EngineCommandManager,
previousPathToNode: PathToNode = [],
pipeInfo: {
isInPipe: boolean
previousResults: any[]
expressionIndex: number
body: PipeExpression['body']
sourceRangeOverride?: SourceRange
} = {
isInPipe: false,
previousResults: [],
expressionIndex: 0,
body: [],
}
) {
const {
isInPipe,
previousResults,
expressionIndex,
body,
sourceRangeOverride,
} = pipeInfo
const functionName = expression?.callee?.name
const _pipeInfo = {
...pipeInfo,
isInPipe: false,
}
const fnArgs = await Promise.all(
expression?.arguments?.map(async (arg) => {
if (arg.type === 'Literal') {
return arg.value
} else if (arg.type === 'Identifier') {
await new Promise((r) => setTimeout(r)) // push into next even loop, but also probably should fix this
const temp = await getMemoryItem(programMemory, arg.name, [
[arg.start, arg.end],
])
return temp?.type === 'userVal' ? temp.value : temp
} else if (arg.type === 'PipeSubstitution') {
return previousResults[expressionIndex - 1]
} else if (arg.type === 'ArrayExpression') {
return await executeArrayExpression(
programMemory,
arg,
engineCommandManager,
pipeInfo
)
} else if (arg.type === 'CallExpression') {
const result: any = await executeCallExpression(
programMemory,
arg,
engineCommandManager,
previousPathToNode,
_pipeInfo
)
return result
} else if (arg.type === 'ObjectExpression') {
return await executeObjectExpression(
programMemory,
arg,
engineCommandManager,
_pipeInfo
)
} else if (arg.type === 'UnaryExpression') {
return getUnaryExpressionResult(
arg,
programMemory,
engineCommandManager,
_pipeInfo
)
} else if (arg.type === 'BinaryExpression') {
return getBinaryExpressionResult(
arg,
programMemory,
engineCommandManager,
_pipeInfo
)
}
throw new KCLSyntaxError('Invalid argument type in function call', [
[arg.start, arg.end],
])
})
)
if (functionName in internalFns) {
const fnNameWithSketchOrExtrude = functionName as InternalFnNames
const result = await internalFns[fnNameWithSketchOrExtrude](
{
programMemory,
sourceRange: sourceRangeOverride || [expression.start, expression.end],
engineCommandManager,
code: JSON.stringify(expression),
},
fnArgs[0],
fnArgs[1],
fnArgs[2]
)
return isInPipe
? await executePipeBody(
body,
programMemory,
engineCommandManager,
previousPathToNode,
expressionIndex + 1,
[...previousResults, result]
)
: result
}
const result = await programMemory.root[functionName].value(...fnArgs)
return isInPipe
? await executePipeBody(
body,
programMemory,
engineCommandManager,
previousPathToNode,
expressionIndex + 1,
[...previousResults, result]
)
: result
console.log(kclError)
throw kclError
}
}

View File

@ -182,14 +182,14 @@ describe('Testing moveValueIntoNewVariable', () => {
const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
const abc = 3
const identifierGuy = 5
const yo = 5 + 6
const part001 = startSketchAt([-1.2, 4.83])
|> line([2.8, 0], %)
|> angledLine([100 + 100, 3.09], %)
|> angledLine([abc, 3.09], %)
|> angledLine([def('yo'), 3.09], %)
|> angledLine([def(yo), 3.09], %)
|> angledLine([ghi(%), 3.09], %)
|> angledLine([jkl('yo') + 2, 3.09], %)
const yo = 5 + 6
|> angledLine([jkl(yo) + 2, 3.09], %)
const yo2 = hmm([identifierGuy + 5])
show(part001)`
it('should move a binary expression into a new variable', async () => {
@ -231,7 +231,7 @@ show(part001)`
'newVar'
)
const newCode = recast(modifiedAst)
expect(newCode).toContain(`const newVar = def('yo')`)
expect(newCode).toContain(`const newVar = def(yo)`)
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
})
it('should move a binary expression with call expression into a new variable', async () => {
@ -245,7 +245,7 @@ show(part001)`
'newVar'
)
const newCode = recast(modifiedAst)
expect(newCode).toContain(`const newVar = jkl('yo') + 2`)
expect(newCode).toContain(`const newVar = jkl(yo) + 2`)
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
})
it('should move a identifier into a new variable', async () => {

View File

@ -1,9 +1,9 @@
import { Program } from './abstractSyntaxTreeTypes'
import { recast_js } from '../wasm-lib/pkg/wasm_lib'
import { recast_wasm } from '../wasm-lib/pkg/wasm_lib'
export const recast = (ast: Program): string => {
try {
const s: string = recast_js(JSON.stringify(ast))
const s: string = recast_wasm(JSON.stringify(ast))
return s
} catch (e) {
// TODO: do something real with the error.

View File

@ -1,8 +1,8 @@
import { SourceRange } from '../executor'
import { Selections } from '../../useStore'
import { VITE_KC_API_WS_MODELING_URL } from '../../env'
import { SourceRange } from 'lang/executor'
import { Selections } from 'useStore'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from '../../lib/exportSave'
import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid'
interface ResultCommand {
@ -32,11 +32,12 @@ interface CursorSelectionsArgs {
idBasedSelections: { type: string; id: string }[]
}
export type EngineCommand = Models['WebSocketMessages_type']
interface NewTrackArgs {
conn: EngineConnection
mediaStream: MediaStream
}
type OkResponse = Models['OkModelingCmdResponse_type']
type WebSocketResponse = Models['WebSocketResponses_type']
type WebSocketResponse = Models['OkWebSocketResponseData_type']
// EngineConnection encapsulates the connection(s) to the Engine
// for the EngineCommandManager; namely, the underlying WebSocket
@ -46,42 +47,76 @@ export class EngineConnection {
pc?: RTCPeerConnection
lossyDataChannel?: RTCDataChannel
onConnectionStarted: (conn: EngineConnection) => void = () => {}
waitForReady: Promise<void> = new Promise(() => {})
private resolveReady = () => {}
private ready: boolean
readonly url: string
private readonly token?: string
private onWebsocketOpen: (engineConnection: EngineConnection) => void
private onDataChannelOpen: (engineConnection: EngineConnection) => void
private onEngineConnectionOpen: (engineConnection: EngineConnection) => void
private onConnectionStarted: (engineConnection: EngineConnection) => void
private onClose: (engineConnection: EngineConnection) => void
private onNewTrack: (track: NewTrackArgs) => void
constructor({
url,
token,
onConnectionStarted,
onWebsocketOpen = () => {},
onNewTrack = () => {},
onEngineConnectionOpen = () => {},
onConnectionStarted = () => {},
onClose = () => {},
onDataChannelOpen = () => {},
}: {
url: string
token?: string
onConnectionStarted: (conn: EngineConnection) => void
onWebsocketOpen?: (engineConnection: EngineConnection) => void
onDataChannelOpen?: (engineConnection: EngineConnection) => void
onEngineConnectionOpen?: (engineConnection: EngineConnection) => void
onConnectionStarted?: (engineConnection: EngineConnection) => void
onClose?: (engineConnection: EngineConnection) => void
onNewTrack?: (track: NewTrackArgs) => void
}) {
this.url = url
this.token = token
this.ready = false
this.onWebsocketOpen = onWebsocketOpen
this.onDataChannelOpen = onDataChannelOpen
this.onEngineConnectionOpen = onEngineConnectionOpen
this.onConnectionStarted = onConnectionStarted
this.onClose = onClose
this.onNewTrack = onNewTrack
// TODO(paultag): This isn't right; this should be when the
// connection is in a good place, and tied to the connect() method,
// but this is part of a larger refactor to untangle logic. Once the
// Connection is pulled apart, we can rework how ready is represented.
// This was just the easiest way to ensure some level of parity between
// the CommandManager and the Connection until I send a rework for
// retry logic.
this.waitForReady = new Promise((resolve) => {
this.resolveReady = resolve
})
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000
setInterval(() => {
if (this.isReady()) {
// When we're online, every 10 seconds, we'll attempt to put a 'ping'
// command through the WebSocket connection. This will help both ends
// of the connection maintain the TCP connection without hitting a
// timeout condition.
this.send({ type: 'ping' })
}
}, pingIntervalMs)
}
// isReady will return true only when the WebRTC *and* WebSocket connection
// are connected. During setup, the WebSocket connection comes online first,
// which is used to establish the WebRTC connection. The EngineConnection
// is not "Ready" until both are connected.
isReady() {
return this.ready
}
// connect will attempt to connect to the Engine over a WebSocket, and
// establish the WebRTC connections.
//
// This will attempt the full handshake, and retry if the connection
// did not establish.
connect() {
this.websocket = new WebSocket(this.url, [])
// TODO(paultag): make this safe to call multiple times, and figure out
// when a connection is in progress (state: connecting or something).
this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer'
this.pc = new RTCPeerConnection()
@ -89,18 +124,22 @@ export class EngineConnection {
this.websocket.addEventListener('open', (event) => {
console.log('Connected to websocket, waiting for ICE servers')
if (this.token) {
this.websocket?.send(
JSON.stringify({ headers: { Authorization: `Bearer ${this.token}` } })
)
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
}
})
this.websocket.addEventListener('open', (event) => {
this.onWebsocketOpen(this)
})
this.websocket.addEventListener('close', (event) => {
console.log('websocket connection closed', event)
this.close()
})
this.websocket.addEventListener('error', (event) => {
console.log('websocket connection error', event)
this.close()
})
this.websocket.addEventListener('message', (event) => {
@ -115,31 +154,59 @@ export class EngineConnection {
return
}
const message: WebSocketResponse = JSON.parse(event.data)
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
if (
message.type === 'sdp_answer' &&
message.answer.type !== 'unspecified'
) {
this.pc?.setRemoteDescription(
new RTCSessionDescription({
type: message.answer.type,
sdp: message.answer.sdp,
})
)
} else if (message.type === 'trickle_ice') {
this.pc?.addIceCandidate(message.candidate as RTCIceCandidateInit)
} else if (message.type === 'ice_server_info' && this.pc) {
if (!message.success) {
if (message.request_id) {
console.error(`Error in response to request ${message.request_id}:`)
} else {
console.error(`Error from server:`)
}
message?.errors?.forEach((error) => {
console.error(` - ${error.error_code}: ${error.message}`)
})
return
}
let resp = message.resp
if (!resp) {
// If there's no body to the response, we can bail here.
return
}
if (resp.type === 'sdp_answer') {
let answer = resp.data?.answer
if (!answer || answer.type === 'unspecified') {
return
}
if (this.pc?.signalingState !== 'stable') {
// If the connection is stable, we shouldn't bother updating the
// SDP, since we have a stable connection to the backend. If we
// need to renegotiate, the whole PeerConnection needs to get
// tore down.
this.pc?.setRemoteDescription(
new RTCSessionDescription({
type: answer.type,
sdp: answer.sdp,
})
)
}
} else if (resp.type === 'trickle_ice') {
let candidate = resp.data?.candidate
this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
} else if (resp.type === 'ice_server_info' && this.pc) {
console.log('received ice_server_info')
let ice_servers = resp.data?.ice_servers
if (message.ice_servers.length > 0) {
if (ice_servers?.length > 0) {
// When we set the Configuration, we want to always force
// iceTransportPolicy to 'relay', since we know the topology
// of the ICE/STUN/TUN server and the engine. We don't wish to
// talk to the engine in any configuration /other/ than relay
// from a infra POV.
this.pc.setConfiguration({
iceServers: message.ice_servers,
iceServers: ice_servers,
iceTransportPolicy: 'relay',
})
} else {
@ -152,29 +219,21 @@ export class EngineConnection {
// until the end of this function is setup of our end of the
// PeerConnection and waiting for events to fire our callbacks.
this.pc.addEventListener('connectionstatechange', (e) =>
console.log(this.pc?.iceConnectionState)
)
this.pc.addEventListener('connectionstatechange', (event) => {
// if (this.pc?.iceConnectionState === 'disconnected') {
// this.close()
// }
})
this.pc.addEventListener('icecandidate', (event) => {
if (!this.pc || !this.websocket) return
if (event.candidate === null) {
console.log('sent sdp_offer')
this.websocket.send(
JSON.stringify({
type: 'sdp_offer',
offer: this.pc.localDescription,
})
)
} else {
if (event.candidate !== null) {
console.log('sending trickle ice candidate')
const { candidate } = event
this.websocket?.send(
JSON.stringify({
type: 'trickle_ice',
candidate: candidate.toJSON(),
})
)
this.send({
type: 'trickle_ice',
candidate: candidate.toJSON(),
})
}
})
@ -189,43 +248,90 @@ export class EngineConnection {
.then(async (descriptionInit) => {
await this?.pc?.setLocalDescription(descriptionInit)
console.log('sent sdp_offer begin')
const msg = JSON.stringify({
this.send({
type: 'sdp_offer',
offer: this.pc?.localDescription,
})
this.websocket?.send(msg)
})
.catch(console.log)
}
// TODO(paultag): This ought to be both controllable, as well as something
// like exponential backoff to have some grace on the backend, as well as
// fix responsiveness for clients that had a weird network hiccup.
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
setTimeout(() => {
if (this.isReady()) {
return
}
console.log('engine connection timeout on connection, retrying')
this.close()
this.connect()
}, connectionTimeoutMs)
})
this.pc.addEventListener('track', (event) => {
console.log('received track', event)
const mediaStream = event.streams[0]
this.onNewTrack({
conn: this,
mediaStream: mediaStream,
})
})
// During startup, we'll track the time from `connect` being called
// until the 'done' event fires.
let connectionStarted = new Date()
this.pc.addEventListener('datachannel', (event) => {
this.lossyDataChannel = event.channel
console.log('accepted lossy data channel', event.channel.label)
this.lossyDataChannel.addEventListener('open', (event) => {
this.resolveReady()
console.log('lossy data channel opened', event)
this.onDataChannelOpen(this)
let timeToConnectMs = new Date().getTime() - connectionStarted.getTime()
console.log(`engine connection time to connect: ${timeToConnectMs}ms`)
this.onEngineConnectionOpen(this)
this.ready = true
})
this.lossyDataChannel.addEventListener('close', (event) => {
console.log('lossy data channel closed')
this.close()
})
this.lossyDataChannel.addEventListener('error', (event) => {
console.log('lossy data channel error')
this.close()
})
})
if (this.onConnectionStarted) this.onConnectionStarted(this)
this.onConnectionStarted(this)
}
send(message: object) {
// TODO(paultag): Add in logic to determine the connection state and
// take actions if needed?
this.websocket?.send(JSON.stringify(message))
}
close() {
this.websocket?.close()
this.pc?.close()
this.lossyDataChannel?.close()
this.websocket = undefined
this.pc = undefined
this.lossyDataChannel = undefined
this.onClose(this)
this.ready = false
}
}
export type EngineCommand = Models['WebSocketRequest_type']
export class EngineCommandManager {
artifactMap: ArtifactMap = {}
sourceRangeMap: SourceRangeMap = {}
@ -258,97 +364,106 @@ export class EngineCommandManager {
this.engineConnection = new EngineConnection({
url,
token,
onConnectionStarted: (conn) => {
this.engineConnection?.pc?.addEventListener('track', (event) => {
console.log('received track', event)
const mediaStream = event.streams[0]
setMediaStream(mediaStream)
})
this.engineConnection?.pc?.addEventListener('datachannel', (event) => {
onEngineConnectionOpen: () => {
this.resolveReady()
setIsStreamReady(true)
},
onClose: () => {
setIsStreamReady(false)
},
onConnectionStarted: (engineConnection) => {
engineConnection?.pc?.addEventListener('datachannel', (event) => {
let lossyDataChannel = event.channel
lossyDataChannel.addEventListener('message', (event) => {
const result: OkResponse = JSON.parse(event.data)
const result: Models['OkModelingCmdResponse_type'] = JSON.parse(
event.data
)
if (
result.type === 'highlight_set_entity' &&
result.sequence &&
result.sequence > this.inSequence
result?.data?.sequence &&
result.data.sequence > this.inSequence
) {
this.onHoverCallback(result.entity_id)
this.inSequence = result.sequence
this.onHoverCallback(result.data.entity_id)
this.inSequence = result.data.sequence
}
})
})
// When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection.
conn.websocket?.addEventListener('message', (event) => {
engineConnection.websocket?.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data)
} else if (
typeof event.data === 'string' &&
event.data.toLocaleLowerCase().startsWith('error')
) {
console.warn('something went wrong: ', event.data)
} else {
const message: WebSocketResponse = JSON.parse(event.data)
if (message.type === 'modeling') {
const id = message.cmd_id
const command = this.artifactMap[id]
if ('ok' in message.result) {
const result: OkResponse = message.result.ok
if (result.type === 'select_with_point') {
if (result.entity_id) {
this.onClickCallback({
id: result.entity_id,
type: 'default',
})
} else {
this.onClickCallback()
}
}
}
if (command && command.type === 'pending') {
const resolve = command.resolve
this.artifactMap[id] = {
type: 'result',
data: message.result,
}
resolve({
id,
})
} else {
this.artifactMap[id] = {
type: 'result',
data: message.result,
}
}
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
message.success &&
message.resp.type === 'modeling' &&
message.request_id
) {
this.handleModelingCommand(message.resp, message.request_id)
}
}
})
},
})
onNewTrack: ({ mediaStream }) => {
console.log('received track', mediaStream)
// TODO(paultag): this isn't quite right, and the double promises is
// pretty grim.
this.engineConnection?.waitForReady.then(this.resolveReady)
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.log('peer is not sending video to us')
this.engineConnection?.close()
this.engineConnection?.connect()
})
this.waitForReady.then(() => {
setIsStreamReady(true)
setMediaStream(mediaStream)
},
})
this.engineConnection?.connect()
}
handleModelingCommand(message: WebSocketResponse, id: string) {
if (message.type !== 'modeling') {
return
}
const modelingResponse = message.data.modeling_response
const command = this.artifactMap[id]
if (modelingResponse.type === 'select_with_point') {
if (modelingResponse?.data?.entity_id) {
this.onClickCallback({
id: modelingResponse?.data?.entity_id,
type: 'default',
})
} else {
this.onClickCallback()
}
}
if (command && command.type === 'pending') {
const resolve = command.resolve
this.artifactMap[id] = {
type: 'result',
data: modelingResponse,
}
resolve({
id,
})
} else {
this.artifactMap[id] = {
type: 'result',
data: modelingResponse,
}
}
}
tearDown() {
// close all channels, sockets and WebRTC connections
this.engineConnection?.close()
}
startNewSession() {
this.artifactMap = {}
this.sourceRangeMap = {}
@ -372,8 +487,8 @@ export class EngineCommandManager {
otherSelections: Selections['otherSelections']
idBasedSelections: { type: string; id: string }[]
}) {
if (this.engineConnection?.websocket?.readyState === 0) {
console.log('socket not open')
if (!this.engineConnection?.isReady()) {
console.log('engine connection isnt ready')
return
}
this.sendSceneCommand({
@ -393,7 +508,7 @@ export class EngineCommandManager {
})
}
sendSceneCommand(command: EngineCommand) {
if (this.engineConnection?.websocket?.readyState === 0) {
if (!this.engineConnection?.isReady()) {
console.log('socket not ready')
return
}
@ -417,26 +532,24 @@ export class EngineCommandManager {
return
}
console.log('sending command', command)
this.engineConnection?.websocket?.send(JSON.stringify(command))
this.engineConnection?.send(command)
}
sendModellingCommand({
sendModelingCommand({
id,
params,
range,
command,
}: {
id: string
params: any
range: SourceRange
command: EngineCommand
}): Promise<any> {
this.sourceRangeMap[id] = range
if (this.engineConnection?.websocket?.readyState === 0) {
if (!this.engineConnection?.isReady()) {
console.log('socket not ready')
return new Promise(() => {})
}
this.engineConnection?.websocket?.send(JSON.stringify(command))
this.engineConnection?.send(command)
let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
@ -448,6 +561,25 @@ export class EngineCommandManager {
}
return promise
}
sendModelingCommandFromWasm(
id: string,
rangeStr: string,
commandStr: string
): Promise<any> {
if (id === undefined) {
throw new Error('id is undefined')
}
if (rangeStr === undefined) {
throw new Error('rangeStr is undefined')
}
if (commandStr === undefined) {
throw new Error('commandStr is undefined')
}
const command: EngineCommand = JSON.parse(commandStr)
const range: SourceRange = JSON.parse(rangeStr)
return this.sendModelingCommand({ id, range, command })
}
commandResult(id: string): Promise<any> {
const command = this.artifactMap[id]
if (!command) {

View File

@ -1,88 +0,0 @@
import { InternalFn } from './stdTypes'
import {
ExtrudeGroup,
ExtrudeSurface,
SketchGroup,
Position,
Rotation,
} from '../executor'
import { clockwiseSign } from './std'
import { generateUuidFromHashSeed } from '../../lib/uuid'
export const extrude: InternalFn = (
{ sourceRange, engineCommandManager, code },
length: number,
sketchVal: SketchGroup
): ExtrudeGroup => {
const sketch = sketchVal
const { position, rotation } = sketchVal
const id = generateUuidFromHashSeed(
JSON.stringify({
code,
sourceRange,
data: {
length,
sketchVal,
},
})
)
const extrudeSurfaces: ExtrudeSurface[] = []
const extrusionDirection = clockwiseSign(sketch.value.map((line) => line.to))
engineCommandManager.sendModellingCommand({
id,
params: [
{
length,
extrusionDirection: extrusionDirection,
},
],
range: sourceRange,
command: {
type: 'modeling_cmd_req',
cmd: {
type: 'extrude',
target: sketch.id,
distance: length,
cap: true,
},
cmd_id: id,
},
})
return {
type: 'extrudeGroup',
id,
value: extrudeSurfaces, // TODO, this is just an empty array now, should be deleted.
height: length,
position,
rotation,
__meta: [
{
sourceRange,
pathToNode: [], // TODO
},
{
sourceRange: sketchVal.__meta[0].sourceRange,
pathToNode: sketchVal.__meta[0].pathToNode,
},
],
}
}
export const getExtrudeWallTransform: InternalFn = (
_,
pathName: string,
extrudeGroup: ExtrudeGroup
): {
position: Position
quaternion: Rotation
} => {
const path = extrudeGroup?.value.find((path) => path.name === pathName)
if (!path) throw new Error(`Could not find path with name ${pathName}`)
return {
position: path.position,
quaternion: path.rotation,
}
}

View File

@ -23,12 +23,7 @@ import { GuiModes, toolTips, TooTip } from '../../useStore'
import { splitPathAtPipeExpression } from '../modifyAst'
import { generateUuidFromHashSeed } from '../../lib/uuid'
import {
SketchLineHelper,
ModifyAstBase,
InternalFn,
TransformCallback,
} from './stdTypes'
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
import {
createLiteral,
@ -42,10 +37,7 @@ import {
} from '../modifyAst'
import { roundOff, getLength, getAngle } from '../../lib/utils'
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
import {
intersectionWithParallelLine,
perpendicularDistance,
} from 'sketch-helpers'
import { perpendicularDistance } from 'sketch-helpers'
export type Coords2d = [number, number]
@ -115,45 +107,6 @@ function makeId(seed: string | any) {
}
export const lineTo: SketchLineHelper = {
fn: (
{ sourceRange, code },
data:
| [number, number]
| {
to: [number, number]
tag?: string
},
previousSketch: SketchGroup
): SketchGroup => {
if (!previousSketch)
throw new Error('lineTo must be called after startSketchAt')
const sketchGroup = { ...previousSketch }
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
const to = 'to' in data ? data.to : data
const id = makeId({
code,
sourceRange,
data,
})
const currentPath: Path = {
type: 'toPoint',
to,
from,
__geoMeta: {
sourceRange,
id,
pathToNode: [], // TODO
},
}
if ('tag' in data) {
currentPath.name = data.tag
}
return {
...sketchGroup,
value: [...sketchGroup.value, currentPath],
}
},
add: ({
node,
pathToNode,
@ -221,77 +174,6 @@ export const lineTo: SketchLineHelper = {
}
export const line: SketchLineHelper = {
fn: (
{ sourceRange, engineCommandManager, code },
data:
| [number, number]
| 'default'
| {
to: [number, number] | 'default'
// name?: string
tag?: string
},
previousSketch: SketchGroup
): SketchGroup => {
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
const sketchGroup = { ...previousSketch }
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
let args: [number, number] = [0.2, 1]
if (data !== 'default' && 'to' in data && data.to !== 'default') {
args = data.to
} else if (data !== 'default' && !('to' in data)) {
args = data
}
const to: [number, number] = [from[0] + args[0], from[1] + args[1]]
const lineData: LineData = {
from: [...from, 0],
to: [...to, 0],
}
const id = makeId({
code,
sourceRange,
data,
})
engineCommandManager.sendModellingCommand({
id,
params: [lineData, previousSketch],
range: sourceRange,
command: {
type: 'modeling_cmd_req',
cmd: {
type: 'extend_path',
path: sketchGroup.id,
segment: {
type: 'line',
end: {
x: lineData.to[0],
y: lineData.to[1],
z: 0,
},
},
},
cmd_id: id,
},
})
const currentPath: Path = {
type: 'toPoint',
to,
from,
__geoMeta: {
id,
sourceRange,
pathToNode: [], // TODO
},
}
if (data !== 'default' && 'tag' in data) {
currentPath.name = data.tag
}
return {
...sketchGroup,
value: [...sketchGroup.value, currentPath],
}
},
add: ({
node,
previousProgramMemory,
@ -385,25 +267,6 @@ export const line: SketchLineHelper = {
}
export const xLineTo: SketchLineHelper = {
fn: (
meta,
data:
| number
| {
to: number
// name?: string
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('bad bad bad')
const from = getCoordsFromPaths(
previousSketch,
previousSketch.value.length - 1
)
const [xVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data]
return lineTo.fn(meta, { to: [xVal, from[1]], tag }, previousSketch)
},
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
@ -452,25 +315,6 @@ export const xLineTo: SketchLineHelper = {
}
export const yLineTo: SketchLineHelper = {
fn: (
meta,
data:
| number
| {
to: number
// name?: string
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('bad bad bad')
const from = getCoordsFromPaths(
previousSketch,
previousSketch.value.length - 1
)
const [yVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data]
return lineTo.fn(meta, { to: [from[0], yVal], tag }, previousSketch)
},
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
@ -519,21 +363,6 @@ export const yLineTo: SketchLineHelper = {
}
export const xLine: SketchLineHelper = {
fn: (
meta,
data:
| number
| {
length: number
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('bad bad bad')
const [xVal, tag] =
typeof data !== 'number' ? [data.length, data.tag] : [data]
return line.fn(meta, { to: [xVal, 0], tag }, previousSketch)
},
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
@ -584,22 +413,6 @@ export const xLine: SketchLineHelper = {
}
export const yLine: SketchLineHelper = {
fn: (
meta,
data:
| number
| {
length: number
// name?: string
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('bad bad bad')
const [yVal, tag] =
typeof data !== 'number' ? [data.length, data.tag] : [data]
return line.fn(meta, { to: [0, yVal], tag }, previousSketch)
},
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
@ -644,48 +457,6 @@ export const yLine: SketchLineHelper = {
}
export const angledLine: SketchLineHelper = {
fn: (
{ sourceRange, engineCommandManager, code },
data:
| [number, number]
| {
angle: number
length: number
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
const sketchGroup = { ...previousSketch }
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
const [angle, length] = 'angle' in data ? [data.angle, data.length] : data
const to: [number, number] = [
from[0] + length * Math.cos((angle * Math.PI) / 180),
from[1] + length * Math.sin((angle * Math.PI) / 180),
]
const id = makeId({
code,
sourceRange,
data,
})
const currentPath: Path = {
type: 'toPoint',
to,
from,
__geoMeta: {
id,
sourceRange,
pathToNode: [], // TODO
},
}
if ('tag' in data) {
currentPath.name = data.tag
}
return {
...sketchGroup,
value: [...sketchGroup.value, currentPath],
}
},
add: ({
node,
pathToNode,
@ -753,26 +524,6 @@ export const angledLine: SketchLineHelper = {
}
export const angledLineOfXLength: SketchLineHelper = {
fn: (
{ sourceRange, programMemory, engineCommandManager, code },
data:
| [number, number]
| {
angle: number
length: number
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
const [angle, length, tag] =
'angle' in data ? [data.angle, data.length, data.tag] : data
return line.fn(
{ sourceRange, programMemory, engineCommandManager, code },
{ to: getYComponent(angle, length), tag },
previousSketch
)
},
add: ({
node,
previousProgramMemory,
@ -846,26 +597,6 @@ export const angledLineOfXLength: SketchLineHelper = {
}
export const angledLineOfYLength: SketchLineHelper = {
fn: (
{ sourceRange, programMemory, engineCommandManager, code },
data:
| [number, number]
| {
angle: number
length: number
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
const [angle, length, tag] =
'angle' in data ? [data.angle, data.length, data.tag] : data
return line.fn(
{ sourceRange, programMemory, engineCommandManager, code },
{ to: getXComponent(angle, length), tag },
previousSketch
)
},
add: ({
node,
previousProgramMemory,
@ -940,33 +671,6 @@ export const angledLineOfYLength: SketchLineHelper = {
}
export const angledLineToX: SketchLineHelper = {
fn: (
{ sourceRange, programMemory, engineCommandManager, code },
data:
| [number, number]
| {
angle: number
to: number
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
const from = getCoordsFromPaths(
previousSketch,
previousSketch.value.length - 1
)
const [angle, xTo, tag] =
'angle' in data ? [data.angle, data.to, data.tag] : data
const xComponent = xTo - from[0]
const yComponent = xComponent * Math.tan((angle * Math.PI) / 180)
const yTo = from[1] + yComponent
return lineTo.fn(
{ sourceRange, programMemory, engineCommandManager, code },
{ to: [xTo, yTo], tag },
previousSketch
)
},
add: ({
node,
pathToNode,
@ -1036,33 +740,6 @@ export const angledLineToX: SketchLineHelper = {
}
export const angledLineToY: SketchLineHelper = {
fn: (
{ sourceRange, programMemory, engineCommandManager, code },
data:
| [number, number]
| {
angle: number
to: number
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
const from = getCoordsFromPaths(
previousSketch,
previousSketch.value.length - 1
)
const [angle, yTo, tag] =
'angle' in data ? [data.angle, data.to, data.tag] : data
const yComponent = yTo - from[1]
const xComponent = yComponent / Math.tan((angle * Math.PI) / 180)
const xTo = from[0] + xComponent
return lineTo.fn(
{ sourceRange, programMemory, engineCommandManager, code },
{ to: [xTo, yTo], tag },
previousSketch
)
},
add: ({
node,
pathToNode,
@ -1133,37 +810,6 @@ export const angledLineToY: SketchLineHelper = {
}
export const angledLineThatIntersects: SketchLineHelper = {
fn: (
{ sourceRange, programMemory, engineCommandManager, code },
data: {
angle: number
intersectTag: string
offset?: number
tag?: string
},
previousSketch: SketchGroup
) => {
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
const intersectPath = previousSketch.value.find(
({ name }) => name === data.intersectTag
)
if (!intersectPath) throw new Error('intersectTag must match a line')
const from = getCoordsFromPaths(
previousSketch,
previousSketch.value.length - 1
)
const to = intersectionWithParallelLine({
line1: [intersectPath.from, intersectPath.to],
line1Offset: data.offset || 0,
line2Point: from,
line2Angle: data.angle,
})
return lineTo.fn(
{ sourceRange, programMemory, engineCommandManager, code },
{ to, tag: data.tag },
previousSketch
)
},
add: ({
node,
pathToNode,
@ -1526,142 +1172,6 @@ function addTagWithTo(
}
}
export const close: InternalFn = (
{ sourceRange, engineCommandManager, code },
sketchGroup: SketchGroup
): SketchGroup => {
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
const to = sketchGroup.start
? sketchGroup.start.from
: getCoordsFromPaths(sketchGroup, 0)
const lineData: LineData = {
from: [...from, 0],
to: [...to, 0],
}
const id = makeId({
code,
sourceRange,
data: sketchGroup,
})
engineCommandManager.sendModellingCommand({
id,
params: [lineData],
range: sourceRange,
command: {
type: 'modeling_cmd_req',
cmd: {
type: 'close_path',
path_id: sketchGroup.id,
},
cmd_id: id,
},
})
const currentPath: Path = {
type: 'toPoint',
to,
from,
__geoMeta: {
id,
sourceRange,
pathToNode: [], // TODO
},
}
const newValue = [...sketchGroup.value]
newValue.push(currentPath)
return {
...sketchGroup,
value: newValue,
}
}
export const startSketchAt: InternalFn = (
{ sourceRange, programMemory, engineCommandManager, code },
data:
| [number, number]
| 'default'
| {
to: [number, number] | 'default'
// name?: string
tag?: string
}
): SketchGroup => {
let to: [number, number] = [0, 0]
if (data !== 'default' && 'to' in data && data.to !== 'default') {
to = data.to
} else if (data !== 'default' && !('to' in data)) {
to = data
}
const lineData: { to: [number, number, number] } = {
to: [...to, 0],
}
const id = makeId({
code,
sourceRange,
data,
})
const pathId = makeId({
code,
sourceRange,
data,
isPath: true,
})
engineCommandManager.sendModellingCommand({
id: pathId,
params: [lineData],
range: sourceRange,
command: {
type: 'modeling_cmd_req',
cmd: {
type: 'start_path',
},
cmd_id: pathId,
},
})
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'move_path_pen',
path: pathId,
to: {
x: lineData.to[0],
y: lineData.to[1],
z: 0,
},
},
cmd_id: id,
})
const currentPath: Path = {
type: 'base',
to,
from: to,
__geoMeta: {
id,
sourceRange,
pathToNode: [], // TODO
},
}
if (data !== 'default' && 'tag' in data) {
currentPath.name = data.tag
}
return {
type: 'sketchGroup',
start: currentPath,
value: [],
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
id: pathId,
__meta: [
{
sourceRange,
pathToNode: [], // TODO
},
],
}
}
export function getYComponent(
angleDegree: number,
xComponent: number

View File

@ -391,6 +391,7 @@ show(part001)`
type: 'toPoint',
to: [5.62, 1.79],
from: [3.48, 0.44],
name: '',
})
})
it('verify it works when the segment is in the `start` property', async () => {
@ -400,6 +401,6 @@ show(part001)`
programMemory.root['part001'] as SketchGroup,
[index, index]
).segment
expect(segment).toEqual({ type: 'base', to: [0, 0.04], from: [0, 0.04] })
expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' })
})
})

View File

@ -1,4 +1,3 @@
import { getAngle } from '../../lib/utils'
import { TooTip, toolTips } from '../../useStore'
import {
Program,
@ -6,7 +5,6 @@ import {
CallExpression,
} from '../abstractSyntaxTreeTypes'
import { SketchGroup, SourceRange } from '../executor'
import { InternalFn } from './stdTypes'
export function getSketchSegmentFromSourceRange(
sketchGroup: SketchGroup,
@ -36,79 +34,6 @@ export function getSketchSegmentFromSourceRange(
}
}
export const segLen: InternalFn = (
_,
segName: string,
sketchGroup: SketchGroup
): number => {
const line = sketchGroup?.value.find((seg) => seg?.name === segName)
// maybe this should throw, but the language doesn't have a way to handle errors yet
if (!line) return 0
return Math.sqrt(
(line.from[1] - line.to[1]) ** 2 + (line.from[0] - line.to[0]) ** 2
)
}
export const segAng: InternalFn = (
_,
segName: string,
sketchGroup: SketchGroup
): number => {
const line = sketchGroup?.value.find((seg) => seg.name === segName)
// maybe this should throw, but the language doesn't have a way to handle errors yet
if (!line) return 0
return getAngle(line.from, line.to)
}
function segEndFactory(which: 'x' | 'y'): InternalFn {
return (_, segName: string, sketchGroup: SketchGroup): number => {
const line =
sketchGroup?.start?.name === segName
? sketchGroup?.start
: sketchGroup?.value.find((seg) => seg.name === segName)
// maybe this should throw, but the language doesn't have a way to handle errors yet
if (!line) return 0
return which === 'x' ? line.to[0] : line.to[1]
}
}
export const segEndX: InternalFn = segEndFactory('x')
export const segEndY: InternalFn = segEndFactory('y')
function lastSegFactory(which: 'x' | 'y'): InternalFn {
return (_, sketchGroup: SketchGroup): number => {
const lastLine = sketchGroup?.value[sketchGroup.value.length - 1]
return which === 'x' ? lastLine.to[0] : lastLine.to[1]
}
}
export const lastSegX: InternalFn = lastSegFactory('x')
export const lastSegY: InternalFn = lastSegFactory('y')
function angleToMatchLengthFactory(which: 'x' | 'y'): InternalFn {
return (_, segName: string, to: number, sketchGroup: SketchGroup): number => {
const isX = which === 'x'
const lineToMatch = sketchGroup?.value.find((seg) => seg.name === segName)
// maybe this should throw, but the language doesn't have a way to handle errors yet
if (!lineToMatch) return 0
const lengthToMatch = Math.sqrt(
(lineToMatch.from[1] - lineToMatch.to[1]) ** 2 +
(lineToMatch.from[0] - lineToMatch.to[0]) ** 2
)
const lastLine = sketchGroup?.value[sketchGroup.value.length - 1]
const diff = Math.abs(to - (isX ? lastLine.to[0] : lastLine.to[1]))
const angleR = Math[isX ? 'acos' : 'asin'](diff / lengthToMatch)
return diff > lengthToMatch ? 0 : (angleR * 180) / Math.PI
}
}
export const angleToMatchLengthX: InternalFn = angleToMatchLengthFactory('x')
export const angleToMatchLengthY: InternalFn = angleToMatchLengthFactory('y')
export function isSketchVariablesLinked(
secondaryVarDec: VariableDeclarator,
primaryVarDec: VariableDeclarator,

View File

@ -1,170 +0,0 @@
import {
lineTo,
xLineTo,
yLineTo,
line,
xLine,
yLine,
angledLine,
angledLineOfXLength,
angledLineToX,
angledLineOfYLength,
angledLineToY,
close,
startSketchAt,
angledLineThatIntersects,
} from './sketch'
import {
segLen,
segAng,
angleToMatchLengthX,
angleToMatchLengthY,
segEndX,
segEndY,
lastSegX,
lastSegY,
} from './sketchConstraints'
import { getExtrudeWallTransform, extrude } from './extrude'
import { InternalFn, InternalFnNames } from './stdTypes'
// const transform: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
// { sourceRange }: InternalFirstArg,
// transformInfo: {
// position: Position
// quaternion: Rotation
// },
// sketch: T
// ): T => {
// const quaternionToApply = new Quaternion(...transformInfo?.quaternion)
// const newQuaternion = new Quaternion(...sketch.rotation).multiply(
// quaternionToApply.invert()
// )
// const oldPosition = new Vector3(...sketch?.position)
// const newPosition = oldPosition
// .applyQuaternion(quaternionToApply)
// .add(new Vector3(...transformInfo?.position))
// return {
// ...sketch,
// position: newPosition.toArray(),
// rotation: newQuaternion.toArray(),
// __meta: [
// ...sketch.__meta,
// {
// sourceRange,
// pathToNode: [], // TODO
// },
// ],
// }
// }
// const translate: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
// { sourceRange }: InternalFirstArg,
// vec3: [number, number, number],
// sketch: T
// ): T => {
// const oldPosition = new Vector3(...sketch.position)
// const newPosition = oldPosition.add(new Vector3(...vec3))
// return {
// ...sketch,
// position: newPosition.toArray(),
// __meta: [
// ...sketch.__meta,
// {
// sourceRange,
// pathToNode: [], // TODO
// },
// ],
// }
// }
const min: InternalFn = (_, a: number, b: number): number => Math.min(a, b)
const legLen: InternalFn = (_, hypotenuse: number, leg: number): number =>
Math.sqrt(
hypotenuse ** 2 - Math.min(Math.abs(leg), Math.abs(hypotenuse)) ** 2
)
const legAngX: InternalFn = (_, hypotenuse: number, leg: number): number =>
(Math.acos(Math.min(leg, hypotenuse) / hypotenuse) * 180) / Math.PI
const legAngY: InternalFn = (_, hypotenuse: number, leg: number): number =>
(Math.asin(Math.min(leg, hypotenuse) / hypotenuse) * 180) / Math.PI
export const internalFns: { [key in InternalFnNames]: InternalFn } = {
// TODO - re-enable these
// rx: rotateOnAxis([1, 0, 0]), // Enable rotations #152
// ry: rotateOnAxis([0, 1, 0]),
// rz: rotateOnAxis([0, 0, 1]),
extrude,
// translate,
// transform,
getExtrudeWallTransform,
min,
legLen,
legAngX,
legAngY,
segEndX,
segEndY,
lastSegX,
lastSegY,
segLen,
segAng,
angleToMatchLengthX,
angleToMatchLengthY,
lineTo: lineTo.fn,
xLineTo: xLineTo.fn,
yLineTo: yLineTo.fn,
line: line.fn,
xLine: xLine.fn,
yLine: yLine.fn,
angledLine: angledLine.fn,
angledLineOfXLength: angledLineOfXLength.fn,
angledLineToX: angledLineToX.fn,
angledLineOfYLength: angledLineOfYLength.fn,
angledLineToY: angledLineToY.fn,
angledLineThatIntersects: angledLineThatIntersects.fn,
startSketchAt,
close,
}
// function rotateOnAxis<T extends SketchGroup | ExtrudeGroup>(
// axisMultiplier: [number, number, number]
// ): InternalFn {
// return ({ sourceRange }, rotationD: number, sketch: T): T => {
// const rotationR = rotationD * (Math.PI / 180)
// const rotateVec = new Vector3(...axisMultiplier)
// const quaternion = new Quaternion()
// quaternion.setFromAxisAngle(rotateVec, rotationR)
// const position = new Vector3(...sketch.position)
// .applyQuaternion(quaternion)
// .toArray()
// const existingQuat = new Quaternion(...sketch.rotation)
// const rotation = quaternion.multiply(existingQuat).toArray()
// return {
// ...sketch,
// rotation,
// position,
// __meta: [
// ...sketch.__meta,
// {
// sourceRange,
// pathToNode: [], // TODO
// },
// ],
// }
// }
// }
export function clockwiseSign(points: [number, number][]): number {
let sum = 0
for (let i = 0; i < points.length; i++) {
const currentPoint = points[i]
const nextPoint = points[(i + 1) % points.length]
sum += (nextPoint[0] - currentPoint[0]) * (nextPoint[1] + currentPoint[1])
}
return sum >= 0 ? 1 : -1
}

View File

@ -17,44 +17,6 @@ export interface PathReturn {
currentPath: Path
}
export type InternalFn = (internals: InternalFirstArg, ...args: any[]) => any
export type InternalFnNames =
// TODO re-enable these
// | 'translate'
// | 'transform'
// | 'rx' // Enable rotations #152
// | 'ry'
// | 'rz'
| 'extrude'
| 'getExtrudeWallTransform'
| 'min'
| 'legLen'
| 'legAngX'
| 'legAngY'
| 'segEndX'
| 'segEndY'
| 'lastSegX'
| 'lastSegY'
| 'segLen'
| 'segAng'
| 'angleToMatchLengthX'
| 'angleToMatchLengthY'
| 'lineTo'
| 'yLineTo'
| 'xLineTo'
| 'line'
| 'yLine'
| 'xLine'
| 'angledLine'
| 'angledLineOfXLength'
| 'angledLineToX'
| 'angledLineOfYLength'
| 'angledLineToY'
| 'startSketchAt'
| 'close'
| 'angledLineThatIntersects'
export interface ModifyAstBase {
node: Program
previousProgramMemory: ProgramMemory
@ -87,7 +49,6 @@ export type SketchCallTransfromMap = {
}
export interface SketchLineHelper {
fn: InternalFn
add: (a: addCall) => {
modifiedAst: Program
pathToNode: PathToNode

View File

@ -1,8 +1,8 @@
import { lexer_js } from '../wasm-lib/pkg/wasm_lib'
import { initPromise } from './rust'
import { Token } from '../wasm-lib/bindings/Token'
import { Token } from '../wasm-lib/kcl/bindings/Token'
export type { Token } from '../wasm-lib/bindings/Token'
export type { Token } from '../wasm-lib/kcl/bindings/Token'
export async function asyncLexer(str: string): Promise<Token[]> {
await initPromise

View File

@ -26,3 +26,12 @@ export function updateCursors(
setCursor(newSelections)
}
}
export function isReducedMotion(): boolean {
return (
typeof window !== 'undefined' &&
window.matchMedia &&
// TODO/Note I (Kurt) think '(prefers-reduced-motion: reduce)' and '(prefers-reduced-motion)' are equivalent, but not 100% sure
window.matchMedia('(prefers-reduced-motion)').matches
)
}

126
src/lib/commands.ts Normal file
View File

@ -0,0 +1,126 @@
import { AnyStateMachine, EventFrom, StateFrom } from 'xstate'
import { isTauri } from './isTauri'
type InitialCommandBarMetaArg = {
name: string
type: 'string' | 'select'
description?: string
defaultValue?: string
options: string | Array<{ name: string }>
}
type Platform = 'both' | 'web' | 'desktop'
export type CommandBarMeta = {
[key: string]:
| {
displayValue: (args: string[]) => string
args: InitialCommandBarMetaArg[]
hide?: Platform
}
| {
hide?: Platform
}
}
export type Command = {
owner: string
name: string
callback: Function
meta?: {
displayValue(args: string[]): string | string
args: SubCommand[]
}
}
export type SubCommand = {
name: string
type: 'select' | 'string'
description?: string
options?: Partial<{ name: string }>[]
}
interface CommandBarArgs<T extends AnyStateMachine> {
type: EventFrom<T>['type']
state: StateFrom<T>
commandBarMeta?: CommandBarMeta
send: Function
owner: string
}
export function createMachineCommand<T extends AnyStateMachine>({
type,
state,
commandBarMeta,
send,
owner,
}: CommandBarArgs<T>): Command | null {
const lookedUpMeta = commandBarMeta && commandBarMeta[type]
if (lookedUpMeta && 'hide' in lookedUpMeta) {
const { hide } = lookedUpMeta
if (hide === 'both') return null
else if (hide === 'desktop' && isTauri()) return null
else if (hide === 'web' && !isTauri()) return null
}
let replacedArgs
if (lookedUpMeta && 'args' in lookedUpMeta) {
replacedArgs = lookedUpMeta.args.map((arg) => {
const optionsFromContext = state.context[
arg.options as keyof typeof state.context
] as { name: string }[] | string | undefined
const defaultValueFromContext = state.context[
arg.defaultValue as keyof typeof state.context
] as string | undefined
console.log(arg.name, { defaultValueFromContext })
const options =
arg.options instanceof Array
? arg.options.map((o) => ({
...o,
description:
defaultValueFromContext === o.name ? '(current)' : '',
}))
: !optionsFromContext || typeof optionsFromContext === 'string'
? [
{
name: optionsFromContext,
description: arg.description || '',
},
]
: optionsFromContext.map((o) => ({
name: o.name || '',
description: arg.description || '',
}))
return {
...arg,
options,
}
}) as any[]
}
// We have to recreate this object every time,
// otherwise we'll have stale state in the CommandBar
// after completing our first action
const meta = lookedUpMeta
? {
...lookedUpMeta,
args: replacedArgs,
}
: undefined
return {
name: type,
owner,
callback: (data: EventFrom<T, typeof type>) => {
if (data !== undefined && data !== null) {
send(type, { data })
} else {
send(type)
}
},
meta: meta as any,
}
}

View File

@ -1,16 +1,13 @@
import { useAuthMachine } from '../hooks/useAuthMachine'
export default function fetcher(input: RequestInfo, init: RequestInit = {}) {
const fetcherWithToken = async (token?: string): Promise<JSON> => {
const headers = { ...init.headers } as Record<string, string>
if (token) {
headers.Authorization = `Bearer ${token}`
}
export default async function fetcher<JSON = any>(
input: RequestInfo,
init: RequestInit = {}
): Promise<JSON> {
const [token] = useAuthMachine((s) => s?.context?.token)
const headers = { ...init.headers } as Record<string, string>
if (token) {
headers.Authorization = `Bearer ${token}`
const credentials = 'include' as RequestCredentials
const res = await fetch(input, { ...init, credentials, headers })
return res.json()
}
const credentials = 'include' as RequestCredentials
const res = await fetch(input, { ...init, credentials, headers })
return res.json()
return fetcherWithToken
}

View File

@ -1,9 +0,0 @@
import { Themes } from '../useStore'
export function getSystemTheme(): Exclude<Themes, 'system'> {
return typeof window !== 'undefined' &&
'matchMedia' in window &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? Themes.Dark
: Themes.Light
}

64
src/lib/sorting.ts Normal file
View File

@ -0,0 +1,64 @@
import {
faArrowDown,
faArrowUp,
faCircleDot,
} from '@fortawesome/free-solid-svg-icons'
import { ProjectWithEntryPointMetadata } from '../Router'
const DESC = ':desc'
export function getSortIcon(currentSort: string, newSort: string) {
if (currentSort === newSort) {
return faArrowUp
} else if (currentSort === newSort + DESC) {
return faArrowDown
}
return faCircleDot
}
export function getNextSearchParams(currentSort: string, newSort: string) {
if (currentSort === null || !currentSort)
return { sort_by: newSort + (newSort !== 'modified' ? DESC : '') }
if (currentSort.includes(newSort) && !currentSort.includes(DESC))
return { sort_by: '' }
return {
sort_by: newSort + (currentSort.includes(DESC) ? '' : DESC),
}
}
export function getSortFunction(sortBy: string) {
const sortByName = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (a.name && b.name) {
return sortBy.includes('desc')
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
}
return 0
}
const sortByModified = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (
a.entrypoint_metadata?.modifiedAt &&
b.entrypoint_metadata?.modifiedAt
) {
return !sortBy || sortBy.includes('desc')
? b.entrypoint_metadata.modifiedAt.getTime() -
a.entrypoint_metadata.modifiedAt.getTime()
: a.entrypoint_metadata.modifiedAt.getTime() -
b.entrypoint_metadata.modifiedAt.getTime()
}
return 0
}
if (sortBy?.includes('name')) {
return sortByName
} else {
return sortByModified
}
}

View File

@ -1,6 +1,11 @@
import { FileEntry, createDir, exists, writeTextFile } from '@tauri-apps/api/fs'
import {
FileEntry,
createDir,
exists,
readDir,
writeTextFile,
} from '@tauri-apps/api/fs'
import { documentDir } from '@tauri-apps/api/path'
import { useStore } from '../useStore'
import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router'
import { metadata } from 'tauri-plugin-fs-extra-api'
@ -12,35 +17,31 @@ const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
export const MAX_PADDING = 7
// Initializes the project directory and returns the path
export async function initializeProjectDirectory() {
export async function initializeProjectDirectory(directory: string) {
if (!isTauri()) {
throw new Error(
'initializeProjectDirectory() can only be called from a Tauri app'
)
}
const { defaultDir: projectDir, setDefaultDir } = useStore.getState()
if (projectDir && projectDir.dir.length > 0) {
const dirExists = await exists(projectDir.dir)
if (directory) {
const dirExists = await exists(directory)
if (!dirExists) {
await createDir(projectDir.dir, { recursive: true })
await createDir(directory, { recursive: true })
}
return projectDir
return directory
}
const appData = await documentDir()
const docDirectory = await documentDir()
const INITIAL_DEFAULT_DIR = {
dir: appData + PROJECT_FOLDER,
}
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
const defaultDirExists = await exists(INITIAL_DEFAULT_DIR.dir)
const defaultDirExists = await exists(INITIAL_DEFAULT_DIR)
if (!defaultDirExists) {
await createDir(INITIAL_DEFAULT_DIR.dir, { recursive: true })
await createDir(INITIAL_DEFAULT_DIR, { recursive: true })
}
setDefaultDir(INITIAL_DEFAULT_DIR)
return INITIAL_DEFAULT_DIR
}
@ -51,6 +52,25 @@ export function isProjectDirectory(fileOrDir: Partial<FileEntry>) {
)
}
// Read the contents of a directory
// and return the valid projects
export async function getProjectsInDir(projectDir: string) {
const readProjects = (
await readDir(projectDir, {
recursive: true,
})
).filter(isProjectDirectory)
const projectsWithMetadata = await Promise.all(
readProjects.map(async (p) => ({
entrypoint_metadata: await metadata(p.path + '/' + PROJECT_ENTRYPOINT),
...p,
}))
)
return projectsWithMetadata
}
// Creates a new file in the default directory with the default project name
// Returns the path to the new file
export async function createNewProject(

View File

@ -1,6 +1,10 @@
import { Program } from '../lang/abstractSyntaxTreeTypes'
import { ProgramMemory, _executor } from '../lang/executor'
import { EngineCommandManager } from '../lang/std/engineConnection'
import {
EngineCommandManager,
EngineCommand,
} from '../lang/std/engineConnection'
import { SourceRange } from 'lang/executor'
class MockEngineCommandManager {
constructor(mockParams: {
@ -10,13 +14,43 @@ class MockEngineCommandManager {
startNewSession() {}
waitForAllCommands() {}
waitForReady = new Promise<void>((resolve) => resolve())
sendModellingCommand() {}
sendModelingCommand({
id,
range,
command,
}: {
id: string
range: SourceRange
command: EngineCommand
}): Promise<any> {
return Promise.resolve()
}
sendModelingCommandFromWasm(
id: string,
rangeStr: string,
commandStr: string
): Promise<any> {
if (id === undefined) {
throw new Error('id is undefined')
}
if (rangeStr === undefined) {
throw new Error('rangeStr is undefined')
}
if (commandStr === undefined) {
throw new Error('commandStr is undefined')
}
console.log('sendModelingCommandFromWasm', id, rangeStr, commandStr)
const command: EngineCommand = JSON.parse(commandStr)
const range: SourceRange = JSON.parse(rangeStr)
return this.sendModelingCommand({ id, range, command })
}
sendSceneCommand() {}
}
export async function enginelessExecutor(
ast: Program,
pm: ProgramMemory = { root: {}, pendingMemory: {} }
pm: ProgramMemory = { root: {} }
): Promise<ProgramMemory> {
const mockEngineCommandManager = new MockEngineCommandManager({
setIsStreamReady: () => {},
@ -31,7 +65,7 @@ export async function enginelessExecutor(
export async function executor(
ast: Program,
pm: ProgramMemory = { root: {}, pendingMemory: {} }
pm: ProgramMemory = { root: {} }
): Promise<ProgramMemory> {
const engineCommandManager = new EngineCommandManager({
setIsStreamReady: () => {},

22
src/lib/theme.ts Normal file
View File

@ -0,0 +1,22 @@
export enum Themes {
Light = 'light',
Dark = 'dark',
System = 'system',
}
export function getSystemTheme(): Exclude<Themes, 'system'> {
return typeof window !== 'undefined' &&
'matchMedia' in window &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? Themes.Dark
: Themes.Light
}
export function setThemeClass(theme: Themes) {
const systemTheme = theme === Themes.System && getSystemTheme()
if (theme === Themes.Dark || systemTheme === Themes.Dark) {
document.body.classList.add('dark')
} else {
document.body.classList.remove('dark')
}
}

View File

@ -1,6 +1,7 @@
import { createMachine, assign } from 'xstate'
import { Models } from '@kittycad/lib'
import withBaseURL from '../lib/withBaseURL'
import { CommandBarMeta } from '../lib/commands'
export interface UserContext {
user?: Models['User_type']
@ -9,16 +10,22 @@ export interface UserContext {
export type Events =
| {
type: 'logout'
type: 'Log out'
}
| {
type: 'tryLogin'
type: 'Log in'
token?: string
}
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
export const authCommandBarMeta: CommandBarMeta = {
'Log in': {
hide: 'both',
},
}
export const authMachine = createMachine<UserContext, Events>(
{
id: 'Auth',
@ -50,7 +57,7 @@ export const authMachine = createMachine<UserContext, Events>(
loggedIn: {
entry: ['goToIndexPage'],
on: {
logout: {
'Log out': {
target: 'loggedOut',
},
},
@ -58,10 +65,10 @@ export const authMachine = createMachine<UserContext, Events>(
loggedOut: {
entry: ['goToSignInPage'],
on: {
tryLogin: {
'Log in': {
target: 'checkIfLoggedIn',
actions: assign({
token: (context, event) => {
token: (_, event) => {
const token = event.token || ''
localStorage.setItem(TOKEN_PERSIST_KEY, token)
return token
@ -71,7 +78,7 @@ export const authMachine = createMachine<UserContext, Events>(
},
},
},
schema: { events: {} as { type: 'logout' } | { type: 'tryLogin' } },
schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
predictableActionArguments: true,
preserveActionOrder: true,
context: { token: persistedToken },
@ -91,12 +98,16 @@ async function getUser(context: UserContext) {
}
if (!context.token && '__TAURI__' in window) throw 'not log in'
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
try {
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
} catch (e) {
console.error(e)
}
}

218
src/machines/homeMachine.ts Normal file
View File

@ -0,0 +1,218 @@
import { assign, createMachine } from 'xstate'
import { ProjectWithEntryPointMetadata } from '../Router'
import { CommandBarMeta } from '../lib/commands'
export const homeCommandMeta: CommandBarMeta = {
'Create project': {
displayValue: (args: string[]) => `Create project "${args[0]}"`,
args: [
{
name: 'name',
type: 'string',
description: '(default)',
options: 'defaultProjectName',
},
],
},
'Open project': {
displayValue: (args: string[]) => `Open project "${args[0]}"`,
args: [
{
name: 'name',
type: 'select',
options: 'projects',
},
],
},
'Delete project': {
displayValue: (args: string[]) => `Delete project "${args[0]}"`,
args: [
{
name: 'name',
type: 'select',
options: 'projects',
},
],
},
'Rename project': {
displayValue: (args: string[]) =>
`Rename project "${args[0]}" to "${args[1]}"`,
args: [
{
name: 'oldName',
type: 'select',
options: 'projects',
},
{
name: 'newName',
type: 'string',
description: '(default)',
options: 'defaultProjectName',
},
],
},
assign: {
hide: 'both',
},
}
export const homeMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
id: 'Home machine',
initial: 'Reading projects',
context: {
projects: [] as ProjectWithEntryPointMetadata[],
defaultProjectName: '',
defaultDirectory: '',
},
on: {
assign: {
actions: assign((_, event) => ({
...event.data,
})),
target: '.Reading projects',
},
},
states: {
'Has no projects': {
on: {
'Create project': {
target: 'Creating project',
},
},
},
'Has projects': {
on: {
'Rename project': {
target: 'Renaming project',
},
'Create project': {
target: 'Creating project',
},
'Delete project': {
target: 'Deleting project',
},
'Open project': {
target: 'Opening project',
},
},
},
'Creating project': {
invoke: {
id: 'create-project',
src: 'createProject',
onDone: [
{
target: 'Reading projects',
actions: ['toastSuccess'],
},
],
onError: [
{
target: 'Reading projects',
actions: ['toastError'],
},
],
},
},
'Renaming project': {
invoke: {
id: 'rename-project',
src: 'renameProject',
onDone: [
{
target: '#Home machine.Reading projects',
actions: ['toastSuccess'],
},
],
onError: [
{
target: '#Home machine.Reading projects',
actions: ['toastError'],
},
],
},
},
'Deleting project': {
invoke: {
id: 'delete-project',
src: 'deleteProject',
onDone: [
{
actions: ['toastSuccess'],
target: '#Home machine.Reading projects',
},
],
onError: {
actions: ['toastError'],
target: '#Home machine.Has projects',
},
},
},
'Reading projects': {
invoke: {
id: 'read-projects',
src: 'readProjects',
onDone: [
{
cond: 'Has at least 1 project',
target: 'Has projects',
actions: ['setProjects'],
},
{
target: 'Has no projects',
actions: ['setProjects'],
},
],
onError: [
{
target: 'Has no projects',
actions: ['toastError'],
},
],
},
},
'Opening project': {
entry: ['navigateToProject'],
},
},
schema: {
events: {} as
| { type: 'Open project'; data: { name: string } }
| { type: 'Rename project'; data: { oldName: string; newName: string } }
| { type: 'Create project'; data: { name: string } }
| { type: 'Delete project'; data: { name: string } }
| { type: 'navigate'; data: { name: string } }
| {
type: 'done.invoke.read-projects'
data: ProjectWithEntryPointMetadata[]
}
| { type: 'assign'; data: { [key: string]: any } },
},
predictableActionArguments: true,
preserveActionOrder: true,
tsTypes: {} as import('./homeMachine.typegen').Typegen0,
},
{
actions: {
setProjects: assign((_, event) => {
return { projects: event.data as ProjectWithEntryPointMetadata[] }
}),
},
}
)

View File

@ -0,0 +1,99 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true
internalEvents: {
'done.invoke.create-project': {
type: 'done.invoke.create-project'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.delete-project': {
type: 'done.invoke.delete-project'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.read-projects': {
type: 'done.invoke.read-projects'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.rename-project': {
type: 'done.invoke.rename-project'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'error.platform.create-project': {
type: 'error.platform.create-project'
data: unknown
}
'error.platform.delete-project': {
type: 'error.platform.delete-project'
data: unknown
}
'error.platform.read-projects': {
type: 'error.platform.read-projects'
data: unknown
}
'error.platform.rename-project': {
type: 'error.platform.rename-project'
data: unknown
}
'xstate.init': { type: 'xstate.init' }
}
invokeSrcNameMap: {
createProject: 'done.invoke.create-project'
deleteProject: 'done.invoke.delete-project'
readProjects: 'done.invoke.read-projects'
renameProject: 'done.invoke.rename-project'
}
missingImplementations: {
actions: 'navigateToProject' | 'toastError' | 'toastSuccess'
delays: never
guards: 'Has at least 1 project'
services:
| 'createProject'
| 'deleteProject'
| 'readProjects'
| 'renameProject'
}
eventsCausingActions: {
navigateToProject: 'Open project'
setProjects: 'done.invoke.read-projects'
toastError:
| 'error.platform.create-project'
| 'error.platform.delete-project'
| 'error.platform.read-projects'
| 'error.platform.rename-project'
toastSuccess:
| 'done.invoke.create-project'
| 'done.invoke.delete-project'
| 'done.invoke.rename-project'
}
eventsCausingDelays: {}
eventsCausingGuards: {
'Has at least 1 project': 'done.invoke.read-projects'
}
eventsCausingServices: {
createProject: 'Create project'
deleteProject: 'Delete project'
readProjects:
| 'assign'
| 'done.invoke.create-project'
| 'done.invoke.delete-project'
| 'done.invoke.rename-project'
| 'error.platform.create-project'
| 'error.platform.rename-project'
| 'xstate.init'
renameProject: 'Rename project'
}
matchesStates:
| 'Creating project'
| 'Deleting project'
| 'Has no projects'
| 'Has projects'
| 'Opening project'
| 'Reading projects'
| 'Renaming project'
tags: never
}

View File

@ -0,0 +1,193 @@
import { assign, createMachine } from 'xstate'
import { BaseUnit, baseUnitsUnion } from '../useStore'
import { CommandBarMeta } from '../lib/commands'
import { Themes } from '../lib/theme'
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
export const settingsCommandBarMeta: CommandBarMeta = {
'Set Theme': {
displayValue: (args: string[]) => 'Change the app theme',
args: [
{
name: 'theme',
type: 'select',
defaultValue: 'theme',
options: Object.values(Themes).map((v) => ({ name: v })) as {
name: string
}[],
},
],
},
'Set Default Project Name': {
displayValue: (args: string[]) => 'Set a new default project name',
hide: 'web',
args: [
{
name: 'defaultProjectName',
type: 'string',
description: '(default)',
defaultValue: 'defaultProjectName',
options: 'defaultProjectName',
},
],
},
'Set Default Directory': {
hide: 'both',
},
'Set Unit System': {
displayValue: (args: string[]) => 'Set your default unit system',
args: [
{
name: 'unitSystem',
type: 'select',
defaultValue: 'unitSystem',
options: [{ name: 'imperial' }, { name: 'metric' }],
},
],
},
'Set Base Unit': {
displayValue: (args: string[]) => 'Set your default base unit',
args: [
{
name: 'baseUnit',
type: 'select',
defaultValue: 'baseUnit',
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
},
],
},
'Set Onboarding Status': {
hide: 'both',
},
}
export const settingsMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */
id: 'Settings',
predictableActionArguments: true,
context: {
theme: Themes.System,
defaultProjectName: '',
unitSystem: 'imperial' as 'imperial' | 'metric',
baseUnit: 'in' as BaseUnit,
defaultDirectory: '',
showDebugPanel: false,
onboardingStatus: '',
},
initial: 'idle',
states: {
idle: {
on: {
'Set Theme': {
actions: [
assign({
theme: (_, event) => event.data.theme,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Default Project Name': {
actions: [
assign({
defaultProjectName: (_, event) => event.data.defaultProjectName,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Default Directory': {
actions: [
assign({
defaultDirectory: (_, event) => event.data.defaultDirectory,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Unit System': {
actions: [
assign({
unitSystem: (_, event) => event.data.unitSystem,
baseUnit: (_, event) =>
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Base Unit': {
actions: [
assign({ baseUnit: (_, event) => event.data.baseUnit }),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Toggle Debug Panel': {
actions: [
assign({
showDebugPanel: (context) => {
return !context.showDebugPanel
},
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Onboarding Status': {
actions: [
assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
],
target: 'idle',
internal: true,
},
},
},
},
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: {
events: {} as
| { type: 'Set Theme'; data: { theme: Themes } }
| {
type: 'Set Default Project Name'
data: { defaultProjectName: string }
}
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| {
type: 'Set Unit System'
data: { unitSystem: 'imperial' | 'metric' }
}
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
| { type: 'Toggle Debug Panel' },
},
},
{
actions: {
persistSettings: (context) => {
try {
localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context))
} catch (e) {
console.error(e)
}
},
},
}
)

View File

@ -0,0 +1,37 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true
internalEvents: {
'xstate.init': { type: 'xstate.init' }
}
invokeSrcNameMap: {}
missingImplementations: {
actions: 'toastSuccess'
delays: never
guards: never
services: never
}
eventsCausingActions: {
persistSettings:
| 'Set Base Unit'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Onboarding Status'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
toastSuccess:
| 'Set Base Unit'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
}
eventsCausingDelays: {}
eventsCausingGuards: {}
eventsCausingServices: {}
matchesStates: 'idle'
tags: never
}

View File

@ -1,93 +1,139 @@
import { FormEvent, useCallback, useEffect, useState } from 'react'
import { readDir, removeDir, renameFile } from '@tauri-apps/api/fs'
import { FormEvent, useEffect } from 'react'
import { removeDir, renameFile } from '@tauri-apps/api/fs'
import {
createNewProject,
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
isProjectDirectory,
PROJECT_ENTRYPOINT,
getProjectsInDir,
} from '../lib/tauriFS'
import { ActionButton } from '../components/ActionButton'
import {
faArrowDown,
faArrowUp,
faCircleDot,
faPlus,
} from '@fortawesome/free-solid-svg-icons'
import { useStore } from '../useStore'
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
import { toast } from 'react-hot-toast'
import { AppHeader } from '../components/AppHeader'
import ProjectCard from '../components/ProjectCard'
import { useLoaderData, useSearchParams } from 'react-router-dom'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
import Loading from '../components/Loading'
import { metadata } from 'tauri-plugin-fs-extra-api'
const DESC = ':desc'
import { useMachine } from '@xstate/react'
import { homeCommandMeta, homeMachine } from '../machines/homeMachine'
import { ContextFrom, EventFrom } from 'xstate'
import { paths } from '../Router'
import {
getNextSearchParams,
getSortFunction,
getSortIcon,
} from '../lib/sorting'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
// This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types.
const Home = () => {
const [searchParams, setSearchParams] = useSearchParams()
const sort = searchParams.get('sort_by') ?? 'modified:desc'
const { commands, setCommandBarOpen } = useCommandsContext()
const navigate = useNavigate()
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
const [isLoading, setIsLoading] = useState(true)
const [projects, setProjects] = useState(loadedProjects || [])
const { defaultDir, defaultProjectName } = useStore((s) => ({
defaultDir: s.defaultDir,
defaultProjectName: s.defaultProjectName,
}))
const {
settings: {
context: { defaultDirectory, defaultProjectName },
},
} = useGlobalStateContext()
const modifiedSelected = sort?.includes('modified') || !sort || sort === null
const [state, send] = useMachine(homeMachine, {
context: {
projects: loadedProjects,
defaultProjectName,
defaultDirectory,
},
actions: {
navigateToProject: (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine>
) => {
if (event.data && 'name' in event.data) {
setCommandBarOpen(false)
navigate(
`${paths.FILE}/${encodeURIComponent(
context.defaultDirectory + '/' + event.data.name
)}`
)
}
},
toastSuccess: (_, event) => toast.success((event.data || '') + ''),
toastError: (_, event) => toast.error((event.data || '') + ''),
},
services: {
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
getProjectsInDir(context.defaultDirectory),
createProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Create project'>
) => {
let name =
event.data && 'name' in event.data
? event.data.name
: defaultProjectName
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = await getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
const refreshProjects = useCallback(
async (projectDir = defaultDir) => {
const readProjects = (
await readDir(projectDir.dir, {
await createNewProject(context.defaultDirectory + '/' + name)
return `Successfully created "${name}"`
},
renameProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Rename project'>
) => {
const { oldName, newName } = event.data
let name = newName ? newName : context.defaultProjectName
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = await getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await renameFile(
context.defaultDirectory + '/' + oldName,
context.defaultDirectory + '/' + name
)
return `Successfully renamed "${oldName}" to "${name}"`
},
deleteProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Delete project'>
) => {
await removeDir(context.defaultDirectory + '/' + event.data.name, {
recursive: true,
})
).filter(isProjectDirectory)
const projectsWithMetadata = await Promise.all(
readProjects.map(async (p) => ({
entrypoint_metadata: await metadata(
p.path + '/' + PROJECT_ENTRYPOINT
),
...p,
}))
)
setProjects(projectsWithMetadata)
return `Successfully deleted "${event.data.name}"`
},
},
[defaultDir, setProjects]
)
guards: {
'Has at least 1 project': (_, event: EventFrom<typeof homeMachine>) => {
if (event.type !== 'done.invoke.read-projects') return false
return event?.data?.length ? event.data?.length >= 1 : false
},
},
})
const { projects } = state.context
const [searchParams, setSearchParams] = useSearchParams()
const sort = searchParams.get('sort_by') ?? 'modified:desc'
const isSortByModified = sort?.includes('modified') || !sort || sort === null
useStateMachineCommands<typeof homeMachine>({
commands,
send,
state,
commandBarMeta: homeCommandMeta,
owner: 'home',
})
useEffect(() => {
refreshProjects(defaultDir).then(() => {
setIsLoading(false)
})
}, [setIsLoading, refreshProjects, defaultDir])
async function handleNewProject() {
let projectName = defaultProjectName
if (doesProjectNameNeedInterpolated(projectName)) {
const nextIndex = await getNextProjectIndex(defaultProjectName, projects)
projectName = interpolateProjectNameWithIndex(
defaultProjectName,
nextIndex
)
}
await createNewProject(defaultDir.dir + '/' + projectName).catch((err) => {
console.error('Error creating project:', err)
toast.error('Error creating project')
})
await refreshProjects()
toast.success('Project created')
}
send({ type: 'assign', data: { defaultProjectName, defaultDirectory } })
}, [defaultDirectory, defaultProjectName, send])
async function handleRenameProject(
e: FormEvent<HTMLFormElement>,
@ -96,85 +142,14 @@ const Home = () => {
const { newProjectName } = Object.fromEntries(
new FormData(e.target as HTMLFormElement)
)
if (newProjectName && project.name && newProjectName !== project.name) {
const dir = project.path?.slice(0, project.path?.lastIndexOf('/'))
await renameFile(project.path, dir + '/' + newProjectName).catch(
(err) => {
console.error('Error renaming project:', err)
toast.error('Error renaming project')
}
)
await refreshProjects()
toast.success('Project renamed')
}
send('Rename project', {
data: { oldName: project.name, newName: newProjectName },
})
}
async function handleDeleteProject(project: ProjectWithEntryPointMetadata) {
if (project.path) {
await removeDir(project.path, { recursive: true }).catch((err) => {
console.error('Error deleting project:', err)
toast.error('Error deleting project')
})
await refreshProjects()
toast.success('Project deleted')
}
}
function getSortIcon(sortBy: string) {
if (sort === sortBy) {
return faArrowUp
} else if (sort === sortBy + DESC) {
return faArrowDown
}
return faCircleDot
}
function getNextSearchParams(sortBy: string) {
if (sort === null || !sort)
return { sort_by: sortBy + (sortBy !== 'modified' ? DESC : '') }
if (sort.includes(sortBy) && !sort.includes(DESC)) return { sort_by: '' }
return {
sort_by: sortBy + (sort.includes(DESC) ? '' : DESC),
}
}
function getSortFunction(sortBy: string) {
const sortByName = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (a.name && b.name) {
return sortBy.includes('desc')
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
}
return 0
}
const sortByModified = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (
a.entrypoint_metadata?.modifiedAt &&
b.entrypoint_metadata?.modifiedAt
) {
return !sortBy || sortBy.includes('desc')
? b.entrypoint_metadata.modifiedAt.getTime() -
a.entrypoint_metadata.modifiedAt.getTime()
: a.entrypoint_metadata.modifiedAt.getTime() -
b.entrypoint_metadata.modifiedAt.getTime()
}
return 0
}
if (sortBy?.includes('name')) {
return sortByName
} else {
return sortByModified
}
send('Delete project', { data: { name: project.name || '' } })
}
return (
@ -191,9 +166,9 @@ const Home = () => {
? 'text-chalkboard-80 dark:text-chalkboard-40'
: ''
}
onClick={() => setSearchParams(getNextSearchParams('name'))}
onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
icon={{
icon: getSortIcon('name'),
icon: getSortIcon(sort, 'name'),
bgClassName: !sort?.includes('name')
? 'bg-liquid-50 dark:bg-liquid-70'
: '',
@ -207,17 +182,19 @@ const Home = () => {
<ActionButton
Element="button"
className={
!modifiedSelected
!isSortByModified
? 'text-chalkboard-80 dark:text-chalkboard-40'
: ''
}
onClick={() => setSearchParams(getNextSearchParams('modified'))}
onClick={() =>
setSearchParams(getNextSearchParams(sort, 'modified'))
}
icon={{
icon: sort ? getSortIcon('modified') : faArrowDown,
bgClassName: !modifiedSelected
icon: sort ? getSortIcon(sort, 'modified') : faArrowDown,
bgClassName: !isSortByModified
? 'bg-liquid-50 dark:bg-liquid-70'
: '',
iconClassName: !modifiedSelected
iconClassName: !isSortByModified
? 'text-liquid-80 dark:text-liquid-30'
: '',
}}
@ -230,11 +207,11 @@ const Home = () => {
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
Are being saved at{' '}
<code className="text-liquid-80 dark:text-liquid-30">
{defaultDir.dir}
{defaultDirectory}
</code>
, which you can change in your <Link to="settings">Settings</Link>.
</p>
{isLoading ? (
{state.matches('Reading projects') ? (
<Loading>Loading your Projects...</Loading>
) : (
<>
@ -256,7 +233,7 @@ const Home = () => {
)}
<ActionButton
Element="button"
onClick={handleNewProject}
onClick={() => send('Create project')}
icon={{ icon: faPlus }}
>
New file

View File

@ -1,32 +1,28 @@
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
import { baseUnits, useStore } from '../../useStore'
import { BaseUnit, baseUnits } from '../../useStore'
import { ActionButton } from '../../components/ActionButton'
import { SettingsSection } from '../Settings'
import { Toggle } from '../../components/Toggle/Toggle'
import { useState } from 'react'
import { onboardingPaths, useDismiss, useNextClick } from '.'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
export default function Units() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.CAMERA)
const {
defaultUnitSystem: ogDefaultUnitSystem,
setDefaultUnitSystem: saveDefaultUnitSystem,
defaultBaseUnit: ogDefaultBaseUnit,
setDefaultBaseUnit: saveDefaultBaseUnit,
} = useStore((s) => ({
defaultUnitSystem: s.defaultUnitSystem,
setDefaultUnitSystem: s.setDefaultUnitSystem,
defaultBaseUnit: s.defaultBaseUnit,
setDefaultBaseUnit: s.setDefaultBaseUnit,
}))
const [defaultUnitSystem, setDefaultUnitSystem] =
useState(ogDefaultUnitSystem)
const [defaultBaseUnit, setDefaultBaseUnit] = useState(ogDefaultBaseUnit)
settings: {
send,
context: { unitSystem, baseUnit },
},
} = useGlobalStateContext()
const [tempUnitSystem, setTempUnitSystem] = useState(unitSystem)
const [tempBaseUnit, setTempBaseUnit] = useState(baseUnit)
function handleNextClick() {
saveDefaultUnitSystem(defaultUnitSystem)
saveDefaultBaseUnit(defaultBaseUnit)
send({ type: 'Set Unit System', data: { unitSystem: tempUnitSystem } })
send({ type: 'Set Base Unit', data: { baseUnit: tempBaseUnit } })
next()
}
@ -42,9 +38,9 @@ export default function Units() {
offLabel="Imperial"
onLabel="Metric"
name="settings-units"
checked={defaultUnitSystem === 'metric'}
checked={tempUnitSystem === 'metric'}
onChange={(e) =>
setDefaultUnitSystem(e.target.checked ? 'metric' : 'imperial')
setTempUnitSystem(e.target.checked ? 'metric' : 'imperial')
}
/>
</SettingsSection>
@ -55,10 +51,10 @@ export default function Units() {
<select
id="base-unit"
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={defaultBaseUnit}
onChange={(e) => setDefaultBaseUnit(e.target.value)}
value={tempBaseUnit}
onChange={(e) => setTempBaseUnit(e.target.value as BaseUnit)}
>
{baseUnits[defaultUnitSystem].map((unit) => (
{baseUnits[unitSystem].map((unit) => (
<option key={unit} value={unit}>
{unit}
</option>

View File

@ -1,13 +1,12 @@
import { useHotkeys } from 'react-hotkeys-hook'
import { Outlet, useNavigate } from 'react-router-dom'
import { useStore } from '../../useStore'
import Introduction from './Introduction'
import Units from './Units'
import Camera from './Camera'
import Sketching from './Sketching'
import { useCallback } from 'react'
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
export const onboardingPaths = {
INDEX: '/',
@ -36,29 +35,35 @@ export const onboardingRoutes = [
]
export function useNextClick(newStatus: string) {
const { setOnboardingStatus } = useStore((s) => ({
setOnboardingStatus: s.setOnboardingStatus,
}))
const {
settings: { send },
} = useGlobalStateContext()
const navigate = useNavigate()
return useCallback(() => {
setOnboardingStatus(newStatus)
send({
type: 'Set Onboarding Status',
data: { onboardingStatus: newStatus },
})
navigate((newStatus !== onboardingPaths.UNITS ? '..' : '.') + newStatus)
}, [newStatus, setOnboardingStatus, navigate])
}, [newStatus, send, navigate])
}
export function useDismiss() {
const { setOnboardingStatus } = useStore((s) => ({
setOnboardingStatus: s.setOnboardingStatus,
}))
const {
settings: { send },
} = useGlobalStateContext()
const navigate = useNavigate()
return useCallback(
(path: string) => {
setOnboardingStatus('dismissed')
send({
type: 'Set Onboarding Status',
data: { onboardingStatus: 'dismissed' },
})
navigate(path)
},
[setOnboardingStatus, navigate]
[send, navigate]
)
}

View File

@ -6,59 +6,46 @@ import {
import { ActionButton } from '../components/ActionButton'
import { AppHeader } from '../components/AppHeader'
import { open } from '@tauri-apps/api/dialog'
import { Themes, baseUnits, useStore } from '../useStore'
import { useRef } from 'react'
import { toast } from 'react-hot-toast'
import { BaseUnit, baseUnits } from '../useStore'
import { Toggle } from '../components/Toggle/Toggle'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { useHotkeys } from 'react-hotkeys-hook'
import { IndexLoaderData, paths } from '../Router'
import { Themes } from '../lib/theme'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
export const Settings = () => {
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
const navigate = useNavigate()
useHotkeys('esc', () => navigate('../'))
const {
defaultDir,
setDefaultDir,
defaultProjectName,
setDefaultProjectName,
defaultUnitSystem,
setDefaultUnitSystem,
defaultBaseUnit,
setDefaultBaseUnit,
setDebugPanel,
debugPanel,
setOnboardingStatus,
theme,
setTheme,
} = useStore((s) => ({
defaultDir: s.defaultDir,
setDefaultDir: s.setDefaultDir,
defaultProjectName: s.defaultProjectName,
setDefaultProjectName: s.setDefaultProjectName,
defaultUnitSystem: s.defaultUnitSystem,
setDefaultUnitSystem: s.setDefaultUnitSystem,
defaultBaseUnit: s.defaultBaseUnit,
setDefaultBaseUnit: s.setDefaultBaseUnit,
setDebugPanel: s.setDebugPanel,
debugPanel: s.debugPanel,
setOnboardingStatus: s.setOnboardingStatus,
theme: s.theme,
setTheme: s.setTheme,
}))
const ogDefaultDir = useRef(defaultDir)
const ogDefaultProjectName = useRef(defaultProjectName)
settings: {
send,
state: {
context: {
defaultProjectName,
showDebugPanel,
defaultDirectory,
unitSystem,
baseUnit,
theme,
},
},
},
} = useGlobalStateContext()
async function handleDirectorySelection() {
const newDirectory = await open({
directory: true,
defaultPath: (defaultDir.base || '') + (defaultDir.dir || paths.INDEX),
defaultPath: defaultDirectory || paths.INDEX,
title: 'Choose a new default directory',
})
if (newDirectory && newDirectory !== null && !Array.isArray(newDirectory)) {
setDefaultDir({ base: defaultDir.base, dir: newDirectory })
send({
type: 'Set Default Directory',
data: { defaultDirectory: newDirectory },
})
}
}
@ -102,18 +89,8 @@ export const Settings = () => {
<div className="w-full flex gap-4 p-1 rounded border border-chalkboard-30">
<input
className="flex-1 px-2 bg-transparent"
value={defaultDir.dir}
onChange={(e) => {
setDefaultDir({
base: defaultDir.base,
dir: e.target.value,
})
}}
onBlur={() => {
ogDefaultDir.current.dir !== defaultDir.dir &&
toast.success('Default directory updated')
ogDefaultDir.current.dir = defaultDir.dir
}}
value={defaultDirectory}
disabled
/>
<ActionButton
Element="button"
@ -137,15 +114,15 @@ export const Settings = () => {
>
<input
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={defaultProjectName}
onChange={(e) => {
setDefaultProjectName(e.target.value)
}}
onBlur={() => {
ogDefaultProjectName.current !== defaultProjectName &&
toast.success('Default project name updated')
ogDefaultProjectName.current = defaultProjectName
defaultValue={defaultProjectName}
onBlur={(e) => {
send({
type: 'Set Default Project Name',
data: { defaultProjectName: e.target.value },
})
}}
autoCapitalize="off"
autoComplete="off"
/>
</SettingsSection>
</>
@ -158,12 +135,13 @@ export const Settings = () => {
offLabel="Imperial"
onLabel="Metric"
name="settings-units"
checked={defaultUnitSystem === 'metric'}
checked={unitSystem === 'metric'}
onChange={(e) => {
const newUnitSystem = e.target.checked ? 'metric' : 'imperial'
setDefaultUnitSystem(newUnitSystem)
setDefaultBaseUnit(baseUnits[newUnitSystem][0])
toast.success('Unit system set to ' + newUnitSystem)
send({
type: 'Set Unit System',
data: { unitSystem: newUnitSystem },
})
}}
/>
</SettingsSection>
@ -174,13 +152,15 @@ export const Settings = () => {
<select
id="base-unit"
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={defaultBaseUnit}
value={baseUnit}
onChange={(e) => {
setDefaultBaseUnit(e.target.value)
toast.success('Base unit changed to ' + e.target.value)
send({
type: 'Set Base Unit',
data: { baseUnit: e.target.value as BaseUnit },
})
}}
>
{baseUnits[defaultUnitSystem].map((unit) => (
{baseUnits[unitSystem as keyof typeof baseUnits].map((unit) => (
<option key={unit} value={unit}>
{unit}
</option>
@ -193,12 +173,9 @@ export const Settings = () => {
>
<Toggle
name="settings-debug-panel"
checked={debugPanel}
checked={showDebugPanel}
onChange={(e) => {
setDebugPanel(e.target.checked)
toast.success(
'Debug panel toggled ' + (e.target.checked ? 'on' : 'off')
)
send('Toggle Debug Panel')
}}
/>
</SettingsSection>
@ -211,12 +188,10 @@ export const Settings = () => {
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={theme}
onChange={(e) => {
setTheme(e.target.value as Themes)
toast.success(
'Theme changed to ' +
e.target.value.slice(0, 1).toLocaleUpperCase() +
e.target.value.slice(1)
)
send({
type: 'Set Theme',
data: { theme: e.target.value as Themes },
})
}}
>
{Object.entries(Themes).map(([label, value]) => (
@ -233,7 +208,10 @@ export const Settings = () => {
<ActionButton
Element="button"
onClick={() => {
setOnboardingStatus('')
send({
type: 'Set Onboarding Status',
data: { onboardingStatus: '' },
})
navigate('..' + paths.ONBOARDING.INDEX)
}}
icon={{ icon: faArrowRotateBack }}

View File

@ -1,20 +1,22 @@
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
import { ActionButton } from '../components/ActionButton'
import { isTauri } from '../lib/isTauri'
import { Themes, useStore } from '../useStore'
import { invoke } from '@tauri-apps/api/tauri'
import { useNavigate } from 'react-router-dom'
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
import { getSystemTheme } from '../lib/getSystemTheme'
import { Themes, getSystemTheme } from '../lib/theme'
import { paths } from '../Router'
import { useAuthMachine } from '../hooks/useAuthMachine'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
const SignIn = () => {
const navigate = useNavigate()
const { theme } = useStore((s) => ({
theme: s.theme,
}))
const [_, send] = useAuthMachine()
const {
auth: { send },
settings: {
state: {
context: { theme },
},
},
} = useGlobalStateContext()
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
const signInTauri = async () => {
// We want to invoke our command to login via device auth.
@ -22,7 +24,7 @@ const SignIn = () => {
const token: string = await invoke('login', {
host: VITE_KC_API_BASE_URL,
})
send({ type: 'tryLogin', token })
send({ type: 'Log in', token })
} catch (error) {
console.error('login button', error)
}

View File

@ -13,7 +13,6 @@ import {
} from './lang/executor'
import { recast } from './lang/recast'
import { EditorSelection } from '@codemirror/state'
import { BaseDirectory } from '@tauri-apps/api/fs'
import {
ArtifactMap,
SourceRangeMap,
@ -95,22 +94,14 @@ export type GuiModes =
position: Position
}
type UnitSystem = 'imperial' | 'metric'
export enum Themes {
Light = 'light',
Dark = 'dark',
System = 'system',
}
export const baseUnits: Record<UnitSystem, string[]> = {
export const baseUnits = {
imperial: ['in', 'ft'],
metric: ['mm', 'cm', 'm'],
}
} as const
interface DefaultDir {
base?: BaseDirectory
dir: string
}
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
@ -181,21 +172,8 @@ export interface StoreState {
streamHeight: number
}) => void
// tauri specific app settings
defaultDir: DefaultDir
setDefaultDir: (dir: DefaultDir) => void
defaultProjectName: string
setDefaultProjectName: (defaultProjectName: string) => void
defaultUnitSystem: UnitSystem
setDefaultUnitSystem: (defaultUnitSystem: UnitSystem) => void
defaultBaseUnit: string
setDefaultBaseUnit: (defaultBaseUnit: string) => void
showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void
onboardingStatus: string
setOnboardingStatus: (status: string) => void
theme: Themes
setTheme: (theme: Themes) => void
isBannerDismissed: boolean
setBannerDismissed: (isBannerDismissed: boolean) => void
openPanes: PaneType[]
@ -205,8 +183,6 @@ export interface StoreState {
path: string
}[]
setHomeMenuItems: (items: { name: string; path: string }[]) => void
debugPanel: boolean
setDebugPanel: (debugPanel: boolean) => void
}
let pendingAstUpdates: number[] = []
@ -385,18 +361,6 @@ export const useStore = create<StoreState>()(
defaultDir: {
dir: '',
},
setDefaultDir: (dir) => set({ defaultDir: dir }),
defaultProjectName: 'new-project-$nnn',
setDefaultProjectName: (defaultProjectName) =>
set({ defaultProjectName }),
defaultUnitSystem: 'imperial',
setDefaultUnitSystem: (defaultUnitSystem) => set({ defaultUnitSystem }),
defaultBaseUnit: 'in',
setDefaultBaseUnit: (defaultBaseUnit) => set({ defaultBaseUnit }),
onboardingStatus: '',
setOnboardingStatus: (onboardingStatus) => set({ onboardingStatus }),
theme: Themes.System,
setTheme: (theme) => set({ theme }),
isBannerDismissed: false,
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
openPanes: ['code'],
@ -405,25 +369,13 @@ export const useStore = create<StoreState>()(
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
debugPanel: false,
setDebugPanel: (debugPanel) => set({ debugPanel }),
}),
{
name: 'store',
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) =>
[
'code',
'defaultDir',
'defaultProjectName',
'defaultUnitSystem',
'defaultBaseUnit',
'debugPanel',
'onboardingStatus',
'theme',
'openPanes',
].includes(key)
['code', 'openPanes'].includes(key)
)
),
}

1544
src/wasm-lib/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,23 +8,20 @@ edition = "2021"
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.75"
backtrace = "0.3"
bincode = "1.3.3"
bson = { version = "2.6.1", features = ["uuid-1", "chrono"] }
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad = { version = "0.2.15", default-features = false, features = ["js"] }
lazy_static = "1.4.0"
regex = "1.7.1"
serde = {version = "1.0.152", features = ["derive"] }
serde-wasm-bindgen = "0.3.0"
serde_json = "1.0.93"
thiserror = "1.0.47"
ts-rs = { git = "https://github.com/kittycad/ts-rs.git", branch = "serde_json", features = ["serde-json-impl", "uuid-impl"] }
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
[profile.release]
panic = "abort"
debug = true
[dev-dependencies]
pretty_assertions = "1.4.0"
[workspace]
members = [
"derive-docs",
"kcl"
]

View File

@ -0,0 +1,24 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.0"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
convert_case = "0.6.0"
proc-macro2 = "1"
quote = "1"
serde = { version = "1.0.186", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.29", features = ["full"] }
[dev-dependencies]
expectorate = "1.0.7"
openapitor = "0.0.5"
pretty_assertions = "1.4.0"

View File

@ -0,0 +1,532 @@
// Copyright 2023 Oxide Computer Company
//! This package defines macro attributes associated with HTTP handlers. These
//! attributes are used both to define an HTTP API and to generate an OpenAPI
//! Spec (OAS) v3 document that describes the API.
// Clippy's style advice is definitely valuable, but not worth the trouble for
// automated enforcement.
#![allow(clippy::style)]
use convert_case::Casing;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use serde::Deserialize;
use serde_tokenstream::{from_tokenstream, Error};
use syn::{
parse::{Parse, ParseStream},
Attribute, Signature, Visibility,
};
#[derive(Deserialize, Debug)]
struct StdlibMetadata {
/// The name of the function in the API.
name: String,
/// Tags for the function.
#[serde(default)]
tags: Vec<String>,
/// Whether the function is unpublished.
/// Then docs will not be generated.
#[serde(default)]
unpublished: bool,
/// Whether the function is deprecated.
/// Then specific docs detailing that this is deprecated will be generated.
#[serde(default)]
deprecated: bool,
}
#[proc_macro_attribute]
pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_output(do_stdlib(attr.into(), item.into()))
}
fn do_stdlib(
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let metadata = from_tokenstream(&attr)?;
do_stdlib_inner(metadata, attr, item)
}
fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
match res {
Err(err) => err.to_compile_error().into(),
Ok((stdlib_docs, errors)) => {
let compiler_errors = errors.iter().map(|err| err.to_compile_error());
let output = quote! {
#stdlib_docs
#( #compiler_errors )*
};
output.into()
}
}
}
fn do_stdlib_inner(
metadata: StdlibMetadata,
_attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let ast: ItemFnForSignature = syn::parse2(item.clone())?;
let mut errors = Vec::new();
if ast.sig.constness.is_some() {
errors.push(Error::new_spanned(
&ast.sig.constness,
"stdlib functions may not be const functions",
));
}
if ast.sig.asyncness.is_some() {
errors.push(Error::new_spanned(
&ast.sig.fn_token,
"stdlib functions must not be async",
));
}
if ast.sig.unsafety.is_some() {
errors.push(Error::new_spanned(
&ast.sig.unsafety,
"stdlib functions may not be unsafe",
));
}
if ast.sig.abi.is_some() {
errors.push(Error::new_spanned(
&ast.sig.abi,
"stdlib functions may not use an alternate ABI",
));
}
if !ast.sig.generics.params.is_empty() {
errors.push(Error::new_spanned(
&ast.sig.generics,
"generics are not permitted for stdlib functions",
));
}
if ast.sig.variadic.is_some() {
errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here"));
}
let name = metadata.name;
let name_ident = format_ident!("{}", name.to_case(convert_case::Case::UpperCamel));
let name_str = name.to_string();
let fn_name = &ast.sig.ident;
let fn_name_str = fn_name.to_string().replace("inner_", "");
let fn_name_ident = format_ident!("{}", fn_name_str);
let _visibility = &ast.vis;
let (summary_text, description_text) = extract_doc_from_attrs(&ast.attrs);
let comment_text = {
let mut buf = String::new();
buf.push_str("Std lib function: ");
buf.push_str(&name_str);
if let Some(s) = &summary_text {
buf.push_str("\n");
buf.push_str(&s);
}
if let Some(s) = &description_text {
buf.push_str("\n");
buf.push_str(&s);
}
buf
};
let description_doc_comment = quote! {
#[doc = #comment_text]
};
let summary = if let Some(summary) = summary_text {
quote! { #summary }
} else {
quote! { "" }
};
let description = if let Some(description) = description_text {
quote! { #description }
} else {
quote! { "" }
};
let tags = metadata
.tags
.iter()
.map(|tag| {
quote! { #tag.to_string() }
})
.collect::<Vec<_>>();
let deprecated = if metadata.deprecated {
quote! { true }
} else {
quote! { false }
};
let unpublished = if metadata.unpublished {
quote! { true }
} else {
quote! { false }
};
let docs_crate = get_crate(None);
// When the user attaches this proc macro to a function with the wrong type
// signature, the resulting errors can be deeply inscrutable. To attempt to
// make failures easier to understand, we inject code that asserts the types
// of the various parameters. We do this by calling dummy functions that
// require a type that satisfies SharedExtractor or ExclusiveExtractor.
let mut arg_types = Vec::new();
for arg in ast.sig.inputs.iter() {
// Get the name of the argument.
let arg_name = match arg {
syn::FnArg::Receiver(pat) => {
let span = pat.self_token.span.unwrap();
span.source_text().unwrap().to_string()
}
syn::FnArg::Typed(pat) => match &*pat.pat {
syn::Pat::Ident(ident) => ident.ident.to_string(),
_ => {
errors.push(Error::new_spanned(
&pat.pat,
"stdlib functions may not use destructuring patterns",
));
continue;
}
},
};
let ty = match arg {
syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
};
let ty_string = ty.to_string().replace('&', "").replace("mut", "").replace(' ', "");
let ty_string = ty_string.trim().to_string();
let ty_ident = if ty_string.starts_with("Vec<") {
let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
let ty_ident = format_ident!("{}", ty_string);
quote! {
Vec<#ty_ident>
}
} else {
let ty_ident = format_ident!("{}", ty_string);
quote! {
#ty_ident
}
};
let ty_string = clean_type(&ty_string);
if ty_string != "Args" {
let schema = if ty_ident.to_string().starts_with("Vec < ") {
quote! {
<#ty_ident>::json_schema(&mut generator)
}
} else {
quote! {
#ty_ident::json_schema(&mut generator)
}
};
arg_types.push(quote! {
#docs_crate::StdLibFnArg {
name: #arg_name.to_string(),
type_: #ty_string.to_string(),
schema: #schema,
required: true,
}
});
}
}
let ret_ty = ast.sig.output.clone();
let ret_ty_string = ret_ty
.into_token_stream()
.to_string()
.replace("-> ", "")
.replace("Result < ", "")
.replace(", KclError >", "");
let ret_ty_string = ret_ty_string.trim().to_string();
let ret_ty_ident = format_ident!("{}", ret_ty_string);
let ret_ty_string = clean_type(&ret_ty_string);
let return_type = quote! {
#docs_crate::StdLibFnArg {
name: "".to_string(),
type_: #ret_ty_string.to_string(),
schema: #ret_ty_ident::json_schema(&mut generator),
required: true,
}
};
// For reasons that are not well understood unused constants that use the
// (default) call_site() Span do not trigger the dead_code lint. Because
// defining but not using an endpoint is likely a programming error, we
// want to be sure to have the compiler flag this. We force this by using
// the span from the name of the function to which this macro was applied.
let span = ast.sig.ident.span();
let const_struct = quote_spanned! {span=>
pub(crate) const #name_ident: #name_ident = #name_ident {};
};
// The final TokenStream returned will have a few components that reference
// `#name_ident`, the name of the function to which this macro was applied...
let stream = quote! {
// ... a struct type called `#name_ident` that has no members
#[allow(non_camel_case_types, missing_docs)]
#description_doc_comment
pub(crate) struct #name_ident {}
// ... a constant of type `#name` whose identifier is also #name_ident
#[allow(non_upper_case_globals, missing_docs)]
#description_doc_comment
#const_struct
impl #docs_crate::StdLibFn for #name_ident
{
fn name(&self) -> String {
#name_str.to_string()
}
fn summary(&self) -> String {
#summary.to_string()
}
fn description(&self) -> String {
#description.to_string()
}
fn tags(&self) -> Vec<String> {
vec![#(#tags),*]
}
fn args(&self) -> Vec<#docs_crate::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![#(#arg_types),*]
}
fn return_value(&self) -> #docs_crate::StdLibFnArg {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
#return_type
}
fn unpublished(&self) -> bool {
#unpublished
}
fn deprecated(&self) -> bool {
#deprecated
}
fn std_lib_fn(&self) -> crate::std::StdFn {
#fn_name_ident
}
}
#item
};
// Prepend the usage message if any errors were detected.
if !errors.is_empty() {
errors.insert(0, Error::new_spanned(&ast.sig, ""));
}
Ok((stream, errors))
}
#[allow(dead_code)]
fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
quote!(#(#compile_errors)*)
}
fn get_crate(var: Option<String>) -> proc_macro2::TokenStream {
if let Some(s) = var {
if let Ok(ts) = syn::parse_str(s.as_str()) {
return ts;
}
}
quote!(crate::docs)
}
fn extract_doc_from_attrs(attrs: &[syn::Attribute]) -> (Option<String>, Option<String>) {
let doc = syn::Ident::new("doc", proc_macro2::Span::call_site());
let mut lines = attrs.iter().flat_map(|attr| {
if let syn::Meta::NameValue(nv) = &attr.meta {
if nv.path.is_ident(&doc) {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s), ..
}) = &nv.value
{
return normalize_comment_string(s.value());
}
}
}
Vec::new()
});
// Skip initial blank lines; they make for excessively terse summaries.
let summary = loop {
match lines.next() {
Some(s) if s.is_empty() => (),
next => break next,
}
};
// Skip initial blank description lines.
let first = loop {
match lines.next() {
Some(s) if s.is_empty() => (),
next => break next,
}
};
match (summary, first) {
(None, _) => (None, None),
(summary, None) => (summary, None),
(Some(summary), Some(first)) => (
Some(summary),
Some(
lines
.fold(first, |acc, comment| {
if acc.ends_with('-') || acc.ends_with('\n') || acc.is_empty() {
// Continuation lines and newlines.
format!("{}{}", acc, comment)
} else if comment.is_empty() {
// Handle fully blank comments as newlines we keep.
format!("{}\n", acc)
} else {
// Default to space-separating comment fragments.
format!("{} {}", acc, comment)
}
})
.trim_end()
.to_string(),
),
),
}
}
fn normalize_comment_string(s: String) -> Vec<String> {
s.split('\n')
.enumerate()
.map(|(idx, s)| {
// Rust-style comments are intrinsically single-line. We don't want
// to trim away formatting such as an initial '*'.
if idx == 0 {
s.trim_start().trim_end()
} else {
let trimmed = s.trim_start().trim_end();
trimmed
.strip_prefix("* ")
.unwrap_or_else(|| trimmed.strip_prefix('*').unwrap_or(trimmed))
}
})
.map(ToString::to_string)
.collect()
}
/// Represent an item without concern for its body which may (or may not)
/// contain syntax errors.
struct ItemFnForSignature {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub sig: Signature,
pub _block: proc_macro2::TokenStream,
}
impl Parse for ItemFnForSignature {
fn parse(input: ParseStream) -> syn::parse::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let vis: Visibility = input.parse()?;
let sig: Signature = input.parse()?;
let block = input.parse()?;
Ok(ItemFnForSignature {
attrs,
vis,
sig,
_block: block,
})
}
}
fn clean_type(t: &str) -> String {
let mut t = t.to_string();
// Turn vecs into arrays.
if t.starts_with("Vec<") {
t = t.replace("Vec<", "[").replace('>', "]");
}
if t == "f64" {
return "number".to_string();
} else if t == "str" {
return "string".to_string();
} else {
return t.replace("f64", "number").to_string();
}
}
#[cfg(test)]
mod tests {
use quote::quote;
use super::*;
#[test]
fn test_stdlib_line_to() {
let (item, errors) = do_stdlib(
quote! {
name = "lineTo",
},
quote! {
fn inner_line_to(
data: LineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
Ok(())
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty());
expectorate::assert_contents("tests/lineTo.gen", &openapitor::types::get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_min() {
let (item, errors) = do_stdlib(
quote! {
name = "min",
},
quote! {
fn inner_min(
/// The args to do shit to.
args: Vec<f64>
) -> f64 {
let mut min = std::f64::MAX;
for arg in args.iter() {
if *arg < min {
min = *arg;
}
}
min
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty());
expectorate::assert_contents("tests/min.gen", &openapitor::types::get_text_fmt(&item).unwrap());
}
}

View File

@ -0,0 +1,76 @@
#[allow(non_camel_case_types, missing_docs)]
#[doc = "Std lib function: lineTo"]
pub(crate) struct LineTo {}
#[allow(non_upper_case_globals, missing_docs)]
#[doc = "Std lib function: lineTo"]
pub(crate) const LineTo: LineTo = LineTo {};
impl crate::docs::StdLibFn for LineTo {
fn name(&self) -> String {
"lineTo".to_string()
}
fn summary(&self) -> String {
"".to_string()
}
fn description(&self) -> String {
"".to_string()
}
fn tags(&self) -> Vec<String> {
vec![]
}
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![
crate::docs::StdLibFnArg {
name: "data".to_string(),
type_: "LineToData".to_string(),
schema: LineToData::json_schema(&mut generator),
required: true,
},
crate::docs::StdLibFnArg {
name: "sketch_group".to_string(),
type_: "SketchGroup".to_string(),
schema: SketchGroup::json_schema(&mut generator),
required: true,
},
]
}
fn return_value(&self) -> crate::docs::StdLibFnArg {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
crate::docs::StdLibFnArg {
name: "".to_string(),
type_: "SketchGroup".to_string(),
schema: SketchGroup::json_schema(&mut generator),
required: true,
}
}
fn unpublished(&self) -> bool {
false
}
fn deprecated(&self) -> bool {
false
}
fn std_lib_fn(&self) -> crate::std::StdFn {
line_to
}
}
fn inner_line_to(
data: LineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
Ok(())
}

View File

@ -0,0 +1,71 @@
#[allow(non_camel_case_types, missing_docs)]
#[doc = "Std lib function: min"]
pub(crate) struct Min {}
#[allow(non_upper_case_globals, missing_docs)]
#[doc = "Std lib function: min"]
pub(crate) const Min: Min = Min {};
impl crate::docs::StdLibFn for Min {
fn name(&self) -> String {
"min".to_string()
}
fn summary(&self) -> String {
"".to_string()
}
fn description(&self) -> String {
"".to_string()
}
fn tags(&self) -> Vec<String> {
vec![]
}
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![crate::docs::StdLibFnArg {
name: "args".to_string(),
type_: "[number]".to_string(),
schema: <Vec<f64>>::json_schema(&mut generator),
required: true,
}]
}
fn return_value(&self) -> crate::docs::StdLibFnArg {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
crate::docs::StdLibFnArg {
name: "".to_string(),
type_: "number".to_string(),
schema: f64::json_schema(&mut generator),
required: true,
}
}
fn unpublished(&self) -> bool {
false
}
fn deprecated(&self) -> bool {
false
}
fn std_lib_fn(&self) -> crate::std::StdFn {
min
}
}
fn inner_min(#[doc = r" The args to do shit to."] args: Vec<f64>) -> f64 {
let mut min = std::f64::MAX;
for arg in args.iter() {
if *arg < min {
min = *arg;
}
}
min
}

View File

@ -0,0 +1,43 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language"
version = "0.1.3"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
derive-docs = { version = "0.1.0" }
kittycad = { version = "0.2.15", default-features = false, features = ["js"] }
lazy_static = "1.4.0"
parse-display = "0.8.2"
regex = "1.7.1"
schemars = { version = "0.8", features = ["url", "uuid1"] }
serde = {version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
thiserror = "1.0.47"
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "uuid-impl"] }
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.64" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
bson = { version = "2.6.1", features = ["uuid-1", "chrono"] }
futures = { version = "0.3.28" }
reqwest = { version = "0.11.20", default-features = false }
tokio = { version = "1.32.0", features = ["full"] }
tokio-tungstenite = { version = "0.20.0", features = ["rustls-tls-native-roots"] }
[profile.release]
panic = "abort"
debug = true
[dev-dependencies]
expectorate = "1.0.7"
pretty_assertions = "1.4.0"
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }

View File

@ -0,0 +1,952 @@
//! Data types for the AST.
use std::collections::HashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Map;
use crate::{
engine::EngineConnection,
errors::{KclError, KclErrorDetails},
executor::{MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange},
};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Program {
pub start: usize,
pub end: usize,
pub body: Vec<BodyItem>,
pub non_code_meta: NoneCodeMeta,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum BodyItem {
ExpressionStatement(ExpressionStatement),
VariableDeclaration(VariableDeclaration),
ReturnStatement(ReturnStatement),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Value {
Literal(Box<Literal>),
Identifier(Box<Identifier>),
BinaryExpression(Box<BinaryExpression>),
FunctionExpression(Box<FunctionExpression>),
CallExpression(Box<CallExpression>),
PipeExpression(Box<PipeExpression>),
PipeSubstitution(Box<PipeSubstitution>),
ArrayExpression(Box<ArrayExpression>),
ObjectExpression(Box<ObjectExpression>),
MemberExpression(Box<MemberExpression>),
UnaryExpression(Box<UnaryExpression>),
}
pub trait ValueMeta {
fn start(&self) -> usize;
fn end(&self) -> usize;
}
macro_rules! impl_value_meta {
{$name:ident} => {
impl crate::abstract_syntax_tree_types::ValueMeta for $name {
fn start(&self) -> usize {
self.start
}
fn end(&self) -> usize {
self.end
}
}
impl From<$name> for crate::executor::SourceRange {
fn from(v: $name) -> Self {
Self([v.start, v.end])
}
}
impl From<&$name> for crate::executor::SourceRange {
fn from(v: &$name) -> Self {
Self([v.start, v.end])
}
}
impl From<&Box<$name>> for crate::executor::SourceRange {
fn from(v: &Box<$name>) -> Self {
Self([v.start, v.end])
}
}
};
}
pub(crate) use impl_value_meta;
impl Value {
pub fn start(&self) -> usize {
match self {
Value::Literal(literal) => literal.start(),
Value::Identifier(identifier) => identifier.start(),
Value::BinaryExpression(binary_expression) => binary_expression.start(),
Value::FunctionExpression(function_expression) => function_expression.start(),
Value::CallExpression(call_expression) => call_expression.start(),
Value::PipeExpression(pipe_expression) => pipe_expression.start(),
Value::PipeSubstitution(pipe_substitution) => pipe_substitution.start(),
Value::ArrayExpression(array_expression) => array_expression.start(),
Value::ObjectExpression(object_expression) => object_expression.start(),
Value::MemberExpression(member_expression) => member_expression.start(),
Value::UnaryExpression(unary_expression) => unary_expression.start(),
}
}
pub fn end(&self) -> usize {
match self {
Value::Literal(literal) => literal.end(),
Value::Identifier(identifier) => identifier.end(),
Value::BinaryExpression(binary_expression) => binary_expression.end(),
Value::FunctionExpression(function_expression) => function_expression.end(),
Value::CallExpression(call_expression) => call_expression.end(),
Value::PipeExpression(pipe_expression) => pipe_expression.end(),
Value::PipeSubstitution(pipe_substitution) => pipe_substitution.end(),
Value::ArrayExpression(array_expression) => array_expression.end(),
Value::ObjectExpression(object_expression) => object_expression.end(),
Value::MemberExpression(member_expression) => member_expression.end(),
Value::UnaryExpression(unary_expression) => unary_expression.end(),
}
}
}
impl From<Value> for crate::executor::SourceRange {
fn from(value: Value) -> Self {
Self([value.start(), value.end()])
}
}
impl From<&Value> for crate::executor::SourceRange {
fn from(value: &Value) -> Self {
Self([value.start(), value.end()])
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum BinaryPart {
Literal(Box<Literal>),
Identifier(Box<Identifier>),
BinaryExpression(Box<BinaryExpression>),
CallExpression(Box<CallExpression>),
UnaryExpression(Box<UnaryExpression>),
}
impl From<BinaryPart> for crate::executor::SourceRange {
fn from(value: BinaryPart) -> Self {
Self([value.start(), value.end()])
}
}
impl From<&BinaryPart> for crate::executor::SourceRange {
fn from(value: &BinaryPart) -> Self {
Self([value.start(), value.end()])
}
}
impl BinaryPart {
pub fn start(&self) -> usize {
match self {
BinaryPart::Literal(literal) => literal.start(),
BinaryPart::Identifier(identifier) => identifier.start(),
BinaryPart::BinaryExpression(binary_expression) => binary_expression.start(),
BinaryPart::CallExpression(call_expression) => call_expression.start(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.start(),
}
}
pub fn end(&self) -> usize {
match self {
BinaryPart::Literal(literal) => literal.end(),
BinaryPart::Identifier(identifier) => identifier.end(),
BinaryPart::BinaryExpression(binary_expression) => binary_expression.end(),
BinaryPart::CallExpression(call_expression) => call_expression.end(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.end(),
}
}
pub fn get_result(
&self,
memory: &mut ProgramMemory,
pipe_info: &mut PipeInfo,
stdlib: &crate::std::StdLib,
engine: &mut EngineConnection,
) -> Result<MemoryItem, KclError> {
pipe_info.is_in_pipe = false;
match self {
BinaryPart::Literal(literal) => Ok(literal.into()),
BinaryPart::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
Ok(value.clone())
}
BinaryPart::BinaryExpression(binary_expression) => {
binary_expression.get_result(memory, pipe_info, stdlib, engine)
}
BinaryPart::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, stdlib, engine),
BinaryPart::UnaryExpression(unary_expression) => {
// Return an error this should not happen.
Err(KclError::Semantic(KclErrorDetails {
message: format!("UnaryExpression should not be a BinaryPart: {:?}", unary_expression),
source_ranges: vec![unary_expression.into()],
}))
}
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct NoneCodeNode {
pub start: usize,
pub end: usize,
pub value: String,
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct NoneCodeMeta {
pub none_code_nodes: HashMap<usize, NoneCodeNode>,
pub start: Option<NoneCodeNode>,
}
// implement Deserialize manually because we to force the keys of none_code_nodes to be usize
// and by default the ts type { [statementIndex: number]: NoneCodeNode } serializes to a string i.e. "0", "1", etc.
impl<'de> Deserialize<'de> for NoneCodeMeta {
fn deserialize<D>(deserializer: D) -> Result<NoneCodeMeta, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct NoneCodeMetaHelper {
none_code_nodes: HashMap<String, NoneCodeNode>,
start: Option<NoneCodeNode>,
}
let helper = NoneCodeMetaHelper::deserialize(deserializer)?;
let mut none_code_nodes = HashMap::new();
for (key, value) in helper.none_code_nodes {
none_code_nodes.insert(key.parse().map_err(serde::de::Error::custom)?, value);
}
Ok(NoneCodeMeta {
none_code_nodes,
start: helper.start,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ExpressionStatement {
pub start: usize,
pub end: usize,
pub expression: Value,
}
impl_value_meta!(ExpressionStatement);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct CallExpression {
pub start: usize,
pub end: usize,
pub callee: Identifier,
pub arguments: Vec<Value>,
pub optional: bool,
}
impl_value_meta!(CallExpression);
impl CallExpression {
pub fn execute(
&self,
memory: &mut ProgramMemory,
pipe_info: &mut PipeInfo,
stdlib: &crate::std::StdLib,
engine: &mut EngineConnection,
) -> Result<MemoryItem, KclError> {
let fn_name = self.callee.name.clone();
let mut fn_args: Vec<MemoryItem> = Vec::with_capacity(self.arguments.len());
for arg in &self.arguments {
let result: MemoryItem = match arg {
Value::Literal(literal) => literal.into(),
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
value.clone()
}
Value::BinaryExpression(binary_expression) => {
binary_expression.get_result(memory, pipe_info, stdlib, engine)?
}
Value::CallExpression(call_expression) => {
pipe_info.is_in_pipe = false;
call_expression.execute(memory, pipe_info, stdlib, engine)?
}
Value::UnaryExpression(unary_expression) => {
unary_expression.get_result(memory, pipe_info, stdlib, engine)?
}
Value::ObjectExpression(object_expression) => {
object_expression.execute(memory, pipe_info, stdlib, engine)?
}
Value::ArrayExpression(array_expression) => {
array_expression.execute(memory, pipe_info, stdlib, engine)?
}
Value::PipeExpression(pipe_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("PipeExpression not implemented here: {:?}", pipe_expression),
source_ranges: vec![pipe_expression.into()],
}));
}
Value::PipeSubstitution(pipe_substitution) => pipe_info
.previous_results
.get(&pipe_info.index - 1)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("PipeSubstitution index out of bounds: {:?}", pipe_info),
source_ranges: vec![pipe_substitution.into()],
})
})?
.clone(),
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
source_ranges: vec![function_expression.into()],
}));
}
};
fn_args.push(result);
}
if let Some(func) = stdlib.fns.get(&fn_name) {
// Attempt to call the function.
let mut args = crate::std::Args::new(fn_args, self.into(), engine);
let result = func(&mut args)?;
if pipe_info.is_in_pipe {
pipe_info.index += 1;
pipe_info.previous_results.push(result);
execute_pipe_body(memory, &pipe_info.body.clone(), pipe_info, self.into(), stdlib, engine)
} else {
Ok(result)
}
} else {
let mem = memory.clone();
let func = mem.get(&fn_name, self.into())?;
let result = func.call_fn(&fn_args, memory, engine)?.ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of function {} is undefined", fn_name),
source_ranges: vec![self.into()],
})
})?;
let result = result.get_value()?;
if pipe_info.is_in_pipe {
pipe_info.index += 1;
pipe_info.previous_results.push(result);
execute_pipe_body(memory, &pipe_info.body.clone(), pipe_info, self.into(), stdlib, engine)
} else {
Ok(result)
}
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct VariableDeclaration {
pub start: usize,
pub end: usize,
pub declarations: Vec<VariableDeclarator>,
pub kind: String, // Change to enum if there are specific values
}
impl_value_meta!(VariableDeclaration);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct VariableDeclarator {
pub start: usize,
pub end: usize,
pub id: Identifier,
pub init: Value,
}
impl_value_meta!(VariableDeclarator);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct Literal {
pub start: usize,
pub end: usize,
pub value: serde_json::Value,
pub raw: String,
}
impl_value_meta!(Literal);
impl From<Literal> for MemoryItem {
fn from(literal: Literal) -> Self {
MemoryItem::UserVal {
value: literal.value.clone(),
meta: vec![Metadata {
source_range: literal.into(),
}],
}
}
}
impl From<&Box<Literal>> for MemoryItem {
fn from(literal: &Box<Literal>) -> Self {
MemoryItem::UserVal {
value: literal.value.clone(),
meta: vec![Metadata {
source_range: literal.into(),
}],
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct Identifier {
pub start: usize,
pub end: usize,
pub name: String,
}
impl_value_meta!(Identifier);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct PipeSubstitution {
pub start: usize,
pub end: usize,
}
impl_value_meta!(PipeSubstitution);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ArrayExpression {
pub start: usize,
pub end: usize,
pub elements: Vec<Value>,
}
impl_value_meta!(ArrayExpression);
impl ArrayExpression {
pub fn execute(
&self,
memory: &mut ProgramMemory,
pipe_info: &mut PipeInfo,
stdlib: &crate::std::StdLib,
engine: &mut EngineConnection,
) -> Result<MemoryItem, KclError> {
let mut results = Vec::with_capacity(self.elements.len());
for element in &self.elements {
let result = match element {
Value::Literal(literal) => literal.into(),
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
value.clone()
}
Value::BinaryExpression(binary_expression) => {
binary_expression.get_result(memory, pipe_info, stdlib, engine)?
}
Value::CallExpression(call_expression) => {
pipe_info.is_in_pipe = false;
call_expression.execute(memory, pipe_info, stdlib, engine)?
}
Value::UnaryExpression(unary_expression) => {
unary_expression.get_result(memory, pipe_info, stdlib, engine)?
}
Value::ObjectExpression(object_expression) => {
object_expression.execute(memory, pipe_info, stdlib, engine)?
}
Value::ArrayExpression(array_expression) => {
array_expression.execute(memory, pipe_info, stdlib, engine)?
}
Value::PipeExpression(pipe_expression) => {
pipe_expression.get_result(memory, pipe_info, stdlib, engine)?
}
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
source_ranges: vec![pipe_substitution.into()],
}));
}
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
source_ranges: vec![function_expression.into()],
}));
}
}
.get_json_value()?;
results.push(result);
}
Ok(MemoryItem::UserVal {
value: results.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ObjectExpression {
pub start: usize,
pub end: usize,
pub properties: Vec<ObjectProperty>,
}
impl ObjectExpression {
pub fn execute(
&self,
memory: &mut ProgramMemory,
pipe_info: &mut PipeInfo,
stdlib: &crate::std::StdLib,
engine: &mut EngineConnection,
) -> Result<MemoryItem, KclError> {
let mut object = Map::new();
for property in &self.properties {
let result = match &property.value {
Value::Literal(literal) => literal.into(),
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
value.clone()
}
Value::BinaryExpression(binary_expression) => {
binary_expression.get_result(memory, pipe_info, stdlib, engine)?
}
Value::CallExpression(call_expression) => {
pipe_info.is_in_pipe = false;
call_expression.execute(memory, pipe_info, stdlib, engine)?
}
Value::UnaryExpression(unary_expression) => {
unary_expression.get_result(memory, pipe_info, stdlib, engine)?
}
Value::ObjectExpression(object_expression) => {
object_expression.execute(memory, pipe_info, stdlib, engine)?
}
Value::ArrayExpression(array_expression) => {
array_expression.execute(memory, pipe_info, stdlib, engine)?
}
Value::PipeExpression(pipe_expression) => {
pipe_expression.get_result(memory, pipe_info, stdlib, engine)?
}
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
source_ranges: vec![pipe_substitution.into()],
}));
}
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
source_ranges: vec![function_expression.into()],
}));
}
};
object.insert(property.key.name.clone(), result.get_json_value()?);
}
Ok(MemoryItem::UserVal {
value: object.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
})
}
}
impl_value_meta!(ObjectExpression);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ObjectProperty {
pub start: usize,
pub end: usize,
pub key: Identifier,
pub value: Value,
}
impl_value_meta!(ObjectProperty);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum MemberObject {
MemberExpression(Box<MemberExpression>),
Identifier(Box<Identifier>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum LiteralIdentifier {
Identifier(Box<Identifier>),
Literal(Box<Literal>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct MemberExpression {
pub start: usize,
pub end: usize,
pub object: MemberObject,
pub property: LiteralIdentifier,
pub computed: bool,
}
impl_value_meta!(MemberExpression);
impl MemberExpression {
pub fn get_result(&self, memory: &mut ProgramMemory) -> Result<MemoryItem, KclError> {
let property_name = match &self.property {
LiteralIdentifier::Identifier(identifier) => identifier.name.to_string(),
LiteralIdentifier::Literal(literal) => {
let value = literal.value.clone();
// Parse this as a string.
if let serde_json::Value::String(string) = value {
string
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Expected string literal for property name, found {:?}", value),
source_ranges: vec![literal.into()],
}));
}
}
};
let object = 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::Object(map) = object {
if let Some(value) = map.get(&property_name) {
Ok(MemoryItem::UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
})
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("Property {} not found in object", property_name),
source_ranges: vec![self.clone().into()],
}))
}
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression object is not an object: {:?}", object),
source_ranges: vec![self.clone().into()],
}))
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct ObjectKeyInfo {
pub key: LiteralIdentifier,
pub index: usize,
pub computed: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct BinaryExpression {
pub start: usize,
pub end: usize,
// TODO: operator should be a type not a string.
pub operator: String,
pub left: BinaryPart,
pub right: BinaryPart,
}
impl_value_meta!(BinaryExpression);
impl BinaryExpression {
pub fn get_result(
&self,
memory: &mut ProgramMemory,
pipe_info: &mut PipeInfo,
stdlib: &crate::std::StdLib,
engine: &mut EngineConnection,
) -> Result<MemoryItem, KclError> {
pipe_info.is_in_pipe = false;
let left_json_value = self
.left
.get_result(memory, pipe_info, stdlib, engine)?
.get_json_value()?;
let right_json_value = self
.right
.get_result(memory, pipe_info, stdlib, engine)?
.get_json_value()?;
// First check if we are doing string concatenation.
if self.operator == "+" {
if let (Some(left), Some(right)) = (
parse_json_value_as_string(&left_json_value),
parse_json_value_as_string(&right_json_value),
) {
let value = serde_json::Value::String(format!("{}{}", left, right));
return Ok(MemoryItem::UserVal {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
});
}
}
let left = parse_json_number_as_f64(&left_json_value, self.left.clone().into())?;
let right = parse_json_number_as_f64(&right_json_value, self.right.clone().into())?;
let value: serde_json::Value = match self.operator.as_str() {
"+" => (left + right).into(),
"-" => (left - right).into(),
"*" => (left * right).into(),
"/" => (left / right).into(),
"%" => (left % right).into(),
_ => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Invalid operator: {}", self.operator),
}))
}
};
Ok(MemoryItem::UserVal {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
})
}
}
pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_f64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid number: {}", j),
})
})
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid number: {}", j),
}))
}
}
pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
if let serde_json::Value::String(n) = &j {
Some(n.clone())
} else {
None
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct UnaryExpression {
pub start: usize,
pub end: usize,
pub operator: String,
pub argument: BinaryPart,
}
impl_value_meta!(UnaryExpression);
impl UnaryExpression {
pub fn get_result(
&self,
memory: &mut ProgramMemory,
pipe_info: &mut PipeInfo,
stdlib: &crate::std::StdLib,
engine: &mut EngineConnection,
) -> Result<MemoryItem, KclError> {
pipe_info.is_in_pipe = false;
let num = parse_json_number_as_f64(
&self
.argument
.get_result(memory, pipe_info, stdlib, engine)?
.get_json_value()?,
self.into(),
)?;
Ok(MemoryItem::UserVal {
value: (-(num)).into(),
meta: vec![Metadata {
source_range: self.into(),
}],
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", tag = "type")]
pub struct PipeExpression {
pub start: usize,
pub end: usize,
pub body: Vec<Value>,
pub non_code_meta: NoneCodeMeta,
}
impl_value_meta!(PipeExpression);
impl PipeExpression {
pub fn get_result(
&self,
memory: &mut ProgramMemory,
pipe_info: &mut PipeInfo,
stdlib: &crate::std::StdLib,
engine: &mut EngineConnection,
) -> Result<MemoryItem, KclError> {
// Reset the previous results.
pipe_info.previous_results = vec![];
pipe_info.index = 0;
execute_pipe_body(memory, &self.body, pipe_info, self.into(), stdlib, engine)
}
}
fn execute_pipe_body(
memory: &mut ProgramMemory,
body: &[Value],
pipe_info: &mut PipeInfo,
source_range: SourceRange,
stdlib: &crate::std::StdLib,
engine: &mut EngineConnection,
) -> Result<MemoryItem, KclError> {
if pipe_info.index == body.len() {
pipe_info.is_in_pipe = false;
return Ok(pipe_info
.previous_results
.last()
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: "Pipe body results should have at least one expression".to_string(),
source_ranges: vec![source_range],
})
})?
.clone());
}
let expression = body.get(pipe_info.index).ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Invalid index for pipe: {}", pipe_info.index),
source_ranges: vec![source_range],
})
})?;
match expression {
Value::BinaryExpression(binary_expression) => {
let result = binary_expression.get_result(memory, pipe_info, stdlib, engine)?;
pipe_info.previous_results.push(result);
pipe_info.index += 1;
execute_pipe_body(memory, body, pipe_info, source_range, stdlib, engine)
}
Value::CallExpression(call_expression) => {
pipe_info.is_in_pipe = true;
pipe_info.body = body.to_vec();
call_expression.execute(memory, pipe_info, stdlib, engine)
}
_ => {
// Return an error this should not happen.
Err(KclError::Semantic(KclErrorDetails {
message: format!("PipeExpression not implemented here: {:?}", expression),
source_ranges: vec![expression.into()],
}))
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct FunctionExpression {
pub start: usize,
pub end: usize,
pub id: Option<Identifier>,
pub params: Vec<Identifier>,
pub body: Program,
}
impl_value_meta!(FunctionExpression);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ReturnStatement {
pub start: usize,
pub end: usize,
pub argument: Value,
}
impl_value_meta!(ReturnStatement);

View File

@ -0,0 +1,189 @@
//! Functions for generating docs for our stdlib functions.
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::std::Primitive;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct StdLibFnData {
/// The name of the function.
pub name: String,
/// The summary of the function.
pub summary: String,
/// The description of the function.
pub description: String,
/// The tags of the function.
pub tags: Vec<String>,
/// The args of the function.
pub args: Vec<StdLibFnArg>,
/// The return value of the function.
pub return_value: StdLibFnArg,
/// If the function is unpublished.
pub unpublished: bool,
/// If the function is deprecated.
pub deprecated: bool,
}
/// This struct defines a single argument to a stdlib function.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct StdLibFnArg {
/// The name of the argument.
pub name: String,
/// The type of the argument.
pub type_: String,
/// The schema of the argument.
pub schema: schemars::schema::Schema,
/// If the argument is required.
pub required: bool,
}
impl StdLibFnArg {
#[allow(dead_code)]
pub fn get_type_string(&self) -> Result<(String, bool)> {
get_type_string_from_schema(&self.schema)
}
#[allow(dead_code)]
pub fn description(&self) -> Option<String> {
get_description_string_from_schema(&self.schema)
}
}
/// This trait defines functions called upon stdlib functions to generate
/// documentation for them.
pub trait StdLibFn {
/// The name of the function.
fn name(&self) -> String;
/// The summary of the function.
fn summary(&self) -> String;
/// The description of the function.
fn description(&self) -> String;
/// The tags of the function.
fn tags(&self) -> Vec<String>;
/// The args of the function.
fn args(&self) -> Vec<StdLibFnArg>;
/// The return value of the function.
fn return_value(&self) -> StdLibFnArg;
/// If the function is unpublished.
fn unpublished(&self) -> bool;
/// If the function is deprecated.
fn deprecated(&self) -> bool;
/// The function itself.
fn std_lib_fn(&self) -> crate::std::StdFn;
/// Return a JSON struct representing the function.
fn to_json(&self) -> Result<StdLibFnData> {
Ok(StdLibFnData {
name: self.name(),
summary: self.summary(),
description: self.description(),
tags: self.tags(),
args: self.args(),
return_value: self.return_value(),
unpublished: self.unpublished(),
deprecated: self.deprecated(),
})
}
}
fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Option<String> {
if let schemars::schema::Schema::Object(o) = schema {
if let Some(metadata) = &o.metadata {
if let Some(description) = &metadata.description {
return Some(description.to_string());
}
}
}
None
}
fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(String, bool)> {
match schema {
schemars::schema::Schema::Object(o) => {
if let Some(format) = &o.format {
if format == "uuid" {
return Ok((Primitive::Uuid.to_string(), false));
} else if format == "double" || format == "uint" {
return Ok((Primitive::Number.to_string(), false));
} else {
anyhow::bail!("unknown format: {}", format);
}
}
if let Some(obj_val) = &o.object {
let mut fn_docs = String::new();
fn_docs.push_str("{\n");
// Let's print out the object's properties.
for (prop_name, prop) in obj_val.properties.iter() {
if prop_name.starts_with('_') {
continue;
}
if let Some(description) = get_description_string_from_schema(prop) {
fn_docs.push_str(&format!("\t// {}\n", description));
}
fn_docs.push_str(&format!(
"\t\"{}\": {},\n",
prop_name,
get_type_string_from_schema(prop)?.0,
));
}
fn_docs.push('}');
return Ok((fn_docs, true));
}
if let Some(array_val) = &o.array {
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
// Let's print out the object's properties.
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
} else if let Some(items) = &array_val.contains {
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
}
}
if let Some(subschemas) = &o.subschemas {
let mut fn_docs = String::new();
if let Some(items) = &subschemas.one_of {
for (i, item) in items.iter().enumerate() {
// Let's print out the object's properties.
fn_docs.push_str(&get_type_string_from_schema(item)?.0.to_string());
if i < items.len() - 1 {
fn_docs.push_str(" |\n");
}
}
} else if let Some(items) = &subschemas.any_of {
for (i, item) in items.iter().enumerate() {
// Let's print out the object's properties.
fn_docs.push_str(&get_type_string_from_schema(item)?.0.to_string());
if i < items.len() - 1 {
fn_docs.push_str(" |\n");
}
}
} else {
anyhow::bail!("unknown subschemas: {:#?}", subschemas);
}
return Ok((fn_docs, true));
}
if let Some(schemars::schema::SingleOrVec::Single(_string)) = &o.instance_type {
return Ok((Primitive::String.to_string(), false));
}
anyhow::bail!("unknown type: {:#?}", o)
}
schemars::schema::Schema::Bool(_) => Ok((Primitive::Bool.to_string(), false)),
}
}

View File

@ -0,0 +1,175 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine.
use std::sync::Arc;
use anyhow::Result;
use futures::{SinkExt, StreamExt};
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
use tokio_tungstenite::tungstenite::Message as WsMsg;
use crate::errors::{KclError, KclErrorDetails};
#[derive(Debug)]
pub struct EngineConnection {
tcp_write: futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>, WsMsg>,
tcp_read_handle: tokio::task::JoinHandle<Result<()>>,
export_notifier: Arc<tokio::sync::Notify>,
snapshot_notifier: Arc<tokio::sync::Notify>,
}
impl Drop for EngineConnection {
fn drop(&mut self) {
// Drop the read handle.
self.tcp_read_handle.abort();
}
}
pub struct TcpRead {
stream: futures::stream::SplitStream<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>>,
}
impl TcpRead {
pub async fn read(&mut self) -> Result<WebSocketResponse> {
let msg = self.stream.next().await.unwrap()?;
let msg: WebSocketResponse = match msg {
WsMsg::Text(text) => serde_json::from_str(&text)?,
WsMsg::Binary(bin) => bson::from_slice(&bin)?,
other => anyhow::bail!("Unexpected websocket message from server: {}", other),
};
Ok(msg)
}
}
impl EngineConnection {
pub async fn new(ws: reqwest::Upgraded, export_dir: &str, snapshot_file: &str) -> Result<EngineConnection> {
// Make sure the export directory exists and that it is a directory.
let export_dir = std::path::Path::new(export_dir).to_owned();
if !export_dir.exists() {
anyhow::bail!("Export directory does not exist: {}", export_dir.display());
}
// Make sure it is a directory.
if !export_dir.is_dir() {
anyhow::bail!("Export directory is not a directory: {}", export_dir.display());
}
let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
ws,
tokio_tungstenite::tungstenite::protocol::Role::Client,
None,
)
.await;
let (tcp_write, tcp_read) = ws_stream.split();
let mut tcp_read = TcpRead { stream: tcp_read };
let export_notifier = Arc::new(tokio::sync::Notify::new());
let export_notifier_clone = export_notifier.clone();
let snapshot_notifier = Arc::new(tokio::sync::Notify::new());
let snapshot_notifier_clone = snapshot_notifier.clone();
let snapshot_file = snapshot_file.to_owned();
let tcp_read_handle = tokio::spawn(async move {
// Get Websocket messages from API server
loop {
match tcp_read.read().await {
Ok(ws_resp) => {
if let Some(success) = ws_resp.success {
if !success {
println!("got ws errors: {:?}", ws_resp.errors);
export_notifier.notify_one();
continue;
}
}
if let Some(msg) = ws_resp.resp {
match msg {
OkWebSocketResponseData::IceServerInfo { ice_servers } => {
println!("got ice server info: {:?}", ice_servers);
}
OkWebSocketResponseData::SdpAnswer { answer } => {
println!("got sdp answer: {:?}", answer);
}
OkWebSocketResponseData::TrickleIce { candidate } => {
println!("got trickle ice: {:?}", candidate);
}
OkWebSocketResponseData::Modeling { modeling_response } => {
if let kittycad::types::OkModelingCmdResponse::TakeSnapshot { data } =
modeling_response
{
if snapshot_file.is_empty() {
println!("Got snapshot, but no snapshot file specified.");
continue;
}
// Save the snapshot locally.
std::fs::write(&snapshot_file, data.contents)?;
snapshot_notifier.notify_one();
}
}
OkWebSocketResponseData::Export { files } => {
// Save the files to our export directory.
for file in files {
let path = export_dir.join(file.name);
std::fs::write(&path, file.contents)?;
println!("Wrote file: {}", path.display());
}
// Tell the export notifier that we have new files.
export_notifier.notify_one();
}
}
}
}
Err(e) => {
println!("got ws error: {:?}", e);
export_notifier.notify_one();
continue;
}
}
}
});
Ok(EngineConnection {
tcp_write,
tcp_read_handle,
export_notifier: export_notifier_clone,
snapshot_notifier: snapshot_notifier_clone,
})
}
pub async fn wait_for_export(&self) {
self.export_notifier.notified().await;
}
pub async fn wait_for_snapshot(&self) {
self.snapshot_notifier.notified().await;
}
pub async fn tcp_send(&mut self, msg: WebSocketRequest) -> Result<()> {
let msg = serde_json::to_string(&msg)?;
self.tcp_write.send(WsMsg::Text(msg)).await?;
Ok(())
}
pub fn send_modeling_cmd(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<(), KclError> {
futures::executor::block_on(self.tcp_send(WebSocketRequest::ModelingCmdReq { cmd, cmd_id: id })).map_err(
|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to send modeling command: {}", e),
source_ranges: vec![source_range],
})
},
)?;
Ok(())
}
}

View File

@ -0,0 +1,24 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine.
use anyhow::Result;
use crate::errors::KclError;
#[derive(Debug)]
pub struct EngineConnection {}
impl EngineConnection {
pub async fn new() -> Result<EngineConnection> {
Ok(EngineConnection {})
}
pub fn send_modeling_cmd(
&mut self,
_id: uuid::Uuid,
_source_range: crate::executor::SourceRange,
_cmd: kittycad::types::ModelingCmd,
) -> Result<(), KclError> {
Ok(())
}
}

View File

@ -0,0 +1,58 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine.
use anyhow::Result;
use kittycad::types::WebSocketRequest;
use wasm_bindgen::prelude::*;
use crate::errors::{KclError, KclErrorDetails};
#[wasm_bindgen(module = "/../../lang/std/engineConnection.ts")]
extern "C" {
#[derive(Debug, Clone)]
pub type EngineCommandManager;
#[wasm_bindgen(method)]
fn sendModelingCommandFromWasm(
this: &EngineCommandManager,
id: String,
rangeStr: String,
cmdStr: String,
) -> js_sys::Promise;
}
#[derive(Debug, Clone)]
pub struct EngineConnection {
manager: EngineCommandManager,
}
impl EngineConnection {
pub async fn new(manager: EngineCommandManager) -> Result<EngineConnection, JsValue> {
Ok(EngineConnection { manager })
}
pub fn send_modeling_cmd(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<(), KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let ws_msg = WebSocketRequest::ModelingCmdReq { cmd, cmd_id: id };
let cmd_str = serde_json::to_string(&ws_msg).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize modeling command: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let _ = self
.manager
.sendModelingCommandFromWasm(id.to_string(), source_range_str, cmd_str);
Ok(())
}
}

View File

@ -0,0 +1,53 @@
//! Functions for managing engine communications.
use wasm_bindgen::prelude::*;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(test))]
pub mod conn;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(test))]
pub use conn::EngineConnection;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
pub mod conn_wasm;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
pub use conn_wasm::EngineConnection;
#[cfg(test)]
pub mod conn_mock;
#[cfg(test)]
pub use conn_mock::EngineConnection;
use crate::executor::SourceRange;
#[derive(Debug)]
#[wasm_bindgen]
pub struct EngineManager {
connection: EngineConnection,
}
#[wasm_bindgen]
impl EngineManager {
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
#[wasm_bindgen(constructor)]
pub async fn new(manager: conn_wasm::EngineCommandManager) -> EngineManager {
EngineManager {
// This unwrap is safe because the connection is always created.
connection: EngineConnection::new(manager).await.unwrap(),
}
}
pub fn send_modeling_cmd(&mut self, id_str: &str, cmd_str: &str) -> Result<(), String> {
let id = uuid::Uuid::parse_str(id_str).map_err(|e| e.to_string())?;
let cmd = serde_json::from_str(cmd_str).map_err(|e| e.to_string())?;
self.connection
.send_modeling_cmd(id, SourceRange::default(), cmd)
.map_err(String::from)?;
Ok(())
}
}

View File

@ -0,0 +1,78 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum KclError {
#[error("syntax: {0:?}")]
Syntax(KclErrorDetails),
#[error("semantic: {0:?}")]
Semantic(KclErrorDetails),
#[error("type: {0:?}")]
Type(KclErrorDetails),
#[error("unimplemented: {0:?}")]
Unimplemented(KclErrorDetails),
#[error("unexpected: {0:?}")]
Unexpected(KclErrorDetails),
#[error("value already defined: {0:?}")]
ValueAlreadyDefined(KclErrorDetails),
#[error("undefined value: {0:?}")]
UndefinedValue(KclErrorDetails),
#[error("invalid expression: {0:?}")]
InvalidExpression(KclErrorDetails),
#[error("engine: {0:?}")]
Engine(KclErrorDetails),
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")]
pub source_ranges: Vec<crate::executor::SourceRange>,
#[serde(rename = "msg")]
pub message: String,
}
impl KclError {
/// Get the error message, line and column from the error and input code.
pub fn get_message_line_column(&self, input: &str) -> (String, Option<usize>, Option<usize>) {
let (type_, source_range, message) = match &self {
KclError::Syntax(e) => ("syntax", e.source_ranges.clone(), e.message.clone()),
KclError::Semantic(e) => ("semantic", e.source_ranges.clone(), e.message.clone()),
KclError::Type(e) => ("type", e.source_ranges.clone(), e.message.clone()),
KclError::Unimplemented(e) => ("unimplemented", e.source_ranges.clone(), e.message.clone()),
KclError::Unexpected(e) => ("unexpected", e.source_ranges.clone(), e.message.clone()),
KclError::ValueAlreadyDefined(e) => ("value already defined", e.source_ranges.clone(), e.message.clone()),
KclError::UndefinedValue(e) => ("undefined value", e.source_ranges.clone(), e.message.clone()),
KclError::InvalidExpression(e) => ("invalid expression", e.source_ranges.clone(), e.message.clone()),
KclError::Engine(e) => ("engine", e.source_ranges.clone(), e.message.clone()),
};
// Calculate the line and column of the error from the source range.
let (line, column) = if let Some(range) = source_range.first() {
let line = input[..range.0[0]].lines().count();
let column = input[..range.0[0]].lines().last().map(|l| l.len()).unwrap_or_default();
(Some(line), Some(column))
} else {
(None, None)
};
(format!("{}: {}", type_, message), line, column)
}
}
/// This is different than to_string() in that it will serialize the Error
/// the struct as JSON so we can deserialize it on the js side.
impl From<KclError> for String {
fn from(error: KclError) -> Self {
serde_json::to_string(&error).unwrap()
}
}
impl From<String> for KclError {
fn from(error: String) -> Self {
serde_json::from_str(&error).unwrap()
}
}

View File

@ -0,0 +1,768 @@
//! The executor for the AST.
use std::collections::HashMap;
use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
abstract_syntax_tree_types::{BodyItem, FunctionExpression, Value},
engine::EngineConnection,
errors::{KclError, KclErrorDetails},
};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ProgramMemory {
pub root: HashMap<String, MemoryItem>,
#[serde(rename = "return")]
pub return_: Option<ProgramReturn>,
}
impl ProgramMemory {
pub fn new() -> Self {
Self {
root: HashMap::new(),
return_: None,
}
}
/// Add to the program memory.
pub fn add(&mut self, key: &str, value: MemoryItem, source_range: SourceRange) -> Result<(), KclError> {
if self.root.get(key).is_some() {
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
message: format!("Cannot redefine {}", key),
source_ranges: vec![source_range],
}));
}
self.root.insert(key.to_string(), value);
Ok(())
}
/// Get a value from the program memory.
pub fn get(&self, key: &str, source_range: SourceRange) -> Result<&MemoryItem, KclError> {
self.root.get(key).ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("memory item key `{}` is not defined", key),
source_ranges: vec![source_range],
})
})
}
}
impl Default for ProgramMemory {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum ProgramReturn {
Arguments(Vec<Value>),
Value(MemoryItem),
}
impl From<ProgramReturn> for Vec<SourceRange> {
fn from(item: ProgramReturn) -> Self {
match item {
ProgramReturn::Arguments(args) => args
.iter()
.map(|arg| {
let r: SourceRange = arg.into();
r
})
.collect(),
ProgramReturn::Value(v) => v.into(),
}
}
}
impl ProgramReturn {
pub fn get_value(&self) -> Result<MemoryItem, KclError> {
match self {
ProgramReturn::Value(v) => Ok(v.clone()),
ProgramReturn::Arguments(args) => Err(KclError::Semantic(KclErrorDetails {
message: format!("Cannot get value from arguments: {:?}", args),
source_ranges: self.clone().into(),
})),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum MemoryItem {
UserVal {
value: serde_json::Value,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
SketchGroup(SketchGroup),
ExtrudeGroup(ExtrudeGroup),
ExtrudeTransform(ExtrudeTransform),
Function {
#[serde(skip)]
func: Option<MemoryFunction>,
expression: Box<FunctionExpression>,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExtrudeTransform {
pub position: Position,
pub rotation: Rotation,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
pub type MemoryFunction = fn(
s: &[MemoryItem],
memory: &ProgramMemory,
expression: &FunctionExpression,
metadata: &[Metadata],
engine: &mut EngineConnection,
) -> Result<Option<ProgramReturn>, KclError>;
impl From<MemoryItem> for Vec<SourceRange> {
fn from(item: MemoryItem) -> Self {
match item {
MemoryItem::UserVal { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
MemoryItem::SketchGroup(s) => s.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeGroup(e) => e.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
}
}
}
impl MemoryItem {
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
if let MemoryItem::UserVal { value, .. } = self {
Ok(value.clone())
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("Not a user value: {:?}", self),
source_ranges: self.clone().into(),
}))
}
}
pub fn call_fn(
&self,
args: &[MemoryItem],
memory: &ProgramMemory,
engine: &mut EngineConnection,
) -> Result<Option<ProgramReturn>, KclError> {
if let MemoryItem::Function { func, expression, meta } = self {
if let Some(func) = func {
func(args, memory, expression, meta, engine)
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("Not a function: {:?}", self),
source_ranges: vec![],
}))
}
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("not a function: {:?}", self),
source_ranges: vec![],
}))
}
}
}
/// A sketch group is a collection of paths.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct SketchGroup {
/// The id of the sketch group.
pub id: uuid::Uuid,
/// The paths in the sketch group.
pub value: Vec<Path>,
/// The starting path.
pub start: BasePath,
/// The position of the sketch group.
pub position: Position,
/// The rotation of the sketch group.
pub rotation: Rotation,
/// Metadata.
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
impl SketchGroup {
pub fn get_path_by_id(&self, id: &uuid::Uuid) -> Option<&Path> {
self.value.iter().find(|p| p.get_id() == *id)
}
pub fn get_path_by_name(&self, name: &str) -> Option<&Path> {
self.value.iter().find(|p| p.get_name() == name)
}
pub fn get_base_by_name_or_start(&self, name: &str) -> Option<&BasePath> {
if self.start.name == name {
Some(&self.start)
} else {
self.value.iter().find(|p| p.get_name() == name).map(|p| p.get_base())
}
}
pub fn get_coords_from_paths(&self) -> Result<Point2d, KclError> {
if self.value.is_empty() {
return Ok(self.start.to.into());
}
let index = self.value.len() - 1;
if let Some(path) = self.value.get(index) {
let base = path.get_base();
Ok(base.to.into())
} else {
Ok(self.start.to.into())
}
}
}
/// An extrude group is a collection of extrude surfaces.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExtrudeGroup {
/// The id of the extrude group.
pub id: uuid::Uuid,
/// The extrude surfaces.
pub value: Vec<ExtrudeSurface>,
/// The height of the extrude group.
pub height: f64,
/// The position of the extrude group.
pub position: Position,
/// The rotation of the extrude group.
pub rotation: Rotation,
/// Metadata.
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
impl ExtrudeGroup {
pub fn get_path_by_id(&self, id: &uuid::Uuid) -> Option<&ExtrudeSurface> {
self.value.iter().find(|p| p.get_id() == *id)
}
pub fn get_path_by_name(&self, name: &str) -> Option<&ExtrudeSurface> {
self.value.iter().find(|p| p.get_name() == name)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum BodyType {
Root,
Sketch,
Block,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Position(pub [f64; 3]);
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Rotation(pub [f64; 4]);
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct SourceRange(pub [usize; 2]);
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Point2d {
pub x: f64,
pub y: f64,
}
impl From<[f64; 2]> for Point2d {
fn from(p: [f64; 2]) -> Self {
Self { x: p[0], y: p[1] }
}
}
impl From<Point2d> for [f64; 2] {
fn from(p: Point2d) -> Self {
[p.x, p.y]
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Point3d {
pub x: f64,
pub y: f64,
pub z: f64,
}
/// Metadata.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
/// The source range.
pub source_range: SourceRange,
}
impl From<SourceRange> for Metadata {
fn from(source_range: SourceRange) -> Self {
Self { source_range }
}
}
/// A base path.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct BasePath {
/// The from point.
pub from: [f64; 2],
/// The to point.
pub to: [f64; 2],
/// The name of the path.
pub name: String,
/// Metadata.
#[serde(rename = "__geoMeta")]
pub geo_meta: GeoMeta,
}
/// Geometry metadata.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct GeoMeta {
/// The id of the geometry.
pub id: uuid::Uuid,
/// Metadata.
#[serde(flatten)]
pub metadata: Metadata,
}
/// A path.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Path {
/// A path that goes to a point.
ToPoint {
#[serde(flatten)]
base: BasePath,
},
/// A path that is horizontal.
Horizontal {
#[serde(flatten)]
base: BasePath,
/// The x coordinate.
x: f64,
},
/// An angled line to.
AngledLineTo {
#[serde(flatten)]
base: BasePath,
/// The x coordinate.
x: Option<f64>,
/// The y coordinate.
y: Option<f64>,
},
/// A base path.
Base {
#[serde(flatten)]
base: BasePath,
},
}
impl Path {
pub fn get_id(&self) -> uuid::Uuid {
match self {
Path::ToPoint { base } => base.geo_meta.id,
Path::Horizontal { base, .. } => base.geo_meta.id,
Path::AngledLineTo { base, .. } => base.geo_meta.id,
Path::Base { base } => base.geo_meta.id,
}
}
pub fn get_name(&self) -> String {
match self {
Path::ToPoint { base } => base.name.clone(),
Path::Horizontal { base, .. } => base.name.clone(),
Path::AngledLineTo { base, .. } => base.name.clone(),
Path::Base { base } => base.name.clone(),
}
}
pub fn get_base(&self) -> &BasePath {
match self {
Path::ToPoint { base } => base,
Path::Horizontal { base, .. } => base,
Path::AngledLineTo { base, .. } => base,
Path::Base { base } => base,
}
}
}
/// An extrude surface.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ExtrudeSurface {
/// An extrude plane.
ExtrudePlane {
/// The position.
position: Position,
/// The rotation.
rotation: Rotation,
/// The name.
name: String,
/// Metadata.
#[serde(flatten)]
geo_meta: GeoMeta,
},
}
impl ExtrudeSurface {
pub fn get_id(&self) -> uuid::Uuid {
match self {
ExtrudeSurface::ExtrudePlane { geo_meta, .. } => geo_meta.id,
}
}
pub fn get_name(&self) -> String {
match self {
ExtrudeSurface::ExtrudePlane { name, .. } => name.clone(),
}
}
pub fn get_position(&self) -> Position {
match self {
ExtrudeSurface::ExtrudePlane { position, .. } => *position,
}
}
pub fn get_rotation(&self) -> Rotation {
match self {
ExtrudeSurface::ExtrudePlane { rotation, .. } => *rotation,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct PipeInfo {
pub previous_results: Vec<MemoryItem>,
pub is_in_pipe: bool,
pub index: usize,
pub body: Vec<Value>,
}
impl PipeInfo {
pub fn new() -> Self {
Self {
previous_results: Vec::new(),
is_in_pipe: false,
index: 0,
body: Vec::new(),
}
}
}
impl Default for PipeInfo {
fn default() -> Self {
Self::new()
}
}
/// Execute a AST's program.
pub fn execute(
program: crate::abstract_syntax_tree_types::Program,
memory: &mut ProgramMemory,
options: BodyType,
engine: &mut EngineConnection,
) -> Result<ProgramMemory, KclError> {
let mut pipe_info = PipeInfo::default();
let stdlib = crate::std::StdLib::new();
// Iterate over the body of the program.
for statement in &program.body {
match statement {
BodyItem::ExpressionStatement(expression_statement) => {
if let Value::CallExpression(call_expr) = &expression_statement.expression {
let fn_name = call_expr.callee.name.to_string();
let mut args: Vec<MemoryItem> = Vec::new();
for arg in &call_expr.arguments {
match arg {
Value::Literal(literal) => args.push(literal.into()),
Value::Identifier(identifier) => {
let memory_item = memory.get(&identifier.name, identifier.into())?;
args.push(memory_item.clone());
}
// We do nothing for the rest.
_ => (),
}
}
if fn_name == "show" {
if options != BodyType::Root {
return Err(KclError::Semantic(KclErrorDetails {
message: "Cannot call show outside of a root".to_string(),
source_ranges: vec![call_expr.into()],
}));
}
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
} else if let Some(func) = memory.clone().root.get(&fn_name) {
func.call_fn(&args, memory, engine)?;
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("No such name {} defined", fn_name),
source_ranges: vec![call_expr.into()],
}));
}
}
}
BodyItem::VariableDeclaration(variable_declaration) => {
for declaration in &variable_declaration.declarations {
let var_name = declaration.id.name.to_string();
let source_range: SourceRange = declaration.init.clone().into();
let metadata = Metadata { source_range };
match &declaration.init {
Value::Literal(literal) => {
memory.add(&var_name, literal.into(), source_range)?;
}
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
memory.add(&var_name, value.clone(), source_range)?;
}
Value::BinaryExpression(binary_expression) => {
let result = binary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::FunctionExpression(function_expression) => {
memory.add(
&var_name,
MemoryItem::Function{
expression: function_expression.clone(),
meta: vec![metadata],
func: Some(|args: &[MemoryItem], memory: &ProgramMemory, function_expression: &FunctionExpression, _metadata: &[Metadata], engine: &mut EngineConnection| -> Result<Option<ProgramReturn>, KclError> {
let mut fn_memory = memory.clone();
if args.len() != function_expression.params.len() {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Expected {} arguments, got {}", function_expression.params.len(), args.len()),
source_ranges: vec![function_expression.into()],
}));
}
// Add the arguments to the memory.
for (index, param) in function_expression.params.iter().enumerate() {
fn_memory.add(
&param.name,
args.clone().get(index).unwrap().clone(),
param.into(),
)?;
}
let result = execute(function_expression.body.clone(), &mut fn_memory, BodyType::Block, engine)?;
Ok(result.return_)
})
},
source_range,
)?;
}
Value::CallExpression(call_expression) => {
let result = call_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::PipeExpression(pipe_expression) => {
let result = pipe_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"pipe substitution not implemented for declaration of variable {}",
var_name
),
source_ranges: vec![pipe_substitution.into()],
}));
}
Value::ArrayExpression(array_expression) => {
let result = array_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::ObjectExpression(object_expression) => {
let result = object_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::MemberExpression(member_expression) => {
let result = member_expression.get_result(memory)?;
memory.add(&var_name, result, source_range)?;
}
Value::UnaryExpression(unary_expression) => {
let result = unary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
}
}
}
BodyItem::ReturnStatement(return_statement) => match &return_statement.argument {
Value::BinaryExpression(bin_expr) => {
let result = bin_expr.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?.clone();
memory.return_ = Some(ProgramReturn::Value(value));
}
_ => (),
},
}
}
Ok(memory.clone())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
let tokens = crate::tokeniser::lexer(code);
let program = crate::parser::abstract_syntax_tree(&tokens)?;
let mut mem: ProgramMemory = Default::default();
let mut engine = EngineConnection::new().await?;
let memory = execute(program, &mut mem, BodyType::Root, &mut engine)?;
Ok(memory)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_assign_two_variables() {
let ast = r#"const myVar = 5
const newVar = myVar + 1"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5),
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
assert_eq!(
serde_json::json!(6.0),
memory.root.get("newVar").unwrap().get_json_value().unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_angled_line_that_intersects() {
let ast_fn = |offset: &str| -> String {
format!(
r#"const part001 = startSketchAt([0, 0])
|> lineTo({{to:[2, 2], tag: "yo"}}, %)
|> lineTo([3, 1], %)
|> angledLineThatIntersects({{
angle: 180,
intersectTag: 'yo',
offset: {},
tag: "yo2"
}}, %)
const intersect = segEndX('yo2', part001)
show(part001)"#,
offset
)
};
let memory = parse_execute(&ast_fn("-1")).await.unwrap();
assert_eq!(
serde_json::json!(1.0 + 2.0f64.sqrt()),
memory.root.get("intersect").unwrap().get_json_value().unwrap()
);
let memory = parse_execute(&ast_fn("0")).await.unwrap();
assert_eq!(
serde_json::json!(1.0000000000000002),
memory.root.get("intersect").unwrap().get_json_value().unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_fn_definitions() {
let ast = r#"const def = (x) => {
return x
}
const ghi = (x) => {
return x
}
const jkl = (x) => {
return x
}
const hmm = (x) => {
return x
}
const yo = 5 + 6
const abc = 3
const identifierGuy = 5
const part001 = startSketchAt([-1.2, 4.83])
|> line([2.8, 0], %)
|> angledLine([100 + 100, 3.01], %)
|> angledLine([abc, 3.02], %)
|> angledLine([def(yo), 3.03], %)
|> angledLine([ghi(2), 3.04], %)
|> angledLine([jkl(yo) + 2, 3.05], %)
|> close(%)
const yo2 = hmm([identifierGuy + 5])
show(part001)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_pipe_substitutions_unary() {
let ast = r#"const myVar = 3
const part001 = startSketchAt([0, 0])
|> line({ to: [3, 4], tag: 'seg01' }, %)
|> line([
min(segLen('seg01', %), myVar),
-legLen(segLen('seg01', %), myVar)
], %)
show(part001)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_pipe_substitutions() {
let ast = r#"const myVar = 3
const part001 = startSketchAt([0, 0])
|> line({ to: [3, 4], tag: 'seg01' }, %)
|> line([
min(segLen('seg01', %), myVar),
legLen(segLen('seg01', %), myVar)
], %)
show(part001)"#;
parse_execute(ast).await.unwrap();
}
}

View File

@ -0,0 +1,10 @@
pub mod abstract_syntax_tree_types;
mod docs;
pub mod engine;
pub mod errors;
pub mod executor;
pub mod math_parser;
pub mod parser;
pub mod recast;
pub mod std;
pub mod tokeniser;

View File

@ -1,12 +1,13 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::abstract_syntax_tree_types::{
BinaryExpression, BinaryPart, CallExpression, Identifier, Literal,
use crate::{
abstract_syntax_tree_types::{BinaryExpression, BinaryPart, CallExpression, Identifier, Literal, ValueMeta},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
parser::{find_closing_brace, is_not_code_token, make_call_expression},
tokeniser::{Token, TokenType},
};
use crate::errors::{KclError, KclErrorDetails};
use crate::parser::{find_closing_brace, is_not_code_token, make_call_expression};
use crate::tokeniser::{Token, TokenType};
pub fn precedence(operator: &str) -> u8 {
// might be useful for reference to make it match
@ -147,7 +148,7 @@ pub fn reverse_polish_notation(
}
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
source_ranges: vec![current_token.into()],
message: format!(
"Unexpected token: {} {:?}",
current_token.value, current_token.token_type
@ -182,6 +183,8 @@ pub struct ParenthesisToken {
pub end: usize,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ParenthesisToken);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
@ -195,10 +198,12 @@ pub struct ExtendedBinaryExpression {
pub end_extended: Option<usize>,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedBinaryExpression);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ExntendedLiteral {
pub struct ExtendedLiteral {
pub start: usize,
pub end: usize,
pub value: serde_json::Value,
@ -207,11 +212,13 @@ pub struct ExntendedLiteral {
pub end_extended: Option<usize>,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedLiteral);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub enum MathExpression {
ExntendedLiteral(Box<ExntendedLiteral>),
ExtendedLiteral(Box<ExtendedLiteral>),
Identifier(Box<Identifier>),
CallExpression(Box<CallExpression>),
BinaryExpression(Box<BinaryExpression>),
@ -219,6 +226,30 @@ pub enum MathExpression {
ParenthesisToken(Box<ParenthesisToken>),
}
impl MathExpression {
pub fn start(&self) -> usize {
match self {
MathExpression::ExtendedLiteral(literal) => literal.start(),
MathExpression::Identifier(identifier) => identifier.start(),
MathExpression::CallExpression(call_expression) => call_expression.start(),
MathExpression::BinaryExpression(binary_expression) => binary_expression.start(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.start(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.start(),
}
}
pub fn end(&self) -> usize {
match self {
MathExpression::ExtendedLiteral(literal) => literal.end(),
MathExpression::Identifier(identifier) => identifier.end(),
MathExpression::CallExpression(call_expression) => call_expression.end(),
MathExpression::BinaryExpression(binary_expression) => binary_expression.end(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.end(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.end(),
}
}
}
fn build_tree(
reverse_polish_notation_tokens: &[Token],
stack: Vec<MathExpression>,
@ -241,54 +272,47 @@ fn build_tree(
}),
a => {
return Err(KclError::InvalidExpression(a.clone()));
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![SourceRange([a.start(), a.end()])],
message: format!("{:?}", a),
}))
}
};
}
let current_token = &reverse_polish_notation_tokens[0];
if current_token.token_type == TokenType::Number
|| current_token.token_type == TokenType::String
{
if current_token.token_type == TokenType::Number || current_token.token_type == TokenType::String {
let mut new_stack = stack;
new_stack.push(MathExpression::ExntendedLiteral(Box::new(
ExntendedLiteral {
value: if current_token.token_type == TokenType::Number {
if let Ok(value) = current_token.value.parse::<i64>() {
serde_json::Value::Number(value.into())
} else if let Ok(value) = current_token.value.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(value) {
serde_json::Value::Number(n)
} else {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![[
current_token.start as i32,
current_token.end as i32,
]],
message: format!("Invalid float: {}", current_token.value),
}));
}
new_stack.push(MathExpression::ExtendedLiteral(Box::new(ExtendedLiteral {
value: if current_token.token_type == TokenType::Number {
if let Ok(value) = current_token.value.parse::<i64>() {
serde_json::Value::Number(value.into())
} else if let Ok(value) = current_token.value.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(value) {
serde_json::Value::Number(n)
} else {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![[
current_token.start as i32,
current_token.end as i32,
]],
message: format!("Invalid integer: {}", current_token.value),
source_ranges: vec![current_token.into()],
message: format!("Invalid float: {}", current_token.value),
}));
}
} else {
let mut str_val = current_token.value.clone();
str_val.remove(0);
str_val.pop();
serde_json::Value::String(str_val)
},
start: current_token.start,
end: current_token.end,
raw: current_token.value.clone(),
end_extended: None,
start_extended: None,
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Invalid integer: {}", current_token.value),
}));
}
} else {
let mut str_val = current_token.value.clone();
str_val.remove(0);
str_val.pop();
serde_json::Value::String(str_val)
},
)));
start: current_token.start,
end: current_token.end,
raw: current_token.value.clone(),
end_extended: None,
start_extended: None,
})));
return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Word {
if reverse_polish_notation_tokens[1].token_type == TokenType::Brace
@ -299,10 +323,7 @@ fn build_tree(
new_stack.push(MathExpression::CallExpression(Box::new(
make_call_expression(reverse_polish_notation_tokens, 0)?.expression,
)));
return build_tree(
&reverse_polish_notation_tokens[closing_brace + 1..],
new_stack,
);
return build_tree(&reverse_polish_notation_tokens[closing_brace + 1..], new_stack);
}
let mut new_stack = stack;
new_stack.push(MathExpression::Identifier(Box::new(Identifier {
@ -313,14 +334,12 @@ fn build_tree(
return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Brace && current_token.value == "(" {
let mut new_stack = stack;
new_stack.push(MathExpression::ParenthesisToken(Box::new(
ParenthesisToken {
value: "(".to_string(),
start: current_token.start,
end: current_token.end,
token_type: MathTokenType::Parenthesis,
},
)));
new_stack.push(MathExpression::ParenthesisToken(Box::new(ParenthesisToken {
value: "(".to_string(),
start: current_token.start,
end: current_token.end,
token_type: MathTokenType::Parenthesis,
})));
return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Brace && current_token.value == ")" {
let inner_node: MathExpression = match &stack[stack.len() - 1] {
@ -346,14 +365,22 @@ fn build_tree(
end_extended: None,
}))
}
MathExpression::ExntendedLiteral(literal) => {
MathExpression::ExntendedLiteral(literal.clone())
MathExpression::ExtendedLiteral(literal) => MathExpression::ExtendedLiteral(literal.clone()),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
a => return Err(KclError::InvalidExpression(a.clone())),
};
let paran = match &stack[stack.len() - 2] {
MathExpression::ParenthesisToken(paran) => paran.clone(),
a => return Err(KclError::InvalidExpression(a.clone())),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
};
let expression = match inner_node {
MathExpression::ExtendedBinaryExpression(bin_exp) => {
@ -378,17 +405,20 @@ fn build_tree(
end_extended: Some(current_token.end),
}))
}
MathExpression::ExntendedLiteral(literal) => {
MathExpression::ExntendedLiteral(Box::new(ExntendedLiteral {
value: literal.value.clone(),
start: literal.start,
end: literal.end,
raw: literal.raw.clone(),
end_extended: Some(current_token.end),
start_extended: Some(paran.start),
MathExpression::ExtendedLiteral(literal) => MathExpression::ExtendedLiteral(Box::new(ExtendedLiteral {
value: literal.value.clone(),
start: literal.start,
end: literal.end,
raw: literal.raw.clone(),
end_extended: Some(current_token.end),
start_extended: Some(paran.start),
})),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
a => return Err(KclError::InvalidExpression(a.clone())),
};
let mut new_stack = stack[0..stack.len() - 2].to_vec();
new_stack.push(expression);
@ -405,7 +435,7 @@ fn build_tree(
})),
bin_exp.start_extended.unwrap_or(bin_exp.start),
),
MathExpression::ExntendedLiteral(lit) => (
MathExpression::ExtendedLiteral(lit) => (
BinaryPart::Literal(Box::new(Literal {
value: lit.value.clone(),
start: lit.start,
@ -415,13 +445,14 @@ fn build_tree(
lit.start_extended.unwrap_or(lit.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),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
MathExpression::BinaryExpression(bin_exp) => {
(BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.start)
}
a => return Err(KclError::InvalidExpression(a.clone())),
};
let right = match &stack[stack.len() - 1] {
MathExpression::ExtendedBinaryExpression(bin_exp) => (
@ -434,7 +465,7 @@ fn build_tree(
})),
bin_exp.end_extended.unwrap_or(bin_exp.end),
),
MathExpression::ExntendedLiteral(lit) => (
MathExpression::ExtendedLiteral(lit) => (
BinaryPart::Literal(Box::new(Literal {
value: lit.value.clone(),
start: lit.start,
@ -444,13 +475,14 @@ fn build_tree(
lit.end_extended.unwrap_or(lit.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),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
MathExpression::BinaryExpression(bin_exp) => {
(BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.end)
}
a => return Err(KclError::InvalidExpression(a.clone())),
};
let right_end = match right.0.clone() {
@ -464,11 +496,7 @@ fn build_tree(
let tree = BinaryExpression {
operator: current_token.value.clone(),
start: left.1,
end: if right.1 > right_end {
right.1
} else {
right_end
},
end: if right.1 > right_end { right.1 } else { right_end },
left: left.0,
right: right.0,
};
@ -516,9 +544,10 @@ pub fn parse_expression(tokens: &[Token]) -> Result<BinaryExpression, KclError>
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_parse_expression() {
let tokens = crate::tokeniser::lexer("1 + 2");
@ -839,8 +868,7 @@ mod test {
#[test]
fn test_reverse_polish_notation_complex() {
let result =
reverse_polish_notation(&crate::tokeniser::lexer("1 + 2 * 3"), &[], &[]).unwrap();
let result = reverse_polish_notation(&crate::tokeniser::lexer("1 + 2 * 3"), &[], &[]).unwrap();
assert_eq!(
result,
vec![
@ -880,8 +908,7 @@ mod test {
#[test]
fn test_reverse_polish_notation_complex_with_parentheses() {
let result =
reverse_polish_notation(&crate::tokeniser::lexer("1 * ( 2 + 3 )"), &[], &[]).unwrap();
let result = reverse_polish_notation(&crate::tokeniser::lexer("1 * ( 2 + 3 )"), &[], &[]).unwrap();
assert_eq!(
result,
vec![

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,15 @@
//! Generates source code from the AST.
//! The inverse of parsing (which generates an AST from the source code)
use wasm_bindgen::prelude::*;
use crate::abstract_syntax_tree_types::{
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, FunctionExpression,
Literal, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, PipeExpression,
Program, UnaryExpression, Value,
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, FunctionExpression, Literal,
LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, PipeExpression, Program, UnaryExpression,
Value,
};
fn recast_literal(literal: Literal) -> String {
if let serde_json::Value::String(value) = literal.value {
let quote = if literal.raw.trim().starts_with('"') {
'"'
} else {
'\''
};
let quote = if literal.raw.trim().starts_with('"') { '"' } else { '\'' };
format!("{}{}{}", quote, value, quote)
} else {
literal.value.to_string()
@ -40,16 +35,13 @@ fn recast_binary_expression(expression: BinaryExpression) -> String {
let should_wrap_right = match expression.right.clone() {
BinaryPart::BinaryExpression(bin_exp) => {
precedence(&expression.operator) > precedence(&bin_exp.operator)
|| expression.operator == "-"
precedence(&expression.operator) > precedence(&bin_exp.operator) || expression.operator == "-"
}
_ => false,
};
let should_wrap_left = match expression.left.clone() {
BinaryPart::BinaryExpression(bin_exp) => {
precedence(&expression.operator) > precedence(&bin_exp.operator)
}
BinaryPart::BinaryExpression(bin_exp) => precedence(&expression.operator) > precedence(&bin_exp.operator),
_ => false,
};
@ -65,12 +57,8 @@ fn recast_binary_part(part: BinaryPart) -> String {
match part {
BinaryPart::Literal(literal) => recast_literal(*literal),
BinaryPart::Identifier(identifier) => identifier.name,
BinaryPart::BinaryExpression(binary_expression) => {
recast_binary_expression(*binary_expression)
}
BinaryPart::CallExpression(call_expression) => {
recast_call_expression(&call_expression, "", false)
}
BinaryPart::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
BinaryPart::CallExpression(call_expression) => recast_call_expression(&call_expression, "", false),
_ => String::new(),
}
}
@ -80,15 +68,11 @@ fn recast_value(node: Value, _indentation: String, is_in_pipe_expression: bool)
match node {
Value::BinaryExpression(bin_exp) => recast_binary_expression(*bin_exp),
Value::ArrayExpression(array_exp) => recast_array_expression(&array_exp, &indentation),
Value::ObjectExpression(ref obj_exp) => {
recast_object_expression(obj_exp, &indentation, is_in_pipe_expression)
}
Value::ObjectExpression(ref obj_exp) => recast_object_expression(obj_exp, &indentation, is_in_pipe_expression),
Value::MemberExpression(mem_exp) => recast_member_expression(*mem_exp),
Value::Literal(literal) => recast_literal(*literal),
Value::FunctionExpression(func_exp) => recast_function(*func_exp),
Value::CallExpression(call_exp) => {
recast_call_expression(&call_exp, &indentation, is_in_pipe_expression)
}
Value::CallExpression(call_exp) => recast_call_expression(&call_exp, &indentation, is_in_pipe_expression),
Value::Identifier(ident) => ident.name,
Value::PipeExpression(pipe_exp) => recast_pipe_expression(&pipe_exp),
Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp),
@ -125,11 +109,7 @@ fn recast_array_expression(expression: &ArrayExpression, indentation: &str) -> S
}
}
fn recast_object_expression(
expression: &ObjectExpression,
indentation: &str,
is_in_pipe_expression: bool,
) -> String {
fn recast_object_expression(expression: &ObjectExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
let flat_recast = format!(
"{{ {} }}",
expression
@ -158,11 +138,7 @@ fn recast_object_expression(
format!(
"{}: {}",
prop.key.name,
recast_value(
prop.value.clone(),
_indentation.clone(),
is_in_pipe_expression
)
recast_value(prop.value.clone(), _indentation.clone(), is_in_pipe_expression)
)
})
.collect::<Vec<String>>()
@ -174,11 +150,7 @@ fn recast_object_expression(
}
}
fn recast_call_expression(
expression: &CallExpression,
indentation: &str,
is_in_pipe_expression: bool,
) -> String {
fn recast_call_expression(expression: &CallExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
format!(
"{}({})",
expression.callee.name,
@ -200,9 +172,7 @@ fn recast_argument(argument: Value, indentation: &str, is_in_pipe_expression: bo
Value::ObjectExpression(object_exp) => {
recast_object_expression(&object_exp, indentation, is_in_pipe_expression)
}
Value::CallExpression(call_exp) => {
recast_call_expression(&call_exp, indentation, is_in_pipe_expression)
}
Value::CallExpression(call_exp) => recast_call_expression(&call_exp, indentation, is_in_pipe_expression),
Value::FunctionExpression(function_exp) => recast_function(*function_exp),
Value::PipeSubstitution(_) => "%".to_string(),
Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp),
@ -223,9 +193,7 @@ fn recast_member_expression(expression: MemberExpression) -> String {
};
match expression.object {
MemberObject::MemberExpression(member_exp) => {
recast_member_expression(*member_exp) + key_str.as_str()
}
MemberObject::MemberExpression(member_exp) => recast_member_expression(*member_exp) + key_str.as_str(),
MemberObject::Identifier(identifier) => identifier.name + key_str.as_str(),
}
}
@ -262,9 +230,7 @@ fn recast_unary_expression(expression: UnaryExpression) -> String {
let bin_part_val = match expression.argument {
BinaryPart::Literal(literal) => Value::Literal(literal),
BinaryPart::Identifier(identifier) => Value::Identifier(identifier),
BinaryPart::BinaryExpression(binary_expression) => {
Value::BinaryExpression(binary_expression)
}
BinaryPart::BinaryExpression(binary_expression) => Value::BinaryExpression(binary_expression),
BinaryPart::CallExpression(call_expression) => Value::CallExpression(call_expression),
BinaryPart::UnaryExpression(unary_expression) => Value::UnaryExpression(unary_expression),
};
@ -279,23 +245,13 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
ast.body
.iter()
.map(|statement| match statement.clone() {
BodyItem::ExpressionStatement(expression_statement) => {
match expression_statement.expression {
Value::BinaryExpression(binary_expression) => {
recast_binary_expression(*binary_expression)
}
Value::ArrayExpression(array_expression) => {
recast_array_expression(&array_expression, "")
}
Value::ObjectExpression(object_expression) => {
recast_object_expression(&object_expression, "", false)
}
Value::CallExpression(call_expression) => {
recast_call_expression(&call_expression, "", false)
}
_ => "Expression".to_string(),
}
}
BodyItem::ExpressionStatement(expression_statement) => match expression_statement.expression {
Value::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
Value::ArrayExpression(array_expression) => recast_array_expression(&array_expression, ""),
Value::ObjectExpression(object_expression) => recast_object_expression(&object_expression, "", false),
Value::CallExpression(call_expression) => recast_call_expression(&call_expression, "", false),
_ => "Expression".to_string(),
},
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration
.declarations
.iter()
@ -309,22 +265,16 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
})
.collect::<String>(),
BodyItem::ReturnStatement(return_statement) => {
format!(
"return {}",
recast_argument(return_statement.argument, "", false)
)
format!("return {}", recast_argument(return_statement.argument, "", false))
}
})
.enumerate()
.map(|(index, recast_str)| {
let is_legit_custom_whitespace_or_comment =
|str: String| str != " " && str != "\n" && str != " ";
let is_legit_custom_whitespace_or_comment = |str: String| str != " " && str != "\n" && str != " ";
// determine the value of startString
let last_white_space_or_comment = if index > 0 {
let tmp = if let Some(non_code_node) =
ast.non_code_meta.none_code_nodes.get(&(index - 1))
{
let tmp = if let Some(non_code_node) = ast.non_code_meta.none_code_nodes.get(&(index - 1)) {
non_code_node.value.clone()
} else {
" ".to_string()
@ -334,12 +284,11 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
" ".to_string()
};
// indentation of this line will be covered by the previous if we're using a custom whitespace or comment
let mut start_string =
if is_legit_custom_whitespace_or_comment(last_white_space_or_comment) {
String::new()
} else {
indentation.to_owned()
};
let mut start_string = if is_legit_custom_whitespace_or_comment(last_white_space_or_comment) {
String::new()
} else {
indentation.to_owned()
};
if index == 0 {
if let Some(start) = ast.non_code_meta.start.clone() {
start_string = start.value;
@ -357,13 +306,10 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
} else {
"\n".to_string()
};
let mut custom_white_space_or_comment =
match ast.non_code_meta.none_code_nodes.get(&index) {
Some(custom_white_space_or_comment) => {
custom_white_space_or_comment.value.clone()
}
None => String::new(),
};
let mut custom_white_space_or_comment = match ast.non_code_meta.none_code_nodes.get(&index) {
Some(custom_white_space_or_comment) => custom_white_space_or_comment.value.clone(),
None => String::new(),
};
if !is_legit_custom_whitespace_or_comment(custom_white_space_or_comment.clone()) {
custom_white_space_or_comment = String::new();
}
@ -399,14 +345,3 @@ pub fn recast_function(expression: FunctionExpression) -> String {
)
)
}
// wasm_bindgen wrapper for recast
// test for this function and by extension the recaster are done in javascript land src/lang/recast.test.ts
#[wasm_bindgen]
pub fn recast_js(json_str: &str) -> Result<JsValue, JsError> {
// deserialize the ast from a stringified json
let program: Program = serde_json::from_str(json_str).map_err(JsError::from)?;
let result = recast(&program, "", false);
Ok(serde_wasm_bindgen::to_value(&result)?)
}

View File

@ -0,0 +1,79 @@
//! Functions related to extruding.
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, ExtrudeTransform, MemoryItem, SketchGroup},
std::Args,
};
/// Extrudes by a given amount.
pub fn extrude(args: &mut Args) -> Result<MemoryItem, KclError> {
let (length, sketch_group) = args.get_number_sketch_group()?;
let result = inner_extrude(length, sketch_group, args)?;
Ok(MemoryItem::ExtrudeGroup(result))
}
/// Extrudes by a given amount.
#[stdlib {
name = "extrude"
}]
fn inner_extrude(length: f64, sketch_group: SketchGroup, args: &mut Args) -> Result<ExtrudeGroup, KclError> {
let id = uuid::Uuid::new_v4();
let cmd = kittycad::types::ModelingCmd::Extrude {
target: sketch_group.id,
distance: length,
cap: true,
};
args.send_modeling_cmd(id, cmd)?;
Ok(ExtrudeGroup {
id,
// TODO, this is just an empty array now, should be deleted. This
// comment was originally in the JS code.
value: Default::default(),
height: length,
position: sketch_group.position,
rotation: sketch_group.rotation,
meta: sketch_group.meta,
})
}
/// Returns the extrude wall transform.
pub fn get_extrude_wall_transform(args: &mut Args) -> Result<MemoryItem, KclError> {
let (surface_name, extrude_group) = args.get_path_name_extrude_group()?;
let result = inner_get_extrude_wall_transform(&surface_name, extrude_group, args)?;
Ok(MemoryItem::ExtrudeTransform(result))
}
/// Returns the extrude wall transform.
#[stdlib {
name = "getExtrudeWallTransform"
}]
fn inner_get_extrude_wall_transform(
surface_name: &str,
extrude_group: ExtrudeGroup,
args: &mut Args,
) -> Result<ExtrudeTransform, KclError> {
let surface = extrude_group.get_path_by_name(surface_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a surface name that exists in the given ExtrudeGroup, found `{}`",
surface_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(ExtrudeTransform {
position: surface.get_position(),
rotation: surface.get_rotation(),
meta: extrude_group.meta,
})
}

View File

@ -0,0 +1,600 @@
//! Functions implemented for language execution.
pub mod extrude;
pub mod segment;
pub mod sketch;
pub mod utils;
// TODO: Something that would be nice is if we could generate docs for Kcl based on the
// actual stdlib functions below.
use std::collections::HashMap;
use anyhow::Result;
use derive_docs::stdlib;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
abstract_syntax_tree_types::parse_json_number_as_f64,
engine::EngineConnection,
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, MemoryItem, Metadata, SketchGroup, SourceRange},
};
pub type FnMap = HashMap<String, StdFn>;
pub type StdFn = fn(&mut Args) -> Result<MemoryItem, KclError>;
pub struct StdLib {
#[allow(dead_code)]
internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>>,
pub fns: FnMap,
}
impl StdLib {
pub fn new() -> Self {
let internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>> = vec![
Box::new(Min),
Box::new(LegLen),
Box::new(LegAngX),
Box::new(LegAngY),
Box::new(crate::std::extrude::Extrude),
Box::new(crate::std::extrude::GetExtrudeWallTransform),
Box::new(crate::std::segment::SegEndX),
Box::new(crate::std::segment::SegEndY),
Box::new(crate::std::segment::LastSegX),
Box::new(crate::std::segment::LastSegY),
Box::new(crate::std::segment::SegLen),
Box::new(crate::std::segment::SegAng),
Box::new(crate::std::segment::AngleToMatchLengthX),
Box::new(crate::std::segment::AngleToMatchLengthY),
Box::new(crate::std::sketch::LineTo),
Box::new(crate::std::sketch::Line),
Box::new(crate::std::sketch::XLineTo),
Box::new(crate::std::sketch::XLine),
Box::new(crate::std::sketch::YLineTo),
Box::new(crate::std::sketch::YLine),
Box::new(crate::std::sketch::AngledLineToX),
Box::new(crate::std::sketch::AngledLineToY),
Box::new(crate::std::sketch::AngledLine),
Box::new(crate::std::sketch::AngledLineOfXLength),
Box::new(crate::std::sketch::AngledLineOfYLength),
Box::new(crate::std::sketch::AngledLineThatIntersects),
Box::new(crate::std::sketch::StartSketchAt),
Box::new(crate::std::sketch::Close),
];
let mut fns = HashMap::new();
for internal_fn_name in &internal_fn_names {
fns.insert(internal_fn_name.name().to_string(), internal_fn_name.std_lib_fn());
}
Self { internal_fn_names, fns }
}
}
impl Default for StdLib {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct Args<'a> {
pub args: Vec<MemoryItem>,
pub source_range: SourceRange,
engine: &'a mut EngineConnection,
}
impl<'a> Args<'a> {
pub fn new(args: Vec<MemoryItem>, source_range: SourceRange, engine: &'a mut EngineConnection) -> Self {
Self {
args,
source_range,
engine,
}
}
pub fn send_modeling_cmd(&mut self, id: uuid::Uuid, cmd: kittycad::types::ModelingCmd) -> Result<(), KclError> {
self.engine.send_modeling_cmd(id, self.source_range, cmd)
}
fn make_user_val_from_json(&self, j: serde_json::Value) -> Result<MemoryItem, KclError> {
Ok(MemoryItem::UserVal {
value: j,
meta: vec![Metadata {
source_range: self.source_range,
}],
})
}
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> {
self.make_user_val_from_json(serde_json::Value::Number(serde_json::Number::from_f64(f).ok_or_else(
|| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert `{}` to a number", f),
source_ranges: vec![self.source_range],
})
},
)?))
}
fn get_number_array(&self) -> Result<Vec<f64>, KclError> {
let mut numbers: Vec<f64> = Vec::new();
for arg in &self.args {
let parsed = arg.get_json_value()?;
numbers.push(parse_json_number_as_f64(&parsed, self.source_range)?);
}
Ok(numbers)
}
fn get_hypotenuse_leg(&self) -> Result<(f64, f64), KclError> {
let numbers = self.get_number_array()?;
if numbers.len() != 2 {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a number array of length 2, found `{:?}`", numbers),
source_ranges: vec![self.source_range],
}));
}
Ok((numbers[0], numbers[1]))
}
fn get_segment_name_sketch_group(&self) -> Result<(String, SketchGroup), KclError> {
// Iterate over our args, the first argument should be a UserVal with a string value.
// The second argument should be a SketchGroup.
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let segment_name = if let serde_json::Value::String(s) = first_value {
s.to_string()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
let sketch_group = if let MemoryItem::SketchGroup(sg) = second_value {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
Ok((segment_name, sketch_group))
}
fn get_sketch_group(&self) -> Result<SketchGroup, KclError> {
let first_value = self.args.first().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
let sketch_group = if let MemoryItem::SketchGroup(sg) = first_value {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
Ok(sketch_group)
}
fn get_data<T: serde::de::DeserializeOwned>(&self) -> Result<T, KclError> {
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let data: T = serde_json::from_value(first_value).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to deserialize struct from JSON: {}", e),
source_ranges: vec![self.source_range],
})
})?;
Ok(data)
}
fn get_data_and_sketch_group<T: serde::de::DeserializeOwned>(&self) -> Result<(T, SketchGroup), KclError> {
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let data: T = serde_json::from_value(first_value).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to deserialize struct from JSON: {}", e),
source_ranges: vec![self.source_range],
})
})?;
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
let sketch_group = if let MemoryItem::SketchGroup(sg) = second_value {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
Ok((data, sketch_group))
}
fn get_segment_name_to_number_sketch_group(&self) -> Result<(String, f64, SketchGroup), KclError> {
// Iterate over our args, the first argument should be a UserVal with a string value.
// The second argument should be a number.
// The third argument should be a SketchGroup.
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let segment_name = if let serde_json::Value::String(s) = first_value {
s.to_string()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
let second_value = self
.args
.get(1)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a number as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let to_number = parse_json_number_as_f64(&second_value, self.source_range)?;
let third_value = self.args.get(2).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the third argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
let sketch_group = if let MemoryItem::SketchGroup(sg) = third_value {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the third argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
Ok((segment_name, to_number, sketch_group))
}
fn get_number_sketch_group(&self) -> Result<(f64, SketchGroup), KclError> {
// Iterate over our args, the first argument should be a number.
// The second argument should be a SketchGroup.
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()?;
let number = parse_json_number_as_f64(&first_value, self.source_range)?;
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
let sketch_group = if let MemoryItem::SketchGroup(sg) = second_value {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
Ok((number, sketch_group))
}
fn get_path_name_extrude_group(&self) -> Result<(String, ExtrudeGroup), KclError> {
// Iterate over our args, the first argument should be a UserVal with a string value.
// The second argument should be a ExtrudeGroup.
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let path_name = if let serde_json::Value::String(s) = first_value {
s.to_string()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a ExtrudeGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range],
})
})?;
let extrude_group = if let MemoryItem::ExtrudeGroup(sg) = second_value {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a ExtrudeGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range],
}));
};
Ok((path_name, extrude_group))
}
}
/// Returns the minimum of the given arguments.
/// TODO fix min
pub fn min(args: &mut Args) -> Result<MemoryItem, KclError> {
let nums = args.get_number_array()?;
let result = inner_min(nums);
args.make_user_val_from_f64(result)
}
/// Returns the minimum of the given arguments.
#[stdlib {
name = "min",
}]
fn inner_min(args: Vec<f64>) -> f64 {
let mut min = std::f64::MAX;
for arg in args.iter() {
if *arg < min {
min = *arg;
}
}
min
}
/// Returns the length of the given leg.
pub fn leg_length(args: &mut Args) -> Result<MemoryItem, KclError> {
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
let result = inner_leg_length(hypotenuse, leg);
args.make_user_val_from_f64(result)
}
/// Returns the length of the given leg.
#[stdlib {
name = "legLen",
}]
fn inner_leg_length(hypotenuse: f64, leg: f64) -> f64 {
(hypotenuse.powi(2) - f64::min(hypotenuse.abs(), leg.abs()).powi(2)).sqrt()
}
/// Returns the angle of the given leg for x.
pub fn leg_angle_x(args: &mut Args) -> Result<MemoryItem, KclError> {
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
let result = inner_leg_angle_x(hypotenuse, leg);
args.make_user_val_from_f64(result)
}
/// Returns the angle of the given leg for x.
#[stdlib {
name = "legAngX",
}]
fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).acos() * 180.0 / std::f64::consts::PI
}
/// Returns the angle of the given leg for y.
pub fn leg_angle_y(args: &mut Args) -> Result<MemoryItem, KclError> {
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
let result = inner_leg_angle_y(hypotenuse, leg);
args.make_user_val_from_f64(result)
}
/// Returns the angle of the given leg for y.
#[stdlib {
name = "legAngY",
}]
fn inner_leg_angle_y(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).asin() * 180.0 / std::f64::consts::PI
}
/// The primitive types that can be used in a KCL file.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Display, FromStr)]
#[serde(rename_all = "lowercase")]
#[display(style = "lowercase")]
pub enum Primitive {
/// A boolean value.
Bool,
/// A number value.
Number,
/// A string value.
String,
/// A uuid value.
Uuid,
}
#[cfg(test)]
mod tests {
use crate::std::StdLib;
#[test]
fn test_generate_stdlib_markdown_docs() {
let stdlib = StdLib::new();
let mut buf = String::new();
buf.push_str("<!--- DO NOT EDIT THIS FILE. IT IS AUTOMATICALLY GENERATED. -->\n\n");
buf.push_str("# KCL Standard Library\n\n");
// Generate a table of contents.
buf.push_str("## Table of Contents\n\n");
buf.push_str("* [Functions](#functions)\n");
for internal_fn in &stdlib.internal_fn_names {
if internal_fn.unpublished() || internal_fn.deprecated() {
continue;
}
buf.push_str(&format!("\t* [`{}`](#{})\n", internal_fn.name(), internal_fn.name()));
}
buf.push_str("\n\n");
buf.push_str("## Functions\n\n");
for internal_fn in &stdlib.internal_fn_names {
if internal_fn.unpublished() {
continue;
}
let mut fn_docs = String::new();
if internal_fn.deprecated() {
fn_docs.push_str(&format!("### {} DEPRECATED\n\n", internal_fn.name()));
} else {
fn_docs.push_str(&format!("### {}\n\n", internal_fn.name()));
}
fn_docs.push_str(&format!("{}\n\n", internal_fn.summary()));
fn_docs.push_str(&format!("{}\n\n", internal_fn.description()));
fn_docs.push_str("```\n");
fn_docs.push_str(&format!("{}(", internal_fn.name()));
for (i, arg) in internal_fn.args().iter().enumerate() {
if i > 0 {
fn_docs.push_str(", ");
}
fn_docs.push_str(&format!("{}: {}", arg.name, arg.type_));
}
fn_docs.push_str(") -> ");
fn_docs.push_str(&internal_fn.return_value().type_);
fn_docs.push_str("\n```\n\n");
fn_docs.push_str("#### Arguments\n\n");
for arg in internal_fn.args() {
let (format, should_be_indented) = arg.get_type_string().unwrap();
if let Some(description) = arg.description() {
fn_docs.push_str(&format!("* `{}`: `{}` - {}\n", arg.name, arg.type_, description));
} else {
fn_docs.push_str(&format!("* `{}`: `{}`\n", arg.name, arg.type_));
}
if should_be_indented {
fn_docs.push_str(&format!("```\n{}\n```\n", format));
}
}
fn_docs.push_str("\n#### Returns\n\n");
let return_type = internal_fn.return_value();
if let Some(description) = return_type.description() {
fn_docs.push_str(&format!("* `{}` - {}\n", return_type.type_, description));
} else {
fn_docs.push_str(&format!("* `{}`\n", return_type.type_));
}
let (format, should_be_indented) = return_type.get_type_string().unwrap();
if should_be_indented {
fn_docs.push_str(&format!("```\n{}\n```\n", format));
}
fn_docs.push_str("\n\n\n");
buf.push_str(&fn_docs);
}
expectorate::assert_contents("../../../docs/kcl.md", &buf);
}
#[test]
fn test_generate_stdlib_json_schema() {
let stdlib = StdLib::new();
let mut json_data = vec![];
for internal_fn in &stdlib.internal_fn_names {
json_data.push(internal_fn.to_json().unwrap());
}
expectorate::assert_contents(
"../../../docs/kcl.json",
&serde_json::to_string_pretty(&json_data).unwrap(),
);
}
}

View File

@ -0,0 +1,290 @@
//! Functions related to line segments.
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{MemoryItem, SketchGroup},
std::{utils::get_angle, Args},
};
/// Returns the segment end of x.
pub fn segment_end_x(args: &mut Args) -> Result<MemoryItem, KclError> {
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
let result = inner_segment_end_x(&segment_name, sketch_group, args)?;
args.make_user_val_from_f64(result)
}
/// Returns the segment end of x.
#[stdlib {
name = "segEndX",
}]
fn inner_segment_end_x(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let line = sketch_group.get_base_by_name_or_start(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(line.to[0])
}
/// Returns the segment end of y.
pub fn segment_end_y(args: &mut Args) -> Result<MemoryItem, KclError> {
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
let result = inner_segment_end_y(&segment_name, sketch_group, args)?;
args.make_user_val_from_f64(result)
}
/// Returns the segment end of y.
#[stdlib {
name = "segEndY",
}]
fn inner_segment_end_y(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let line = sketch_group.get_base_by_name_or_start(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(line.to[1])
}
/// Returns the last segment of x.
pub fn last_segment_x(args: &mut Args) -> Result<MemoryItem, KclError> {
let sketch_group = args.get_sketch_group()?;
let result = inner_last_segment_x(sketch_group, args)?;
args.make_user_val_from_f64(result)
}
/// Returns the last segment of x.
#[stdlib {
name = "lastSegX",
}]
fn inner_last_segment_x(sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let last_line = sketch_group
.value
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup with at least one segment, found `{:?}`",
sketch_group
),
source_ranges: vec![args.source_range],
})
})?
.get_base();
Ok(last_line.to[0])
}
/// Returns the last segment of y.
pub fn last_segment_y(args: &mut Args) -> Result<MemoryItem, KclError> {
let sketch_group = args.get_sketch_group()?;
let result = inner_last_segment_y(sketch_group, args)?;
args.make_user_val_from_f64(result)
}
/// Returns the last segment of y.
#[stdlib {
name = "lastSegY",
}]
fn inner_last_segment_y(sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let last_line = sketch_group
.value
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup with at least one segment, found `{:?}`",
sketch_group
),
source_ranges: vec![args.source_range],
})
})?
.get_base();
Ok(last_line.to[1])
}
/// Returns the length of the segment.
pub fn segment_length(args: &mut Args) -> Result<MemoryItem, KclError> {
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
let result = inner_segment_length(&segment_name, sketch_group, args)?;
args.make_user_val_from_f64(result)
}
/// Returns the length of the segment.
#[stdlib {
name = "segLen",
}]
fn inner_segment_length(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
let line = path.get_base();
let result = ((line.from[1] - line.to[1]).powi(2) + (line.from[0] - line.to[0]).powi(2)).sqrt();
Ok(result)
}
/// Returns the angle of the segment.
pub fn segment_angle(args: &mut Args) -> Result<MemoryItem, KclError> {
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
let result = inner_segment_angle(&segment_name, sketch_group, args)?;
args.make_user_val_from_f64(result)
}
/// Returns the angle of the segment.
#[stdlib {
name = "segAng",
}]
fn inner_segment_angle(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
let line = path.get_base();
let result = get_angle(&line.from, &line.to);
Ok(result)
}
/// Returns the angle to match the given length for x.
pub fn angle_to_match_length_x(args: &mut Args) -> Result<MemoryItem, KclError> {
let (segment_name, to, sketch_group) = args.get_segment_name_to_number_sketch_group()?;
let result = inner_angle_to_match_length_x(&segment_name, to, sketch_group, args)?;
args.make_user_val_from_f64(result)
}
/// Returns the angle to match the given length for x.
#[stdlib {
name = "angleToMatchLengthX",
}]
fn inner_angle_to_match_length_x(
segment_name: &str,
to: f64,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<f64, KclError> {
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
let line = path.get_base();
let length = ((line.from[1] - line.to[1]).powi(2) + (line.from[0] - line.to[0]).powi(2)).sqrt();
let last_line = sketch_group
.value
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup with at least one segment, found `{:?}`",
sketch_group
),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let diff = (to - last_line.to[0]).abs();
let angle_r = diff / length.acos();
if diff > length {
Ok(0.0)
} else {
Ok(angle_r * 180.0 / std::f64::consts::PI)
}
}
/// Returns the angle to match the given length for y.
pub fn angle_to_match_length_y(args: &mut Args) -> Result<MemoryItem, KclError> {
let (segment_name, to, sketch_group) = args.get_segment_name_to_number_sketch_group()?;
let result = inner_angle_to_match_length_y(&segment_name, to, sketch_group, args)?;
args.make_user_val_from_f64(result)
}
/// Returns the angle to match the given length for y.
#[stdlib {
name = "angleToMatchLengthY",
}]
fn inner_angle_to_match_length_y(
segment_name: &str,
to: f64,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<f64, KclError> {
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
let line = path.get_base();
let length = ((line.from[1] - line.to[1]).powi(2) + (line.from[0] - line.to[0]).powi(2)).sqrt();
let last_line = sketch_group
.value
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup with at least one segment, found `{:?}`",
sketch_group
),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let diff = (to - last_line.to[1]).abs();
let angle_r = diff / length.asin();
if diff > length {
Ok(0.0)
} else {
Ok(angle_r * 180.0 / std::f64::consts::PI)
}
}

View File

@ -0,0 +1,750 @@
//! Functions related to sketching.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::{ModelingCmd, Point3D};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{BasePath, GeoMeta, MemoryItem, Path, Point2d, Position, Rotation, SketchGroup},
std::{
utils::{get_x_component, get_y_component, intersection_with_parallel_line},
Args,
},
};
/// Data to draw a line to a point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum LineToData {
/// A point with a tag.
PointWithTag {
/// The to point.
to: [f64; 2],
/// The tag.
tag: String,
},
/// A point.
Point([f64; 2]),
}
/// Draw a line to a point.
pub fn line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (LineToData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_line_to(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line to a point.
#[stdlib {
name = "lineTo",
}]
fn inner_line_to(data: LineToData, sketch_group: SketchGroup, args: &Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let to = match data {
LineToData::PointWithTag { to, .. } => to,
LineToData::Point(to) => to,
};
let id = uuid::Uuid::new_v4();
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to,
name: if let LineToData::PointWithTag { tag, .. } = data {
tag.to_string()
} else {
"".to_string()
},
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(current_path);
Ok(new_sketch_group)
}
/// Data to draw a line to a point on an axis.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum AxisLineToData {
/// A point with a tag.
PointWithTag {
/// The to point.
to: f64,
/// The tag.
tag: String,
},
/// A point.
Point(f64),
}
/// Draw a line to a point on the x-axis.
pub fn x_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AxisLineToData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_x_line_to(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line to a point on the x-axis.
#[stdlib {
name = "xLineTo",
}]
fn inner_x_line_to(data: AxisLineToData, sketch_group: SketchGroup, args: &Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let line_to_data = match data {
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { to: [to, from.y], tag },
AxisLineToData::Point(data) => LineToData::Point([data, from.y]),
};
let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?;
Ok(new_sketch_group)
}
/// Draw a line to a point on the y-axis.
pub fn y_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AxisLineToData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_y_line_to(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line to a point on the y-axis.
#[stdlib {
name = "yLineTo",
}]
fn inner_y_line_to(data: AxisLineToData, sketch_group: SketchGroup, args: &Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let line_to_data = match data {
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { to: [from.x, to], tag },
AxisLineToData::Point(data) => LineToData::Point([from.x, data]),
};
let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?;
Ok(new_sketch_group)
}
/// Data to draw a line.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum LineData {
/// A point with a tag.
PointWithTag {
/// The to point.
to: PointOrDefault,
/// The tag.
tag: String,
},
/// A point.
Point([f64; 2]),
/// A string like `default`.
Default(String),
}
/// A point or a default value.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum PointOrDefault {
/// A point.
Point([f64; 2]),
/// A string like `default`.
Default(String),
}
impl PointOrDefault {
fn get_point_with_default(&self, default: [f64; 2]) -> [f64; 2] {
match self {
PointOrDefault::Point(point) => *point,
PointOrDefault::Default(_) => default,
}
}
}
/// Draw a line.
pub fn line(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (LineData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_line(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line.
#[stdlib {
name = "line",
}]
fn inner_line(data: LineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let default = [0.2, 1.0];
let inner_args = match &data {
LineData::PointWithTag { to, .. } => to.get_point_with_default(default),
LineData::Point(to) => *to,
LineData::Default(_) => default,
};
let to = [from.x + inner_args[0], from.y + inner_args[1]];
let id = uuid::Uuid::new_v4();
args.send_modeling_cmd(
id,
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Line {
end: Point3D {
x: to[0],
y: to[1],
z: 0.0,
},
},
},
)?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to,
name: if let LineData::PointWithTag { tag, .. } = data {
tag.to_string()
} else {
"".to_string()
},
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(current_path);
Ok(new_sketch_group)
}
/// Data to draw a line on an axis.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum AxisLineData {
/// The length with a tag.
LengthWithTag {
/// The length of the line.
length: f64,
/// The tag.
tag: String,
},
/// The length.
Length(f64),
}
/// Draw a line on the x-axis.
pub fn x_line(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AxisLineData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_x_line(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line on the x-axis.
#[stdlib {
name = "xLine",
}]
fn inner_x_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([length, 0.0]),
tag,
},
AxisLineData::Length(length) => LineData::Point([length, 0.0]),
};
let new_sketch_group = inner_line(line_data, sketch_group, args)?;
Ok(new_sketch_group)
}
/// Draw a line on the y-axis.
pub fn y_line(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AxisLineData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_y_line(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line on the y-axis.
#[stdlib {
name = "yLine",
}]
fn inner_y_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([0.0, length]),
tag,
},
AxisLineData::Length(length) => LineData::Point([0.0, length]),
};
let new_sketch_group = inner_line(line_data, sketch_group, args)?;
Ok(new_sketch_group)
}
/// Data to draw an angled line.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum AngledLineData {
/// An angle and length with a tag.
AngleWithTag {
/// The angle of the line.
angle: f64,
/// The length of the line.
length: f64,
/// The tag.
tag: String,
},
/// An angle and length.
AngleAndLength([f64; 2]),
}
/// Draw an angled line.
pub fn angled_line(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AngledLineData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_angled_line(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw an angled line.
#[stdlib {
name = "angledLine",
}]
fn inner_angled_line(
data: AngledLineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to: [f64; 2] = [
from.x + length * f64::cos(angle * std::f64::consts::PI / 180.0),
from.y + length * f64::sin(angle * std::f64::consts::PI / 180.0),
];
let id = uuid::Uuid::new_v4();
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to,
name: if let AngledLineData::AngleWithTag { tag, .. } = data {
tag.to_string()
} else {
"".to_string()
},
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(current_path);
Ok(new_sketch_group)
}
/// Draw an angled line of a given x length.
pub fn angled_line_of_x_length(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AngledLineData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_angled_line_of_x_length(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw an angled line of a given x length.
#[stdlib {
name = "angledLineOfXLength",
}]
fn inner_angled_line_of_x_length(
data: AngledLineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to = get_y_component(angle, length);
let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag {
to: PointOrDefault::Point(to),
tag,
}
} else {
LineData::Point(to)
},
sketch_group,
args,
)?;
Ok(new_sketch_group)
}
/// Data to draw an angled line to a point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum AngledLineToData {
/// An angle and point with a tag.
AngleWithTag {
/// The angle of the line.
angle: f64,
/// The point to draw to.
to: f64,
/// The tag.
tag: String,
},
/// An angle and point to draw to.
AngleAndPoint([f64; 2]),
}
/// Draw an angled line to a given x coordinate.
pub fn angled_line_to_x(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AngledLineToData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_angled_line_to_x(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw an angled line to a given x coordinate.
#[stdlib {
name = "angledLineToX",
}]
fn inner_angled_line_to_x(
data: AngledLineToData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let (angle, x_to) = match &data {
AngledLineToData::AngleWithTag { angle, to, .. } => (*angle, *to),
AngledLineToData::AngleAndPoint(angle_and_to) => (angle_and_to[0], angle_and_to[1]),
};
let x_component = x_to - from.x;
let y_component = x_component * f64::tan(angle * std::f64::consts::PI / 180.0);
let y_to = from.y + y_component;
let new_sketch_group = inner_line_to(
if let AngledLineToData::AngleWithTag { tag, .. } = data {
LineToData::PointWithTag { to: [x_to, y_to], tag }
} else {
LineToData::Point([x_to, y_to])
},
sketch_group,
args,
)?;
Ok(new_sketch_group)
}
/// Draw an angled line of a given y length.
pub fn angled_line_of_y_length(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AngledLineData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_angled_line_of_y_length(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw an angled line of a given y length.
#[stdlib {
name = "angledLineOfYLength",
}]
fn inner_angled_line_of_y_length(
data: AngledLineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to = get_x_component(angle, length);
let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag {
to: PointOrDefault::Point(to),
tag,
}
} else {
LineData::Point(to)
},
sketch_group,
args,
)?;
Ok(new_sketch_group)
}
/// Draw an angled line to a given y coordinate.
pub fn angled_line_to_y(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AngledLineToData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_angled_line_to_y(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw an angled line to a given y coordinate.
#[stdlib {
name = "angledLineToY",
}]
fn inner_angled_line_to_y(
data: AngledLineToData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let (angle, y_to) = match &data {
AngledLineToData::AngleWithTag { angle, to, .. } => (*angle, *to),
AngledLineToData::AngleAndPoint(angle_and_to) => (angle_and_to[0], angle_and_to[1]),
};
let y_component = y_to - from.y;
let x_component = y_component / f64::tan(angle * std::f64::consts::PI / 180.0);
let x_to = from.x + x_component;
let new_sketch_group = inner_line_to(
if let AngledLineToData::AngleWithTag { tag, .. } = data {
LineToData::PointWithTag { to: [x_to, y_to], tag }
} else {
LineToData::Point([x_to, y_to])
},
sketch_group,
args,
)?;
Ok(new_sketch_group)
}
/// Data for drawing an angled line that intersects with a given line.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
// TODO: make sure the docs on the args below are correct.
pub struct AngeledLineThatIntersectsData {
/// The angle of the line.
pub angle: f64,
/// The tag of the line to intersect with.
pub intersect_tag: String,
/// The offset from the intersecting line.
pub offset: Option<f64>,
/// The tag.
pub tag: Option<String>,
}
/// Draw an angled line that intersects with a given line.
pub fn angled_line_that_intersects(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AngeledLineThatIntersectsData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_angled_line_that_intersects(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw an angled line that intersects with a given line.
#[stdlib {
name = "angledLineThatIntersects",
}]
fn inner_angled_line_that_intersects(
data: AngeledLineThatIntersectsData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
let intersect_path = sketch_group
.get_path_by_name(&data.intersect_tag)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a line that exists in the given SketchGroup, found `{}`",
data.intersect_tag
),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let from = sketch_group.get_coords_from_paths()?;
let to = intersection_with_parallel_line(
&[intersect_path.from, intersect_path.to],
data.offset.unwrap_or_default(),
data.angle,
from.into(),
);
let line_to_data = if let Some(tag) = data.tag {
LineToData::PointWithTag { to, tag }
} else {
LineToData::Point(to)
};
let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?;
Ok(new_sketch_group)
}
/// Start a sketch at a given point.
pub fn start_sketch_at(args: &mut Args) -> Result<MemoryItem, KclError> {
let data: LineData = args.get_data()?;
let sketch_group = inner_start_sketch_at(data, args)?;
Ok(MemoryItem::SketchGroup(sketch_group))
}
/// Start a sketch at a given point.
#[stdlib {
name = "startSketchAt",
}]
fn inner_start_sketch_at(data: LineData, args: &mut Args) -> Result<SketchGroup, KclError> {
let default = [0.0, 0.0];
let to = match &data {
LineData::PointWithTag { to, .. } => to.get_point_with_default(default),
LineData::Point(to) => *to,
LineData::Default(_) => default,
};
let id = uuid::Uuid::new_v4();
let path_id = uuid::Uuid::new_v4();
args.send_modeling_cmd(path_id, ModelingCmd::StartPath {})?;
args.send_modeling_cmd(
id,
ModelingCmd::MovePathPen {
path: path_id,
to: Point3D {
x: to[0],
y: to[1],
z: 0.0,
},
},
)?;
let current_path = BasePath {
from: to,
to,
name: if let LineData::PointWithTag { tag, .. } = data {
tag.to_string()
} else {
"".to_string()
},
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
};
let sketch_group = SketchGroup {
id: path_id,
position: Position([0.0, 0.0, 0.0]),
rotation: Rotation([0.0, 0.0, 0.0, 1.0]),
value: vec![],
start: current_path,
meta: vec![args.source_range.into()],
};
Ok(sketch_group)
}
/// Close the current sketch.
pub fn close(args: &mut Args) -> Result<MemoryItem, KclError> {
let sketch_group = args.get_sketch_group()?;
let new_sketch_group = inner_close(sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Close the current sketch.
#[stdlib {
name = "close",
}]
fn inner_close(sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let to: Point2d = sketch_group.start.from.into();
let id = uuid::Uuid::new_v4();
args.send_modeling_cmd(
id,
ModelingCmd::ClosePath {
path_id: sketch_group.id,
},
)?;
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(Path::ToPoint {
base: BasePath {
from: from.into(),
to: to.into(),
// TODO: should we use a different name?
name: "".into(),
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
});
Ok(new_sketch_group)
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::std::sketch::{LineData, PointOrDefault};
#[test]
fn test_deserialize_line_data() {
let mut str_json = "\"default\"".to_string();
let data: LineData = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, LineData::Default("default".to_string()));
let data = LineData::Point([0.0, 1.0]);
str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, "[0.0,1.0]");
str_json = "[0, 1]".to_string();
let data: LineData = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, LineData::Point([0.0, 1.0]));
str_json = "{ \"to\": [0.0, 1.0], \"tag\": \"thing\" }".to_string();
let data: LineData = serde_json::from_str(&str_json).unwrap();
assert_eq!(
data,
LineData::PointWithTag {
to: PointOrDefault::Point([0.0, 1.0]),
tag: "thing".to_string()
}
);
}
}

View File

@ -0,0 +1,244 @@
pub fn get_angle(a: &[f64; 2], b: &[f64; 2]) -> f64 {
let x = b[0] - a[0];
let y = b[1] - a[1];
normalise_angle(y.atan2(x) * 180.0 / std::f64::consts::PI)
}
pub fn normalise_angle(angle: f64) -> f64 {
let result = ((angle % 360.0) + 360.0) % 360.0;
if result > 180.0 {
result - 360.0
} else {
result
}
}
#[allow(dead_code)]
pub fn clockwise_sign(points: &[[f64; 2]]) -> i32 {
let mut sum = 0.0;
for i in 0..points.len() {
let current_point = points[i];
let next_point = points[(i + 1) % points.len()];
sum += (next_point[0] - current_point[0]) * (next_point[1] + current_point[1]);
}
if sum >= 0.0 {
1
} else {
-1
}
}
#[allow(dead_code)]
pub fn normalize_rad(angle: f64) -> f64 {
let draft = angle % (2.0 * std::f64::consts::PI);
if draft < 0.0 {
draft + 2.0 * std::f64::consts::PI
} else {
draft
}
}
/// Gives the ▲-angle between from and to angles (shortest path), use radians.
///
/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
/// # Examples
///
/// ```
/// assert_eq!(
/// kcl_lib::std::utils::delta_angle(std::f64::consts::PI / 8.0, std::f64::consts::PI / 4.0),
/// std::f64::consts::PI / 8.0
/// );
/// ```
#[allow(dead_code)]
pub fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
let norm_from_angle = normalize_rad(from_angle);
let norm_to_angle = normalize_rad(to_angle);
let provisional = norm_to_angle - norm_from_angle;
if provisional > -std::f64::consts::PI && provisional <= std::f64::consts::PI {
return provisional;
}
if provisional > std::f64::consts::PI {
return provisional - 2.0 * std::f64::consts::PI;
}
if provisional < -std::f64::consts::PI {
return provisional + 2.0 * std::f64::consts::PI;
}
0.0
}
/// Calculates the distance between two points.
///
/// # Examples
///
/// ```
/// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[0.0, 5.0]),
/// 5.0
/// );
/// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[3.0, 4.0]),
/// 5.0
/// );
/// ```
#[allow(dead_code)]
pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 {
let x1 = point_a[0];
let y1 = point_a[1];
let x2 = point_b[0];
let y2 = point_b[1];
((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt()
}
pub fn calculate_intersection_of_two_lines(line1: &[[f64; 2]; 2], line2_angle: f64, line2_point: [f64; 2]) -> [f64; 2] {
let line2_point_b = [
line2_point[0] + f64::cos(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
line2_point[1] + f64::sin(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
];
intersect(line1[0], line1[1], line2_point, line2_point_b)
}
pub fn intersect(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2], p4: [f64; 2]) -> [f64; 2] {
let slope = |p1: [f64; 2], p2: [f64; 2]| (p1[1] - p2[1]) / (p1[0] - p2[0]);
let constant = |p1: [f64; 2], p2: [f64; 2]| p1[1] - slope(p1, p2) * p1[0];
let get_y = |for_x: f64, p1: [f64; 2], p2: [f64; 2]| slope(p1, p2) * for_x + constant(p1, p2);
if p1[0] == p2[0] {
return [p1[0], get_y(p1[0], p3, p4)];
}
if p3[0] == p4[0] {
return [p3[0], get_y(p3[0], p1, p2)];
}
let x = (constant(p3, p4) - constant(p1, p2)) / (slope(p1, p2) - slope(p3, p4));
let y = get_y(x, p1, p2);
[x, y]
}
pub fn intersection_with_parallel_line(
line1: &[[f64; 2]; 2],
line1_offset: f64,
line2_angle: f64,
line2_point: [f64; 2],
) -> [f64; 2] {
calculate_intersection_of_two_lines(&offset_line(line1_offset, line1[0], line1[1]), line2_angle, line2_point)
}
fn offset_line(offset: f64, p1: [f64; 2], p2: [f64; 2]) -> [[f64; 2]; 2] {
if p1[0] == p2[0] {
let direction = (p1[1] - p2[1]).signum();
return [[p1[0] + offset * direction, p1[1]], [p2[0] + offset * direction, p2[1]]];
}
if p1[1] == p2[1] {
let direction = (p2[0] - p1[0]).signum();
return [[p1[0], p1[1] + offset * direction], [p2[0], p2[1] + offset * direction]];
}
let x_offset = offset / f64::sin(f64::atan2(p1[1] - p2[1], p1[0] - p2[0]));
[[p1[0] + x_offset, p1[1]], [p2[0] + x_offset, p2[1]]]
}
pub fn get_y_component(angle_degree: f64, x_component: f64) -> [f64; 2] {
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360
let y_component = x_component * f64::tan(normalised_angle * std::f64::consts::PI / 180.0);
let sign = if normalised_angle > 90.0 && normalised_angle <= 270.0 {
-1.0
} else {
1.0
};
[sign * x_component, sign * y_component]
}
pub fn get_x_component(angle_degree: f64, y_component: f64) -> [f64; 2] {
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360
let x_component = y_component / f64::tan(normalised_angle * std::f64::consts::PI / 180.0);
let sign = if normalised_angle > 180.0 && normalised_angle <= 360.0 {
-1.0
} else {
1.0
};
[sign * x_component, sign * y_component]
}
#[cfg(test)]
mod tests {
// Here you can bring your functions into scope
use pretty_assertions::assert_eq;
use super::{get_x_component, get_y_component};
static EACH_QUAD: [(i32, [i32; 2]); 12] = [
(-315, [1, 1]),
(-225, [-1, 1]),
(-135, [-1, -1]),
(-45, [1, -1]),
(45, [1, 1]),
(135, [-1, 1]),
(225, [-1, -1]),
(315, [1, -1]),
(405, [1, 1]),
(495, [-1, 1]),
(585, [-1, -1]),
(675, [1, -1]),
];
#[test]
fn test_get_y_component() {
let mut expected = Vec::new();
let mut results = Vec::new();
for &(angle, expected_result) in EACH_QUAD.iter() {
let res = get_y_component(angle as f64, 1.0);
results.push([res[0].round() as i32, res[1].round() as i32]);
expected.push(expected_result);
}
assert_eq!(results, expected);
let result = get_y_component(0.0, 1.0);
assert_eq!(result[0] as i32, 1);
assert_eq!(result[1] as i32, 0);
let result = get_y_component(90.0, 1.0);
assert_eq!(result[0] as i32, 1);
assert!(result[1] > 100000.0);
let result = get_y_component(180.0, 1.0);
assert_eq!(result[0] as i32, -1);
assert!((result[1] - 0.0).abs() < f64::EPSILON);
let result = get_y_component(270.0, 1.0);
assert_eq!(result[0] as i32, -1);
assert!(result[1] < -100000.0);
}
#[test]
fn test_get_x_component() {
let mut expected = Vec::new();
let mut results = Vec::new();
for &(angle, expected_result) in EACH_QUAD.iter() {
let res = get_x_component(angle as f64, 1.0);
results.push([res[0].round() as i32, res[1].round() as i32]);
expected.push(expected_result);
}
assert_eq!(results, expected);
let result = get_x_component(0.0, 1.0);
assert!(result[0] > 100000.0);
assert_eq!(result[1] as i32, 1);
let result = get_x_component(90.0, 1.0);
assert!((result[0] - 0.0).abs() < f64::EPSILON);
assert_eq!(result[1] as i32, 1);
let result = get_x_component(180.0, 1.0);
assert!(result[0] < -100000.0);
assert_eq!(result[1] as i32, 1);
let result = get_x_component(270.0, 1.0);
assert!((result[0] - 0.0).abs() < f64::EPSILON);
assert_eq!(result[1] as i32, -1);
}
}

View File

@ -1,7 +1,6 @@
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, ts_rs::TS)]
#[ts(export)]
@ -30,13 +29,24 @@ pub struct Token {
pub value: String,
}
impl From<Token> for crate::executor::SourceRange {
fn from(token: Token) -> Self {
Self([token.start, token.end])
}
}
impl From<&Token> for crate::executor::SourceRange {
fn from(token: &Token) -> Self {
Self([token.start, token.end])
}
}
lazy_static! {
static ref NUMBER: Regex = Regex::new(r"^-?\d+(\.\d+)?").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 STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
static ref OPERATOR: Regex =
Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
static ref OPERATOR: Regex = Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
static ref BLOCK_START: Regex = Regex::new(r"^\{").unwrap();
static ref BLOCK_END: Regex = Regex::new(r"^\}").unwrap();
static ref PARAN_START: Regex = Regex::new(r"^\(").unwrap();
@ -103,9 +113,7 @@ fn is_block_comment(character: &str) -> bool {
}
fn match_first(str: &str, regex: &Regex) -> Option<String> {
regex
.find(str)
.map(|the_match| the_match.as_str().to_string())
regex.find(str).map(|the_match| the_match.as_str().to_string())
}
fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
@ -240,11 +248,7 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
}
pub fn lexer(str: &str) -> Vec<Token> {
fn recursively_tokenise(
str: &str,
current_index: usize,
previous_tokens: Vec<Token>,
) -> Vec<Token> {
fn recursively_tokenise(str: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
if current_index >= str.len() {
return previous_tokens;
}
@ -260,19 +264,12 @@ pub fn lexer(str: &str) -> Vec<Token> {
recursively_tokenise(str, 0, Vec::new())
}
// wasm_bindgen wrapper for lexer
// test for this function and by extension lexer are done in javascript land src/lang/tokeniser.test.ts
#[wasm_bindgen]
pub fn lexer_js(str: &str) -> Result<JsValue, JsError> {
let tokens = lexer(str);
Ok(serde_wasm_bindgen::to_value(&tokens)?)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn is_number_test() {
assert!(is_number("1"));

View File

@ -0,0 +1,6 @@
max_width = 120
edition = "2018"
format_code_in_doc_comments = true
format_strings = false
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -1,286 +0,0 @@
//! Data types for the AST.
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Program {
pub start: usize,
pub end: usize,
pub body: Vec<BodyItem>,
pub non_code_meta: NoneCodeMeta,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub enum BodyItem {
ExpressionStatement(ExpressionStatement),
VariableDeclaration(VariableDeclaration),
ReturnStatement(ReturnStatement),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Value {
Literal(Box<Literal>),
Identifier(Box<Identifier>),
BinaryExpression(Box<BinaryExpression>),
FunctionExpression(Box<FunctionExpression>),
CallExpression(Box<CallExpression>),
PipeExpression(Box<PipeExpression>),
PipeSubstitution(Box<PipeSubstitution>),
ArrayExpression(Box<ArrayExpression>),
ObjectExpression(Box<ObjectExpression>),
MemberExpression(Box<MemberExpression>),
UnaryExpression(Box<UnaryExpression>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub enum BinaryPart {
Literal(Box<Literal>),
Identifier(Box<Identifier>),
BinaryExpression(Box<BinaryExpression>),
CallExpression(Box<CallExpression>),
UnaryExpression(Box<UnaryExpression>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct NoneCodeNode {
pub start: usize,
pub end: usize,
pub value: String,
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct NoneCodeMeta {
pub none_code_nodes: HashMap<usize, NoneCodeNode>,
pub start: Option<NoneCodeNode>,
}
// implement Deserialize manually because we to force the keys of none_code_nodes to be usize
// and by default the ts type { [statementIndex: number]: NoneCodeNode } serializes to a string i.e. "0", "1", etc.
impl<'de> Deserialize<'de> for NoneCodeMeta {
fn deserialize<D>(deserializer: D) -> Result<NoneCodeMeta, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct NoneCodeMetaHelper {
none_code_nodes: HashMap<String, NoneCodeNode>,
start: Option<NoneCodeNode>,
}
let helper = NoneCodeMetaHelper::deserialize(deserializer)?;
let mut none_code_nodes = HashMap::new();
for (key, value) in helper.none_code_nodes {
none_code_nodes.insert(key.parse().map_err(serde::de::Error::custom)?, value);
}
Ok(NoneCodeMeta {
none_code_nodes,
start: helper.start,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ExpressionStatement {
pub start: usize,
pub end: usize,
pub expression: Value,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct CallExpression {
pub start: usize,
pub end: usize,
pub callee: Identifier,
pub arguments: Vec<Value>,
pub optional: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct VariableDeclaration {
pub start: usize,
pub end: usize,
pub declarations: Vec<VariableDeclarator>,
pub kind: String, // Change to enum if there are specific values
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct VariableDeclarator {
pub start: usize,
pub end: usize,
pub id: Identifier,
pub init: Value,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct Literal {
pub start: usize,
pub end: usize,
pub value: serde_json::Value,
pub raw: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct Identifier {
pub start: usize,
pub end: usize,
pub name: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct PipeSubstitution {
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ArrayExpression {
pub start: usize,
pub end: usize,
pub elements: Vec<Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ObjectExpression {
pub start: usize,
pub end: usize,
pub properties: Vec<ObjectProperty>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ObjectProperty {
pub start: usize,
pub end: usize,
pub key: Identifier,
pub value: Value,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub enum MemberObject {
MemberExpression(Box<MemberExpression>),
Identifier(Box<Identifier>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub enum LiteralIdentifier {
Identifier(Box<Identifier>),
Literal(Box<Literal>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct MemberExpression {
pub start: usize,
pub end: usize,
pub object: MemberObject,
pub property: LiteralIdentifier,
pub computed: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
pub struct ObjectKeyInfo {
pub key: LiteralIdentifier,
pub index: usize,
pub computed: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct BinaryExpression {
pub start: usize,
pub end: usize,
pub operator: String,
pub left: BinaryPart,
pub right: BinaryPart,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct UnaryExpression {
pub start: usize,
pub end: usize,
pub operator: String,
pub argument: BinaryPart,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase", tag = "type")]
pub struct PipeExpression {
pub start: usize,
pub end: usize,
pub body: Vec<Value>,
pub non_code_meta: NoneCodeMeta,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct FunctionExpression {
pub start: usize,
pub end: usize,
pub id: Option<Identifier>,
pub params: Vec<Identifier>,
pub body: BlockStatement,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase", tag = "type")]
pub struct BlockStatement {
pub start: usize,
pub end: usize,
pub body: Vec<BodyItem>,
pub non_code_meta: NoneCodeMeta,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ReturnStatement {
pub start: usize,
pub end: usize,
pub argument: Value,
}

View File

@ -1,39 +0,0 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum KclError {
#[error("syntax: {0:?}")]
Syntax(KclErrorDetails),
#[error("semantic: {0:?}")]
Semantic(KclErrorDetails),
#[error("type: {0:?}")]
Type(KclErrorDetails),
#[error("unimplemented: {0:?}")]
Unimplemented(KclErrorDetails),
#[error("value already defined: {0:?}")]
ValueAlreadyDefined(KclErrorDetails),
#[error("undefined value: {0:?}")]
UndefinedValue(KclErrorDetails),
#[error("invalid expression: {0:?}")]
InvalidExpression(crate::math_parser::MathExpression),
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")]
pub source_ranges: Vec<[i32; 2]>,
#[serde(rename = "msg")]
pub message: String,
}
/// This is different than to_string() in that it will serialize the Error
/// the struct as JSON so we can deserialize it on the js side.
impl From<KclError> for String {
fn from(error: KclError) -> Self {
serde_json::to_string(&error).unwrap()
}
}

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