Compare commits

..

10 Commits

Author SHA1 Message Date
b4fb903bd0 Add recursive isEven() test 2024-07-22 20:03:49 -04:00
1b8688f274 Add lexical scope and redefining variables in functions (#3015)
* Fix to allow variable shadowing inside functions

* Implement closures

* Fix KCL test code to not reference future tag definition

* Remove tag declarator from function parameters

This is an example where the scoping change revealed a subtle issue
with TagDeclarators.  You cannot bind a new tag using a function
parameter.

The issue is that evaluating a TagDeclarator like $foo binds an
identifier to its corresponding TagIdentifier, but returns the
TagDeclarator.  If you have a TagDeclarator passed in as a parameter
to a function, you can never get its corresponding TagIdentifier.

This seems like a case where TagDeclarator evaluation needs to be
revisited, especially now that we have scoped tags.

* Fix to query return, functions, and tag declarator AST nodes correctly
2024-07-22 19:43:40 -04:00
397839da84 Fix syntax highlighting on code pane open/close (#3083) 2024-07-20 01:45:38 -07:00
ac120838e5 setIsLoading false earlier (#3072)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-19 22:18:31 -04:00
e6a2ac9c4a Typecheck KCL args via generics, not handwritten impls (#3025)
In KCL, arguments to functions are passed in the Args struct. This struct contains a list of args, but each arg could be any KCL type (they're stored in an enum of all possible types). To get args of the correct type, these enums are fallibly converted into the type expected for the matching parameter.

Until now, the fallible conversion was handwritten for nearly each function. This is unnecessary, I've replaced it with composable traits.
2024-07-19 20:30:13 -05:00
6e7e6e96cf Make engine reconnection test pass every time (#3066)
* Ensure that isFreezeFrame is reset by isFirstRender, because it can't be a freeze frame if it's the first render

* `restart`-type engine starts should count as first renders

* Ensure we don't see a loading spinner after network is reconnected in test

* Make `waitForPageLoad` robust against if the page has already loaded
and make it actually wait for the Start Sketch button to be enabled

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-07-18 16:16:17 -04:00
73e155d79b Fix JS error about fill-rule when opening user menu (#3069) 2024-07-18 20:13:40 +00:00
a782f26ec2 Use ..Default::default() with a struct constructor (#3068)
As @jtran pointed out - I had misunderstood the behavior of
Default::default(), we can instead rely on this syntax to do the same
thing. This won't use each field's default value -- rather, it'll use
the type's Default, and override each field. Neat!

Signed-off-by: Paul Tagliamonte <paul@zoo.dev>
2024-07-18 15:20:50 -04:00
01076c3aed Remove sidebar menus in favor of lil' popovers (#3046)
* Convert user menu to a popover from a sidebar

* Move the user menu over to the left menu cluster

* Replace project sidebar with popover-style menu

* Styling tweaks, give export button a proper tooltip when disabled

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

* Filter orphan breaks, tweak space to remove mouse gaps

* Unify with and without avatar image code

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

* Rerun CI

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

* Rerun CI

* Prepare to move UserSidebarMenu over to right

* Revert AppHeader tweaks

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

* Rerun CI

* Fix typo in README

* Fix export E2E tests that relied on button text

* Missed the data-testid we used to have on the data-testid we had on the settings button

* Dang I missed another testId

* Update snapshots

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

* Rerun CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-18 14:29:15 -04:00
fe512611ac Fix doc comment to match generated docs (#3067) 2024-07-18 18:00:06 +00:00
61 changed files with 1664 additions and 1287 deletions

View File

@ -7221,6 +7221,7 @@ test.describe('Test network and connection issues', () => {
// Expect the network to be up // Expect the network to be up
await expect(page.getByText('Network Health (Connected)')).toBeVisible() await expect(page.getByText('Network Health (Connected)')).toBeVisible()
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
// Click off the code pane. // Click off the code pane.
await page.mouse.click(100, 100) await page.mouse.click(100, 100)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -16,14 +16,14 @@ export const TEST_COLORS = {
} as const } as const
async function waitForPageLoad(page: Page) { async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone // wait for all spinners to be gone
await page await expect(page.getByTestId('loading')).not.toBeAttached({
.getByTestId('loading') timeout: 20_000,
.waitFor({ state: 'detached', timeout: 20_000 }) })
await page.getByTestId('start-sketch').waitFor() await expect(page.getByTestId('start-sketch')).toBeEnabled({
timeout: 20_000,
})
} }
async function removeCurrentCode(page: Page) { async function removeCurrentCode(page: Page) {
@ -471,8 +471,10 @@ export const doExport = async (
page: Page page: Page
): Promise<Paths> => { ): Promise<Paths> => {
await page.getByRole('button', { name: APP_NAME }).click() await page.getByRole('button', { name: APP_NAME }).click()
await expect(page.getByRole('button', { name: 'Export Part' })).toBeVisible() await expect(
await page.getByRole('button', { name: 'Export Part' }).click() page.getByRole('button', { name: 'Export', exact: false })
).toBeVisible()
await page.getByRole('button', { name: 'Export', exact: false }).click()
await expect(page.getByTestId('command-bar')).toBeVisible() await expect(page.getByTestId('command-bar')).toBeVisible()
// Go through export via command bar // Go through export via command bar

View File

@ -77,7 +77,7 @@ describe('ZMA authorized user flows', () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]') const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton) await click(menuButton)
const settingsButton = await $('[data-testid="settings-button"]') const settingsButton = await $('[data-testid="user-settings"]')
await click(settingsButton) await click(settingsButton)
const projectDirInput = await $('[data-testid="project-directory-input"]') const projectDirInput = await $('[data-testid="project-directory-input"]')

108
src-tauri/Cargo.lock generated
View File

@ -332,7 +332,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -367,7 +367,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -407,7 +407,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -550,7 +550,7 @@ dependencies = [
"proc-macro-crate 3.1.0", "proc-macro-crate 3.1.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"syn_derive", "syn_derive",
] ]
@ -823,7 +823,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1073,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1083,7 +1083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1107,7 +1107,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim 0.10.0", "strsim 0.10.0",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1118,7 +1118,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1179,7 +1179,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"synstructure", "synstructure",
] ]
@ -1216,7 +1216,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1227,7 +1227,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1288,7 +1288,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1320,7 +1320,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1427,7 +1427,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1588,7 +1588,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1704,7 +1704,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1980,7 +1980,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2008,7 +2008,7 @@ dependencies = [
"inflections", "inflections",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2083,7 +2083,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3377,7 +3377,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.8.3", "regex-syntax 0.8.3",
"structmeta", "structmeta",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3496,7 +3496,7 @@ dependencies = [
"phf_shared 0.11.2", "phf_shared 0.11.2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3564,7 +3564,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4438,7 +4438,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals", "serde_derive_internals",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4558,7 +4558,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4569,7 +4569,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4602,7 +4602,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4623,7 +4623,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4665,7 +4665,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4933,7 +4933,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4944,7 +4944,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4966,7 +4966,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -4999,9 +4999,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.70" version = "2.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5017,7 +5017,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -5034,7 +5034,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -5251,7 +5251,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"syn 2.0.70", "syn 2.0.71",
"tauri-utils", "tauri-utils",
"thiserror", "thiserror",
"time", "time",
@ -5269,7 +5269,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"tauri-codegen", "tauri-codegen",
"tauri-utils", "tauri-utils",
] ]
@ -5627,22 +5627,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.61" version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.61" version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -5740,7 +5740,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -5940,7 +5940,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -5969,7 +5969,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -6099,7 +6099,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"termcolor", "termcolor",
] ]
@ -6316,7 +6316,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -6415,7 +6415,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -6449,7 +6449,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -6590,7 +6590,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -6696,7 +6696,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -6707,7 +6707,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -7159,7 +7159,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]

View File

@ -47,7 +47,6 @@ import {
PipeExpression, PipeExpression,
Program, Program,
ProgramMemory, ProgramMemory,
programMemoryInit,
recast, recast,
SketchGroup, SketchGroup,
ExtrudeGroup, ExtrudeGroup,
@ -130,7 +129,7 @@ export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
export class SceneEntities { export class SceneEntities {
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
scene: Scene scene: Scene
sceneProgramMemory: ProgramMemory = { root: {}, return: null } sceneProgramMemory: ProgramMemory = ProgramMemory.empty()
activeSegments: { [key: string]: Group } = {} activeSegments: { [key: string]: Group } = {}
intersectionPlane: Mesh | null = null intersectionPlane: Mesh | null = null
axisGroup: Group | null = null axisGroup: Group | null = null
@ -550,9 +549,9 @@ export class SceneEntities {
const variableDeclarationName = const variableDeclarationName =
_node1.node?.declarations?.[0]?.id?.name || '' _node1.node?.declarations?.[0]?.id?.name || ''
const sg = kclManager.programMemory.root[ const sg = kclManager.programMemory.get(
variableDeclarationName variableDeclarationName
] as SketchGroup ) as SketchGroup
const lastSeg = sg.value.slice(-1)[0] || sg.start const lastSeg = sg.value.slice(-1)[0] || sg.start
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1` const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
@ -768,9 +767,9 @@ export class SceneEntities {
programMemoryOverride, programMemoryOverride,
}) })
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.root[ const sketchGroup = programMemory.get(
variableDeclarationName variableDeclarationName
] as SketchGroup ) as SketchGroup
const sgPaths = sketchGroup.value const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -820,9 +819,9 @@ export class SceneEntities {
// Prepare to update the THREEjs scene // Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.root[ const sketchGroup = programMemory.get(
variableDeclarationName variableDeclarationName
] as SketchGroup ) as SketchGroup
const sgPaths = sketchGroup.value const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -1081,9 +1080,9 @@ export class SceneEntities {
}) })
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const maybeSketchGroup = programMemory.root[variableDeclarationName] const maybeSketchGroup = programMemory.get(variableDeclarationName)
let sketchGroup = undefined let sketchGroup = undefined
if (maybeSketchGroup.type === 'SketchGroup') { if (maybeSketchGroup?.type === 'SketchGroup') {
sketchGroup = maybeSketchGroup sketchGroup = maybeSketchGroup
} else if ((maybeSketchGroup as ExtrudeGroup).sketchGroup) { } else if ((maybeSketchGroup as ExtrudeGroup).sketchGroup) {
sketchGroup = (maybeSketchGroup as ExtrudeGroup).sketchGroup sketchGroup = (maybeSketchGroup as ExtrudeGroup).sketchGroup
@ -1773,7 +1772,7 @@ function prepareTruncatedMemoryAndAst(
if (err(_node)) return _node if (err(_node)) return _node
const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || '' const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || ''
const lastSeg = ( const lastSeg = (
programMemory.root[variableDeclarationName] as SketchGroup programMemory.get(variableDeclarationName) as SketchGroup
).value.slice(-1)[0] ).value.slice(-1)[0]
if (draftSegment) { if (draftSegment) {
// truncatedAst needs to setup with another segment at the end // truncatedAst needs to setup with another segment at the end
@ -1824,33 +1823,27 @@ function prepareTruncatedMemoryAndAst(
..._ast, ..._ast,
body: [JSON.parse(JSON.stringify(_ast.body[bodyIndex]))], body: [JSON.parse(JSON.stringify(_ast.body[bodyIndex]))],
} }
const programMemoryOverride = programMemoryInit()
if (err(programMemoryOverride)) return programMemoryOverride
// Grab all the TagDeclarators and TagIdentifiers from memory. // Grab all the TagDeclarators and TagIdentifiers from memory.
let start = _node.node.start let start = _node.node.start
for (const key in programMemory.root) { const programMemoryOverride = programMemory.filterVariables(true, (value) => {
const value = programMemory.root[key]
if (!('__meta' in value)) {
continue
}
if ( if (
!('__meta' in value) ||
value.__meta === undefined || value.__meta === undefined ||
value.__meta.length === 0 || value.__meta.length === 0 ||
value.__meta[0].sourceRange === undefined value.__meta[0].sourceRange === undefined
) { ) {
continue return false
} }
if (value.__meta[0].sourceRange[0] >= start) { if (value.__meta[0].sourceRange[0] >= start) {
// We only want things before our start point. // We only want things before our start point.
continue return false
} }
if (value.type === 'TagIdentifier') { return value.type === 'TagIdentifier'
programMemoryOverride.root[key] = JSON.parse(JSON.stringify(value)) })
} if (err(programMemoryOverride)) return programMemoryOverride
}
for (let i = 0; i < bodyIndex; i++) { for (let i = 0; i < bodyIndex; i++) {
const node = _ast.body[i] const node = _ast.body[i]
@ -1858,12 +1851,15 @@ function prepareTruncatedMemoryAndAst(
continue continue
} }
const name = node.declarations[0].id.name const name = node.declarations[0].id.name
// const memoryItem = kclManager.programMemory.root[name] const memoryItem = programMemory.get(name)
const memoryItem = programMemory.root[name]
if (!memoryItem) { if (!memoryItem) {
continue continue
} }
programMemoryOverride.root[name] = JSON.parse(JSON.stringify(memoryItem)) const error = programMemoryOverride.set(
name,
JSON.parse(JSON.stringify(memoryItem))
)
if (err(error)) return error
} }
return { return {
truncatedAst, truncatedAst,
@ -1900,7 +1896,7 @@ export function sketchGroupFromPathToNode({
) )
if (err(_varDec)) return _varDec if (err(_varDec)) return _varDec
const varDec = _varDec.node const varDec = _varDec.node
const result = programMemory.root[varDec?.id?.name || ''] const result = programMemory.get(varDec?.id?.name || '')
if (result?.type === 'ExtrudeGroup') { if (result?.type === 'ExtrudeGroup') {
return result.sketchGroup return result.sketchGroup
} }

View File

@ -49,9 +49,9 @@ export const AppHeader = ({
<> <>
<CommandBarOpenButton /> <CommandBarOpenButton />
<RefreshButton /> <RefreshButton />
<UserSidebarMenu user={user} />
</> </>
)} )}
<UserSidebarMenu user={user} />
</div> </div>
</header> </header>
) )

View File

@ -1,5 +1,5 @@
import { useEffect, useState, useRef } from 'react' import { useEffect, useState, useRef } from 'react'
import { parse, BinaryPart, Value } from '../lang/wasm' import { parse, BinaryPart, Value, ProgramMemory } from '../lang/wasm'
import { import {
createIdentifier, createIdentifier,
createLiteral, createLiteral,
@ -120,8 +120,7 @@ export function useCalc({
}, []) }, [])
useEffect(() => { useEffect(() => {
const allVarNames = Object.keys(programMemory.root) if (programMemory.has(newVariableName)) {
if (allVarNames.includes(newVariableName)) {
setIsNewVariableNameUnique(false) setIsNewVariableNameUnique(false)
} else { } else {
setIsNewVariableNameUnique(true) setIsNewVariableNameUnique(true)
@ -143,17 +142,20 @@ export function useCalc({
const code = `const __result__ = ${value}` const code = `const __result__ = ${value}`
const ast = parse(code) const ast = parse(code)
if (trap(ast)) return if (trap(ast)) return
const _programMem: any = { root: {}, return: null } const _programMem: ProgramMemory = ProgramMemory.empty()
availableVarInfo.variables.forEach(({ key, value }) => { for (const { key, value } of availableVarInfo.variables) {
_programMem.root[key] = { type: 'userVal', value, __meta: [] } const error = _programMem.set(key, {
}) type: 'UserVal',
value,
__meta: [],
})
if (trap(error)) return
}
executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: JSON.parse( programMemoryOverride: kclManager.programMemory.clone(),
JSON.stringify(kclManager.programMemory)
),
}).then(({ programMemory }) => { }).then(({ programMemory }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
@ -163,7 +165,7 @@ export function useCalc({
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value const result = programMemory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
}) })

View File

@ -311,6 +311,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
link: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.5864 4.46513C11.9532 3.09829 14.1693 3.09829 15.5361 4.46513C16.903 5.83196 16.903 8.04804 15.5361 9.41488L13.5364 11.4147C13.5839 10.9639 13.5635 10.5074 13.4752 10.0616L14.829 8.70777C15.8053 7.73146 15.8053 6.14855 14.829 5.17224C13.8527 4.19592 12.2698 4.19592 11.2935 5.17224L9.17217 7.29356C8.19586 8.26987 8.19586 9.85278 9.17217 10.8291C9.53458 11.1915 9.98056 11.4194 10.4481 11.5127C10.3749 11.6902 10.2662 11.8565 10.122 12.0007L9.76392 12.3587C9.28973 12.1899 8.84465 11.9158 8.46507 11.5362C7.09823 10.1694 7.09823 7.95328 8.46507 6.58645L10.5864 4.46513ZM4.46507 10.5864L6.46488 8.58663C6.41734 9.03738 6.43772 9.49394 6.52601 9.93972L5.17217 11.2935C4.19586 12.2699 4.19586 13.8528 5.17217 14.8291C6.14849 15.8054 7.7314 15.8054 8.70771 14.8291L10.829 12.7078C11.8053 11.7315 11.8053 10.1485 10.829 9.17223C10.4666 8.80983 10.0207 8.58195 9.55314 8.48859C9.62635 8.31113 9.73506 8.14487 9.87926 8.00066L10.2373 7.64262C10.7115 7.81138 11.1566 8.08555 11.5361 8.46512C12.903 9.83196 12.903 12.048 11.5361 13.4149L9.41481 15.5362C8.04798 16.903 5.8319 16.903 4.46507 15.5362C3.09823 14.1694 3.09823 11.9533 4.46507 10.5864Z"
fill="currentColor"
/>
</svg>
),
'make-variable': ( 'make-variable': (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@ -163,7 +163,7 @@ export function useCodeMirror(props: UseCodeMirror) {
effects: StateEffect.reconfigure.of(targetExtensions), effects: StateEffect.reconfigure.of(targetExtensions),
}) })
} }
}, [targetExtensions]) }, [targetExtensions, view, isFirstRender])
return { view, setView, container, setContainer, state, setState } return { view, setView, container, setContainer, state, setState }
} }

View File

@ -1,6 +1,6 @@
import { processMemory } from './MemoryPane' import { processMemory } from './MemoryPane'
import { enginelessExecutor } from '../../../lib/testHelpers' import { enginelessExecutor } from '../../../lib/testHelpers'
import { initPromise, parse } from '../../../lang/wasm' import { initPromise, parse, ProgramMemory } from '../../../lang/wasm'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
@ -29,10 +29,7 @@ describe('processMemory', () => {
|> lineTo([2.15, 4.32], %) |> lineTo([2.15, 4.32], %)
// |> rx(90, %)` // |> rx(90, %)`
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast, { const programMemory = await enginelessExecutor(ast, ProgramMemory.empty())
root: {},
return: null,
})
const output = processMemory(programMemory) const output = processMemory(programMemory)
expect(output.myVar).toEqual(5) expect(output.myVar).toEqual(5)
expect(output.otherVar).toEqual(3) expect(output.otherVar).toEqual(3)

View File

@ -82,8 +82,7 @@ export const MemoryPane = () => {
export const processMemory = (programMemory: ProgramMemory) => { export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {} const processedMemory: any = {}
Object.keys(programMemory?.root || {}).forEach((key) => { for (const [key, val] of programMemory?.visibleEntries()) {
const val = programMemory.root[key]
if (typeof val.value !== 'function') { if (typeof val.value !== 'function') {
if (val.type === 'SketchGroup') { if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => { processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
@ -103,6 +102,6 @@ export const processMemory = (programMemory: ProgramMemory) => {
} else if (key !== 'log') { } else if (key !== 'log') {
processedMemory[key] = '__function__' processedMemory[key] = '__function__'
} }
}) }
return processedMemory return processedMemory
} }

View File

@ -1,10 +1,10 @@
import { Popover, Transition } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton, ActionButtonProps } from './ActionButton'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { isTauri } from '../lib/isTauri' import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom' import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment } from 'react' import { Fragment, useMemo } from 'react'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { Logo } from './Logo' import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
@ -12,6 +12,9 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -80,6 +83,10 @@ function ProjectMenuPopover({
project?: IndexLoaderData['project'] project?: IndexLoaderData['project']
file?: IndexLoaderData['file'] file?: IndexLoaderData['file']
}) { }) {
const platform = usePlatform()
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext() const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' } const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
@ -90,13 +97,82 @@ function ProjectMenuPopover({
) )
) )
// We filter this memoized list so that no orphan "break" elements are rendered.
const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
() =>
[
{
id: 'settings',
Element: 'button',
children: (
<>
<span className="flex-1">Project settings</span>
<kbd className="hotkey">{`${platform === 'macos' ? '⌘' : 'Ctrl'}${
isTauri() ? '' : '⬆'
},`}</kbd>
</>
),
onClick: () => {
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS
: paths.HOME + paths.SETTINGS
navigate(targetPath + '?tab=project')
},
},
'break',
{
id: 'export',
Element: 'button',
children: (
<>
<span>Export current part</span>
{!findCommand(exportCommandInfo) && (
<Tooltip position="right" className="!max-w-none min-w-fit">
Awaiting engine connection
</Tooltip>
)}
</>
),
disabled: !findCommand(exportCommandInfo),
onClick: () =>
commandBarSend({
type: 'Find and select command',
data: exportCommandInfo,
}),
},
'break',
{
id: 'go-home',
Element: 'button',
children: 'Go to Home',
className: !isTauri() ? 'hidden' : '',
onClick: () => {
onProjectClose(file || null, project?.path || null, true)
// Clear the scene and end the session.
engineCommandManager.endSession()
},
},
].filter(
(props) =>
props === 'break' ||
(typeof props !== 'string' && !props.className?.includes('hidden'))
) as (ActionButtonProps | 'break')[],
[
platform,
findCommand,
commandBarSend,
engineCommandManager,
onProjectClose,
isTauri,
]
)
return ( return (
<Popover className="relative"> <Popover className="relative">
<Popover.Button <Popover.Button
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 py-1 pl-0 pr-2 flex items-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary dark:hover:bg-chalkboard-90" className="gap-1 rounded-sm h-9 mr-auto max-h-min min-w-max border-0 py-1 px-2 flex items-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary dark:hover:bg-chalkboard-90"
data-testid="project-sidebar-toggle" data-testid="project-sidebar-toggle"
> >
<CustomIcon name="three-dots" className="w-5 h-5 rotate-90" />
<div className="flex flex-col items-start py-0.5"> <div className="flex flex-col items-start py-0.5">
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"> <span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
{isTauri() && file?.name {isTauri() && file?.name
@ -109,68 +185,53 @@ function ProjectMenuPopover({
</span> </span>
)} )}
</div> </div>
<CustomIcon
name="caretDown"
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40 ui-open:rotate-180"
/>
</Popover.Button> </Popover.Button>
<Transition
enter="duration-200 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Popover.Overlay className="fixed inset-0 z-20 bg-chalkboard-110/50" />
</Transition>
<Transition <Transition
enter="duration-100 ease-out" enter="duration-100 ease-out"
enterFrom="opacity-0 -translate-x-1/4" enterFrom="opacity-0 -translate-y-2"
enterTo="opacity-100 translate-x-0" enterTo="opacity-100 translate-y-0"
leave="duration-75 ease-in"
leaveFrom="opacity-100 translate-x-0"
leaveTo="opacity-0 -translate-x-4"
as={Fragment} as={Fragment}
> >
<Popover.Panel <Popover.Panel
className="fixed inset-0 right-auto z-30 grid w-64 h-screen max-h-screen grid-cols-1 border rounded-r-md shadow-md bg-chalkboard-10 dark:bg-chalkboard-100 border-chalkboard-40 dark:border-chalkboard-80" className={`z-10 absolute top-full left-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
style={{ gridTemplateRows: 'auto 1fr auto' }} border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded
shadow-lg`}
> >
{({ close }) => ( {({ close }) => (
<> <ul className="relative flex flex-col items-stretch content-stretch p-0.5">
<div className="flex flex-col gap-2 p-4"> {projectMenuItems.map((props, index) => {
<ActionButton if (props === 'break') {
Element="button" return index !== projectMenuItems.length - 1 ? (
iconStart={{ icon: 'exportFile', className: 'p-1' }} <li key={`break-${index}`} className="contents">
className="border-transparent dark:border-transparent" <hr className="border-chalkboard-20 dark:border-chalkboard-80" />
disabled={!findCommand(exportCommandInfo)} </li>
onClick={() => ) : null
commandBarSend({ }
type: 'Find and select command',
data: exportCommandInfo, const { id, className, children, ...rest } = props
}) return (
} <li key={id} className="contents">
> <ActionButton
Export Part {...rest}
</ActionButton> className={
{isTauri() && ( 'relative !font-sans flex items-center gap-2 rounded-sm py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left ' +
<ActionButton className
Element="button" }
onClick={() => { onMouseUp={() => {
onProjectClose(file || null, project?.path || null, true) close()
// Clear the scene and end the session. }}
engineCommandManager.endSession() >
}} {children}
iconStart={{ </ActionButton>
icon: 'arrowLeft', </li>
className: 'p-1', )
}} })}
className="border-transparent dark:border-transparent" </ul>
>
Go to Home
</ActionButton>
)}
</div>
</>
)} )}
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>

View File

@ -157,6 +157,7 @@ export const Stream = () => {
useEffect(() => { useEffect(() => {
setIsFirstRender(kclManager.isFirstRender) setIsFirstRender(kclManager.isFirstRender)
if (!kclManager.isFirstRender) videoRef.current?.play() if (!kclManager.isFirstRender) videoRef.current?.play()
setIsFreezeFrame(!kclManager.isFirstRender)
}, [kclManager.isFirstRender]) }, [kclManager.isFirstRender])
useEffect(() => { useEffect(() => {
@ -178,6 +179,8 @@ export const Stream = () => {
videoElement: videoRef.current, videoElement: videoRef.current,
}, },
}) })
setIsLoading(false)
}, [mediaStream]) }, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {

View File

@ -8,7 +8,9 @@
--_delay: 200ms; --_delay: 200ms;
--_triangle-width: 8px; --_triangle-width: 8px;
--_triangle-height: 12px; --_triangle-height: 12px;
--_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2)); --_p-inline-arrow-alignment: calc(
50% + calc(var(--isRTL) * var(--_triangle-width) / 2)
);
--_p-block: 4px; --_p-block: 4px;
--_bg: var(--chalkboard-10); --_bg: var(--chalkboard-10);
--_shadow-alpha: 8%; --_shadow-alpha: 8%;
@ -33,7 +35,7 @@
font-weight: normal; font-weight: normal;
line-height: initial; line-height: initial;
letter-spacing: 0; letter-spacing: 0;
padding: var(--_p-block) var(--_p-inline); padding: var(--_p-block) calc(2 * var(--_p-block));
margin: 0; margin: 0;
border-radius: 3px; border-radius: 3px;
background: var(--_bg); background: var(--_bg);
@ -119,7 +121,7 @@
} }
.tooltip.top-right { .tooltip.top-right {
inset-inline-end: var(--_p-inline); inset-inline-end: var(--_p-inline-arrow-alignment);
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height)); inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height));
} }
@ -130,7 +132,7 @@
} }
.tooltip.right { .tooltip.right {
inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-height)); inset-inline-start: calc(100% + var(--_triangle-height));
inset-block-end: 50%; inset-block-end: 50%;
--_y: 50%; --_y: 50%;
} }
@ -142,7 +144,7 @@
} }
.tooltip.bottom-right { .tooltip.bottom-right {
inset-inline-end: var(--_p-inline); inset-inline-end: var(--_p-inline-arrow-alignment);
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height)); inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height));
} }
@ -165,7 +167,7 @@
} }
.tooltip.bottom-left { .tooltip.bottom-left {
inset-inline-start: var(--_p-inline); inset-inline-start: var(--_p-inline-arrow-alignment);
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height)); inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height));
} }
@ -176,7 +178,9 @@
} }
.tooltip.left { .tooltip.left {
inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-height)); inset-inline-end: calc(
100% + var(--_p-inline-arrow-alignment) + var(--_triangle-height)
);
inset-block-end: 50%; inset-block-end: 50%;
--_y: 50%; --_y: 50%;
} }
@ -188,7 +192,7 @@
} }
.tooltip.top-left { .tooltip.top-left {
inset-inline-start: var(--_p-inline); inset-inline-start: var(--_p-inline-arrow-alignment);
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height)); inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height));
} }

View File

@ -25,11 +25,11 @@ export function UnitsMenu() {
border border-solid border-chalkboard-10 dark:border-chalkboard-90 rounded border border-solid border-chalkboard-10 dark:border-chalkboard-90 rounded
shadow-lg`} shadow-lg`}
> >
<ul className="relative flex flex-col gap-0.5 items-stretch content-stretch"> <ul className="relative flex flex-col items-stretch content-stretch p-0.5">
{baseUnitsUnion.map((unit) => ( {baseUnitsUnion.map((unit) => (
<li key={unit} className="contents"> <li key={unit} className="contents">
<button <button
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left" className="flex items-center gap-2 m-0 py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onClick={() => { onClick={() => {
settings.send({ settings.send({
type: 'set.modeling.defaultUnit', type: 'set.modeling.defaultUnit',

View File

@ -1,18 +1,20 @@
import { Popover, Transition } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton, ActionButtonProps } from './ActionButton'
import { faBug, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useState } from 'react' import { Fragment, useMemo, useState } from 'react'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import usePlatform from 'hooks/usePlatform'
import { isTauri } from 'lib/isTauri'
import { CustomIcon } from './CustomIcon'
type User = Models['User_type'] type User = Models['User_type']
const UserSidebarMenu = ({ user }: { user?: User }) => { const UserSidebarMenu = ({ user }: { user?: User }) => {
const platform = usePlatform()
const location = useLocation() const location = useLocation()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const displayedName = getDisplayName(user) const displayedName = getDisplayName(user)
@ -20,6 +22,128 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const navigate = useNavigate() const navigate = useNavigate()
const send = useSettingsAuthContext()?.auth?.send const send = useSettingsAuthContext()?.auth?.send
// We filter this memoized list so that no orphan "break" elements are rendered.
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
() =>
[
{
id: 'settings',
Element: 'button',
children: (
<>
<span className="flex-1">User settings</span>
<kbd className="hotkey">{`${platform === 'macos' ? '⌘' : 'Ctrl'}${
isTauri() ? '' : '⬆'
},`}</kbd>
</>
),
'data-testid': 'user-settings',
onClick: () => {
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS
: paths.HOME + paths.SETTINGS
navigate(targetPath + '?tab=user')
},
},
{
id: 'keybindings',
Element: 'button',
children: 'Keyboard shortcuts',
onClick: () => {
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS
: paths.HOME + paths.SETTINGS
navigate(targetPath + '?tab=keybindings')
},
},
{
id: 'account',
Element: 'externalLink',
to: 'https://zoo.dev/account',
children: (
<>
<span className="flex-1">Manage account</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
'break',
{
id: 'request-feature',
Element: 'externalLink',
to: 'https://github.com/KittyCAD/modeling-app/discussions',
children: (
<>
<span className="flex-1">Request a feature</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
{
id: 'report-bug',
Element: 'externalLink',
to: 'https://github.com/KittyCAD/modeling-app/issues/new/choose',
children: (
<>
<span className="flex-1">Report a bug</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
{
id: 'community',
Element: 'externalLink',
to: 'https://discord.gg/JQEpHR7Nt2',
children: (
<>
<span className="flex-1">Ask the community</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
{
id: 'release-notes',
Element: 'externalLink',
to: 'https://github.com/KittyCAD/modeling-app/releases',
children: (
<>
<span className="flex-1">Release notes</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
'break',
{
id: 'sign-out',
Element: 'button',
'data-testid': 'user-sidebar-sign-out',
children: 'Sign out',
onClick: () => send('Log out'),
className: '', // Just making TS's filter type coercion happy 😠
},
].filter(
(props) =>
props === 'break' ||
(typeof props !== 'string' && !props.className?.includes('hidden'))
) as (ActionButtonProps | 'break')[],
[platform, location, filePath, navigate, send]
)
// This image host goes down sometimes. We will instead rewrite the // This image host goes down sometimes. We will instead rewrite the
// resource to be a local one. // resource to be a local one.
if (user?.image === 'https://placekitten.com/200/200') { if (user?.image === 'https://placekitten.com/200/200') {
@ -43,139 +167,90 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
return ( return (
<Popover className="relative"> <Popover className="relative">
{user?.image && !imageLoadFailed ? ( <Popover.Button
<Popover.Button className="relative group border-0 w-fit min-w-max p-0 rounded-l-full focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary"
className="relative border-0 rounded-full w-fit min-w-max p-0 group" data-testid="user-sidebar-toggle"
data-testid="user-sidebar-toggle"
>
<div className="rounded-full border overflow-hidden">
<img
src={user?.image || ''}
alt={user?.name || ''}
className="h-8 w-8 rounded-full"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
</div>
<Tooltip position="bottom-right" delay={1000}>
User menu
</Tooltip>
</Popover.Button>
) : (
<ActionButton
Element={Popover.Button}
iconStart={{ icon: 'menu' }}
className="border-transparent !px-0"
data-testid="user-sidebar-toggle"
>
<Tooltip position="bottom-right" delay={1000}>
User menu
</Tooltip>
</ActionButton>
)}
<Transition
enter="duration-200 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
> >
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" /> <div className="flex items-center">
</Transition> <div className="rounded-full border overflow-hidden">
{user?.image && !imageLoadFailed ? (
<img
src={user?.image || ''}
alt={user?.name || ''}
className="h-7 w-7 rounded-full"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
) : (
<CustomIcon
name="person"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40 bg-chalkboard-20 dark:bg-chalkboard-80"
/>
)}
</div>
<CustomIcon
name="caretDown"
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40 ui-open:rotate-180"
/>
</div>
<Tooltip position="bottom-right" delay={1000} hoverOnly>
User menu
</Tooltip>
</Popover.Button>
<Transition <Transition
enter="duration-100 ease-out" enter="duration-100 ease-out"
enterFrom="opacity-0 translate-x-1/4" enterFrom="opacity-0 -translate-y-2"
enterTo="opacity-100 translate-x-0" enterTo="opacity-100 translate-y-0"
leave="duration-75 ease-in"
leaveFrom="opacity-100 translate-x-0"
leaveTo="opacity-0 translate-x-4"
as={Fragment} as={Fragment}
> >
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-90 border border-chalkboard-30 dark:border-chalkboard-80 shadow-md rounded-l-md overflow-hidden"> <Popover.Panel
className={`z-10 absolute top-full right-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded
shadow-lg`}
>
{({ close }) => ( {({ close }) => (
<> <>
{user && ( {user && (
<div className="flex items-center gap-4 px-4 py-3 bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80"> <div className="flex flex-col gap-1 px-2.5 py-3 bg-chalkboard-20 dark:bg-chalkboard-80/50">
{user.image && !imageLoadFailed && ( <p className="m-0 text-mono text-xs" data-testid="username">
<div className="rounded-full shadow-inner overflow-hidden"> {displayedName || ''}
<img </p>
src={user.image} {displayedName !== user.email && (
alt={user.name || ''} <p
className="h-8 w-8" className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs"
referrerPolicy="no-referrer" data-testid="email"
onError={() => setImageLoadFailed(true)} >
/> {user.email}
</div>
)}
<div>
<p className="m-0 text-mono" data-testid="username">
{displayedName || ''}
</p> </p>
{displayedName !== user.email && ( )}
<p
className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs"
data-testid="email"
>
{user.email}
</p>
)}
</div>
</div> </div>
)} )}
<div className="p-4 flex flex-col gap-2"> <ul className="relative flex flex-col items-stretch content-stretch p-0.5">
<ActionButton {userMenuItems.map((props, index) => {
Element="button" if (props === 'break') {
iconStart={{ icon: 'settings' }} return index !== userMenuItems.length - 1 ? (
className="border-transparent dark:border-transparent hover:bg-transparent" <li key={`break-${index}`} className="contents">
onClick={() => { <hr className="border-chalkboard-20 dark:border-chalkboard-80" />
// since /settings is a nested route the sidebar doesn't close </li>
// automatically when navigating to it ) : null
close() }
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS const { id, children, ...rest } = props
: paths.HOME + paths.SETTINGS return (
navigate(targetPath) <li key={id} className="contents">
}} <ActionButton
data-testid="settings-button" {...rest}
> className="!font-sans flex items-center gap-2 rounded-sm py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
Settings onMouseUp={() => {
</ActionButton> close()
<ActionButton }}
Element="externalLink" >
to="https://github.com/KittyCAD/modeling-app/discussions" {children}
iconStart={{ icon: faGithub, className: 'p-1', size: 'sm' }} </ActionButton>
className="border-transparent dark:border-transparent" </li>
> )
Request a feature })}
</ActionButton> </ul>
<ActionButton
Element="externalLink"
to="https://github.com/KittyCAD/modeling-app/issues/new/choose"
iconStart={{ icon: faBug, className: 'p-1', size: 'sm' }}
className="border-transparent dark:border-transparent"
>
Report a bug
</ActionButton>
<ActionButton
Element="button"
onClick={() => send('Log out')}
iconStart={{
icon: faSignOutAlt,
className: 'p-1',
bgClassName: '!bg-transparent',
size: 'sm',
iconClassName: '!text-destroy-70',
}}
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60 hover:bg-destroy-10/20 dark:hover:bg-destroy-80/20"
data-testid="user-sidebar-sign-out"
>
Sign out
</ActionButton>
</div>
</> </>
)} )}
</Popover.Panel> </Popover.Panel>

View File

@ -45,9 +45,6 @@ export function useSetupEngineManager(
streamRef?.current?.offsetWidth ?? 0, streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0 streamRef?.current?.offsetHeight ?? 0
) )
if (restart) {
kclManager.isFirstRender = false
}
engineCommandManager.start({ engineCommandManager.start({
restart, restart,
setMediaStream: (mediaStream) => setMediaStream(mediaStream), setMediaStream: (mediaStream) => setMediaStream(mediaStream),

View File

@ -260,8 +260,17 @@ code {
@layer components { @layer components {
kbd.hotkey { kbd.hotkey {
@apply font-mono text-xs inline-block px-1 py-0.5 rounded-sm; @apply font-mono text-xs inline-block px-0.5 py-[2px] rounded;
/* This is the only place in our code where layout is impacted by theme.
* We may not want that later, if hotkeys are possibly visible
* while switching theme, but more padding feels better in dark mode.
*/
@apply dark:px-1;
@apply text-chalkboard-70 dark:text-chalkboard-40;
@apply bg-chalkboard-20 dark:bg-chalkboard-90; @apply bg-chalkboard-20 dark:bg-chalkboard-90;
@apply border border-t-0 border-b-2 border-chalkboard-30 dark:border-chalkboard-80;
} }
} }

View File

@ -14,9 +14,7 @@ import {
Program, Program,
ProgramMemory, ProgramMemory,
recast, recast,
SketchGroup,
SourceRange, SourceRange,
ExtrudeGroup,
} from 'lang/wasm' } from 'lang/wasm'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons' import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
@ -33,10 +31,7 @@ export class KclManager {
}, },
digest: null, digest: null,
} }
private _programMemory: ProgramMemory = { private _programMemory: ProgramMemory = ProgramMemory.empty()
root: {},
return: null,
}
private _logs: string[] = [] private _logs: string[] = []
private _kclErrors: KCLError[] = [] private _kclErrors: KCLError[] = []
private _isExecuting = false private _isExecuting = false
@ -505,10 +500,7 @@ function defaultSelectionFilter(
programMemory: ProgramMemory, programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
) { ) {
const firstSketchOrExtrudeGroup = Object.values(programMemory.root).find( programMemory.hasSketchOrExtrudeGroup() &&
(node) => node.type === 'ExtrudeGroup' || node.type === 'SketchGroup'
) as SketchGroup | ExtrudeGroup
firstSketchOrExtrudeGroup &&
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),

View File

@ -16,7 +16,7 @@ const mySketch001 = startSketchOn('XY')
// |> rx(45, %)` // |> rx(45, %)`
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.root?.mySketch001 const sketch001 = programMemory?.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'SketchGroup', type: 'SketchGroup',
on: expect.any(Object), on: expect.any(Object),
@ -66,7 +66,7 @@ const mySketch001 = startSketchOn('XY')
|> extrude(2, %)` |> extrude(2, %)`
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.root?.mySketch001 const sketch001 = programMemory?.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'ExtrudeGroup', type: 'ExtrudeGroup',
id: expect.any(String), id: expect.any(String),
@ -146,7 +146,7 @@ const sk2 = startSketchOn('XY')
` `
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2] const geos = [programMemory?.get('theExtrude'), programMemory?.get('sk2')]
expect(geos).toEqual([ expect(geos).toEqual([
{ {
type: 'ExtrudeGroup', type: 'ExtrudeGroup',

View File

@ -12,25 +12,25 @@ describe('test executor', () => {
it('test assigning two variables, the second summing with the first', async () => { it('test assigning two variables, the second summing with the first', async () => {
const code = `const myVar = 5 const code = `const myVar = 5
const newVar = myVar + 1` const newVar = myVar + 1`
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(5) expect(mem.get('myVar')?.value).toBe(5)
expect(root.newVar.value).toBe(6) expect(mem.get('newVar')?.value).toBe(6)
}) })
it('test assigning a var with a string', async () => { it('test assigning a var with a string', async () => {
const code = `const myVar = "a str"` const code = `const myVar = "a str"`
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe('a str') expect(mem.get('myVar')?.value).toBe('a str')
}) })
it('test assigning a var by cont concatenating two strings string execute', async () => { it('test assigning a var by cont concatenating two strings string execute', async () => {
const code = fs.readFileSync( const code = fs.readFileSync(
'./src/lang/testExamples/variableDeclaration.cado', './src/lang/testExamples/variableDeclaration.cado',
'utf-8' 'utf-8'
) )
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe('a str another str') expect(mem.get('myVar')?.value).toBe('a str another str')
}) })
it('fn funcN = () => {} execute', async () => { it('fn funcN = () => {} execute', async () => {
const { root } = await exe( const mem = await exe(
[ [
'fn funcN = (a, b) => {', 'fn funcN = (a, b) => {',
' return a + b', ' return a + b',
@ -39,8 +39,8 @@ const newVar = myVar + 1`
'const magicNum = funcN(9, theVar)', 'const magicNum = funcN(9, theVar)',
].join('\n') ].join('\n')
) )
expect(root.theVar.value).toBe(60) expect(mem.get('theVar')?.value).toBe(60)
expect(root.magicNum.value).toBe(69) expect(mem.get('magicNum')?.value).toBe(69)
}) })
it('sketch declaration', async () => { it('sketch declaration', async () => {
let code = `const mySketch = startSketchOn('XY') let code = `const mySketch = startSketchOn('XY')
@ -50,9 +50,9 @@ const newVar = myVar + 1`
|> lineTo([5,-1], %, "rightPath") |> lineTo([5,-1], %, "rightPath")
// |> close(%) // |> close(%)
` `
const { root } = await exe(code) const mem = await exe(code)
// geo is three js buffer geometry and is very bloated to have in tests // geo is three js buffer geometry and is very bloated to have in tests
const minusGeo = root.mySketch.value const minusGeo = mem.get('mySketch')?.value
expect(minusGeo).toEqual([ expect(minusGeo).toEqual([
{ {
type: 'ToPoint', type: 'ToPoint',
@ -104,8 +104,8 @@ const newVar = myVar + 1`
'fn myFn = (a) => { return a + 1 }', 'fn myFn = (a) => { return a + 1 }',
'const myVar = 5 + 1 |> myFn(%)', 'const myVar = 5 + 1 |> myFn(%)',
].join('\n') ].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(7) expect(mem.get('myVar')?.value).toBe(7)
}) })
// Enable rotations #152 // Enable rotations #152
@ -117,16 +117,16 @@ const newVar = myVar + 1`
// ' |> lineTo([1, 1], %)', // ' |> lineTo([1, 1], %)',
// 'const rotated = rx(90, mySk1)', // 'const rotated = rx(90, mySk1)',
// ].join('\n') // ].join('\n')
// const { root } = await exe(code) // const mem = await exe(code)
// expect(root.mySk1.value).toHaveLength(3) // expect(mem.get('mySk1')?.value).toHaveLength(3)
// expect(root?.rotated?.type).toBe('SketchGroup') // expect(mem.get('rotated')?.type).toBe('SketchGroup')
// if ( // if (
// root?.mySk1?.type !== 'SketchGroup' || // mem.get('mySk1')?.type !== 'SketchGroup' ||
// root?.rotated?.type !== 'SketchGroup' // mem.get('rotated')?.type !== 'SketchGroup'
// ) // )
// throw new Error('not a sketch group') // throw new Error('not a sketch group')
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1]) // expect(mem.get('mySk1')?.rotation).toEqual([0, 0, 0, 1])
// expect(root.rotated.rotation.map((a) => a.toFixed(4))).toEqual([ // expect(mem.get('rotated')?.rotation.map((a) => a.toFixed(4))).toEqual([
// '0.7071', // '0.7071',
// '0.0000', // '0.0000',
// '0.0000', // '0.0000',
@ -144,8 +144,8 @@ const newVar = myVar + 1`
' |> lineTo([1,1], %)', ' |> lineTo([1,1], %)',
// ' |> rx(90, %)', // ' |> rx(90, %)',
].join('\n') ].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.mySk1).toEqual({ expect(mem.get('mySk1')).toEqual({
type: 'SketchGroup', type: 'SketchGroup',
on: expect.any(Object), on: expect.any(Object),
start: { start: {
@ -214,36 +214,37 @@ const newVar = myVar + 1`
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join( const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
'\n' '\n'
) )
const { root } = await exe(code) const mem = await exe(code)
// TODO path to node is probably wrong here, zero indexes are not correct // TODO path to node is probably wrong here, zero indexes are not correct
expect(root).toEqual({ expect(mem.get('three')).toEqual({
three: { type: 'UserVal',
type: 'UserVal', value: 3,
value: 3, __meta: [
__meta: [ {
{ sourceRange: [14, 15],
sourceRange: [14, 15], },
}, ],
],
},
yo: {
type: 'UserVal',
value: [1, '2', 3, 9],
__meta: [
{
sourceRange: [27, 49],
},
],
},
}) })
expect(mem.get('yo')).toEqual({
type: 'UserVal',
value: [1, '2', 3, 9],
__meta: [
{
sourceRange: [27, 49],
},
],
})
// Check that there are no other variables or environments.
expect(mem.numEnvironments()).toBe(1)
expect(mem.numVariables(0)).toBe(2)
}) })
it('execute object expression', async () => { it('execute object expression', async () => {
const code = [ const code = [
'const three = 3', 'const three = 3',
"const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}", "const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}",
].join('\n') ].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.yo).toEqual({ expect(mem.get('yo')).toEqual({
type: 'UserVal', type: 'UserVal',
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 }, value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
__meta: [ __meta: [
@ -257,8 +258,8 @@ const newVar = myVar + 1`
const code = ["const yo = {a: {b: '123'}}", "const myVar = yo.a['b']"].join( const code = ["const yo = {a: {b: '123'}}", "const myVar = yo.a['b']"].join(
'\n' '\n'
) )
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar).toEqual({ expect(mem.get('myVar')).toEqual({
type: 'UserVal', type: 'UserVal',
value: '123', value: '123',
__meta: [ __meta: [
@ -273,81 +274,81 @@ const newVar = myVar + 1`
describe('testing math operators', () => { describe('testing math operators', () => {
it('can sum', async () => { it('can sum', async () => {
const code = ['const myVar = 1 + 2'].join('\n') const code = ['const myVar = 1 + 2'].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(3) expect(mem.get('myVar')?.value).toBe(3)
}) })
it('can subtract', async () => { it('can subtract', async () => {
const code = ['const myVar = 1 - 2'].join('\n') const code = ['const myVar = 1 - 2'].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(-1) expect(mem.get('myVar')?.value).toBe(-1)
}) })
it('can multiply', async () => { it('can multiply', async () => {
const code = ['const myVar = 1 * 2'].join('\n') const code = ['const myVar = 1 * 2'].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(2) expect(mem.get('myVar')?.value).toBe(2)
}) })
it('can divide', async () => { it('can divide', async () => {
const code = ['const myVar = 1 / 2'].join('\n') const code = ['const myVar = 1 / 2'].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(0.5) expect(mem.get('myVar')?.value).toBe(0.5)
}) })
it('can modulus', async () => { it('can modulus', async () => {
const code = ['const myVar = 5 % 2'].join('\n') const code = ['const myVar = 5 % 2'].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(1) expect(mem.get('myVar')?.value).toBe(1)
}) })
it('can do multiple operations', async () => { it('can do multiple operations', async () => {
const code = ['const myVar = 1 + 2 * 3'].join('\n') const code = ['const myVar = 1 + 2 * 3'].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(7) expect(mem.get('myVar')?.value).toBe(7)
}) })
it('big example with parans', async () => { it('big example with parans', async () => {
const code = ['const myVar = 1 + 2 * (3 - 4) / -5 + 6'].join('\n') const code = ['const myVar = 1 + 2 * (3 - 4) / -5 + 6'].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(7.4) expect(mem.get('myVar')?.value).toBe(7.4)
}) })
it('with identifier', async () => { it('with identifier', async () => {
const code = ['const yo = 6', 'const myVar = yo / 2'].join('\n') const code = ['const yo = 6', 'const myVar = yo / 2'].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(3) expect(mem.get('myVar')?.value).toBe(3)
}) })
it('with lots of testing', async () => { it('with lots of testing', async () => {
const code = ['const myVar = 2 * ((2 + 3 ) / 4 + 5)'].join('\n') const code = ['const myVar = 2 * ((2 + 3 ) / 4 + 5)'].join('\n')
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(12.5) expect(mem.get('myVar')?.value).toBe(12.5)
}) })
it('with callExpression at start', async () => { it('with callExpression at start', async () => {
const code = 'const myVar = min(4, 100) + 2' const code = 'const myVar = min(4, 100) + 2'
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(6) expect(mem.get('myVar')?.value).toBe(6)
}) })
it('with callExpression at end', async () => { it('with callExpression at end', async () => {
const code = 'const myVar = 2 + min(4, 100)' const code = 'const myVar = 2 + min(4, 100)'
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(6) expect(mem.get('myVar')?.value).toBe(6)
}) })
it('with nested callExpression', async () => { it('with nested callExpression', async () => {
const code = 'const myVar = 2 + min(100, legLen(5, 3))' const code = 'const myVar = 2 + min(100, legLen(5, 3))'
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(6) expect(mem.get('myVar')?.value).toBe(6)
}) })
it('with unaryExpression', async () => { it('with unaryExpression', async () => {
const code = 'const myVar = -min(100, 3)' const code = 'const myVar = -min(100, 3)'
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(-3) expect(mem.get('myVar')?.value).toBe(-3)
}) })
it('with unaryExpression in callExpression', async () => { it('with unaryExpression in callExpression', async () => {
const code = 'const myVar = min(-legLen(5, 4), 5)' const code = 'const myVar = min(-legLen(5, 4), 5)'
const code2 = 'const myVar = min(5 , -legLen(5, 4))' const code2 = 'const myVar = min(5 , -legLen(5, 4))'
const { root } = await exe(code) const mem = await exe(code)
const { root: root2 } = await exe(code2) const mem2 = await exe(code2)
expect(root.myVar.value).toBe(-3) expect(mem.get('myVar')?.value).toBe(-3)
expect(root.myVar.value).toBe(root2.myVar.value) expect(mem.get('myVar')?.value).toBe(mem2.get('myVar')?.value)
}) })
it('with unaryExpression in ArrayExpression', async () => { it('with unaryExpression in ArrayExpression', async () => {
const code = 'const myVar = [1,-legLen(5, 4)]' const code = 'const myVar = [1,-legLen(5, 4)]'
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toEqual([1, -3]) expect(mem.get('myVar')?.value).toEqual([1, -3])
}) })
it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => { it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => {
const code = [ const code = [
@ -355,8 +356,8 @@ describe('testing math operators', () => {
' |> startProfileAt([0, 0], %)', ' |> startProfileAt([0, 0], %)',
'|> line([-2.21, -legLen(5, min(3, 999))], %)', '|> line([-2.21, -legLen(5, min(3, 999))], %)',
].join('\n') ].join('\n')
const { root } = await exe(code) const mem = await exe(code)
const sketch = root.part001 const sketch = mem.get('part001')
// result of `-legLen(5, min(3, 999))` should be -4 // result of `-legLen(5, min(3, 999))` should be -4
const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1] const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1]
expect(yVal).toBe(-4) expect(yVal).toBe(-4)
@ -373,8 +374,8 @@ describe('testing math operators', () => {
`], %)`, `], %)`,
``, ``,
].join('\n') ].join('\n')
const { root } = await exe(code) const mem = await exe(code)
const sketch = root.part001 const sketch = mem.get('part001')
// expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0 // expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0
expect((sketch as SketchGroup).value?.[1]?.from).toEqual([3, 4]) expect((sketch as SketchGroup).value?.[1]?.from).toEqual([3, 4])
expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0]) expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0])
@ -382,18 +383,18 @@ describe('testing math operators', () => {
`-legLen(segLen('seg01', %), myVar)`, `-legLen(segLen('seg01', %), myVar)`,
`legLen(segLen('seg01', %), myVar)` `legLen(segLen('seg01', %), myVar)`
) )
const { root: removedUnaryExpRoot } = await exe(removedUnaryExp) const removedUnaryExpMem = await exe(removedUnaryExp)
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001 const removedUnaryExpMemSketch = removedUnaryExpMem.get('part001')
// without the minus sign, the y value should be 8 // without the minus sign, the y value should be 8
expect((removedUnaryExpRootSketch as SketchGroup).value?.[1]?.to).toEqual([ expect((removedUnaryExpMemSketch as SketchGroup).value?.[1]?.to).toEqual([
6, 8, 6, 8,
]) ])
}) })
it('with nested callExpression and binaryExpression', async () => { it('with nested callExpression and binaryExpression', async () => {
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))' const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
const { root } = await exe(code) const mem = await exe(code)
expect(root.myVar.value).toBe(5) expect(mem.get('myVar')?.value).toBe(5)
}) })
}) })
@ -421,7 +422,7 @@ const theExtrude = startSketchOn('XY')
async function exe( async function exe(
code: string, code: string,
programMemory: ProgramMemory = { root: {}, return: null } programMemory: ProgramMemory = ProgramMemory.empty()
) { ) {
const ast = parse(code) const ast = parse(code)

View File

@ -79,20 +79,14 @@ export async function executeAst({
return { return {
errors: [e], errors: [e],
logs: [], logs: [],
programMemory: { programMemory: ProgramMemory.empty(),
root: {},
return: null,
},
} }
} else { } else {
console.log(e) console.log(e)
return { return {
logs: [e], logs: [e],
errors: [], errors: [],
programMemory: { programMemory: ProgramMemory.empty(),
root: {},
return: null,
},
} }
} }
} }

View File

@ -983,7 +983,7 @@ export async function deleteFromSelection(
if (err(parent)) { if (err(parent)) {
return return
} }
const sketchToPreserve = programMemory.root[sketchName] as SketchGroup const sketchToPreserve = programMemory.get(sketchName) as SketchGroup
console.log('sketchName', sketchName) console.log('sketchName', sketchName)
// Can't kick off multiple requests at once as getFaceDetails // Can't kick off multiple requests at once as getFaceDetails
// is three engine calls in one and they conflict // is three engine calls in one and they conflict

View File

@ -130,8 +130,14 @@ function moreNodePathFromSourceRange(
const isInRange = _node.start <= start && _node.end >= end const isInRange = _node.start <= start && _node.end >= end
if ((_node.type === 'Identifier' || _node.type === 'Literal') && isInRange) if (
(_node.type === 'Identifier' ||
_node.type === 'Literal' ||
_node.type === 'TagDeclarator') &&
isInRange
) {
return path return path
}
if (_node.type === 'CallExpression' && isInRange) { if (_node.type === 'CallExpression' && isInRange) {
const { callee, arguments: args } = _node const { callee, arguments: args } = _node
@ -277,6 +283,15 @@ function moreNodePathFromSourceRange(
} }
} }
} }
return path
}
if (_node.type === 'ReturnStatement' && isInRange) {
const { argument } = _node
if (argument.start <= start && argument.end >= end) {
path.push(['argument', 'ReturnStatement'])
return moreNodePathFromSourceRange(argument, sourceRange, path)
}
return path
} }
if (_node.type === 'MemberExpression' && isInRange) { if (_node.type === 'MemberExpression' && isInRange) {
const { object, property } = _node const { object, property } = _node
@ -459,8 +474,8 @@ export function findAllPreviousVariablesPath(
bodyItems?.forEach?.((item) => { bodyItems?.forEach?.((item) => {
if (item.type !== 'VariableDeclaration' || item.end > startRange) return if (item.type !== 'VariableDeclaration' || item.end > startRange) return
const varName = item.declarations[0].id.name const varName = item.declarations[0].id.name
const varValue = programMemory?.root[varName] const varValue = programMemory?.get(varName)
if (typeof varValue?.value !== type) return if (!varValue || typeof varValue?.value !== type) return
variables.push({ variables.push({
key: varName, key: varName,
value: varValue.value, value: varValue.value,
@ -640,7 +655,7 @@ export function isLinesParallelAndConstrained(
if (err(_varDec)) return _varDec if (err(_varDec)) return _varDec
const varDec = _varDec.node const varDec = _varDec.node
const varName = (varDec as VariableDeclaration)?.declarations[0]?.id?.name const varName = (varDec as VariableDeclaration)?.declarations[0]?.id?.name
const path = programMemory?.root[varName] as SketchGroup const path = programMemory?.get(varName) as SketchGroup
const _primarySegment = getSketchSegmentFromSourceRange( const _primarySegment = getSketchSegmentFromSourceRange(
path, path,
primaryLine.range primaryLine.range
@ -687,7 +702,7 @@ export function isLinesParallelAndConstrained(
constraintType === 'angle' || constraintLevel === 'full' constraintType === 'angle' || constraintLevel === 'full'
// get the previous segment // get the previous segment
const prevSegment = (programMemory.root[varName] as SketchGroup).value[ const prevSegment = (programMemory.get(varName) as SketchGroup).value[
secondaryIndex - 1 secondaryIndex - 1
] ]
const prevSourceRange = prevSegment.__geoMeta.sourceRange const prevSourceRange = prevSegment.__geoMeta.sourceRange
@ -757,7 +772,7 @@ export function hasExtrudeSketchGroup({
const varDec = varDecMeta.node const varDec = varDecMeta.node
if (varDec.type !== 'VariableDeclaration') return false if (varDec.type !== 'VariableDeclaration') return false
const varName = varDec.declarations[0].id.name const varName = varDec.declarations[0].id.name
const varValue = programMemory?.root[varName] const varValue = programMemory?.get(varName)
return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup' return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup'
} }

View File

@ -1009,8 +1009,8 @@ export const angledLineOfXLength: SketchLineHelper = {
const { node: varDec } = nodeMeta2 const { node: varDec } = nodeMeta2
const variableName = varDec.id.name const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName] const sketch = previousProgramMemory?.get(variableName)
if (sketch.type !== 'SketchGroup') { if (!sketch || sketch.type !== 'SketchGroup') {
return new Error('not a SketchGroup') return new Error('not a SketchGroup')
} }
const angle = createLiteral(roundOff(getAngle(from, to), 0)) const angle = createLiteral(roundOff(getAngle(from, to), 0))
@ -1105,8 +1105,8 @@ export const angledLineOfYLength: SketchLineHelper = {
if (err(nodeMeta2)) return nodeMeta2 if (err(nodeMeta2)) return nodeMeta2
const { node: varDec } = nodeMeta2 const { node: varDec } = nodeMeta2
const variableName = varDec.id.name const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName] const sketch = previousProgramMemory?.get(variableName)
if (sketch.type !== 'SketchGroup') { if (!sketch || sketch.type !== 'SketchGroup') {
return new Error('not a SketchGroup') return new Error('not a SketchGroup')
} }
@ -1443,7 +1443,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
const { node: varDec } = nodeMeta2 const { node: varDec } = nodeMeta2
const varName = varDec.declarations[0].id.name const varName = varDec.declarations[0].id.name
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup const sketchGroup = previousProgramMemory.get(varName) as SketchGroup
const intersectPath = sketchGroup.value.find( const intersectPath = sketchGroup.value.find(
({ tag }: Path) => tag && tag.value === intersectTagName ({ tag }: Path) => tag && tag.value === intersectTagName
) )

View File

@ -363,7 +363,7 @@ const part001 = startSketchOn('XY')
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
const index = code.indexOf('// normal-segment') - 7 const index = code.indexOf('// normal-segment') - 7
const _segment = getSketchSegmentFromSourceRange( const _segment = getSketchSegmentFromSourceRange(
programMemory.root['part001'] as SketchGroup, programMemory.get('part001') as SketchGroup,
[index, index] [index, index]
) )
if (err(_segment)) throw _segment if (err(_segment)) throw _segment
@ -379,7 +379,7 @@ const part001 = startSketchOn('XY')
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
const index = code.indexOf('// segment-in-start') - 7 const index = code.indexOf('// segment-in-start') - 7
const _segment = getSketchSegmentFromSourceRange( const _segment = getSketchSegmentFromSourceRange(
programMemory.root['part001'] as SketchGroup, programMemory.get('part001') as SketchGroup,
[index, index] [index, index]
) )
if (err(_segment)) throw _segment if (err(_segment)) throw _segment

View File

@ -1636,8 +1636,8 @@ export function transformAstSketchLines({
}) })
const varName = varDec.node.id.name const varName = varDec.node.id.name
let sketchGroup = programMemory.root?.[varName] let sketchGroup = programMemory.get(varName)
if (sketchGroup.type === 'ExtrudeGroup') { if (sketchGroup?.type === 'ExtrudeGroup') {
sketchGroup = sketchGroup.sketchGroup sketchGroup = sketchGroup.sketchGroup
} }
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') if (!sketchGroup || sketchGroup.type !== 'SketchGroup')

View File

@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
offset: ${offset}, offset: ${offset},
}, %, "yo2") }, %, "yo2")
const intersect = segEndX('yo2', part001)` const intersect = segEndX('yo2', part001)`
const { root } = await enginelessExecutor(parse(code('-1'))) const mem = await enginelessExecutor(parse(code('-1')))
expect(root.intersect.value).toBe(1 + Math.sqrt(2)) expect(mem.get('intersect')?.value).toBe(1 + Math.sqrt(2))
const { root: noOffset } = await enginelessExecutor(parse(code('0'))) const noOffset = await enginelessExecutor(parse(code('0')))
expect(noOffset.intersect.value).toBeCloseTo(1) expect(noOffset.get('intersect')?.value).toBeCloseTo(1)
}) })
}) })

View File

@ -143,14 +143,200 @@ interface Memory {
[key: string]: MemoryItem [key: string]: MemoryItem
} }
export interface ProgramMemory { type EnvironmentRef = number
root: Memory
const ROOT_ENVIRONMENT_REF: EnvironmentRef = 0
interface Environment {
bindings: Memory
parent: EnvironmentRef | null
}
function emptyEnvironment(): Environment {
return { bindings: {}, parent: null }
}
interface RawProgramMemory {
environments: Environment[]
currentEnv: EnvironmentRef
return: ProgramReturn | null return: ProgramReturn | null
} }
/**
* This duplicates logic in Rust. The hope is to keep ProgramMemory internals
* isolated from the rest of the TypeScript code so that we can move it to Rust
* in the future.
*/
export class ProgramMemory {
private environments: Environment[]
private currentEnv: EnvironmentRef
private return: ProgramReturn | null
/**
* Empty memory doesn't include prelude definitions.
*/
static empty(): ProgramMemory {
return new ProgramMemory()
}
static fromRaw(raw: RawProgramMemory): ProgramMemory {
return new ProgramMemory(raw.environments, raw.currentEnv, raw.return)
}
constructor(
environments: Environment[] = [emptyEnvironment()],
currentEnv: EnvironmentRef = ROOT_ENVIRONMENT_REF,
returnVal: ProgramReturn | null = null
) {
this.environments = environments
this.currentEnv = currentEnv
this.return = returnVal
}
/**
* Returns a deep copy.
*/
clone(): ProgramMemory {
return ProgramMemory.fromRaw(JSON.parse(JSON.stringify(this.toRaw())))
}
has(name: string): boolean {
let envRef = this.currentEnv
while (true) {
const env = this.environments[envRef]
if (env.bindings.hasOwnProperty(name)) {
return true
}
if (!env.parent) {
break
}
envRef = env.parent
}
return false
}
get(name: string): MemoryItem | null {
let envRef = this.currentEnv
while (true) {
const env = this.environments[envRef]
if (env.bindings.hasOwnProperty(name)) {
return env.bindings[name]
}
if (!env.parent) {
break
}
envRef = env.parent
}
return null
}
set(name: string, value: MemoryItem): Error | null {
if (this.environments.length === 0) {
return new Error('No environment to set memory in')
}
const env = this.environments[this.currentEnv]
env.bindings[name] = value
return null
}
/**
* Returns a new ProgramMemory with only `MemoryItem`s that pass the
* predicate. Values are deep copied.
*
* Note: Return value of the returned ProgramMemory is always null.
*/
filterVariables(
keepPrelude: boolean,
predicate: (value: MemoryItem) => boolean
): ProgramMemory | Error {
const environments: Environment[] = []
for (const [i, env] of this.environments.entries()) {
let bindings: Memory
if (i === ROOT_ENVIRONMENT_REF && keepPrelude) {
// Get prelude definitions. Create these first so that they're always
// first in iteration order.
const memoryOrError = programMemoryInit()
if (err(memoryOrError)) return memoryOrError
bindings = memoryOrError.environments[0].bindings
} else {
bindings = emptyEnvironment().bindings
}
for (const [name, value] of Object.entries(env.bindings)) {
// Check the predicate.
if (!predicate(value)) {
continue
}
// Deep copy.
bindings[name] = JSON.parse(JSON.stringify(value))
}
environments.push({ bindings, parent: env.parent })
}
return new ProgramMemory(environments, this.currentEnv, null)
}
numEnvironments(): number {
return this.environments.length
}
numVariables(envRef: EnvironmentRef): number {
return Object.keys(this.environments[envRef]).length
}
/**
* Returns all variable entries in memory that are visible, in a flat
* structure. If variables are shadowed, they're not visible, and therefore,
* not included.
*
* This should only be used to display in the MemoryPane UI.
*/
visibleEntries(): Map<string, MemoryItem> {
const map = new Map<string, MemoryItem>()
let envRef = this.currentEnv
while (true) {
const env = this.environments[envRef]
for (const [name, value] of Object.entries(env.bindings)) {
// Don't include shadowed variables.
if (!map.has(name)) {
map.set(name, value)
}
}
if (!env.parent) {
break
}
envRef = env.parent
}
return map
}
/**
* Returns true if any visible variables are a SketchGroup or ExtrudeGroup.
*/
hasSketchOrExtrudeGroup(): boolean {
for (const node of this.visibleEntries().values()) {
if (node.type === 'ExtrudeGroup' || node.type === 'SketchGroup') {
return true
}
}
return false
}
/**
* Return the representation that can be serialized to JSON. This should only
* be used within this module.
*/
toRaw(): RawProgramMemory {
return {
environments: this.environments,
currentEnv: this.currentEnv,
return: this.return,
}
}
}
export const executor = async ( export const executor = async (
node: Program, node: Program,
programMemory: ProgramMemory | Error = { root: {}, return: null }, programMemory: ProgramMemory | Error = ProgramMemory.empty(),
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
isMock: boolean = false isMock: boolean = false
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
@ -171,7 +357,7 @@ export const executor = async (
export const _executor = async ( export const _executor = async (
node: Program, node: Program,
programMemory: ProgramMemory | Error = { root: {}, return: null }, programMemory: ProgramMemory | Error = ProgramMemory.empty(),
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
isMock: boolean isMock: boolean
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
@ -186,15 +372,15 @@ export const _executor = async (
baseUnit = baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm' (await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
} }
const memory: ProgramMemory = await execute_wasm( const memory: RawProgramMemory = await execute_wasm(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemory), JSON.stringify(programMemory.toRaw()),
baseUnit, baseUnit,
engineCommandManager, engineCommandManager,
fileSystemManager, fileSystemManager,
isMock isMock
) )
return memory return ProgramMemory.fromRaw(memory)
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
const parsed: RustKclError = JSON.parse(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())
@ -329,10 +515,17 @@ export function getTangentialArcToInfo({
} }
} }
/**
* Returns new ProgramMemory with prelude definitions.
*/
export function programMemoryInit(): ProgramMemory | Error { export function programMemoryInit(): ProgramMemory | Error {
try { try {
const memory: ProgramMemory = program_memory_init() const memory: RawProgramMemory = program_memory_init()
return memory return new ProgramMemory(
memory.environments,
memory.currentEnv,
memory.return
)
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
const parsed: RustKclError = JSON.parse(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())

View File

@ -75,7 +75,7 @@ class MockEngineCommandManager {
export async function enginelessExecutor( export async function enginelessExecutor(
ast: Program | Error, ast: Program | Error,
pm: ProgramMemory | Error = { root: {}, return: null } pm: ProgramMemory | Error = ProgramMemory.empty()
): Promise<ProgramMemory> { ): Promise<ProgramMemory> {
if (err(ast)) return Promise.reject(ast) if (err(ast)) return Promise.reject(ast)
if (err(pm)) return Promise.reject(pm) if (err(pm)) return Promise.reject(pm)
@ -93,7 +93,7 @@ export async function enginelessExecutor(
export async function executor( export async function executor(
ast: Program, ast: Program,
pm: ProgramMemory = { root: {}, return: null } pm: ProgramMemory = ProgramMemory.empty()
): Promise<ProgramMemory> { ): Promise<ProgramMemory> {
const engineCommandManager = new EngineCommandManager() const engineCommandManager = new EngineCommandManager()
engineCommandManager.start({ engineCommandManager.start({

View File

@ -3,7 +3,7 @@ import { kclManager, engineCommandManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { findUniqueName } from 'lang/modifyAst' import { findUniqueName } from 'lang/modifyAst'
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst' import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
import { Value, parse } from 'lang/wasm' import { ProgramMemory, Value, parse } from 'lang/wasm'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'lang/langHelpers'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
@ -60,9 +60,8 @@ export function useCalculateKclExpression({
}, []) }, [])
useEffect(() => { useEffect(() => {
const allVarNames = Object.keys(programMemory.root)
if ( if (
allVarNames.includes(newVariableName) || programMemory.has(newVariableName) ||
newVariableName === '' || newVariableName === '' ||
!isValidVariableName(newVariableName) !isValidVariableName(newVariableName)
) { ) {
@ -89,17 +88,20 @@ export function useCalculateKclExpression({
if (err(ast)) return if (err(ast)) return
if (trap(ast, { suppress: true })) return if (trap(ast, { suppress: true })) return
const _programMem: any = { root: {}, return: null } const _programMem: ProgramMemory = ProgramMemory.empty()
availableVarInfo.variables.forEach(({ key, value }) => { for (const { key, value } of availableVarInfo.variables) {
_programMem.root[key] = { type: 'userVal', value, __meta: [] } const error = _programMem.set(key, {
}) type: 'UserVal',
value,
__meta: [],
})
if (trap(error, { suppress: true })) return
}
const { programMemory } = await executeAst({ const { programMemory } = await executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: JSON.parse( programMemoryOverride: kclManager.programMemory.clone(),
JSON.stringify(kclManager.programMemory)
),
}) })
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
@ -109,7 +111,7 @@ export function useCalculateKclExpression({
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value const result = programMemory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
} }

View File

@ -1139,8 +1139,8 @@ export const modelingMachine = createMachine(
) )
if (err(varDecNode)) return if (err(varDecNode)) return
const sketchVar = varDecNode.node.declarations[0].id.name const sketchVar = varDecNode.node.declarations[0].id.name
const sketchGroup = kclManager.programMemory.root[sketchVar] const sketchGroup = kclManager.programMemory.get(sketchVar)
if (sketchGroup.type !== 'SketchGroup') return if (sketchGroup?.type !== 'SketchGroup') return
const idArtifact = engineCommandManager.artifactMap[sketchGroup.id] const idArtifact = engineCommandManager.artifactMap[sketchGroup.id]
if (idArtifact.commandType !== 'start_path') return if (idArtifact.commandType !== 'start_path') return
const extrusionArtifactId = (idArtifact as any)?.extrusions?.[0] const extrusionArtifactId = (idArtifact as any)?.extrusions?.[0]

View File

@ -1388,7 +1388,7 @@ impl CallExpression {
} }
FunctionKind::UserDefined => { FunctionKind::UserDefined => {
let func = memory.get(&fn_name, self.into())?; let func = memory.get(&fn_name, self.into())?;
let result = func.call_fn(fn_args, memory.clone(), ctx.clone()).await.map_err(|e| { let result = func.call_fn(fn_args, ctx.clone()).await.map_err(|e| {
// Add the call expression to the source ranges. // Add the call expression to the source ranges.
e.add_source_ranges(vec![self.into()]) e.add_source_ranges(vec![self.into()])
})?; })?;
@ -2880,6 +2880,30 @@ impl BinaryExpression {
pipe_info: &PipeInfo, pipe_info: &PipeInfo,
ctx: &ExecutorContext, ctx: &ExecutorContext,
) -> Result<MemoryItem, KclError> { ) -> Result<MemoryItem, KclError> {
// First check if we are doing short-circuiting logical operator.
if self.operator == BinaryOperator::LogicalOr {
let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let left = json_to_bool(&left_json_value);
if left {
// Short-circuit.
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(left),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let right = json_to_bool(&right_json_value);
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(right),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?; let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?; let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
@ -2909,6 +2933,9 @@ impl BinaryExpression {
BinaryOperator::Div => (left / right).into(), BinaryOperator::Div => (left / right).into(),
BinaryOperator::Mod => (left % right).into(), BinaryOperator::Mod => (left % right).into(),
BinaryOperator::Pow => (left.powf(right)).into(), BinaryOperator::Pow => (left.powf(right)).into(),
BinaryOperator::LogicalOr => {
unreachable!("LogicalOr should have been handled above")
}
}; };
Ok(MemoryItem::UserVal(UserVal { Ok(MemoryItem::UserVal(UserVal {
@ -2950,6 +2977,27 @@ pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
} }
} }
pub fn json_to_bool(j: &serde_json::Value) -> bool {
match j {
JValue::Null => false,
JValue::Bool(b) => *b,
JValue::Number(n) => {
if let Some(n) = n.as_u64() {
n != 0
} else if let Some(n) = n.as_i64() {
n != 0
} else if let Some(x) = n.as_f64() {
x != 0.0 && !x.is_nan()
} else {
false
}
}
JValue::String(s) => !s.is_empty(),
JValue::Array(a) => !a.is_empty(),
JValue::Object(_) => false,
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display, Bake)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display, Bake)]
#[databake(path = kcl_lib::ast::types)] #[databake(path = kcl_lib::ast::types)]
#[ts(export)] #[ts(export)]
@ -2980,6 +3028,10 @@ pub enum BinaryOperator {
#[serde(rename = "^")] #[serde(rename = "^")]
#[display("^")] #[display("^")]
Pow, Pow,
/// Logical OR.
#[serde(rename = "||")]
#[display("||")]
LogicalOr,
} }
/// Mathematical associativity. /// Mathematical associativity.
@ -3008,6 +3060,7 @@ impl BinaryOperator {
BinaryOperator::Div => *b"div", BinaryOperator::Div => *b"div",
BinaryOperator::Mod => *b"mod", BinaryOperator::Mod => *b"mod",
BinaryOperator::Pow => *b"pow", BinaryOperator::Pow => *b"pow",
BinaryOperator::LogicalOr => *b"lor",
} }
} }
@ -3018,6 +3071,7 @@ impl BinaryOperator {
BinaryOperator::Add | BinaryOperator::Sub => 11, BinaryOperator::Add | BinaryOperator::Sub => 11,
BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => 12, BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => 12,
BinaryOperator::Pow => 6, BinaryOperator::Pow => 6,
BinaryOperator::LogicalOr => 3,
} }
} }
@ -3025,7 +3079,7 @@ impl BinaryOperator {
/// Taken from <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table> /// Taken from <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table>
pub fn associativity(&self) -> Associativity { pub fn associativity(&self) -> Associativity {
match self { match self {
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod => Associativity::Left, Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod | Self::LogicalOr => Associativity::Left,
Self::Pow => Associativity::Right, Self::Pow => Associativity::Right,
} }
} }
@ -3089,6 +3143,21 @@ impl UnaryExpression {
pipe_info: &PipeInfo, pipe_info: &PipeInfo,
ctx: &ExecutorContext, ctx: &ExecutorContext,
) -> Result<MemoryItem, KclError> { ) -> Result<MemoryItem, KclError> {
if self.operator == UnaryOperator::Not {
let value = self
.argument
.get_result(memory, pipe_info, ctx)
.await?
.get_json_value()?;
let negated = !json_to_bool(&value);
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let num = parse_json_number_as_f64( let num = parse_json_number_as_f64(
&self &self
.argument .argument

View File

@ -160,19 +160,13 @@ impl EngineConnection {
Ok(()) Ok(())
} }
#[allow(clippy::field_reassign_with_default)]
pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> { pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> {
// allowing the field_reassign_with_default lint here because the let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
// defaults for this object don't match the type defaults. We want // 4294967296 bytes, which is around 4.2 GB.
// to inherent the default config max_message_size: Some(0x100000000),
// max_frame_size: Some(0x100000000),
// See the `impl Default for WebSocketConfig` in ..Default::default()
// `tungstenite/protocol/mod.rs` };
let mut wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default();
// 4294967296 bytes, which is around 4.2 GB.
wsconfig.max_message_size = Some(0x100000000);
wsconfig.max_frame_size = Some(0x100000000);
let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket( let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
ws, ws,

View File

@ -23,7 +23,8 @@ use crate::{
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ProgramMemory { pub struct ProgramMemory {
pub root: HashMap<String, MemoryItem>, pub environments: Vec<Environment>,
pub current_env: EnvironmentRef,
#[serde(rename = "return")] #[serde(rename = "return")]
pub return_: Option<ProgramReturn>, pub return_: Option<ProgramReturn>,
} }
@ -31,7 +32,105 @@ pub struct ProgramMemory {
impl ProgramMemory { impl ProgramMemory {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
root: HashMap::from([ environments: vec![Environment::root()],
current_env: EnvironmentRef::root(),
return_: None,
}
}
pub fn new_env_for_call(&mut self, parent: EnvironmentRef) -> EnvironmentRef {
let new_env_ref = EnvironmentRef(self.environments.len());
let new_env = Environment::new(parent);
self.environments.push(new_env);
new_env_ref
}
/// Add to the program memory in the current scope.
pub fn add(&mut self, key: &str, value: MemoryItem, source_range: SourceRange) -> Result<(), KclError> {
if self.environments[self.current_env.index()].contains_key(key) {
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
message: format!("Cannot redefine `{}`", key),
source_ranges: vec![source_range],
}));
}
self.environments[self.current_env.index()].insert(key.to_string(), value);
Ok(())
}
/// Get a value from the program memory.
/// Return Err if not found.
pub fn get(&self, var: &str, source_range: SourceRange) -> Result<&MemoryItem, KclError> {
let mut env_ref = self.current_env;
loop {
let env = &self.environments[env_ref.index()];
if let Some(item) = env.bindings.get(var) {
return Ok(item);
}
if let Some(parent) = env.parent {
env_ref = parent;
} else {
break;
}
}
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("memory item key `{}` is not defined", var),
source_ranges: vec![source_range],
}))
}
/// Find all extrude groups in the memory that are on a specific sketch group id.
/// This does not look inside closures. But as long as we do not allow
/// mutation of variables in KCL, closure memory should be a subset of this.
pub fn find_extrude_groups_on_sketch_group(&self, sketch_group_id: uuid::Uuid) -> Vec<Box<ExtrudeGroup>> {
self.environments
.iter()
.flat_map(|env| {
env.bindings
.values()
.filter_map(|item| match item {
MemoryItem::ExtrudeGroup(eg) if eg.sketch_group.id == sketch_group_id => Some(eg.clone()),
_ => None,
})
.collect::<Vec<_>>()
})
.collect()
}
}
impl Default for ProgramMemory {
fn default() -> Self {
Self::new()
}
}
/// An index pointing to an environment.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
pub struct EnvironmentRef(usize);
impl EnvironmentRef {
pub fn root() -> Self {
Self(0)
}
pub fn index(&self) -> usize {
self.0
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
pub struct Environment {
bindings: HashMap<String, MemoryItem>,
parent: Option<EnvironmentRef>,
}
impl Environment {
pub fn root() -> Self {
Self {
// Prelude
bindings: HashMap::from([
( (
"ZERO".to_string(), "ZERO".to_string(),
MemoryItem::UserVal(UserVal { MemoryItem::UserVal(UserVal {
@ -61,28 +160,19 @@ impl ProgramMemory {
}), }),
), ),
]), ]),
return_: None, parent: None,
} }
} }
/// Add to the program memory. pub fn new(parent: EnvironmentRef) -> Self {
pub fn add(&mut self, key: &str, value: MemoryItem, source_range: SourceRange) -> Result<(), KclError> { Self {
if self.root.contains_key(key) { bindings: HashMap::new(),
return Err(KclError::ValueAlreadyDefined(KclErrorDetails { parent: Some(parent),
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.
/// Return Err if not found.
pub fn get(&self, key: &str, source_range: SourceRange) -> Result<&MemoryItem, KclError> { pub fn get(&self, key: &str, source_range: SourceRange) -> Result<&MemoryItem, KclError> {
self.root.get(key).ok_or_else(|| { self.bindings.get(key).ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails { KclError::UndefinedValue(KclErrorDetails {
message: format!("memory item key `{}` is not defined", key), message: format!("memory item key `{}` is not defined", key),
source_ranges: vec![source_range], source_ranges: vec![source_range],
@ -90,21 +180,12 @@ impl ProgramMemory {
}) })
} }
/// Find all extrude groups in the memory that are on a specific sketch group id. pub fn insert(&mut self, key: String, value: MemoryItem) {
pub fn find_extrude_groups_on_sketch_group(&self, sketch_group_id: uuid::Uuid) -> Vec<Box<ExtrudeGroup>> { self.bindings.insert(key, value);
self.root
.values()
.filter_map(|item| match item {
MemoryItem::ExtrudeGroup(eg) if eg.sketch_group.id == sketch_group_id => Some(eg.clone()),
_ => None,
})
.collect()
} }
}
impl Default for ProgramMemory { pub fn contains_key(&self, key: &str) -> bool {
fn default() -> Self { self.bindings.contains_key(key)
Self::new()
} }
} }
@ -161,6 +242,7 @@ pub enum MemoryItem {
#[serde(skip)] #[serde(skip)]
func: Option<MemoryFunction>, func: Option<MemoryFunction>,
expression: Box<FunctionExpression>, expression: Box<FunctionExpression>,
memory: Box<ProgramMemory>,
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
meta: Vec<Metadata>, meta: Vec<Metadata>,
}, },
@ -620,7 +702,7 @@ impl MemoryItem {
.map(Some) .map(Some)
} }
fn as_user_val(&self) -> Option<&UserVal> { pub fn as_user_val(&self) -> Option<&UserVal> {
if let MemoryItem::UserVal(x) = self { if let MemoryItem::UserVal(x) = self {
Some(x) Some(x)
} else { } else {
@ -642,27 +724,21 @@ impl MemoryItem {
} }
/// If this value is of type function, return it. /// If this value is of type function, return it.
pub fn get_function(&self, source_ranges: Vec<SourceRange>) -> Result<FnAsArg<'_>, KclError> { pub fn get_function(&self) -> Option<FnAsArg<'_>> {
let MemoryItem::Function { let MemoryItem::Function {
func, func,
expression, expression,
memory,
meta: _, meta: _,
} = &self } = &self
else { else {
return Err(KclError::Semantic(KclErrorDetails { return None;
message: "not an in-memory function".to_string(),
source_ranges,
}));
}; };
let func = func.as_ref().ok_or_else(|| { let func = func.as_ref()?;
KclError::Semantic(KclErrorDetails { Some(FnAsArg {
message: format!("Not an in-memory function: {:?}", expression),
source_ranges,
})
})?;
Ok(FnAsArg {
func, func,
expr: expression.to_owned(), expr: expression.to_owned(),
memory: memory.to_owned(),
}) })
} }
@ -736,10 +812,15 @@ impl MemoryItem {
pub async fn call_fn( pub async fn call_fn(
&self, &self,
args: Vec<MemoryItem>, args: Vec<MemoryItem>,
memory: ProgramMemory,
ctx: ExecutorContext, ctx: ExecutorContext,
) -> Result<Option<ProgramReturn>, KclError> { ) -> Result<Option<ProgramReturn>, KclError> {
let MemoryItem::Function { func, expression, meta } = &self else { let MemoryItem::Function {
func,
expression,
memory: closure_memory,
meta,
} = &self
else {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: "not a in memory function".to_string(), message: "not a in memory function".to_string(),
source_ranges: vec![], source_ranges: vec![],
@ -751,7 +832,14 @@ impl MemoryItem {
source_ranges: vec![], source_ranges: vec![],
})); }));
}; };
func(args, memory, expression.clone(), meta.clone(), ctx).await func(
args,
closure_memory.as_ref().clone(),
expression.clone(),
meta.clone(),
ctx,
)
.await
} }
} }
@ -1560,16 +1648,13 @@ impl ExecutorContext {
memory.return_ = result.return_; memory.return_ = result.return_;
} }
FunctionKind::UserDefined => { FunctionKind::UserDefined => {
if let Some(func) = memory.clone().root.get(&fn_name) { // TODO: Why do we change the source range to
let result = func.call_fn(args.clone(), memory.clone(), self.clone()).await?; // the call expression instead of keeping the
// range of the callee?
let func = memory.get(&fn_name, call_expr.into())?;
let result = func.call_fn(args.clone(), self.clone()).await?;
memory.return_ = result; memory.return_ = result;
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("No such name {} defined", fn_name),
source_ranges: vec![call_expr.into()],
}));
}
} }
} }
} }
@ -1680,7 +1765,15 @@ impl ExecutorContext {
_metadata: Vec<Metadata>, _metadata: Vec<Metadata>,
ctx: ExecutorContext| { ctx: ExecutorContext| {
Box::pin(async move { Box::pin(async move {
let mut fn_memory = assign_args_to_params(&function_expression, args, memory.clone())?; // Create a new environment to execute the function
// body in so that local variables shadow variables
// in the parent scope. The new environment's
// parent should be the environment of the closure.
let mut body_memory = memory.clone();
let closure_env = memory.current_env;
let body_env = body_memory.new_env_for_call(closure_env);
body_memory.current_env = body_env;
let mut fn_memory = assign_args_to_params(&function_expression, args, body_memory)?;
let result = ctx let result = ctx
.inner_execute(&function_expression.body, &mut fn_memory, BodyType::Block) .inner_execute(&function_expression.body, &mut fn_memory, BodyType::Block)
@ -1690,10 +1783,14 @@ impl ExecutorContext {
}) })
}, },
); );
// Cloning memory here is crucial for semantics so that we close
// over variables. Variables defined lexically later shouldn't
// be available to the function body.
MemoryItem::Function { MemoryItem::Function {
expression: function_expression.clone(), expression: function_expression.clone(),
meta: vec![metadata.to_owned()], meta: vec![metadata.to_owned()],
func: Some(mem_func), func: Some(mem_func),
memory: Box::new(memory.clone()),
} }
} }
Value::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, self).await?, Value::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, self).await?,
@ -1796,7 +1893,8 @@ fn assign_args_to_params(
return Err(err_wrong_number_args); return Err(err_wrong_number_args);
} }
// Add the arguments to the memory. // Add the arguments to the memory. A new call frame should have already
// been created.
for (index, param) in function_expression.params.iter().enumerate() { for (index, param) in function_expression.params.iter().enumerate() {
if let Some(arg) = args.get(index) { if let Some(arg) = args.get(index) {
// Argument was provided. // Argument was provided.
@ -1862,11 +1960,19 @@ const newVar = myVar + 1"#;
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(5), serde_json::json!(5),
memory.root.get("myVar").unwrap().get_json_value().unwrap() memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
); );
assert_eq!( assert_eq!(
serde_json::json!(6.0), serde_json::json!(6.0),
memory.root.get("newVar").unwrap().get_json_value().unwrap() memory
.get("newVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
); );
} }
@ -1891,13 +1997,21 @@ const intersect = segEndX('yo2', part001)"#,
let memory = parse_execute(&ast_fn("-1")).await.unwrap(); let memory = parse_execute(&ast_fn("-1")).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(1.0 + 2.0f64.sqrt()), serde_json::json!(1.0 + 2.0f64.sqrt()),
memory.root.get("intersect").unwrap().get_json_value().unwrap() memory
.get("intersect", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
); );
let memory = parse_execute(&ast_fn("0")).await.unwrap(); let memory = parse_execute(&ast_fn("0")).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(1.0000000000000002), serde_json::json!(1.0000000000000002),
memory.root.get("intersect").unwrap().get_json_value().unwrap() memory
.get("intersect", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
); );
} }
@ -2215,13 +2329,252 @@ const thisBox = box([[0,0], 6, 10, 3])
parse_execute(ast).await.unwrap(); parse_execute(ast).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_function_cannot_access_future_definitions() {
let ast = r#"
fn returnX = () => {
// x shouldn't be defined yet.
return x
}
const x = 5
const answer = returnX()"#;
let result = parse_execute(ast).await;
let err = result.unwrap_err().downcast::<KclError>().unwrap();
assert_eq!(
err,
KclError::UndefinedValue(KclErrorDetails {
message: "memory item key `x` is not defined".to_owned(),
source_ranges: vec![SourceRange([64, 65]), SourceRange([97, 106])],
}),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_pattern_transform_function_cannot_access_future_definitions() {
let ast = r#"
fn transform = (replicaId) => {
// x shouldn't be defined yet.
let scale = x
return {
translate: [0, 0, replicaId * 10],
scale: [scale, 1, 0],
}
}
fn layer = () => {
return startSketchOn("XY")
|> circle([0, 0], 1, %, 'tag1')
|> extrude(10, %)
}
const x = 5
// The 10 layers are replicas of each other, with a transform applied to each.
let shape = layer() |> patternTransform(10, transform, %)
"#;
let result = parse_execute(ast).await;
let err = result.unwrap_err().downcast::<KclError>().unwrap();
assert_eq!(
err,
KclError::UndefinedValue(KclErrorDetails {
message: "memory item key `x` is not defined".to_owned(),
source_ranges: vec![SourceRange([80, 81])],
}),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_with_parameter_redefined_outside() {
let ast = r#"
fn myIdentity = (x) => {
return x
}
const x = 33
const two = myIdentity(2)"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(2),
memory
.get("two", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(33),
memory
.get("x", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_referencing_variable_in_parent_scope() {
let ast = r#"
const x = 22
const y = 3
fn add = (x) => {
return x + y
}
const answer = add(2)"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5.0),
memory
.get("answer", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(22),
memory
.get("x", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_redefining_variable_in_parent_scope() {
let ast = r#"
const x = 1
fn foo = () => {
const x = 2
return x
}
const answer = foo()"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(2),
memory
.get("answer", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(1),
memory
.get("x", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_pattern_transform_function_redefining_variable_in_parent_scope() {
let ast = r#"
const scale = 100
fn transform = (replicaId) => {
// Redefine same variable as in parent scope.
const scale = 2
return {
translate: [0, 0, replicaId * 10],
scale: [scale, 1, 0],
}
}
fn layer = () => {
return startSketchOn("XY")
|> circle([0, 0], 1, %, 'tag1')
|> extrude(10, %)
}
// The 10 layers are replicas of each other, with a transform applied to each.
let shape = layer() |> patternTransform(10, transform, %)"#;
let memory = parse_execute(ast).await.unwrap();
// TODO: Assert that scale 2 was used.
assert_eq!(
serde_json::json!(100),
memory
.get("scale", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_ycombinator_is_even() {
let ast = r#"
// Heavily inspired by: https://raganwald.com/2018/09/10/why-y.html
fn why = (f) => {
fn inner = (maker) => {
fn inner2 = (x) => {
return f(maker(maker), x)
}
return inner2
}
return inner(
(maker) => {
fn inner2 = (x) => {
return f(maker(maker), x)
}
return inner2
}
)
}
fn innerIsEven = (self, n) => {
return !n || !self(n - 1)
}
const isEven = why(innerIsEven)
const two = isEven(2)
const three = isEven(3)
"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(true),
memory
.get("two", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(false),
memory
.get("three", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() { async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#; let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(5.0), serde_json::json!(5.0),
memory.root.get("myVar").unwrap().get_json_value().unwrap() memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
); );
} }
@ -2231,7 +2584,11 @@ const thisBox = box([[0,0], 6, 10, 3])
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(7.4), serde_json::json!(7.4),
memory.root.get("myVar").unwrap().get_json_value().unwrap() memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
); );
} }
@ -2241,7 +2598,11 @@ const thisBox = box([[0,0], 6, 10, 3])
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(1.0), serde_json::json!(1.0),
memory.root.get("myVar").unwrap().get_json_value().unwrap() memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
); );
} }
@ -2251,7 +2612,11 @@ const thisBox = box([[0,0], 6, 10, 3])
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(std::f64::consts::TAU), serde_json::json!(std::f64::consts::TAU),
memory.root.get("myVar").unwrap().get_json_value().unwrap() memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
); );
} }
@ -2261,7 +2626,11 @@ const thisBox = box([[0,0], 6, 10, 3])
let memory = parse_execute(ast).await.unwrap(); let memory = parse_execute(ast).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(7.4), serde_json::json!(7.4),
memory.root.get("thing").unwrap().get_json_value().unwrap() memory
.get("thing", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
); );
} }
@ -2391,7 +2760,9 @@ const bracket = startSketchOn('XY')
fn additional_program_memory(items: &[(String, MemoryItem)]) -> ProgramMemory { fn additional_program_memory(items: &[(String, MemoryItem)]) -> ProgramMemory {
let mut program_memory = ProgramMemory::new(); let mut program_memory = ProgramMemory::new();
for (name, item) in items { for (name, item) in items {
program_memory.root.insert(name.to_string(), item.clone()); program_memory
.add(name.as_str(), item.clone(), SourceRange::default())
.unwrap();
} }
program_memory program_memory
} }

View File

@ -299,6 +299,7 @@ fn binary_operator(i: TokenSlice) -> PResult<BinaryOperator> {
"*" => BinaryOperator::Mul, "*" => BinaryOperator::Mul,
"%" => BinaryOperator::Mod, "%" => BinaryOperator::Mod,
"^" => BinaryOperator::Pow, "^" => BinaryOperator::Pow,
"||" => BinaryOperator::LogicalOr,
_ => { _ => {
return Err(KclError::Syntax(KclErrorDetails { return Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(), source_ranges: token.as_source_ranges(),
@ -1136,11 +1137,11 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
let (operator, op_token) = any let (operator, op_token) = any
.try_map(|token: Token| match token.token_type { .try_map(|token: Token| match token.token_type {
TokenType::Operator if token.value == "-" => Ok((UnaryOperator::Neg, token)), TokenType::Operator if token.value == "-" => Ok((UnaryOperator::Neg, token)),
// TODO: negation. Original parser doesn't support `not` yet.
TokenType::Operator => Err(KclError::Syntax(KclErrorDetails { TokenType::Operator => Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(), source_ranges: token.as_source_ranges(),
message: format!("{EXPECTED} but found {} which is an operator, but not a unary one (unary operators apply to just a single operand, your operator applies to two or more operands)", token.value.as_str(),), message: format!("{EXPECTED} but found {} which is an operator, but not a unary one (unary operators apply to just a single operand, your operator applies to two or more operands)", token.value.as_str(),),
})), })),
TokenType::Bang => Ok((UnaryOperator::Not, token)),
other => Err(KclError::Syntax(KclErrorDetails { source_ranges: token.as_source_ranges(), message: format!("{EXPECTED} but found {} which is {}", token.value.as_str(), other,) })), other => Err(KclError::Syntax(KclErrorDetails { source_ranges: token.as_source_ranges(), message: format!("{EXPECTED} but found {} which is {}", token.value.as_str(), other,) })),
}) })
.context(expected("a unary expression, e.g. -x or -3")) .context(expected("a unary expression, e.g. -x or -3"))

View File

@ -79,7 +79,7 @@ impl From<ParseError<&[Token], ContextError>> for KclError {
// See https://github.com/KittyCAD/modeling-app/issues/784 // See https://github.com/KittyCAD/modeling-app/issues/784
KclError::Syntax(KclErrorDetails { KclError::Syntax(KclErrorDetails {
source_ranges: bad_token.as_source_ranges(), source_ranges: bad_token.as_source_ranges(),
message: "Unexpected token".to_string(), message: format!("Unexpected token: {}", bad_token.value),
}) })
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@ use crate::{
ast::types::FunctionExpression, ast::types::FunctionExpression,
docs::StdLibFn, docs::StdLibFn,
errors::KclError, errors::KclError,
executor::{MemoryItem, SketchGroup, SketchSurface}, executor::{MemoryItem, ProgramMemory, SketchGroup, SketchSurface},
std::kcl_stdlib::KclStdLibFn, std::kcl_stdlib::KclStdLibFn,
}; };
pub use args::Args; pub use args::Args;
@ -281,6 +281,7 @@ pub enum Primitive {
pub struct FnAsArg<'a> { pub struct FnAsArg<'a> {
pub func: &'a crate::executor::MemoryFunction, pub func: &'a crate::executor::MemoryFunction,
pub expr: Box<FunctionExpression>, pub expr: Box<FunctionExpression>,
pub memory: Box<ProgramMemory>,
} }
#[cfg(test)] #[cfg(test)]

View File

@ -87,7 +87,7 @@ pub async fn pattern_transform(args: Args) -> Result<MemoryItem, KclError> {
fn_expr: transform.expr, fn_expr: transform.expr,
meta: vec![args.source_range.into()], meta: vec![args.source_range.into()],
ctx: args.ctx.clone(), ctx: args.ctx.clone(),
memory: args.current_program_memory.clone(), memory: *transform.memory,
}, },
extr, extr,
&args, &args,
@ -116,7 +116,7 @@ pub async fn pattern_transform(args: Args) -> Result<MemoryItem, KclError> {
/// // Each layer is just a pretty thin cylinder. /// // Each layer is just a pretty thin cylinder.
/// fn layer = () => { /// fn layer = () => {
/// return startSketchOn("XY") // or some other plane idk /// return startSketchOn("XY") // or some other plane idk
/// |> circle([0, 0], 1, %, 'tag1') /// |> circle([0, 0], 1, %, $tag1)
/// |> extrude(h, %) /// |> extrude(h, %)
/// } /// }
/// // The vase is 100 layers tall. /// // The vase is 100 layers tall.

View File

@ -90,7 +90,7 @@ fn word(i: &mut Located<&str>) -> PResult<Token> {
fn operator(i: &mut Located<&str>) -> PResult<Token> { fn operator(i: &mut Located<&str>) -> PResult<Token> {
let (value, range) = alt(( let (value, range) = alt((
">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "|", "^", ">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "||", "|", "^",
)) ))
.with_span() .with_span()
.parse_next(i)?; .parse_next(i)?;

View File

@ -1308,7 +1308,7 @@ async fn serial_test_stdlib_kcl_error_right_code_path() {
assert!(result.is_err()); assert!(result.is_err());
assert_eq!( assert_eq!(
result.err().unwrap().to_string(), result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([157, 175])], message: "Expected a SketchGroup or SketchSurface as the third argument, found `[UserVal(UserVal { value: Array [Number(2), Number(2)], meta: [Metadata { source_range: SourceRange([164, 170]) }] }), UserVal(UserVal { value: Number(0.5), meta: [Metadata { source_range: SourceRange([172, 174]) }] })]`" }"# r#"semantic: KclErrorDetails { source_ranges: [SourceRange([157, 175])], message: "Expected an argument at index 2" }"#,
); );
} }
@ -1406,7 +1406,7 @@ const part = rectShape([0, 0], 20, 20)
assert!(result.is_err()); assert!(result.is_err());
assert_eq!( assert_eq!(
result.err().unwrap().to_string(), result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([891, 940])], message: "Expected a [number, number] as the first argument, found `[UserVal(UserVal { value: String(\"XY\"), meta: [Metadata { source_range: SourceRange([898, 902]) }] }), UserVal(UserVal { value: Array [Number(-6.0), Number(6)], meta: [Metadata { source_range: SourceRange([904, 927]) }] }), UserVal(UserVal { value: Number(1), meta: [Metadata { source_range: SourceRange([760, 761]) }] })]`" }"# r#"semantic: KclErrorDetails { source_ranges: [SourceRange([891, 940])], message: "Argument at index 0 was supposed to be type [f64; 2] but wasn't" }"#,
); );
} }
@ -1784,31 +1784,31 @@ const part002 = startSketchOn(part001, 'end')
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn serial_test_plumbus_fillets() { async fn serial_test_plumbus_fillets() {
let code = r#"fn make_circle = (ext, face, tag ,pos, radius) => { let code = r#"fn make_circle = (ext, face, pos, radius) => {
const sg = startSketchOn(ext, face) const sg = startSketchOn(ext, face)
|> startProfileAt([pos[0] + radius, pos[1]], %) |> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({ |> arc({
angle_end: 360, angle_end: 360,
angle_start: 0, angle_start: 0,
radius: radius radius: radius
}, %, tag) }, %, $arc1)
|> close(%) |> close(%)
return sg return sg
} }
fn pentagon = (len, taga, tagb, tagc) => { fn pentagon = (len) => {
const sg = startSketchOn('XY') const sg = startSketchOn('XY')
|> startProfileAt([-len / 2, -len / 2], %) |> startProfileAt([-len / 2, -len / 2], %)
|> angledLine({ angle: 0, length: len }, %,taga) |> angledLine({ angle: 0, length: len }, %, $a)
|> angledLine({ |> angledLine({
angle: segAng(a, %) + 180 - 108, angle: segAng(a, %) + 180 - 108,
length: len length: len
}, %, tagb) }, %, $b)
|> angledLine({ |> angledLine({
angle: segAng(b, %) + 180 - 108, angle: segAng(b, %) + 180 - 108,
length: len length: len
}, %,tagc) }, %, $c)
|> angledLine({ |> angledLine({
angle: segAng(c, %) + 180 - 108, angle: segAng(c, %) + 180 - 108,
length: len length: len
@ -1821,21 +1821,23 @@ fn pentagon = (len, taga, tagb, tagc) => {
return sg return sg
} }
const p = pentagon(32, $a, $b, $c) const p = pentagon(32)
|> extrude(10, %) |> extrude(10, %)
const plumbus0 = make_circle(p,a, $arc_a, [0, 0], 2.5) const circle0 = make_circle(p, p.sketchGroup.tags.a, [0, 0], 2.5)
const plumbus0 = circle0
|> extrude(10, %) |> extrude(10, %)
|> fillet({ |> fillet({
radius: 0.5, radius: 0.5,
tags: [arc_a, getOppositeEdge(arc_a, %)] tags: [circle0.tags.arc1, getOppositeEdge(circle0.tags.arc1, %)]
}, %) }, %)
const plumbus1 = make_circle(p, b,$arc_b, [0, 0], 2.5) const circle1 = make_circle(p, p.sketchGroup.tags.b, [0, 0], 2.5)
const plumbus1 = circle1
|> extrude(10, %) |> extrude(10, %)
|> fillet({ |> fillet({
radius: 0.5, radius: 0.5,
tags: [arc_b, getOppositeEdge(arc_b, %)] tags: [circle1.tags.arc1, getOppositeEdge(circle1.tags.arc1, %)]
}, %) }, %)
"#; "#;

View File

@ -39,7 +39,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
// We need to get the sketch ID. // We need to get the sketch ID.
// Get the sketch group ID from memory. // Get the sketch group ID from memory.
let MemoryItem::SketchGroup(sketch_group) = memory.root.get(name).unwrap() else { let MemoryItem::SketchGroup(sketch_group) = memory.get(name, SourceRange::default()).unwrap() else {
anyhow::bail!("part001 not found in memory: {:?}", memory); anyhow::bail!("part001 not found in memory: {:?}", memory);
}; };
let sketch_id = sketch_group.id; let sketch_id = sketch_group.id;