Compare commits
13 Commits
v0.24.3
...
paultag/re
Author | SHA1 | Date | |
---|---|---|---|
b2ba7858cf | |||
cfeb4f4575 | |||
a68748abcf | |||
1b8688f274 | |||
0b06e7cd17 | |||
397839da84 | |||
ac120838e5 | |||
e6a2ac9c4a | |||
6e7e6e96cf | |||
73e155d79b | |||
a782f26ec2 | |||
01076c3aed | |||
fe512611ac |
@ -7221,6 +7221,7 @@ test.describe('Test network and connection issues', () => {
|
||||
|
||||
// Expect the network to be up
|
||||
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
||||
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
|
||||
|
||||
// Click off the code pane.
|
||||
await page.mouse.click(100, 100)
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
@ -16,14 +16,14 @@ export const TEST_COLORS = {
|
||||
} as const
|
||||
|
||||
async function waitForPageLoad(page: Page) {
|
||||
// wait for 'Loading stream...' spinner
|
||||
await page.getByTestId('loading-stream').waitFor()
|
||||
// wait for all spinners to be gone
|
||||
await page
|
||||
.getByTestId('loading')
|
||||
.waitFor({ state: 'detached', timeout: 20_000 })
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await page.getByTestId('start-sketch').waitFor()
|
||||
await expect(page.getByTestId('start-sketch')).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
}
|
||||
|
||||
async function removeCurrentCode(page: Page) {
|
||||
@ -471,8 +471,10 @@ export const doExport = async (
|
||||
page: Page
|
||||
): Promise<Paths> => {
|
||||
await page.getByRole('button', { name: APP_NAME }).click()
|
||||
await expect(page.getByRole('button', { name: 'Export Part' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Export Part' }).click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export', exact: false })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Export', exact: false }).click()
|
||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||
|
||||
// Go through export via command bar
|
||||
|
@ -77,7 +77,7 @@ describe('ZMA authorized user flows', () => {
|
||||
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
||||
await click(menuButton)
|
||||
|
||||
const settingsButton = await $('[data-testid="settings-button"]')
|
||||
const settingsButton = await $('[data-testid="user-settings"]')
|
||||
await click(settingsButton)
|
||||
|
||||
const projectDirInput = await $('[data-testid="project-directory-input"]')
|
||||
|
108
src-tauri/Cargo.lock
generated
@ -332,7 +332,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -367,7 +367,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -407,7 +407,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -550,7 +550,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.1.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"syn_derive",
|
||||
]
|
||||
|
||||
@ -823,7 +823,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1073,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1083,7 +1083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1107,7 +1107,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1118,7 +1118,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1179,7 +1179,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@ -1216,7 +1216,7 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1227,7 +1227,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1288,7 +1288,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1320,7 +1320,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1427,7 +1427,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1588,7 +1588,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1704,7 +1704,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1980,7 +1980,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2008,7 +2008,7 @@ dependencies = [
|
||||
"inflections",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2083,7 +2083,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3377,7 +3377,7 @@ dependencies = [
|
||||
"regex",
|
||||
"regex-syntax 0.8.3",
|
||||
"structmeta",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3496,7 +3496,7 @@ dependencies = [
|
||||
"phf_shared 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3564,7 +3564,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4438,7 +4438,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4558,7 +4558,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4569,7 +4569,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4602,7 +4602,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4623,7 +4623,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4665,7 +4665,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4933,7 +4933,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4944,7 +4944,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4966,7 +4966,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4999,9 +4999,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.70"
|
||||
version = "2.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16"
|
||||
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -5017,7 +5017,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5034,7 +5034,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5251,7 +5251,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"tauri-utils",
|
||||
"thiserror",
|
||||
"time",
|
||||
@ -5269,7 +5269,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"tauri-codegen",
|
||||
"tauri-utils",
|
||||
]
|
||||
@ -5627,22 +5627,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5740,7 +5740,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5940,7 +5940,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5969,7 +5969,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6099,7 +6099,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
@ -6316,7 +6316,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6415,7 +6415,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -6449,7 +6449,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -6590,7 +6590,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6696,7 +6696,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6707,7 +6707,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7159,7 +7159,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -47,7 +47,6 @@ import {
|
||||
PipeExpression,
|
||||
Program,
|
||||
ProgramMemory,
|
||||
programMemoryInit,
|
||||
recast,
|
||||
SketchGroup,
|
||||
ExtrudeGroup,
|
||||
@ -130,7 +129,7 @@ export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
|
||||
export class SceneEntities {
|
||||
engineCommandManager: EngineCommandManager
|
||||
scene: Scene
|
||||
sceneProgramMemory: ProgramMemory = { root: {}, return: null }
|
||||
sceneProgramMemory: ProgramMemory = ProgramMemory.empty()
|
||||
activeSegments: { [key: string]: Group } = {}
|
||||
intersectionPlane: Mesh | null = null
|
||||
axisGroup: Group | null = null
|
||||
@ -550,9 +549,9 @@ export class SceneEntities {
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
|
||||
const sg = kclManager.programMemory.root[
|
||||
const sg = kclManager.programMemory.get(
|
||||
variableDeclarationName
|
||||
] as SketchGroup
|
||||
) as SketchGroup
|
||||
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`
|
||||
@ -768,9 +767,9 @@ export class SceneEntities {
|
||||
programMemoryOverride,
|
||||
})
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketchGroup = programMemory.root[
|
||||
const sketchGroup = programMemory.get(
|
||||
variableDeclarationName
|
||||
] as SketchGroup
|
||||
) as SketchGroup
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
@ -820,9 +819,9 @@ export class SceneEntities {
|
||||
|
||||
// Prepare to update the THREEjs scene
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketchGroup = programMemory.root[
|
||||
const sketchGroup = programMemory.get(
|
||||
variableDeclarationName
|
||||
] as SketchGroup
|
||||
) as SketchGroup
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
@ -1081,9 +1080,9 @@ export class SceneEntities {
|
||||
})
|
||||
this.sceneProgramMemory = programMemory
|
||||
|
||||
const maybeSketchGroup = programMemory.root[variableDeclarationName]
|
||||
const maybeSketchGroup = programMemory.get(variableDeclarationName)
|
||||
let sketchGroup = undefined
|
||||
if (maybeSketchGroup.type === 'SketchGroup') {
|
||||
if (maybeSketchGroup?.type === 'SketchGroup') {
|
||||
sketchGroup = maybeSketchGroup
|
||||
} else if ((maybeSketchGroup as ExtrudeGroup).sketchGroup) {
|
||||
sketchGroup = (maybeSketchGroup as ExtrudeGroup).sketchGroup
|
||||
@ -1773,7 +1772,7 @@ function prepareTruncatedMemoryAndAst(
|
||||
if (err(_node)) return _node
|
||||
const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || ''
|
||||
const lastSeg = (
|
||||
programMemory.root[variableDeclarationName] as SketchGroup
|
||||
programMemory.get(variableDeclarationName) as SketchGroup
|
||||
).value.slice(-1)[0]
|
||||
if (draftSegment) {
|
||||
// truncatedAst needs to setup with another segment at the end
|
||||
@ -1824,33 +1823,27 @@ function prepareTruncatedMemoryAndAst(
|
||||
..._ast,
|
||||
body: [JSON.parse(JSON.stringify(_ast.body[bodyIndex]))],
|
||||
}
|
||||
const programMemoryOverride = programMemoryInit()
|
||||
if (err(programMemoryOverride)) return programMemoryOverride
|
||||
|
||||
// Grab all the TagDeclarators and TagIdentifiers from memory.
|
||||
let start = _node.node.start
|
||||
for (const key in programMemory.root) {
|
||||
const value = programMemory.root[key]
|
||||
if (!('__meta' in value)) {
|
||||
continue
|
||||
}
|
||||
const programMemoryOverride = programMemory.filterVariables(true, (value) => {
|
||||
if (
|
||||
!('__meta' in value) ||
|
||||
value.__meta === undefined ||
|
||||
value.__meta.length === 0 ||
|
||||
value.__meta[0].sourceRange === undefined
|
||||
) {
|
||||
continue
|
||||
return false
|
||||
}
|
||||
|
||||
if (value.__meta[0].sourceRange[0] >= start) {
|
||||
// We only want things before our start point.
|
||||
continue
|
||||
return false
|
||||
}
|
||||
|
||||
if (value.type === 'TagIdentifier') {
|
||||
programMemoryOverride.root[key] = JSON.parse(JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
return value.type === 'TagIdentifier'
|
||||
})
|
||||
if (err(programMemoryOverride)) return programMemoryOverride
|
||||
|
||||
for (let i = 0; i < bodyIndex; i++) {
|
||||
const node = _ast.body[i]
|
||||
@ -1858,12 +1851,15 @@ function prepareTruncatedMemoryAndAst(
|
||||
continue
|
||||
}
|
||||
const name = node.declarations[0].id.name
|
||||
// const memoryItem = kclManager.programMemory.root[name]
|
||||
const memoryItem = programMemory.root[name]
|
||||
const memoryItem = programMemory.get(name)
|
||||
if (!memoryItem) {
|
||||
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 {
|
||||
truncatedAst,
|
||||
@ -1900,7 +1896,7 @@ export function sketchGroupFromPathToNode({
|
||||
)
|
||||
if (err(_varDec)) return _varDec
|
||||
const varDec = _varDec.node
|
||||
const result = programMemory.root[varDec?.id?.name || '']
|
||||
const result = programMemory.get(varDec?.id?.name || '')
|
||||
if (result?.type === 'ExtrudeGroup') {
|
||||
return result.sketchGroup
|
||||
}
|
||||
@ -2026,13 +2022,17 @@ export async function getFaceDetails(
|
||||
entity_id: entityId,
|
||||
},
|
||||
})
|
||||
const faceInfo: Models['GetSketchModePlane_type'] = (
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'get_sketch_mode_plane' },
|
||||
})
|
||||
)?.data?.data
|
||||
const resp = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'get_sketch_mode_plane' },
|
||||
})
|
||||
const faceInfo =
|
||||
resp?.success &&
|
||||
resp?.resp.type === 'modeling' &&
|
||||
resp?.resp?.data?.modeling_response?.type === 'get_sketch_mode_plane'
|
||||
? resp?.resp?.data?.modeling_response.data
|
||||
: ({} as Models['GetSketchModePlane_type'])
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
|
@ -49,9 +49,9 @@ export const AppHeader = ({
|
||||
<>
|
||||
<CommandBarOpenButton />
|
||||
<RefreshButton />
|
||||
<UserSidebarMenu user={user} />
|
||||
</>
|
||||
)}
|
||||
<UserSidebarMenu user={user} />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { parse, BinaryPart, Value } from '../lang/wasm'
|
||||
import { parse, BinaryPart, Value, ProgramMemory } from '../lang/wasm'
|
||||
import {
|
||||
createIdentifier,
|
||||
createLiteral,
|
||||
@ -120,8 +120,7 @@ export function useCalc({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const allVarNames = Object.keys(programMemory.root)
|
||||
if (allVarNames.includes(newVariableName)) {
|
||||
if (programMemory.has(newVariableName)) {
|
||||
setIsNewVariableNameUnique(false)
|
||||
} else {
|
||||
setIsNewVariableNameUnique(true)
|
||||
@ -143,17 +142,20 @@ export function useCalc({
|
||||
const code = `const __result__ = ${value}`
|
||||
const ast = parse(code)
|
||||
if (trap(ast)) return
|
||||
const _programMem: any = { root: {}, return: null }
|
||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||
})
|
||||
const _programMem: ProgramMemory = ProgramMemory.empty()
|
||||
for (const { key, value } of availableVarInfo.variables) {
|
||||
const error = _programMem.set(key, {
|
||||
type: 'UserVal',
|
||||
value,
|
||||
__meta: [],
|
||||
})
|
||||
if (trap(error)) return
|
||||
}
|
||||
executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
useFakeExecutor: true,
|
||||
programMemoryOverride: JSON.parse(
|
||||
JSON.stringify(kclManager.programMemory)
|
||||
),
|
||||
programMemoryOverride: kclManager.programMemory.clone(),
|
||||
}).then(({ programMemory }) => {
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
@ -163,7 +165,7 @@ export function useCalc({
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declarations?.[0]?.init
|
||||
const result = programMemory?.root?.__result__?.value
|
||||
const result = programMemory?.get('__result__')?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
})
|
||||
|
@ -311,6 +311,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</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': (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
@ -138,15 +138,23 @@ export const ModelingMachineProvider = ({
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||
|
||||
const settings: Models['CameraSettings_type'] = (
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
)?.data?.data?.settings
|
||||
const resp = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
|
||||
const settings =
|
||||
resp &&
|
||||
resp.success &&
|
||||
resp.resp.type === 'modeling' &&
|
||||
resp.resp.data.modeling_response.type ===
|
||||
'default_camera_get_settings'
|
||||
? resp.resp.data.modeling_response.data.settings
|
||||
: ({} as Models['DefaultCameraGetSettings_type']['settings'])
|
||||
|
||||
if (settings.up.z !== 1) {
|
||||
// workaround for gimbal lock situation
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
|
@ -163,7 +163,7 @@ export function useCodeMirror(props: UseCodeMirror) {
|
||||
effects: StateEffect.reconfigure.of(targetExtensions),
|
||||
})
|
||||
}
|
||||
}, [targetExtensions])
|
||||
}, [targetExtensions, view, isFirstRender])
|
||||
|
||||
return { view, setView, container, setContainer, state, setState }
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { processMemory } from './MemoryPane'
|
||||
import { enginelessExecutor } from '../../../lib/testHelpers'
|
||||
import { initPromise, parse } from '../../../lang/wasm'
|
||||
import { initPromise, parse, ProgramMemory } from '../../../lang/wasm'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
@ -29,10 +29,7 @@ describe('processMemory', () => {
|
||||
|> lineTo([2.15, 4.32], %)
|
||||
// |> rx(90, %)`
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast, {
|
||||
root: {},
|
||||
return: null,
|
||||
})
|
||||
const programMemory = await enginelessExecutor(ast, ProgramMemory.empty())
|
||||
const output = processMemory(programMemory)
|
||||
expect(output.myVar).toEqual(5)
|
||||
expect(output.otherVar).toEqual(3)
|
||||
|
@ -82,8 +82,7 @@ export const MemoryPane = () => {
|
||||
|
||||
export const processMemory = (programMemory: ProgramMemory) => {
|
||||
const processedMemory: any = {}
|
||||
Object.keys(programMemory?.root || {}).forEach((key) => {
|
||||
const val = programMemory.root[key]
|
||||
for (const [key, val] of programMemory?.visibleEntries()) {
|
||||
if (typeof val.value !== 'function') {
|
||||
if (val.type === 'SketchGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||
@ -103,6 +102,6 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
} else if (key !== 'log') {
|
||||
processedMemory[key] = '__function__'
|
||||
}
|
||||
})
|
||||
}
|
||||
return processedMemory
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { ActionButton, ActionButtonProps } from './ActionButton'
|
||||
import { type IndexLoaderData } from 'lib/types'
|
||||
import { paths } from 'lib/paths'
|
||||
import { isTauri } from '../lib/isTauri'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Fragment } from 'react'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Fragment, useMemo } from 'react'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { Logo } from './Logo'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
@ -12,6 +12,9 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
project,
|
||||
@ -80,6 +83,10 @@ function ProjectMenuPopover({
|
||||
project?: IndexLoaderData['project']
|
||||
file?: IndexLoaderData['file']
|
||||
}) {
|
||||
const platform = usePlatform()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const { onProjectClose } = useLspContext()
|
||||
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 (
|
||||
<Popover className="relative">
|
||||
<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"
|
||||
>
|
||||
<CustomIcon name="three-dots" className="w-5 h-5 rotate-90" />
|
||||
<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">
|
||||
{isTauri() && file?.name
|
||||
@ -109,68 +185,53 @@ function ProjectMenuPopover({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<CustomIcon
|
||||
name="caretDown"
|
||||
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40 ui-open:rotate-180"
|
||||
/>
|
||||
</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
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0 -translate-x-1/4"
|
||||
enterTo="opacity-100 translate-x-0"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100 translate-x-0"
|
||||
leaveTo="opacity-0 -translate-x-4"
|
||||
enterFrom="opacity-0 -translate-y-2"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<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"
|
||||
style={{ gridTemplateRows: 'auto 1fr auto' }}
|
||||
className={`z-10 absolute top-full left-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 }) => (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{ icon: 'exportFile', className: 'p-1' }}
|
||||
className="border-transparent dark:border-transparent"
|
||||
disabled={!findCommand(exportCommandInfo)}
|
||||
onClick={() =>
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: exportCommandInfo,
|
||||
})
|
||||
}
|
||||
>
|
||||
Export Part
|
||||
</ActionButton>
|
||||
{isTauri() && (
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
onProjectClose(file || null, project?.path || null, true)
|
||||
// Clear the scene and end the session.
|
||||
engineCommandManager.endSession()
|
||||
}}
|
||||
iconStart={{
|
||||
icon: 'arrowLeft',
|
||||
className: 'p-1',
|
||||
}}
|
||||
className="border-transparent dark:border-transparent"
|
||||
>
|
||||
Go to Home
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<ul className="relative flex flex-col items-stretch content-stretch p-0.5">
|
||||
{projectMenuItems.map((props, index) => {
|
||||
if (props === 'break') {
|
||||
return index !== projectMenuItems.length - 1 ? (
|
||||
<li key={`break-${index}`} className="contents">
|
||||
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
|
||||
const { id, className, children, ...rest } = props
|
||||
return (
|
||||
<li key={id} className="contents">
|
||||
<ActionButton
|
||||
{...rest}
|
||||
className={
|
||||
'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 ' +
|
||||
className
|
||||
}
|
||||
onMouseUp={() => {
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionButton>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
|
@ -157,6 +157,7 @@ export const Stream = () => {
|
||||
useEffect(() => {
|
||||
setIsFirstRender(kclManager.isFirstRender)
|
||||
if (!kclManager.isFirstRender) videoRef.current?.play()
|
||||
setIsFreezeFrame(!kclManager.isFirstRender)
|
||||
}, [kclManager.isFirstRender])
|
||||
|
||||
useEffect(() => {
|
||||
@ -178,6 +179,8 @@ export const Stream = () => {
|
||||
videoElement: videoRef.current,
|
||||
},
|
||||
})
|
||||
|
||||
setIsLoading(false)
|
||||
}, [mediaStream])
|
||||
|
||||
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
|
@ -8,7 +8,9 @@
|
||||
--_delay: 200ms;
|
||||
--_triangle-width: 8px;
|
||||
--_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;
|
||||
--_bg: var(--chalkboard-10);
|
||||
--_shadow-alpha: 8%;
|
||||
@ -33,7 +35,7 @@
|
||||
font-weight: normal;
|
||||
line-height: initial;
|
||||
letter-spacing: 0;
|
||||
padding: var(--_p-block) var(--_p-inline);
|
||||
padding: var(--_p-block) calc(2 * var(--_p-block));
|
||||
margin: 0;
|
||||
border-radius: 3px;
|
||||
background: var(--_bg);
|
||||
@ -119,7 +121,7 @@
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
@ -130,7 +132,7 @@
|
||||
}
|
||||
|
||||
.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%;
|
||||
--_y: 50%;
|
||||
}
|
||||
@ -142,7 +144,7 @@
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
@ -165,7 +167,7 @@
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
@ -176,7 +178,9 @@
|
||||
}
|
||||
|
||||
.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%;
|
||||
--_y: 50%;
|
||||
}
|
||||
@ -188,7 +192,7 @@
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
|
@ -25,11 +25,11 @@ export function UnitsMenu() {
|
||||
border border-solid border-chalkboard-10 dark:border-chalkboard-90 rounded
|
||||
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) => (
|
||||
<li key={unit} className="contents">
|
||||
<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={() => {
|
||||
settings.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faBug, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import { ActionButton, ActionButtonProps } from './ActionButton'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import { paths } from 'lib/paths'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import Tooltip from './Tooltip'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
|
||||
type User = Models['User_type']
|
||||
|
||||
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
const platform = usePlatform()
|
||||
const location = useLocation()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const displayedName = getDisplayName(user)
|
||||
@ -20,6 +22,128 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
const navigate = useNavigate()
|
||||
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
|
||||
// resource to be a local one.
|
||||
if (user?.image === 'https://placekitten.com/200/200') {
|
||||
@ -43,139 +167,90 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{user?.image && !imageLoadFailed ? (
|
||||
<Popover.Button
|
||||
className="relative border-0 rounded-full w-fit min-w-max p-0 group"
|
||||
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.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"
|
||||
data-testid="user-sidebar-toggle"
|
||||
>
|
||||
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
||||
</Transition>
|
||||
|
||||
<div className="flex items-center">
|
||||
<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
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0 translate-x-1/4"
|
||||
enterTo="opacity-100 translate-x-0"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100 translate-x-0"
|
||||
leaveTo="opacity-0 translate-x-4"
|
||||
enterFrom="opacity-0 -translate-y-2"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
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 }) => (
|
||||
<>
|
||||
{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">
|
||||
{user.image && !imageLoadFailed && (
|
||||
<div className="rounded-full shadow-inner overflow-hidden">
|
||||
<img
|
||||
src={user.image}
|
||||
alt={user.name || ''}
|
||||
className="h-8 w-8"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={() => setImageLoadFailed(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="m-0 text-mono" data-testid="username">
|
||||
{displayedName || ''}
|
||||
<div className="flex flex-col gap-1 px-2.5 py-3 bg-chalkboard-20 dark:bg-chalkboard-80/50">
|
||||
<p className="m-0 text-mono text-xs" data-testid="username">
|
||||
{displayedName || ''}
|
||||
</p>
|
||||
{displayedName !== user.email && (
|
||||
<p
|
||||
className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs"
|
||||
data-testid="email"
|
||||
>
|
||||
{user.email}
|
||||
</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 className="p-4 flex flex-col gap-2">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{ icon: 'settings' }}
|
||||
className="border-transparent dark:border-transparent hover:bg-transparent"
|
||||
onClick={() => {
|
||||
// since /settings is a nested route the sidebar doesn't close
|
||||
// automatically when navigating to it
|
||||
close()
|
||||
const targetPath = location.pathname.includes(paths.FILE)
|
||||
? filePath + paths.SETTINGS
|
||||
: paths.HOME + paths.SETTINGS
|
||||
navigate(targetPath)
|
||||
}}
|
||||
data-testid="settings-button"
|
||||
>
|
||||
Settings
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="externalLink"
|
||||
to="https://github.com/KittyCAD/modeling-app/discussions"
|
||||
iconStart={{ icon: faGithub, className: 'p-1', size: 'sm' }}
|
||||
className="border-transparent dark:border-transparent"
|
||||
>
|
||||
Request a feature
|
||||
</ActionButton>
|
||||
<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>
|
||||
<ul className="relative flex flex-col items-stretch content-stretch p-0.5">
|
||||
{userMenuItems.map((props, index) => {
|
||||
if (props === 'break') {
|
||||
return index !== userMenuItems.length - 1 ? (
|
||||
<li key={`break-${index}`} className="contents">
|
||||
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
|
||||
const { id, children, ...rest } = props
|
||||
return (
|
||||
<li key={id} className="contents">
|
||||
<ActionButton
|
||||
{...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"
|
||||
onMouseUp={() => {
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionButton>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
|
@ -21,13 +21,10 @@ export const modelingMachineEvent = modelingMachineAnnotation.of(true)
|
||||
const setDiagnosticsAnnotation = Annotation.define<boolean>()
|
||||
export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(true)
|
||||
|
||||
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
|
||||
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
|
||||
}
|
||||
|
||||
export default class EditorManager {
|
||||
private _editorView: EditorView | null = null
|
||||
private _copilotEnabled: boolean = true
|
||||
private _diagnostics: Diagnostic[] = []
|
||||
|
||||
private _isShiftDown: boolean = false
|
||||
private _selectionRanges: Selections = {
|
||||
@ -118,6 +115,14 @@ export default class EditorManager {
|
||||
this.setDiagnostics([])
|
||||
}
|
||||
|
||||
addDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this._editorView) return
|
||||
diagnostics.forEach((diagnostic) => {
|
||||
this._diagnostics.push(diagnostic)
|
||||
})
|
||||
this.setDiagnostics(this._diagnostics)
|
||||
}
|
||||
|
||||
setDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this._editorView) return
|
||||
|
||||
@ -131,26 +136,6 @@ export default class EditorManager {
|
||||
})
|
||||
}
|
||||
|
||||
addDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this._editorView) return
|
||||
|
||||
forEachDiagnostic(this._editorView.state, function (diag) {
|
||||
diagnostics.push(diag)
|
||||
})
|
||||
|
||||
const uniqueDiagnostics = new Set<Diagnostic>()
|
||||
diagnostics.forEach((diagnostic) => {
|
||||
for (const knownDiagnostic of uniqueDiagnostics.values()) {
|
||||
if (diagnosticIsEqual(diagnostic, knownDiagnostic)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
uniqueDiagnostics.add(diagnostic)
|
||||
})
|
||||
|
||||
this.setDiagnostics([...uniqueDiagnostics])
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this._editorView) {
|
||||
undo(this._editorView)
|
||||
|
@ -45,9 +45,6 @@ export function useSetupEngineManager(
|
||||
streamRef?.current?.offsetWidth ?? 0,
|
||||
streamRef?.current?.offsetHeight ?? 0
|
||||
)
|
||||
if (restart) {
|
||||
kclManager.isFirstRender = false
|
||||
}
|
||||
engineCommandManager.start({
|
||||
restart,
|
||||
setMediaStream: (mediaStream) => setMediaStream(mediaStream),
|
||||
|
@ -260,8 +260,17 @@ code {
|
||||
|
||||
@layer components {
|
||||
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 border border-t-0 border-b-2 border-chalkboard-30 dark:border-chalkboard-80;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,9 +14,7 @@ import {
|
||||
Program,
|
||||
ProgramMemory,
|
||||
recast,
|
||||
SketchGroup,
|
||||
SourceRange,
|
||||
ExtrudeGroup,
|
||||
} from 'lang/wasm'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
||||
@ -33,10 +31,7 @@ export class KclManager {
|
||||
},
|
||||
digest: null,
|
||||
}
|
||||
private _programMemory: ProgramMemory = {
|
||||
root: {},
|
||||
return: null,
|
||||
}
|
||||
private _programMemory: ProgramMemory = ProgramMemory.empty()
|
||||
private _logs: string[] = []
|
||||
private _kclErrors: KCLError[] = []
|
||||
private _isExecuting = false
|
||||
@ -222,7 +217,6 @@ export class KclManager {
|
||||
ast,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
})
|
||||
|
||||
editorManager.addDiagnostics(await lintAst({ ast: ast }))
|
||||
|
||||
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
||||
@ -303,7 +297,6 @@ export class KclManager {
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
useFakeExecutor: true,
|
||||
})
|
||||
|
||||
editorManager.addDiagnostics(await lintAst({ ast: ast }))
|
||||
|
||||
this._logs = logs
|
||||
@ -505,10 +498,7 @@ function defaultSelectionFilter(
|
||||
programMemory: ProgramMemory,
|
||||
engineCommandManager: EngineCommandManager
|
||||
) {
|
||||
const firstSketchOrExtrudeGroup = Object.values(programMemory.root).find(
|
||||
(node) => node.type === 'ExtrudeGroup' || node.type === 'SketchGroup'
|
||||
) as SketchGroup | ExtrudeGroup
|
||||
firstSketchOrExtrudeGroup &&
|
||||
programMemory.hasSketchOrExtrudeGroup() &&
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
|
@ -16,7 +16,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
// |> rx(45, %)`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const sketch001 = programMemory?.root?.mySketch001
|
||||
const sketch001 = programMemory?.get('mySketch001')
|
||||
expect(sketch001).toEqual({
|
||||
type: 'SketchGroup',
|
||||
on: expect.any(Object),
|
||||
@ -66,7 +66,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> extrude(2, %)`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const sketch001 = programMemory?.root?.mySketch001
|
||||
const sketch001 = programMemory?.get('mySketch001')
|
||||
expect(sketch001).toEqual({
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
@ -146,7 +146,7 @@ const sk2 = startSketchOn('XY')
|
||||
`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2]
|
||||
const geos = [programMemory?.get('theExtrude'), programMemory?.get('sk2')]
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'ExtrudeGroup',
|
||||
|
@ -12,25 +12,25 @@ describe('test executor', () => {
|
||||
it('test assigning two variables, the second summing with the first', async () => {
|
||||
const code = `const myVar = 5
|
||||
const newVar = myVar + 1`
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(5)
|
||||
expect(root.newVar.value).toBe(6)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(5)
|
||||
expect(mem.get('newVar')?.value).toBe(6)
|
||||
})
|
||||
it('test assigning a var with a string', async () => {
|
||||
const code = `const myVar = "a str"`
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe('a str')
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe('a str')
|
||||
})
|
||||
it('test assigning a var by cont concatenating two strings string execute', async () => {
|
||||
const code = fs.readFileSync(
|
||||
'./src/lang/testExamples/variableDeclaration.cado',
|
||||
'utf-8'
|
||||
)
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe('a str another str')
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe('a str another str')
|
||||
})
|
||||
it('fn funcN = () => {} execute', async () => {
|
||||
const { root } = await exe(
|
||||
const mem = await exe(
|
||||
[
|
||||
'fn funcN = (a, b) => {',
|
||||
' return a + b',
|
||||
@ -39,8 +39,8 @@ const newVar = myVar + 1`
|
||||
'const magicNum = funcN(9, theVar)',
|
||||
].join('\n')
|
||||
)
|
||||
expect(root.theVar.value).toBe(60)
|
||||
expect(root.magicNum.value).toBe(69)
|
||||
expect(mem.get('theVar')?.value).toBe(60)
|
||||
expect(mem.get('magicNum')?.value).toBe(69)
|
||||
})
|
||||
it('sketch declaration', async () => {
|
||||
let code = `const mySketch = startSketchOn('XY')
|
||||
@ -50,9 +50,9 @@ const newVar = myVar + 1`
|
||||
|> lineTo([5,-1], %, "rightPath")
|
||||
// |> 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
|
||||
const minusGeo = root.mySketch.value
|
||||
const minusGeo = mem.get('mySketch')?.value
|
||||
expect(minusGeo).toEqual([
|
||||
{
|
||||
type: 'ToPoint',
|
||||
@ -104,8 +104,8 @@ const newVar = myVar + 1`
|
||||
'fn myFn = (a) => { return a + 1 }',
|
||||
'const myVar = 5 + 1 |> myFn(%)',
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(7)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(7)
|
||||
})
|
||||
|
||||
// Enable rotations #152
|
||||
@ -117,16 +117,16 @@ const newVar = myVar + 1`
|
||||
// ' |> lineTo([1, 1], %)',
|
||||
// 'const rotated = rx(90, mySk1)',
|
||||
// ].join('\n')
|
||||
// const { root } = await exe(code)
|
||||
// expect(root.mySk1.value).toHaveLength(3)
|
||||
// expect(root?.rotated?.type).toBe('SketchGroup')
|
||||
// const mem = await exe(code)
|
||||
// expect(mem.get('mySk1')?.value).toHaveLength(3)
|
||||
// expect(mem.get('rotated')?.type).toBe('SketchGroup')
|
||||
// if (
|
||||
// root?.mySk1?.type !== 'SketchGroup' ||
|
||||
// root?.rotated?.type !== 'SketchGroup'
|
||||
// mem.get('mySk1')?.type !== 'SketchGroup' ||
|
||||
// mem.get('rotated')?.type !== 'SketchGroup'
|
||||
// )
|
||||
// throw new Error('not a sketch group')
|
||||
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
|
||||
// expect(root.rotated.rotation.map((a) => a.toFixed(4))).toEqual([
|
||||
// expect(mem.get('mySk1')?.rotation).toEqual([0, 0, 0, 1])
|
||||
// expect(mem.get('rotated')?.rotation.map((a) => a.toFixed(4))).toEqual([
|
||||
// '0.7071',
|
||||
// '0.0000',
|
||||
// '0.0000',
|
||||
@ -144,8 +144,8 @@ const newVar = myVar + 1`
|
||||
' |> lineTo([1,1], %)',
|
||||
// ' |> rx(90, %)',
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.mySk1).toEqual({
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('mySk1')).toEqual({
|
||||
type: 'SketchGroup',
|
||||
on: expect.any(Object),
|
||||
start: {
|
||||
@ -214,36 +214,37 @@ const newVar = myVar + 1`
|
||||
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
|
||||
'\n'
|
||||
)
|
||||
const { root } = await exe(code)
|
||||
const mem = await exe(code)
|
||||
// TODO path to node is probably wrong here, zero indexes are not correct
|
||||
expect(root).toEqual({
|
||||
three: {
|
||||
type: 'UserVal',
|
||||
value: 3,
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [14, 15],
|
||||
},
|
||||
],
|
||||
},
|
||||
yo: {
|
||||
type: 'UserVal',
|
||||
value: [1, '2', 3, 9],
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [27, 49],
|
||||
},
|
||||
],
|
||||
},
|
||||
expect(mem.get('three')).toEqual({
|
||||
type: 'UserVal',
|
||||
value: 3,
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [14, 15],
|
||||
},
|
||||
],
|
||||
})
|
||||
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 () => {
|
||||
const code = [
|
||||
'const three = 3',
|
||||
"const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}",
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.yo).toEqual({
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('yo')).toEqual({
|
||||
type: 'UserVal',
|
||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||
__meta: [
|
||||
@ -257,8 +258,8 @@ const newVar = myVar + 1`
|
||||
const code = ["const yo = {a: {b: '123'}}", "const myVar = yo.a['b']"].join(
|
||||
'\n'
|
||||
)
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar).toEqual({
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')).toEqual({
|
||||
type: 'UserVal',
|
||||
value: '123',
|
||||
__meta: [
|
||||
@ -273,81 +274,81 @@ const newVar = myVar + 1`
|
||||
describe('testing math operators', () => {
|
||||
it('can sum', async () => {
|
||||
const code = ['const myVar = 1 + 2'].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(3)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(3)
|
||||
})
|
||||
it('can subtract', async () => {
|
||||
const code = ['const myVar = 1 - 2'].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(-1)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(-1)
|
||||
})
|
||||
it('can multiply', async () => {
|
||||
const code = ['const myVar = 1 * 2'].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(2)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(2)
|
||||
})
|
||||
it('can divide', async () => {
|
||||
const code = ['const myVar = 1 / 2'].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(0.5)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(0.5)
|
||||
})
|
||||
it('can modulus', async () => {
|
||||
const code = ['const myVar = 5 % 2'].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(1)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(1)
|
||||
})
|
||||
it('can do multiple operations', async () => {
|
||||
const code = ['const myVar = 1 + 2 * 3'].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(7)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(7)
|
||||
})
|
||||
it('big example with parans', async () => {
|
||||
const code = ['const myVar = 1 + 2 * (3 - 4) / -5 + 6'].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(7.4)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(7.4)
|
||||
})
|
||||
it('with identifier', async () => {
|
||||
const code = ['const yo = 6', 'const myVar = yo / 2'].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(3)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(3)
|
||||
})
|
||||
it('with lots of testing', async () => {
|
||||
const code = ['const myVar = 2 * ((2 + 3 ) / 4 + 5)'].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(12.5)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(12.5)
|
||||
})
|
||||
it('with callExpression at start', async () => {
|
||||
const code = 'const myVar = min(4, 100) + 2'
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(6)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(6)
|
||||
})
|
||||
it('with callExpression at end', async () => {
|
||||
const code = 'const myVar = 2 + min(4, 100)'
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(6)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(6)
|
||||
})
|
||||
it('with nested callExpression', async () => {
|
||||
const code = 'const myVar = 2 + min(100, legLen(5, 3))'
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(6)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(6)
|
||||
})
|
||||
it('with unaryExpression', async () => {
|
||||
const code = 'const myVar = -min(100, 3)'
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(-3)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(-3)
|
||||
})
|
||||
it('with unaryExpression in callExpression', async () => {
|
||||
const code = 'const myVar = min(-legLen(5, 4), 5)'
|
||||
const code2 = 'const myVar = min(5 , -legLen(5, 4))'
|
||||
const { root } = await exe(code)
|
||||
const { root: root2 } = await exe(code2)
|
||||
expect(root.myVar.value).toBe(-3)
|
||||
expect(root.myVar.value).toBe(root2.myVar.value)
|
||||
const mem = await exe(code)
|
||||
const mem2 = await exe(code2)
|
||||
expect(mem.get('myVar')?.value).toBe(-3)
|
||||
expect(mem.get('myVar')?.value).toBe(mem2.get('myVar')?.value)
|
||||
})
|
||||
it('with unaryExpression in ArrayExpression', async () => {
|
||||
const code = 'const myVar = [1,-legLen(5, 4)]'
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toEqual([1, -3])
|
||||
const mem = await exe(code)
|
||||
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 () => {
|
||||
const code = [
|
||||
@ -355,8 +356,8 @@ describe('testing math operators', () => {
|
||||
' |> startProfileAt([0, 0], %)',
|
||||
'|> line([-2.21, -legLen(5, min(3, 999))], %)',
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
const mem = await exe(code)
|
||||
const sketch = mem.get('part001')
|
||||
// result of `-legLen(5, min(3, 999))` should be -4
|
||||
const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1]
|
||||
expect(yVal).toBe(-4)
|
||||
@ -373,8 +374,8 @@ describe('testing math operators', () => {
|
||||
`], %)`,
|
||||
``,
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
const mem = await exe(code)
|
||||
const sketch = mem.get('part001')
|
||||
// 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]?.to).toEqual([6, 0])
|
||||
@ -382,18 +383,18 @@ describe('testing math operators', () => {
|
||||
`-legLen(segLen('seg01', %), myVar)`,
|
||||
`legLen(segLen('seg01', %), myVar)`
|
||||
)
|
||||
const { root: removedUnaryExpRoot } = await exe(removedUnaryExp)
|
||||
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
|
||||
const removedUnaryExpMem = await exe(removedUnaryExp)
|
||||
const removedUnaryExpMemSketch = removedUnaryExpMem.get('part001')
|
||||
|
||||
// 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,
|
||||
])
|
||||
})
|
||||
it('with nested callExpression and binaryExpression', async () => {
|
||||
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe(5)
|
||||
const mem = await exe(code)
|
||||
expect(mem.get('myVar')?.value).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
@ -421,7 +422,7 @@ const theExtrude = startSketchOn('XY')
|
||||
|
||||
async function exe(
|
||||
code: string,
|
||||
programMemory: ProgramMemory = { root: {}, return: null }
|
||||
programMemory: ProgramMemory = ProgramMemory.empty()
|
||||
) {
|
||||
const ast = parse(code)
|
||||
|
||||
|
@ -79,20 +79,14 @@ export async function executeAst({
|
||||
return {
|
||||
errors: [e],
|
||||
logs: [],
|
||||
programMemory: {
|
||||
root: {},
|
||||
return: null,
|
||||
},
|
||||
programMemory: ProgramMemory.empty(),
|
||||
}
|
||||
} else {
|
||||
console.log(e)
|
||||
return {
|
||||
logs: [e],
|
||||
errors: [],
|
||||
programMemory: {
|
||||
root: {},
|
||||
return: null,
|
||||
},
|
||||
programMemory: ProgramMemory.empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -983,7 +983,7 @@ export async function deleteFromSelection(
|
||||
if (err(parent)) {
|
||||
return
|
||||
}
|
||||
const sketchToPreserve = programMemory.root[sketchName] as SketchGroup
|
||||
const sketchToPreserve = programMemory.get(sketchName) as SketchGroup
|
||||
console.log('sketchName', sketchName)
|
||||
// Can't kick off multiple requests at once as getFaceDetails
|
||||
// is three engine calls in one and they conflict
|
||||
|
@ -130,8 +130,14 @@ function moreNodePathFromSourceRange(
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (_node.type === 'CallExpression' && isInRange) {
|
||||
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) {
|
||||
const { object, property } = _node
|
||||
@ -459,8 +474,8 @@ export function findAllPreviousVariablesPath(
|
||||
bodyItems?.forEach?.((item) => {
|
||||
if (item.type !== 'VariableDeclaration' || item.end > startRange) return
|
||||
const varName = item.declarations[0].id.name
|
||||
const varValue = programMemory?.root[varName]
|
||||
if (typeof varValue?.value !== type) return
|
||||
const varValue = programMemory?.get(varName)
|
||||
if (!varValue || typeof varValue?.value !== type) return
|
||||
variables.push({
|
||||
key: varName,
|
||||
value: varValue.value,
|
||||
@ -640,7 +655,7 @@ export function isLinesParallelAndConstrained(
|
||||
if (err(_varDec)) return _varDec
|
||||
const varDec = _varDec.node
|
||||
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(
|
||||
path,
|
||||
primaryLine.range
|
||||
@ -687,7 +702,7 @@ export function isLinesParallelAndConstrained(
|
||||
constraintType === 'angle' || constraintLevel === 'full'
|
||||
|
||||
// get the previous segment
|
||||
const prevSegment = (programMemory.root[varName] as SketchGroup).value[
|
||||
const prevSegment = (programMemory.get(varName) as SketchGroup).value[
|
||||
secondaryIndex - 1
|
||||
]
|
||||
const prevSourceRange = prevSegment.__geoMeta.sourceRange
|
||||
@ -757,7 +772,7 @@ export function hasExtrudeSketchGroup({
|
||||
const varDec = varDecMeta.node
|
||||
if (varDec.type !== 'VariableDeclaration') return false
|
||||
const varName = varDec.declarations[0].id.name
|
||||
const varValue = programMemory?.root[varName]
|
||||
const varValue = programMemory?.get(varName)
|
||||
return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup'
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,6 @@ import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
let lastMessage = ''
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 10000
|
||||
|
||||
@ -58,9 +56,6 @@ function isHighlightSetEntity_type(
|
||||
|
||||
type WebSocketResponse = Models['WebSocketResponse_type']
|
||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||
type BatchResponseMap = {
|
||||
[key: string]: Models['BatchResponse_type']
|
||||
}
|
||||
|
||||
type ResultCommand = CommandInfo & {
|
||||
type: 'result'
|
||||
@ -1169,6 +1164,15 @@ export enum EngineCommandManagerEvents {
|
||||
* It also maintains an {@link artifactMap} that keeps track of the state of each
|
||||
* command, and the artifacts that have been generated by those commands.
|
||||
*/
|
||||
|
||||
interface PendingMessage {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
idToRangeMap: { [key: string]: SourceRange }
|
||||
resolve: (data: [Models['WebSocketResponse_type']]) => void
|
||||
reject: (reason: string) => void
|
||||
promise: Promise<[Models['WebSocketResponse_type']]>
|
||||
}
|
||||
export class EngineCommandManager extends EventTarget {
|
||||
/**
|
||||
* The artifactMap is a client-side representation of the commands that have been sent
|
||||
@ -1182,6 +1186,25 @@ export class EngineCommandManager extends EventTarget {
|
||||
* of the KCL code that generated it.
|
||||
*/
|
||||
artifactMap: ArtifactMap = {}
|
||||
/**
|
||||
* The pendingCommands object is a map of the commands that have been sent to the engine that are still waiting on a reply
|
||||
*/
|
||||
pendingCommands: {
|
||||
[commandId: string]: PendingMessage
|
||||
} = {}
|
||||
/**
|
||||
* The orderedCommands array of all the the commands sent to the engine, un-folded from batches, and made into one long
|
||||
* list of the individual commands, this is used to process all the commands into the artifactMap
|
||||
*/
|
||||
orderedCommands: {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}[] = []
|
||||
/**
|
||||
* A map of the responses to the @this.orderedCommands, when processing the commands into the artifactMap, this response map allow
|
||||
* us to look up the response by command id
|
||||
*/
|
||||
responseMap: { [commandId: string]: OkWebSocketResponseData } = {}
|
||||
/**
|
||||
* The client-side representation of the scene command artifacts that have been sent to the server;
|
||||
* that is, the *non-modeling* commands and corresponding artifacts.
|
||||
@ -1206,7 +1229,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
defaultPlanes: DefaultPlanes | null = null
|
||||
commandLogs: CommandLog[] = []
|
||||
pendingExport?: {
|
||||
resolve: (filename?: string) => void
|
||||
resolve: (a: null) => void
|
||||
reject: (reason: any) => void
|
||||
}
|
||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||
@ -1435,31 +1458,92 @@ export class EngineCommandManager extends EventTarget {
|
||||
// export we send a binary blob.
|
||||
// Pass this to our export function.
|
||||
exportSave(event.data).then(() => {
|
||||
this.pendingExport?.resolve()
|
||||
this.pendingExport?.resolve(null)
|
||||
}, this.pendingExport?.reject)
|
||||
} else {
|
||||
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
||||
event.data
|
||||
)
|
||||
if (
|
||||
return
|
||||
}
|
||||
|
||||
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
|
||||
const pending = this.pendingCommands[message.request_id || '']
|
||||
|
||||
if (pending && !message.success) {
|
||||
// handle bad case
|
||||
pending.reject(`engine error: ${JSON.stringify(message.errors)}`)
|
||||
delete this.pendingCommands[message.request_id || '']
|
||||
}
|
||||
if (
|
||||
!(
|
||||
pending &&
|
||||
message.success &&
|
||||
(message.resp.type === 'modeling' ||
|
||||
message.resp.type === 'modeling_batch') &&
|
||||
message.request_id
|
||||
) {
|
||||
this.handleModelingCommand(
|
||||
message.resp,
|
||||
message.request_id,
|
||||
message
|
||||
)
|
||||
} else if (
|
||||
!message.success &&
|
||||
message.request_id &&
|
||||
this.artifactMap[message.request_id]
|
||||
) {
|
||||
this.handleFailedModelingCommand(message.request_id, message)
|
||||
}
|
||||
message.resp.type === 'modeling_batch')
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
message.resp.type === 'modeling' &&
|
||||
pending.command.type === 'modeling_cmd_req' &&
|
||||
message.request_id
|
||||
) {
|
||||
this.addCommandLog({
|
||||
type: 'receive-reliable',
|
||||
data: message.resp,
|
||||
id: message?.request_id || '',
|
||||
cmd_type: pending?.command?.cmd?.type,
|
||||
})
|
||||
|
||||
const modelingResponse = message.resp.data.modeling_response
|
||||
|
||||
Object.values(
|
||||
this.subscriptions[modelingResponse.type] || {}
|
||||
).forEach((callback) => callback(modelingResponse))
|
||||
|
||||
this.responseMap[message.request_id] = message.resp
|
||||
} else if (
|
||||
message.resp.type === 'modeling_batch' &&
|
||||
pending.command.type === 'modeling_cmd_batch_req'
|
||||
) {
|
||||
let individualPendingResponses: {
|
||||
[key: string]: Models['WebSocketRequest_type']
|
||||
} = {}
|
||||
pending.command.requests.forEach(({ cmd, cmd_id }) => {
|
||||
individualPendingResponses[cmd_id] = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd,
|
||||
cmd_id,
|
||||
}
|
||||
})
|
||||
Object.entries(message.resp.data.responses).forEach(
|
||||
([key, response]) => {
|
||||
if (!('response' in response)) return
|
||||
const command = individualPendingResponses[key]
|
||||
if (!command) return
|
||||
if (command.type === 'modeling_cmd_req')
|
||||
this.addCommandLog({
|
||||
type: 'receive-reliable',
|
||||
data: {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
},
|
||||
id: key,
|
||||
cmd_type: command?.cmd?.type,
|
||||
})
|
||||
|
||||
this.responseMap[key] = {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pending.resolve([message])
|
||||
delete this.pendingCommands[message.request_id || '']
|
||||
}) as EventListener)
|
||||
|
||||
this.onEngineConnectionNewTrack = ({
|
||||
@ -1485,6 +1569,106 @@ export class EngineCommandManager extends EventTarget {
|
||||
this.onEngineConnectionStarted
|
||||
)
|
||||
}
|
||||
handleIndividualResponse({
|
||||
id,
|
||||
pendingMsg,
|
||||
response,
|
||||
}: {
|
||||
id: string
|
||||
pendingMsg: {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}
|
||||
response: OkWebSocketResponseData
|
||||
}) {
|
||||
const command = pendingMsg
|
||||
if (command?.command?.type !== 'modeling_cmd_req') return
|
||||
if (response?.type !== 'modeling') return
|
||||
const command2 = command.command.cmd
|
||||
|
||||
const range = command.range
|
||||
const pathToNode = getNodePathFromSourceRange(this.getAst(), range)
|
||||
const getParentId = (): string | undefined => {
|
||||
if (command2.type === 'extend_path') return command2.path
|
||||
if (command2.type === 'solid3d_get_extrusion_face_info') {
|
||||
const edgeArtifact = this.artifactMap[command2.edge_id]
|
||||
// edges's parent id is to the original "start_path" artifact
|
||||
if (edgeArtifact && edgeArtifact.parentId) {
|
||||
return edgeArtifact.parentId
|
||||
}
|
||||
}
|
||||
if (command2.type === 'close_path') return command2.path_id
|
||||
if (command2.type === 'extrude') return command2.target
|
||||
// handle other commands that have a parent here
|
||||
}
|
||||
const modelingResponse = response.data.modeling_response
|
||||
|
||||
if (command) {
|
||||
const parentId = getParentId()
|
||||
const artifact = {
|
||||
type: 'result',
|
||||
range: range,
|
||||
pathToNode,
|
||||
commandType: command.command.cmd.type,
|
||||
parentId: parentId,
|
||||
} as ArtifactMapCommand & { extrusions?: string[] }
|
||||
this.artifactMap[id] = artifact
|
||||
if (command2.type === 'extrude') {
|
||||
;(artifact as any).target = command2.target
|
||||
if (this.artifactMap[command2.target]?.commandType === 'start_path') {
|
||||
if ((this.artifactMap[command2.target] as any)?.extrusions?.length) {
|
||||
;(this.artifactMap[command2.target] as any).extrusions.push(id)
|
||||
} else {
|
||||
;(this.artifactMap[command2.target] as any).extrusions = [id]
|
||||
}
|
||||
}
|
||||
}
|
||||
this.artifactMap[id] = artifact
|
||||
if (
|
||||
(command2.type === 'entity_linear_pattern' &&
|
||||
modelingResponse.type === 'entity_linear_pattern') ||
|
||||
(command2.type === 'entity_circular_pattern' &&
|
||||
modelingResponse.type === 'entity_circular_pattern')
|
||||
) {
|
||||
const entities = modelingResponse.data.entity_ids
|
||||
entities?.forEach((entity: string) => {
|
||||
this.artifactMap[entity] = artifact
|
||||
})
|
||||
}
|
||||
if (
|
||||
command2.type === 'solid3d_get_extrusion_face_info' &&
|
||||
modelingResponse.type === 'solid3d_get_extrusion_face_info'
|
||||
) {
|
||||
const parent = this.artifactMap[parentId || '']
|
||||
modelingResponse.data.faces.forEach((face) => {
|
||||
if (face.cap !== 'none' && face.face_id && parent) {
|
||||
this.artifactMap[face.face_id] = {
|
||||
...parent,
|
||||
commandType: 'solid3d_get_extrusion_face_info',
|
||||
additionalData: {
|
||||
type: 'cap',
|
||||
info: face.cap === 'bottom' ? 'start' : 'end',
|
||||
},
|
||||
}
|
||||
}
|
||||
const curveArtifact = this.artifactMap[face?.curve_id || '']
|
||||
if (curveArtifact && face?.face_id) {
|
||||
this.artifactMap[face.face_id] = {
|
||||
...curveArtifact,
|
||||
commandType: 'solid3d_get_extrusion_face_info',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (command) {
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
commandType: command2.type,
|
||||
range,
|
||||
pathToNode,
|
||||
} as ArtifactMapCommand & { extrusions?: string[] }
|
||||
}
|
||||
}
|
||||
|
||||
handleResize({
|
||||
streamWidth,
|
||||
@ -1509,233 +1693,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
this.engineConnection?.send(resizeCmd)
|
||||
}
|
||||
handleModelingCommand(
|
||||
message: OkWebSocketResponseData,
|
||||
id: string,
|
||||
raw: WebSocketResponse
|
||||
) {
|
||||
if (!(message.type === 'modeling' || message.type === 'modeling_batch')) {
|
||||
return
|
||||
}
|
||||
|
||||
const command = this.artifactMap[id]
|
||||
let modelingResponse: Models['OkModelingCmdResponse_type'] = {
|
||||
type: 'empty',
|
||||
}
|
||||
if ('modeling_response' in message.data) {
|
||||
modelingResponse = message.data.modeling_response
|
||||
}
|
||||
if (
|
||||
command?.type === 'pending' &&
|
||||
command.commandType === 'batch' &&
|
||||
command?.additionalData?.type === 'batch-ids'
|
||||
) {
|
||||
if ('responses' in message.data) {
|
||||
const batchResponse = message.data.responses as BatchResponseMap
|
||||
// Iterate over the map of responses.
|
||||
Object.entries(batchResponse).forEach(([key, response]) => {
|
||||
// If the response is a success, we resolve the promise.
|
||||
if ('response' in response && response.response) {
|
||||
this.handleModelingCommand(
|
||||
{
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
},
|
||||
key,
|
||||
{
|
||||
request_id: key,
|
||||
resp: {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
},
|
||||
success: true,
|
||||
}
|
||||
)
|
||||
} else if ('errors' in response) {
|
||||
this.handleFailedModelingCommand(key, {
|
||||
request_id: key,
|
||||
success: false,
|
||||
errors: response.errors,
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
command.additionalData.ids.forEach((id) => {
|
||||
this.handleModelingCommand(message, id, raw)
|
||||
})
|
||||
}
|
||||
// batch artifact is just a container, we don't need to keep it
|
||||
// once we process all the commands inside it
|
||||
const resolve = command.resolve
|
||||
delete this.artifactMap[id]
|
||||
resolve({
|
||||
id,
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
raw,
|
||||
})
|
||||
return
|
||||
}
|
||||
const sceneCommand = this.sceneCommandArtifacts[id]
|
||||
this.addCommandLog({
|
||||
type: 'receive-reliable',
|
||||
data: message,
|
||||
id,
|
||||
cmd_type: command?.commandType || sceneCommand?.commandType,
|
||||
})
|
||||
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
|
||||
(callback) => callback(modelingResponse)
|
||||
)
|
||||
|
||||
if (command && command.type === 'pending') {
|
||||
const resolve = command.resolve
|
||||
const oldArtifact = this.artifactMap[id] as ArtifactMapCommand & {
|
||||
extrusions?: string[]
|
||||
}
|
||||
const artifact = {
|
||||
type: 'result',
|
||||
range: command.range,
|
||||
pathToNode: command.pathToNode,
|
||||
commandType: command.commandType,
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
} as ArtifactMapCommand & { extrusions?: string[] }
|
||||
if (oldArtifact?.extrusions) {
|
||||
artifact.extrusions = oldArtifact.extrusions
|
||||
}
|
||||
this.artifactMap[id] = artifact
|
||||
if (
|
||||
(command.commandType === 'entity_linear_pattern' &&
|
||||
modelingResponse.type === 'entity_linear_pattern') ||
|
||||
(command.commandType === 'entity_circular_pattern' &&
|
||||
modelingResponse.type === 'entity_circular_pattern')
|
||||
) {
|
||||
const entities = modelingResponse.data.entity_ids
|
||||
entities?.forEach((entity: string) => {
|
||||
this.artifactMap[entity] = artifact
|
||||
})
|
||||
}
|
||||
if (
|
||||
command?.commandType === 'solid3d_get_extrusion_face_info' &&
|
||||
modelingResponse.type === 'solid3d_get_extrusion_face_info'
|
||||
) {
|
||||
const parent = this.artifactMap[command?.parentId || '']
|
||||
modelingResponse.data.faces.forEach((face) => {
|
||||
if (face.cap !== 'none' && face.face_id && parent) {
|
||||
this.artifactMap[face.face_id] = {
|
||||
...parent,
|
||||
commandType: 'solid3d_get_extrusion_face_info',
|
||||
additionalData: {
|
||||
type: 'cap',
|
||||
info: face.cap === 'bottom' ? 'start' : 'end',
|
||||
},
|
||||
}
|
||||
}
|
||||
const curveArtifact = this.artifactMap[face?.curve_id || '']
|
||||
if (curveArtifact && face?.face_id) {
|
||||
this.artifactMap[face.face_id] = {
|
||||
...curveArtifact,
|
||||
commandType: 'solid3d_get_extrusion_face_info',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
resolve({
|
||||
id,
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
})
|
||||
} else if (sceneCommand && sceneCommand.type === 'pending') {
|
||||
const resolve = sceneCommand.resolve
|
||||
const artifact = {
|
||||
type: 'result',
|
||||
range: sceneCommand.range,
|
||||
pathToNode: sceneCommand.pathToNode,
|
||||
commandType: sceneCommand.commandType,
|
||||
parentId: sceneCommand.parentId ? sceneCommand.parentId : undefined,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
} as const
|
||||
this.sceneCommandArtifacts[id] = artifact
|
||||
resolve({
|
||||
id,
|
||||
commandType: sceneCommand.commandType,
|
||||
range: sceneCommand.range,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
})
|
||||
} else if (command) {
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
commandType: command?.commandType,
|
||||
range: command?.range,
|
||||
pathToNode: command?.pathToNode,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
}
|
||||
} else {
|
||||
this.sceneCommandArtifacts[id] = {
|
||||
type: 'result',
|
||||
commandType: sceneCommand?.commandType,
|
||||
range: sceneCommand?.range,
|
||||
pathToNode: sceneCommand?.pathToNode,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
}
|
||||
handleFailedModelingCommand(id: string, raw: WebSocketResponse) {
|
||||
const failed = raw as Models['FailureWebSocketResponse_type']
|
||||
const errors = failed.errors
|
||||
if (!id) return
|
||||
const command = this.artifactMap[id]
|
||||
if (command && command.type === 'pending') {
|
||||
this.artifactMap[id] = {
|
||||
type: 'failed',
|
||||
range: command.range,
|
||||
pathToNode: command.pathToNode,
|
||||
commandType: command.commandType,
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
errors,
|
||||
}
|
||||
if (
|
||||
command?.type === 'pending' &&
|
||||
command.commandType === 'batch' &&
|
||||
command?.additionalData?.type === 'batch-ids'
|
||||
) {
|
||||
command.additionalData.ids.forEach((id) => {
|
||||
this.handleFailedModelingCommand(id, raw)
|
||||
})
|
||||
}
|
||||
// batch artifact is just a container, we don't need to keep it
|
||||
// once we process all the commands inside it
|
||||
const resolve = command.resolve
|
||||
delete this.artifactMap[id]
|
||||
resolve({
|
||||
id,
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
errors,
|
||||
raw,
|
||||
})
|
||||
} else {
|
||||
this.artifactMap[id] = {
|
||||
type: 'failed',
|
||||
range: command.range,
|
||||
pathToNode: command.pathToNode,
|
||||
commandType: command.commandType,
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
}
|
||||
tearDown(opts?: { idleMode: boolean }) {
|
||||
if (this.engineConnection) {
|
||||
this.engineConnection.removeEventListener(
|
||||
@ -1768,6 +1726,8 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
async startNewSession() {
|
||||
this.artifactMap = {}
|
||||
this.orderedCommands = []
|
||||
this.responseMap = {}
|
||||
await this.initPlanes()
|
||||
}
|
||||
subscribeTo<T extends ModelTypes>({
|
||||
@ -1841,13 +1801,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
sendSceneCommand(
|
||||
command: EngineCommand,
|
||||
forceWebsocket = false
|
||||
): Promise<any> {
|
||||
): Promise<Models['WebSocketResponse_type'] | null> {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
if (
|
||||
@ -1866,19 +1826,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
command.type === 'modeling_cmd_req' &&
|
||||
command.cmd.type !== lastMessage
|
||||
) {
|
||||
lastMessage = command.cmd.type
|
||||
}
|
||||
if (command.type === 'modeling_cmd_batch_req') {
|
||||
this.engineConnection?.send(command)
|
||||
// TODO - handlePendingCommands does not handle batch commands
|
||||
// return this.handlePendingCommand(command.requests[0].cmd_id, command.cmd)
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
||||
if (command.type !== 'modeling_cmd_req') return Promise.resolve(null)
|
||||
const cmd = command.cmd
|
||||
if (
|
||||
(cmd.type === 'camera_drag_move' ||
|
||||
@ -1891,7 +1845,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
;(cmd as any).sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
} else if (
|
||||
cmd.type === 'highlight_set_entity' &&
|
||||
this.engineConnection?.unreliableDataChannel
|
||||
@ -1899,7 +1853,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
cmd.sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
} else if (
|
||||
cmd.type === 'mouse_move' &&
|
||||
this.engineConnection.unreliableDataChannel
|
||||
@ -1907,9 +1861,9 @@ export class EngineCommandManager extends EventTarget {
|
||||
cmd.sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
} else if (cmd.type === 'export') {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const promise = new Promise<null>((resolve, reject) => {
|
||||
this.pendingExport = { resolve, reject }
|
||||
})
|
||||
this.engineConnection?.send(command)
|
||||
@ -1922,194 +1876,15 @@ export class EngineCommandManager extends EventTarget {
|
||||
;(cmd as any).sequence = this.outSequence++
|
||||
}
|
||||
// since it's not mouse drag or highlighting send over TCP and keep track of the command
|
||||
this.engineConnection?.send(command)
|
||||
return this.handlePendingSceneCommand(command.cmd_id, command.cmd)
|
||||
}
|
||||
sendModelingCommand({
|
||||
id,
|
||||
range,
|
||||
command,
|
||||
ast,
|
||||
idToRangeMap,
|
||||
}: {
|
||||
id: string
|
||||
range: SourceRange
|
||||
command: EngineCommand
|
||||
ast: Program
|
||||
idToRangeMap?: { [key: string]: SourceRange }
|
||||
}): Promise<ResolveCommand | void> {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (typeof command !== 'string') {
|
||||
this.addCommandLog({
|
||||
type: 'send-modeling',
|
||||
data: command,
|
||||
})
|
||||
} else {
|
||||
this.addCommandLog({
|
||||
type: 'send-modeling',
|
||||
data: JSON.parse(command),
|
||||
})
|
||||
}
|
||||
this.engineConnection?.send(command)
|
||||
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
|
||||
return this.handlePendingCommand(id, command?.cmd, ast, range)
|
||||
} else if (
|
||||
typeof command !== 'string' &&
|
||||
command.type === 'modeling_cmd_batch_req'
|
||||
) {
|
||||
return this.handlePendingBatchCommand(id, command.requests, idToRangeMap)
|
||||
} else if (typeof command === 'string') {
|
||||
const parseCommand: EngineCommand = JSON.parse(command)
|
||||
if (parseCommand.type === 'modeling_cmd_req') {
|
||||
return this.handlePendingCommand(id, parseCommand?.cmd, ast, range)
|
||||
} else if (parseCommand.type === 'modeling_cmd_batch_req') {
|
||||
return this.handlePendingBatchCommand(
|
||||
id,
|
||||
parseCommand.requests,
|
||||
idToRangeMap
|
||||
)
|
||||
}
|
||||
}
|
||||
return Promise.reject(new Error('Expected unreachable reached'))
|
||||
}
|
||||
handlePendingSceneCommand(
|
||||
id: string,
|
||||
command: Models['ModelingCmd_type'],
|
||||
ast?: Program,
|
||||
range?: SourceRange
|
||||
) {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise = new Promise((_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
})
|
||||
const pathToNode = ast
|
||||
? getNodePathFromSourceRange(ast, range || [0, 0])
|
||||
: []
|
||||
this.sceneCommandArtifacts[id] = {
|
||||
range: range || [0, 0],
|
||||
pathToNode,
|
||||
type: 'pending',
|
||||
commandType: command.type,
|
||||
promise,
|
||||
resolve,
|
||||
}
|
||||
return promise
|
||||
}
|
||||
handlePendingCommand(
|
||||
id: string,
|
||||
command: Models['ModelingCmd_type'],
|
||||
ast?: Program,
|
||||
range?: SourceRange
|
||||
): Promise<ResolveCommand | void> {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise: Promise<ResolveCommand | void> = new Promise(
|
||||
(_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
}
|
||||
)
|
||||
const getParentId = (): string | undefined => {
|
||||
if (command.type === 'extend_path') return command.path
|
||||
if (command.type === 'solid3d_get_extrusion_face_info') {
|
||||
const edgeArtifact = this.artifactMap[command.edge_id]
|
||||
// edges's parent id is to the original "start_path" artifact
|
||||
if (edgeArtifact && edgeArtifact.parentId) {
|
||||
return edgeArtifact.parentId
|
||||
}
|
||||
}
|
||||
if (command.type === 'close_path') return command.path_id
|
||||
if (command.type === 'extrude') return command.target
|
||||
// handle other commands that have a parent here
|
||||
}
|
||||
const pathToNode = ast
|
||||
? getNodePathFromSourceRange(ast, range || [0, 0])
|
||||
: []
|
||||
this.artifactMap[id] = {
|
||||
range: range || [0, 0],
|
||||
pathToNode,
|
||||
type: 'pending',
|
||||
commandType: command.type,
|
||||
parentId: getParentId(),
|
||||
promise,
|
||||
resolve,
|
||||
}
|
||||
if (command.type === 'extrude') {
|
||||
this.artifactMap[id] = {
|
||||
range: range || [0, 0],
|
||||
pathToNode,
|
||||
type: 'pending',
|
||||
commandType: 'extrude',
|
||||
parentId: getParentId(),
|
||||
promise,
|
||||
target: command.target,
|
||||
resolve,
|
||||
}
|
||||
const target = this.artifactMap[command.target]
|
||||
if (target.commandType === 'start_path') {
|
||||
// tsc cannot infer that target can have extrusions
|
||||
// from the commandType (why?) so we need to cast it
|
||||
const typedTarget = target as (
|
||||
| PendingCommand
|
||||
| ResultCommand
|
||||
| FailedCommand
|
||||
) & { extrusions?: string[] }
|
||||
if (typedTarget?.extrusions?.length) {
|
||||
typedTarget.extrusions.push(id)
|
||||
} else {
|
||||
typedTarget.extrusions = [id]
|
||||
}
|
||||
// Update in the map.
|
||||
this.artifactMap[command.target] = typedTarget
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
async handlePendingBatchCommand(
|
||||
id: string,
|
||||
commands: Models['ModelingCmdReq_type'][],
|
||||
idToRangeMap?: { [key: string]: SourceRange },
|
||||
ast?: Program,
|
||||
range?: SourceRange
|
||||
): Promise<ResolveCommand | void> {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise: Promise<ResolveCommand | void> = new Promise(
|
||||
(_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
}
|
||||
)
|
||||
|
||||
if (!idToRangeMap) {
|
||||
return Promise.reject(
|
||||
new Error('idToRangeMap is required for batch commands')
|
||||
)
|
||||
}
|
||||
|
||||
// Add the overall batch command to the artifact map just so we can track all of the
|
||||
// individual commands that are part of the batch.
|
||||
// we'll delete this artifact once all of the individual commands have been processed.
|
||||
this.artifactMap[id] = {
|
||||
range: range || [0, 0],
|
||||
pathToNode: [],
|
||||
type: 'pending',
|
||||
commandType: 'batch',
|
||||
additionalData: { type: 'batch-ids', ids: commands.map((c) => c.cmd_id) },
|
||||
parentId: undefined,
|
||||
promise,
|
||||
resolve,
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
commands.map((c) =>
|
||||
this.handlePendingCommand(c.cmd_id, c.cmd, ast, idToRangeMap[c.cmd_id])
|
||||
)
|
||||
)
|
||||
return promise
|
||||
return this.sendCommand(command.cmd_id, {
|
||||
command,
|
||||
idToRangeMap: {},
|
||||
range: [0, 0],
|
||||
}).then(([a]) => a)
|
||||
}
|
||||
/**
|
||||
* A wrapper around the sendCommand where all inputs are JSON strings
|
||||
*/
|
||||
async sendModelingCommandFromWasm(
|
||||
id: string,
|
||||
rangeStr: string,
|
||||
@ -2132,53 +1907,88 @@ export class EngineCommandManager extends EventTarget {
|
||||
return Promise.reject(new Error('commandStr is undefined'))
|
||||
}
|
||||
const range: SourceRange = JSON.parse(rangeStr)
|
||||
const command: EngineCommand = JSON.parse(commandStr)
|
||||
const idToRangeMap: { [key: string]: SourceRange } =
|
||||
JSON.parse(idToRangeStr)
|
||||
|
||||
const command: EngineCommand = JSON.parse(commandStr)
|
||||
|
||||
// We only care about the modeling command response.
|
||||
return this.sendModelingCommand({
|
||||
id,
|
||||
range,
|
||||
const resp = await this.sendCommand(id, {
|
||||
command,
|
||||
ast: this.getAst(),
|
||||
range,
|
||||
idToRangeMap,
|
||||
}).then((resp) => {
|
||||
if (!resp) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
'returning modeling cmd response to the rust side is undefined or null'
|
||||
)
|
||||
)
|
||||
}
|
||||
return JSON.stringify(resp.raw)
|
||||
})
|
||||
return JSON.stringify(resp[0])
|
||||
}
|
||||
async commandResult(id: string): Promise<any> {
|
||||
const command = this.artifactMap[id]
|
||||
if (!command) {
|
||||
return Promise.reject(new Error('No command found'))
|
||||
/**
|
||||
* Common send command function used for both modeling and scene commands
|
||||
* So that both have a common way to send pending commands with promises for the responses
|
||||
*/
|
||||
async sendCommand(
|
||||
id: string,
|
||||
message: {
|
||||
command: PendingMessage['command']
|
||||
range: PendingMessage['range']
|
||||
idToRangeMap: PendingMessage['idToRangeMap']
|
||||
}
|
||||
if (command.type === 'result') {
|
||||
return command.data
|
||||
} else if (command.type === 'failed') {
|
||||
return Promise.resolve(command.errors)
|
||||
): Promise<[Models['WebSocketResponse_type']]> {
|
||||
const { promise, resolve, reject } = promiseFactory<any>()
|
||||
this.pendingCommands[id] = {
|
||||
resolve,
|
||||
reject,
|
||||
promise,
|
||||
command: message.command,
|
||||
range: message.range,
|
||||
idToRangeMap: message.idToRangeMap,
|
||||
}
|
||||
return command.promise
|
||||
if (message.command.type === 'modeling_cmd_req') {
|
||||
this.orderedCommands.push({
|
||||
command: message.command,
|
||||
range: message.range,
|
||||
})
|
||||
} else if (message.command.type === 'modeling_cmd_batch_req') {
|
||||
message.command.requests.forEach((req) => {
|
||||
const cmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: req.cmd_id,
|
||||
cmd: req.cmd,
|
||||
}
|
||||
this.orderedCommands.push({
|
||||
command: cmd,
|
||||
range: message.idToRangeMap[req.cmd_id || ''],
|
||||
})
|
||||
})
|
||||
}
|
||||
this.engineConnection?.send(message.command)
|
||||
return promise
|
||||
}
|
||||
async waitForAllCommands(): Promise<{
|
||||
artifactMap: ArtifactMap
|
||||
}> {
|
||||
/**
|
||||
* When an execution takes place we want to wait until we've got replies for all of the commands
|
||||
* When this is done when we build the artifact map synchronously.
|
||||
*/
|
||||
async waitForAllCommands() {
|
||||
const pendingCommands = Object.values(this.artifactMap).filter(
|
||||
({ type }) => type === 'pending'
|
||||
) as PendingCommand[]
|
||||
const proms = pendingCommands.map(({ promise }) => promise)
|
||||
await Promise.all(proms)
|
||||
|
||||
return {
|
||||
artifactMap: this.artifactMap,
|
||||
}
|
||||
const otherPending = Object.values(this.pendingCommands).map(
|
||||
(a) => a.promise
|
||||
)
|
||||
await Promise.all([...proms, otherPending])
|
||||
this.orderedCommands.forEach(({ command, range }) => {
|
||||
// expect all to be `modeling_cmd_req` as batch commands have
|
||||
// already been expanded before being added to orderedCommands
|
||||
if (command.type !== 'modeling_cmd_req') return
|
||||
const id = command.cmd_id
|
||||
const response = this.responseMap[id]
|
||||
this.handleIndividualResponse({
|
||||
id,
|
||||
pendingMsg: {
|
||||
command,
|
||||
range,
|
||||
},
|
||||
response,
|
||||
})
|
||||
})
|
||||
}
|
||||
private async initPlanes() {
|
||||
if (this.planesInitialized()) return
|
||||
@ -2209,7 +2019,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
this.onPlaneSelectCallback = callback
|
||||
}
|
||||
|
||||
async setPlaneHidden(id: string, hidden: boolean): Promise<string> {
|
||||
async setPlaneHidden(id: string, hidden: boolean) {
|
||||
return await this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
@ -2250,3 +2060,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function promiseFactory<T>() {
|
||||
let resolve: (value: T | PromiseLike<T>) => void = () => {}
|
||||
let reject: (value: T | PromiseLike<T>) => void = () => {}
|
||||
const promise = new Promise<T>((_resolve, _reject) => {
|
||||
resolve = _resolve
|
||||
reject = _reject
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
@ -1009,8 +1009,8 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
const { node: varDec } = nodeMeta2
|
||||
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'SketchGroup') {
|
||||
const sketch = previousProgramMemory?.get(variableName)
|
||||
if (!sketch || sketch.type !== 'SketchGroup') {
|
||||
return new Error('not a SketchGroup')
|
||||
}
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
@ -1105,8 +1105,8 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
const { node: varDec } = nodeMeta2
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'SketchGroup') {
|
||||
const sketch = previousProgramMemory?.get(variableName)
|
||||
if (!sketch || sketch.type !== 'SketchGroup') {
|
||||
return new Error('not a SketchGroup')
|
||||
}
|
||||
|
||||
@ -1443,7 +1443,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
|
||||
const { node: varDec } = nodeMeta2
|
||||
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(
|
||||
({ tag }: Path) => tag && tag.value === intersectTagName
|
||||
)
|
||||
|
@ -363,7 +363,7 @@ const part001 = startSketchOn('XY')
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
const index = code.indexOf('// normal-segment') - 7
|
||||
const _segment = getSketchSegmentFromSourceRange(
|
||||
programMemory.root['part001'] as SketchGroup,
|
||||
programMemory.get('part001') as SketchGroup,
|
||||
[index, index]
|
||||
)
|
||||
if (err(_segment)) throw _segment
|
||||
@ -379,7 +379,7 @@ const part001 = startSketchOn('XY')
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
const index = code.indexOf('// segment-in-start') - 7
|
||||
const _segment = getSketchSegmentFromSourceRange(
|
||||
programMemory.root['part001'] as SketchGroup,
|
||||
programMemory.get('part001') as SketchGroup,
|
||||
[index, index]
|
||||
)
|
||||
if (err(_segment)) throw _segment
|
||||
|
@ -1636,8 +1636,8 @@ export function transformAstSketchLines({
|
||||
})
|
||||
|
||||
const varName = varDec.node.id.name
|
||||
let sketchGroup = programMemory.root?.[varName]
|
||||
if (sketchGroup.type === 'ExtrudeGroup') {
|
||||
let sketchGroup = programMemory.get(varName)
|
||||
if (sketchGroup?.type === 'ExtrudeGroup') {
|
||||
sketchGroup = sketchGroup.sketchGroup
|
||||
}
|
||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')
|
||||
|
@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
|
||||
offset: ${offset},
|
||||
}, %, "yo2")
|
||||
const intersect = segEndX('yo2', part001)`
|
||||
const { root } = await enginelessExecutor(parse(code('-1')))
|
||||
expect(root.intersect.value).toBe(1 + Math.sqrt(2))
|
||||
const { root: noOffset } = await enginelessExecutor(parse(code('0')))
|
||||
expect(noOffset.intersect.value).toBeCloseTo(1)
|
||||
const mem = await enginelessExecutor(parse(code('-1')))
|
||||
expect(mem.get('intersect')?.value).toBe(1 + Math.sqrt(2))
|
||||
const noOffset = await enginelessExecutor(parse(code('0')))
|
||||
expect(noOffset.get('intersect')?.value).toBeCloseTo(1)
|
||||
})
|
||||
})
|
||||
|
211
src/lang/wasm.ts
@ -143,14 +143,200 @@ interface Memory {
|
||||
[key: string]: MemoryItem
|
||||
}
|
||||
|
||||
export interface ProgramMemory {
|
||||
root: Memory
|
||||
type EnvironmentRef = number
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory | Error = { root: {}, return: null },
|
||||
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
||||
engineCommandManager: EngineCommandManager,
|
||||
isMock: boolean = false
|
||||
): Promise<ProgramMemory> => {
|
||||
@ -171,7 +357,7 @@ export const executor = async (
|
||||
|
||||
export const _executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory | Error = { root: {}, return: null },
|
||||
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
||||
engineCommandManager: EngineCommandManager,
|
||||
isMock: boolean
|
||||
): Promise<ProgramMemory> => {
|
||||
@ -186,15 +372,15 @@ export const _executor = async (
|
||||
baseUnit =
|
||||
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
|
||||
}
|
||||
const memory: ProgramMemory = await execute_wasm(
|
||||
const memory: RawProgramMemory = await execute_wasm(
|
||||
JSON.stringify(node),
|
||||
JSON.stringify(programMemory),
|
||||
JSON.stringify(programMemory.toRaw()),
|
||||
baseUnit,
|
||||
engineCommandManager,
|
||||
fileSystemManager,
|
||||
isMock
|
||||
)
|
||||
return memory
|
||||
return ProgramMemory.fromRaw(memory)
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
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 {
|
||||
try {
|
||||
const memory: ProgramMemory = program_memory_init()
|
||||
return memory
|
||||
const memory: RawProgramMemory = program_memory_init()
|
||||
return new ProgramMemory(
|
||||
memory.environments,
|
||||
memory.currentEnv,
|
||||
memory.return
|
||||
)
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
|
@ -87,16 +87,20 @@ export async function getEventForSelectWithPoint(
|
||||
// there's plans to get the faceId back from the solid2d creation
|
||||
// https://github.com/KittyCAD/engine/issues/2094
|
||||
// at which point we can add it to the artifact map and remove this logic
|
||||
const parentId = (
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'entity_get_parent_id',
|
||||
entity_id: data.entity_id,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
)?.data?.data?.entity_id
|
||||
const resp = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'entity_get_parent_id',
|
||||
entity_id: data.entity_id,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
const parentId =
|
||||
resp?.success &&
|
||||
resp?.resp?.type === 'modeling' &&
|
||||
resp?.resp?.data?.modeling_response?.type === 'entity_get_parent_id'
|
||||
? resp?.resp?.data?.modeling_response?.data?.entity_id
|
||||
: ''
|
||||
const parentArtifact = engineCommandManager.artifactMap[parentId]
|
||||
if (parentArtifact) {
|
||||
_artifact = parentArtifact
|
||||
@ -576,18 +580,22 @@ export async function sendSelectEventToEngine(
|
||||
el,
|
||||
...streamDimensions,
|
||||
})
|
||||
const result: Models['SelectWithPoint_type'] = await engineCommandManager
|
||||
.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'select_with_point',
|
||||
selected_at_window: { x, y },
|
||||
selection_type: 'add',
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
.then((res) => res.data.data)
|
||||
return result
|
||||
const res = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'select_with_point',
|
||||
selected_at_window: { x, y },
|
||||
selection_type: 'add',
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
if (
|
||||
res?.success &&
|
||||
res?.resp?.type === 'modeling' &&
|
||||
res?.resp?.data?.modeling_response.type === 'select_with_point'
|
||||
)
|
||||
return res?.resp?.data?.modeling_response?.data
|
||||
return { entity_id: '' }
|
||||
}
|
||||
|
||||
export function updateSelections(
|
||||
|
@ -75,7 +75,7 @@ class MockEngineCommandManager {
|
||||
|
||||
export async function enginelessExecutor(
|
||||
ast: Program | Error,
|
||||
pm: ProgramMemory | Error = { root: {}, return: null }
|
||||
pm: ProgramMemory | Error = ProgramMemory.empty()
|
||||
): Promise<ProgramMemory> {
|
||||
if (err(ast)) return Promise.reject(ast)
|
||||
if (err(pm)) return Promise.reject(pm)
|
||||
@ -93,7 +93,7 @@ export async function enginelessExecutor(
|
||||
|
||||
export async function executor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = { root: {}, return: null }
|
||||
pm: ProgramMemory = ProgramMemory.empty()
|
||||
): Promise<ProgramMemory> {
|
||||
const engineCommandManager = new EngineCommandManager()
|
||||
engineCommandManager.start({
|
||||
|
@ -3,7 +3,7 @@ import { kclManager, engineCommandManager } from 'lib/singletons'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { findUniqueName } from 'lang/modifyAst'
|
||||
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 { executeAst } from 'lang/langHelpers'
|
||||
import { err, trap } from 'lib/trap'
|
||||
@ -60,9 +60,8 @@ export function useCalculateKclExpression({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const allVarNames = Object.keys(programMemory.root)
|
||||
if (
|
||||
allVarNames.includes(newVariableName) ||
|
||||
programMemory.has(newVariableName) ||
|
||||
newVariableName === '' ||
|
||||
!isValidVariableName(newVariableName)
|
||||
) {
|
||||
@ -89,17 +88,20 @@ export function useCalculateKclExpression({
|
||||
if (err(ast)) return
|
||||
if (trap(ast, { suppress: true })) return
|
||||
|
||||
const _programMem: any = { root: {}, return: null }
|
||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||
})
|
||||
const _programMem: ProgramMemory = ProgramMemory.empty()
|
||||
for (const { key, value } of availableVarInfo.variables) {
|
||||
const error = _programMem.set(key, {
|
||||
type: 'UserVal',
|
||||
value,
|
||||
__meta: [],
|
||||
})
|
||||
if (trap(error, { suppress: true })) return
|
||||
}
|
||||
const { programMemory } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
useFakeExecutor: true,
|
||||
programMemoryOverride: JSON.parse(
|
||||
JSON.stringify(kclManager.programMemory)
|
||||
),
|
||||
programMemoryOverride: kclManager.programMemory.clone(),
|
||||
})
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
@ -109,7 +111,7 @@ export function useCalculateKclExpression({
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declarations?.[0]?.init
|
||||
const result = programMemory?.root?.__result__?.value
|
||||
const result = programMemory?.get('__result__')?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
}
|
||||
|
@ -1139,8 +1139,8 @@ export const modelingMachine = createMachine(
|
||||
)
|
||||
if (err(varDecNode)) return
|
||||
const sketchVar = varDecNode.node.declarations[0].id.name
|
||||
const sketchGroup = kclManager.programMemory.root[sketchVar]
|
||||
if (sketchGroup.type !== 'SketchGroup') return
|
||||
const sketchGroup = kclManager.programMemory.get(sketchVar)
|
||||
if (sketchGroup?.type !== 'SketchGroup') return
|
||||
const idArtifact = engineCommandManager.artifactMap[sketchGroup.id]
|
||||
if (idArtifact.commandType !== 'start_path') return
|
||||
const extrusionArtifactId = (idArtifact as any)?.extrusions?.[0]
|
||||
|
@ -1388,7 +1388,7 @@ impl CallExpression {
|
||||
}
|
||||
FunctionKind::UserDefined => {
|
||||
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.
|
||||
e.add_source_ranges(vec![self.into()])
|
||||
})?;
|
||||
|
@ -160,19 +160,13 @@ impl EngineConnection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::field_reassign_with_default)]
|
||||
pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> {
|
||||
// allowing the field_reassign_with_default lint here because the
|
||||
// defaults for this object don't match the type defaults. We want
|
||||
// to inherent the default config
|
||||
//
|
||||
// See the `impl Default for WebSocketConfig` in
|
||||
// `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 wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
|
||||
// 4294967296 bytes, which is around 4.2 GB.
|
||||
max_message_size: Some(0x100000000),
|
||||
max_frame_size: Some(0x100000000),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
|
||||
ws,
|
||||
|
@ -23,7 +23,8 @@ use crate::{
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProgramMemory {
|
||||
pub root: HashMap<String, MemoryItem>,
|
||||
pub environments: Vec<Environment>,
|
||||
pub current_env: EnvironmentRef,
|
||||
#[serde(rename = "return")]
|
||||
pub return_: Option<ProgramReturn>,
|
||||
}
|
||||
@ -31,7 +32,105 @@ pub struct ProgramMemory {
|
||||
impl ProgramMemory {
|
||||
pub fn new() -> 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(),
|
||||
MemoryItem::UserVal(UserVal {
|
||||
@ -61,28 +160,19 @@ impl ProgramMemory {
|
||||
}),
|
||||
),
|
||||
]),
|
||||
return_: None,
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add to the program memory.
|
||||
pub fn add(&mut self, key: &str, value: MemoryItem, source_range: SourceRange) -> Result<(), KclError> {
|
||||
if self.root.contains_key(key) {
|
||||
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
|
||||
message: format!("Cannot redefine `{}`", key),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
pub fn new(parent: EnvironmentRef) -> Self {
|
||||
Self {
|
||||
bindings: HashMap::new(),
|
||||
parent: Some(parent),
|
||||
}
|
||||
|
||||
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> {
|
||||
self.root.get(key).ok_or_else(|| {
|
||||
self.bindings.get(key).ok_or_else(|| {
|
||||
KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("memory item key `{}` is not defined", key),
|
||||
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 find_extrude_groups_on_sketch_group(&self, sketch_group_id: uuid::Uuid) -> Vec<Box<ExtrudeGroup>> {
|
||||
self.root
|
||||
.values()
|
||||
.filter_map(|item| match item {
|
||||
MemoryItem::ExtrudeGroup(eg) if eg.sketch_group.id == sketch_group_id => Some(eg.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
pub fn insert(&mut self, key: String, value: MemoryItem) {
|
||||
self.bindings.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProgramMemory {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
self.bindings.contains_key(key)
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,6 +242,7 @@ pub enum MemoryItem {
|
||||
#[serde(skip)]
|
||||
func: Option<MemoryFunction>,
|
||||
expression: Box<FunctionExpression>,
|
||||
memory: Box<ProgramMemory>,
|
||||
#[serde(rename = "__meta")]
|
||||
meta: Vec<Metadata>,
|
||||
},
|
||||
@ -620,7 +702,7 @@ impl MemoryItem {
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn as_user_val(&self) -> Option<&UserVal> {
|
||||
pub fn as_user_val(&self) -> Option<&UserVal> {
|
||||
if let MemoryItem::UserVal(x) = self {
|
||||
Some(x)
|
||||
} else {
|
||||
@ -642,27 +724,21 @@ impl MemoryItem {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
func,
|
||||
expression,
|
||||
memory,
|
||||
meta: _,
|
||||
} = &self
|
||||
else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "not an in-memory function".to_string(),
|
||||
source_ranges,
|
||||
}));
|
||||
return None;
|
||||
};
|
||||
let func = func.as_ref().ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Not an in-memory function: {:?}", expression),
|
||||
source_ranges,
|
||||
})
|
||||
})?;
|
||||
Ok(FnAsArg {
|
||||
let func = func.as_ref()?;
|
||||
Some(FnAsArg {
|
||||
func,
|
||||
expr: expression.to_owned(),
|
||||
memory: memory.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -736,10 +812,15 @@ impl MemoryItem {
|
||||
pub async fn call_fn(
|
||||
&self,
|
||||
args: Vec<MemoryItem>,
|
||||
memory: ProgramMemory,
|
||||
ctx: ExecutorContext,
|
||||
) -> 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 {
|
||||
message: "not a in memory function".to_string(),
|
||||
source_ranges: vec![],
|
||||
@ -751,7 +832,14 @@ impl MemoryItem {
|
||||
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_;
|
||||
}
|
||||
FunctionKind::UserDefined => {
|
||||
if let Some(func) = memory.clone().root.get(&fn_name) {
|
||||
let result = func.call_fn(args.clone(), memory.clone(), self.clone()).await?;
|
||||
// TODO: Why do we change the source range to
|
||||
// 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;
|
||||
} else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("No such name {} defined", fn_name),
|
||||
source_ranges: vec![call_expr.into()],
|
||||
}));
|
||||
}
|
||||
memory.return_ = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1680,7 +1765,15 @@ impl ExecutorContext {
|
||||
_metadata: Vec<Metadata>,
|
||||
ctx: ExecutorContext| {
|
||||
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
|
||||
.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 {
|
||||
expression: function_expression.clone(),
|
||||
meta: vec![metadata.to_owned()],
|
||||
func: Some(mem_func),
|
||||
memory: Box::new(memory.clone()),
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if let Some(arg) = args.get(index) {
|
||||
// Argument was provided.
|
||||
@ -1862,11 +1960,19 @@ const newVar = myVar + 1"#;
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::json!(5),
|
||||
memory.root.get("myVar").unwrap().get_json_value().unwrap()
|
||||
memory
|
||||
.get("myVar", SourceRange::default())
|
||||
.unwrap()
|
||||
.get_json_value()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
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();
|
||||
assert_eq!(
|
||||
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();
|
||||
assert_eq!(
|
||||
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,201 @@ const thisBox = box([[0,0], 6, 10, 3])
|
||||
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_math_execute_with_functions() {
|
||||
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::json!(5.0),
|
||||
memory.root.get("myVar").unwrap().get_json_value().unwrap()
|
||||
memory
|
||||
.get("myVar", SourceRange::default())
|
||||
.unwrap()
|
||||
.get_json_value()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
@ -2231,7 +2533,11 @@ const thisBox = box([[0,0], 6, 10, 3])
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
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 +2547,11 @@ const thisBox = box([[0,0], 6, 10, 3])
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
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 +2561,11 @@ const thisBox = box([[0,0], 6, 10, 3])
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
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 +2575,11 @@ const thisBox = box([[0,0], 6, 10, 3])
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
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 +2709,9 @@ const bracket = startSketchOn('XY')
|
||||
fn additional_program_memory(items: &[(String, MemoryItem)]) -> ProgramMemory {
|
||||
let mut program_memory = ProgramMemory::new();
|
||||
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
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ use crate::{
|
||||
ast::types::FunctionExpression,
|
||||
docs::StdLibFn,
|
||||
errors::KclError,
|
||||
executor::{MemoryItem, SketchGroup, SketchSurface},
|
||||
executor::{MemoryItem, ProgramMemory, SketchGroup, SketchSurface},
|
||||
std::kcl_stdlib::KclStdLibFn,
|
||||
};
|
||||
pub use args::Args;
|
||||
@ -281,6 +281,7 @@ pub enum Primitive {
|
||||
pub struct FnAsArg<'a> {
|
||||
pub func: &'a crate::executor::MemoryFunction,
|
||||
pub expr: Box<FunctionExpression>,
|
||||
pub memory: Box<ProgramMemory>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -87,7 +87,7 @@ pub async fn pattern_transform(args: Args) -> Result<MemoryItem, KclError> {
|
||||
fn_expr: transform.expr,
|
||||
meta: vec![args.source_range.into()],
|
||||
ctx: args.ctx.clone(),
|
||||
memory: args.current_program_memory.clone(),
|
||||
memory: *transform.memory,
|
||||
},
|
||||
extr,
|
||||
&args,
|
||||
@ -116,7 +116,7 @@ pub async fn pattern_transform(args: Args) -> Result<MemoryItem, KclError> {
|
||||
/// // Each layer is just a pretty thin cylinder.
|
||||
/// fn layer = () => {
|
||||
/// return startSketchOn("XY") // or some other plane idk
|
||||
/// |> circle([0, 0], 1, %, 'tag1')
|
||||
/// |> circle([0, 0], 1, %, $tag1)
|
||||
/// |> extrude(h, %)
|
||||
/// }
|
||||
/// // The vase is 100 layers tall.
|
||||
|
@ -1308,7 +1308,7 @@ async fn serial_test_stdlib_kcl_error_right_code_path() {
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
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_eq!(
|
||||
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")]
|
||||
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)
|
||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
||||
|> arc({
|
||||
angle_end: 360,
|
||||
angle_start: 0,
|
||||
radius: radius
|
||||
}, %, tag)
|
||||
}, %, $arc1)
|
||||
|> close(%)
|
||||
|
||||
return sg
|
||||
}
|
||||
|
||||
fn pentagon = (len, taga, tagb, tagc) => {
|
||||
fn pentagon = (len) => {
|
||||
const sg = startSketchOn('XY')
|
||||
|> startProfileAt([-len / 2, -len / 2], %)
|
||||
|> angledLine({ angle: 0, length: len }, %,taga)
|
||||
|> angledLine({ angle: 0, length: len }, %, $a)
|
||||
|> angledLine({
|
||||
angle: segAng(a, %) + 180 - 108,
|
||||
length: len
|
||||
}, %, tagb)
|
||||
}, %, $b)
|
||||
|> angledLine({
|
||||
angle: segAng(b, %) + 180 - 108,
|
||||
length: len
|
||||
}, %,tagc)
|
||||
}, %, $c)
|
||||
|> angledLine({
|
||||
angle: segAng(c, %) + 180 - 108,
|
||||
length: len
|
||||
@ -1821,21 +1821,23 @@ fn pentagon = (len, taga, tagb, tagc) => {
|
||||
return sg
|
||||
}
|
||||
|
||||
const p = pentagon(32, $a, $b, $c)
|
||||
const p = pentagon(32)
|
||||
|> 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, %)
|
||||
|> fillet({
|
||||
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, %)
|
||||
|> fillet({
|
||||
radius: 0.5,
|
||||
tags: [arc_b, getOppositeEdge(arc_b, %)]
|
||||
tags: [circle1.tags.arc1, getOppositeEdge(circle1.tags.arc1, %)]
|
||||
}, %)
|
||||
"#;
|
||||
|
||||
|
@ -39,7 +39,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
|
||||
|
||||
// We need to get the sketch ID.
|
||||
// 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);
|
||||
};
|
||||
let sketch_id = sketch_group.id;
|
||||
|