Compare commits
49 Commits
kurt-try-c
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
f297e0827e | |||
70023a31ef | |||
fb0def6797 | |||
793e3cfa95 | |||
7f25c4ebed | |||
e66204398f | |||
255dbc70da | |||
6e93375f26 | |||
28b30127cb | |||
8064ac8b31 | |||
c3040aa053 | |||
c12b5b67ee | |||
c66f851a3f | |||
13b8ab71d8 | |||
bdeab4f87d | |||
05ccf5e2f4 | |||
7ab015d783 | |||
3d6cfa980f | |||
9f5f1eb8c3 | |||
50fcdff879 | |||
efaae2b193 | |||
7e4ebacb72 | |||
72482506c3 | |||
a51b5b09a3 | |||
53ccc1ed6c | |||
8106749ccf | |||
081e34a600 | |||
541400f4be | |||
39d249030d | |||
f8a69fac73 | |||
24f4bf160f | |||
8011594e24 | |||
0e09affb8f | |||
197a47346a | |||
9d083710e0 | |||
afa7c1dc4e | |||
c74b695a71 | |||
d0c244e05e | |||
a315b77f02 | |||
15c854ff18 | |||
acd3a5717d | |||
8a2555550f | |||
62e75c852a | |||
dd3601ea7b | |||
a5e7782d9a | |||
79b0b70688 | |||
1d134c1be0 | |||
1c58572234 | |||
ecee51e82b |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue
|
||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas
|
||||
|
5
.github/workflows/cargo-clippy.yml
vendored
@ -54,3 +54,8 @@ jobs:
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo clippy --all --tests --benches -- -D warnings
|
||||
# If this fails, run "cargo check" to update Cargo.lock,
|
||||
# then add Cargo.lock to the PR.
|
||||
- name: Check Cargo.lock doesn't need updating
|
||||
run: |
|
||||
cargo check --locked || echo "Pls run cargo check and commit the changed Cargo.lock"
|
||||
|
14
.github/workflows/cargo-test.yml
vendored
@ -59,11 +59,21 @@ jobs:
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
- name: cargo test
|
||||
- name: Compile tests
|
||||
run: |-
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo nextest archive --archive-file tests.tar.zst --workspace --profile ci
|
||||
- name: Start test KCL server
|
||||
run: |-
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo build --quiet --bin kcl-test-server --workspace && ./target/debug/kcl-test-server &
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: |-
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo llvm-cov nextest --all --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
|
||||
cargo llvm-cov nextest --lcov --output-path lcov.info --test-threads=1 --no-fail-fast --profile ci --archive-file tests.tar.zst 2>&1 | tee /tmp/github-actions.log
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
RUST_MIN_STACK: 10485760000
|
||||
|
2
.github/workflows/ci.yml
vendored
@ -180,9 +180,7 @@ jobs:
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# TODO: re-enable for Windows builds, see https://github.com/tauri-apps/tauri/issues/9045
|
||||
- name: Setup Rust cache
|
||||
if: matrix.os != 'windows-latest'
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src-tauri -> target'
|
||||
|
18
.github/workflows/playwright.yml
vendored
@ -46,12 +46,18 @@ jobs:
|
||||
- uses: KittyCAD/action-install-cli@main
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright/
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: dawidd6/action-download-artifact@v4
|
||||
uses: dawidd6/action-download-artifact@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
@ -143,12 +149,20 @@ jobs:
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: dawidd6/action-download-artifact@v4
|
||||
uses: dawidd6/action-download-artifact@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
|
323
docs/kcl/chamfer.md
Normal file
@ -23,6 +23,7 @@ layout: manual
|
||||
* [`atan`](kcl/atan)
|
||||
* [`bezierCurve`](kcl/bezierCurve)
|
||||
* [`ceil`](kcl/ceil)
|
||||
* [`chamfer`](kcl/chamfer)
|
||||
* [`circle`](kcl/circle)
|
||||
* [`close`](kcl/close)
|
||||
* [`cos`](kcl/cos)
|
||||
@ -64,6 +65,7 @@ layout: manual
|
||||
* [`segEndX`](kcl/segEndX)
|
||||
* [`segEndY`](kcl/segEndY)
|
||||
* [`segLen`](kcl/segLen)
|
||||
* [`shell`](kcl/shell)
|
||||
* [`sin`](kcl/sin)
|
||||
* [`sqrt`](kcl/sqrt)
|
||||
* [`startProfileAt`](kcl/startProfileAt)
|
||||
|
311
docs/kcl/shell.md
Normal file
3143
docs/kcl/std.json
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@ -50,3 +50,25 @@ export const TEST_SETTINGS_CORRUPTED = {
|
||||
textWrapping: true,
|
||||
},
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_CODE_GIZMO = `const part001 = startSketchOn('XZ')
|
||||
|> startProfileAt([20, 0], %)
|
||||
|> line([7.13, 4 + 0], %)
|
||||
|> angledLine({ angle: 3 + 0, length: 3.14 + 0 }, %)
|
||||
|> lineTo([20.14 + 0, -0.14 + 0], %)
|
||||
|> xLineTo(29 + 0, %)
|
||||
|> yLine(-3.14 + 0, %, 'a')
|
||||
|> xLine(1.63, %)
|
||||
|> angledLineOfXLength({ angle: 3 + 0, length: 3.14 }, %)
|
||||
|> angledLineOfYLength({ angle: 30, length: 3 + 0 }, %)
|
||||
|> angledLineToX({ angle: 22.14 + 0, to: 12 }, %)
|
||||
|> angledLineToY({ angle: 30, to: 11.14 }, %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: 3.14,
|
||||
intersectTag: 'a',
|
||||
offset: 0
|
||||
}, %)
|
||||
|> tangentialArcTo([13.14 + 0, 13.14], %)
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)
|
||||
`
|
||||
|
@ -12,14 +12,16 @@ 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' })
|
||||
await page
|
||||
.getByTestId('loading')
|
||||
.waitFor({ state: 'detached', timeout: 20_000 })
|
||||
|
||||
await page.getByTestId('start-sketch').waitFor()
|
||||
}
|
||||
|
||||
async function removeCurrentCode(page: Page) {
|
||||
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
|
||||
await page.click('.cm-content')
|
||||
await page.locator('.cm-content').click()
|
||||
await page.keyboard.down(hotkey)
|
||||
await page.keyboard.press('a')
|
||||
await page.keyboard.up(hotkey)
|
||||
@ -28,12 +30,12 @@ async function removeCurrentCode(page: Page) {
|
||||
}
|
||||
|
||||
async function sendCustomCmd(page: Page, cmd: EngineCommand) {
|
||||
await page.fill('[data-testid="custom-cmd-input"]', JSON.stringify(cmd))
|
||||
await page.click('[data-testid="custom-cmd-send-button"]')
|
||||
await page.getByTestId('custom-cmd-input').fill(JSON.stringify(cmd))
|
||||
await page.getByTestId('custom-cmd-send-button').click()
|
||||
}
|
||||
|
||||
async function clearCommandLogs(page: Page) {
|
||||
await page.click('[data-testid="clear-commands"]')
|
||||
await page.getByTestId('clear-commands').click()
|
||||
}
|
||||
|
||||
async function expectCmdLog(page: Page, locatorStr: string) {
|
||||
@ -162,12 +164,7 @@ export const getMovementUtils = (opts: any) => {
|
||||
return ret.then(() => [last.x, last.y])
|
||||
}
|
||||
|
||||
const expectCodeToBe = async (str: string) => {
|
||||
await expect(opts.page.locator('.cm-content')).toHaveText(str)
|
||||
await opts.page.waitForTimeout(100)
|
||||
}
|
||||
|
||||
return { toSU, click00r, expectCodeToBe }
|
||||
return { toSU, click00r }
|
||||
}
|
||||
|
||||
export async function getUtils(page: Page) {
|
||||
@ -228,6 +225,7 @@ export async function getUtils(page: Page) {
|
||||
.locator(locator)
|
||||
.boundingBox()
|
||||
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
|
||||
codeLocator: page.locator('.cm-content'),
|
||||
doAndWaitForCmd: async (
|
||||
fn: () => Promise<void>,
|
||||
commandType: string,
|
||||
|
@ -152,6 +152,7 @@ describe('ZMA (Tauri)', () => {
|
||||
})
|
||||
|
||||
it('signs out', async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
||||
await click(menuButton)
|
||||
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.22.0",
|
||||
"version": "0.22.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
@ -10,7 +10,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.64",
|
||||
"@kittycad/lib": "^0.0.67",
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
|
142
src-tauri/Cargo.lock
generated
@ -200,7 +200,7 @@ dependencies = [
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-updater",
|
||||
"tokio",
|
||||
"toml 0.8.13",
|
||||
"toml 0.8.14",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -601,9 +601,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bson"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d43b38e074cc0de2957f10947e376a1d88b9c4dbab340b590800cc1b2e066b2"
|
||||
checksum = "d8a88e82b9106923b5c4d6edfca9e7db958d4e98a478ec115022e81b9b38e2c8"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"base64 0.13.1",
|
||||
@ -766,7 +766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.8.13",
|
||||
"toml 0.8.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1160,9 +1160,9 @@ checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||
|
||||
[[package]]
|
||||
name = "databake"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82175d72e69414ceafbe2b49686794d3a8bed846e0d50267355f83ea8fdd953a"
|
||||
checksum = "6a04fbfbecca8f0679c8c06fef907594adcc3e2052e11163a6d30535a1a5604d"
|
||||
dependencies = [
|
||||
"databake-derive",
|
||||
"proc-macro2",
|
||||
@ -1171,9 +1171,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "databake-derive"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
|
||||
checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1373,7 +1373,7 @@ dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.8.13",
|
||||
"toml 0.8.14",
|
||||
"vswhom",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
@ -2470,6 +2470,15 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
@ -2578,8 +2587,6 @@ dependencies = [
|
||||
"gltf-json",
|
||||
"js-sys",
|
||||
"kittycad",
|
||||
"kittycad-execution-plan-macros",
|
||||
"kittycad-execution-plan-traits",
|
||||
"lazy_static",
|
||||
"mime_guess",
|
||||
"parse-display",
|
||||
@ -2592,7 +2599,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"toml 0.8.13",
|
||||
"toml 0.8.14",
|
||||
"tower-lsp",
|
||||
"ts-rs",
|
||||
"url",
|
||||
@ -2618,9 +2625,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.3.3"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0cbef813153197e60c0e96f59eea0b75f8418380f414b20250ee81b60e522c3"
|
||||
checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -2633,7 +2640,7 @@ dependencies = [
|
||||
"format_serde_error",
|
||||
"futures",
|
||||
"http 0.2.12",
|
||||
"itertools",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"parse-display",
|
||||
@ -2655,28 +2662,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-execution-plan-macros"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0611fc9b9786175da21d895ffa0f65039e19c9111e94a41b7af999e3b95f045f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-execution-plan-traits"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "123cb47e2780ea8ef3aa67b4db237a27b388d3d3b96db457e274aa4565723151"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.2"
|
||||
@ -2743,7 +2728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.5",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3353,9 +3338,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parse-display"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06af5f9333eb47bd9ba8462d612e37a8328a5cb80b13f0af4de4c3b89f52dee5"
|
||||
checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
|
||||
dependencies = [
|
||||
"parse-display-derive",
|
||||
"regex",
|
||||
@ -3364,9 +3349,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parse-display-derive"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc9252f259500ee570c75adcc4e317fa6f57a1e47747d622e0bf838002a7b790"
|
||||
checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3554,7 +3539,7 @@ dependencies = [
|
||||
"bincode",
|
||||
"either",
|
||||
"fnv",
|
||||
"itertools",
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"quick-xml",
|
||||
@ -3735,9 +3720,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.83"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
|
||||
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -4299,6 +4284,19 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.102.3",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.7.0"
|
||||
@ -5042,7 +5040,7 @@ dependencies = [
|
||||
"cfg-expr",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.13",
|
||||
"toml 0.8.14",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
@ -5195,7 +5193,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"toml 0.8.13",
|
||||
"toml 0.8.14",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@ -5253,7 +5251,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"toml 0.8.13",
|
||||
"toml 0.8.14",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@ -5530,7 +5528,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"swift-rs",
|
||||
"thiserror",
|
||||
"toml 0.8.13",
|
||||
"toml 0.8.14",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"walkdir",
|
||||
@ -5664,9 +5662,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.37.0"
|
||||
version = "1.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@ -5684,9 +5682,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -5715,18 +5713,29 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.21.0"
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls 0.23.7",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls 0.22.4",
|
||||
"rustls 0.23.7",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
@ -5758,14 +5767,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.13"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
|
||||
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.22.13",
|
||||
"toml_edit 0.22.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5814,9 +5823,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.13"
|
||||
version = "0.22.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
|
||||
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"serde",
|
||||
@ -6035,9 +6044,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.21.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
|
||||
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
@ -6046,11 +6055,10 @@ dependencies = [
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rustls 0.22.4",
|
||||
"rustls 0.23.7",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
|
@ -16,7 +16,7 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
|
||||
kittycad = "0.3.0"
|
||||
kittycad = "0.3.5"
|
||||
log = "0.4.21"
|
||||
oauth2 = "4.4.2"
|
||||
serde_json = "1.0"
|
||||
|
@ -74,5 +74,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.22.0"
|
||||
"version": "0.22.1"
|
||||
}
|
||||
|
@ -48,12 +48,14 @@ export type ReactCameraProperties =
|
||||
type: 'perspective'
|
||||
fov?: number
|
||||
position: [number, number, number]
|
||||
target: [number, number, number]
|
||||
quaternion: [number, number, number, number]
|
||||
}
|
||||
| {
|
||||
type: 'orthographic'
|
||||
zoom?: number
|
||||
position: [number, number, number]
|
||||
target: [number, number, number]
|
||||
quaternion: [number, number, number, number]
|
||||
}
|
||||
|
||||
@ -442,7 +444,7 @@ export class CameraControls {
|
||||
this.handleEnd()
|
||||
return
|
||||
}
|
||||
this.throttledEngCmd({
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'default_camera_zoom',
|
||||
@ -454,11 +456,11 @@ export class CameraControls {
|
||||
return
|
||||
}
|
||||
|
||||
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
|
||||
// Else "clientToEngine" (Sketch Mode) or forceUpdate
|
||||
|
||||
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
|
||||
// From onMouseMove zoom handling which seems to be really smooth
|
||||
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
||||
this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed)
|
||||
this.pendingZoom *= 1 + event.deltaY * 0.01
|
||||
this.handleEnd()
|
||||
}
|
||||
|
||||
@ -773,6 +775,75 @@ export class CameraControls {
|
||||
})
|
||||
}
|
||||
|
||||
async updateCameraToAxis(
|
||||
axis: 'x' | 'y' | 'z' | '-x' | '-y' | '-z'
|
||||
): Promise<void> {
|
||||
const distance = this.camera.position.distanceTo(this.target)
|
||||
|
||||
const vantage = this.target.clone()
|
||||
let up = { x: 0, y: 0, z: 1 }
|
||||
|
||||
if (axis === 'x') {
|
||||
vantage.x += distance
|
||||
} else if (axis === 'y') {
|
||||
vantage.y += distance
|
||||
} else if (axis === 'z') {
|
||||
vantage.z += distance
|
||||
up = { x: -1, y: 0, z: 0 }
|
||||
} else if (axis === '-x') {
|
||||
vantage.x -= distance
|
||||
} else if (axis === '-y') {
|
||||
vantage.y -= distance
|
||||
} else if (axis === '-z') {
|
||||
vantage.z -= distance
|
||||
up = { x: -1, y: 0, z: 0 }
|
||||
}
|
||||
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: this.target,
|
||||
vantage: vantage,
|
||||
up: up,
|
||||
},
|
||||
})
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async resetCameraPosition(): Promise<void> {
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: this.target,
|
||||
vantage: {
|
||||
x: this.target.x,
|
||||
y: this.target.y - 128,
|
||||
z: this.target.z + 64,
|
||||
},
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
},
|
||||
})
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'zoom_to_fit',
|
||||
object_ids: [], // leave empty to zoom to all objects
|
||||
padding: 0.2, // padding around the objects
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async tweenCameraToQuaternion(
|
||||
targetQuaternion: Quaternion,
|
||||
targetPosition = new Vector3(),
|
||||
@ -957,6 +1028,11 @@ export class CameraControls {
|
||||
roundOff(this.camera.position.y, 2),
|
||||
roundOff(this.camera.position.z, 2),
|
||||
],
|
||||
target: [
|
||||
roundOff(this.target.x, 2),
|
||||
roundOff(this.target.y, 2),
|
||||
roundOff(this.target.z, 2),
|
||||
],
|
||||
quaternion: [
|
||||
roundOff(this.camera.quaternion.x, 2),
|
||||
roundOff(this.camera.quaternion.y, 2),
|
||||
|
@ -699,6 +699,15 @@ export const CamDebugSettings = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition()
|
||||
}}
|
||||
>
|
||||
Reset Camera Position
|
||||
</button>
|
||||
</div>
|
||||
{camSettings.type === 'perspective' && (
|
||||
<input
|
||||
type="range"
|
||||
@ -816,6 +825,71 @@ export const CamDebugSettings = () => {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
target
|
||||
<ul className="flex">
|
||||
<li>
|
||||
<span className="pl-2 pr-1">x:</span>
|
||||
<input
|
||||
type="number"
|
||||
step={5}
|
||||
data-testid="cam-x-target"
|
||||
value={camSettings.target[0]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
target: [
|
||||
parseFloat(e.target.value),
|
||||
camSettings.target[1],
|
||||
camSettings.target[2],
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="pl-2 pr-1">y:</span>
|
||||
<input
|
||||
type="number"
|
||||
step={5}
|
||||
data-testid="cam-y-target"
|
||||
value={camSettings.target[1]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
target: [
|
||||
camSettings.target[0],
|
||||
parseFloat(e.target.value),
|
||||
camSettings.target[2],
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="pl-2 pr-1">z:</span>
|
||||
<input
|
||||
type="number"
|
||||
step={5}
|
||||
data-testid="cam-z-target"
|
||||
value={camSettings.target[2]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
target: [
|
||||
camSettings.target[0],
|
||||
camSettings.target[1],
|
||||
parseFloat(e.target.value),
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -51,14 +51,6 @@ function CommandBarSelectionInput({
|
||||
inputRef.current?.focus()
|
||||
}, [selection, inputRef])
|
||||
|
||||
// Exit engine's edit mode when this input step is active,
|
||||
// and re-enter it when it's not.
|
||||
// In future the engine's edit mode will go away and this will be handled differently.
|
||||
useEffect(() => {
|
||||
kclManager.exitEditMode()
|
||||
return () => kclManager.defaultSelectionFilter()
|
||||
}, [])
|
||||
|
||||
// Fast-forward through this arg if it's marked as skippable
|
||||
// and we have a valid selection already
|
||||
useEffect(() => {
|
||||
|
199
src/components/ContextMenu.tsx
Normal file
@ -0,0 +1,199 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
|
||||
interface ContextMenuProps
|
||||
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
|
||||
items?: React.ReactElement[]
|
||||
menuTargetElement?: RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
const DefaultContextMenuItems = [
|
||||
<ContextMenuItemRefresh />,
|
||||
<ContextMenuItemCopy />,
|
||||
// add more default context menu items here
|
||||
]
|
||||
|
||||
export function ContextMenu({
|
||||
items = DefaultContextMenuItems,
|
||||
menuTargetElement,
|
||||
className,
|
||||
...props
|
||||
}: ContextMenuProps) {
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: globalThis?.window?.innerWidth,
|
||||
height: globalThis?.window?.innerHeight,
|
||||
})
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
useHotkeys('esc', () => setOpen(false), {
|
||||
enabled: open,
|
||||
})
|
||||
|
||||
const dialogPositionStyle = useMemo(() => {
|
||||
if (!dialogRef.current)
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
}
|
||||
|
||||
return {
|
||||
top:
|
||||
position.y + dialogRef.current.clientHeight > windowSize.height
|
||||
? 'auto'
|
||||
: position.y,
|
||||
left:
|
||||
position.x + dialogRef.current.clientWidth > windowSize.width
|
||||
? 'auto'
|
||||
: position.x,
|
||||
right:
|
||||
position.x + dialogRef.current.clientWidth > windowSize.width
|
||||
? windowSize.width - position.x
|
||||
: 'auto',
|
||||
bottom:
|
||||
position.y + dialogRef.current.clientHeight > windowSize.height
|
||||
? windowSize.height - position.y
|
||||
: 'auto',
|
||||
}
|
||||
}, [position, windowSize, dialogRef.current])
|
||||
|
||||
// Listen for window resize to update context menu position
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowSize({
|
||||
width: globalThis?.window?.innerWidth,
|
||||
height: globalThis?.window?.innerHeight,
|
||||
})
|
||||
}
|
||||
globalThis?.window?.addEventListener('resize', handleResize)
|
||||
return () => {
|
||||
globalThis?.window?.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Add context menu listener to target once mounted
|
||||
useEffect(() => {
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
console.log('context menu', e)
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.x, y: e.y })
|
||||
setOpen(true)
|
||||
}
|
||||
menuTargetElement?.current?.addEventListener(
|
||||
'contextmenu',
|
||||
handleContextMenu
|
||||
)
|
||||
return () => {
|
||||
menuTargetElement?.current?.removeEventListener(
|
||||
'contextmenu',
|
||||
handleContextMenu
|
||||
)
|
||||
}
|
||||
}, [menuTargetElement?.current])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<div
|
||||
className="fixed inset-0 z-50 w-screen h-screen"
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
>
|
||||
<Dialog.Backdrop className="fixed z-10 inset-0" />
|
||||
<Dialog.Panel
|
||||
ref={dialogRef}
|
||||
className={`w-48 fixed bg-chalkboard-10 dark:bg-chalkboard-90
|
||||
border border-solid border-chalkboard-10 dark:border-chalkboard-90 rounded
|
||||
shadow-lg backdrop:fixed backdrop:inset-0 backdrop:bg-primary ${className}`}
|
||||
style={{
|
||||
...dialogPositionStyle,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<ul
|
||||
{...props}
|
||||
className="relative flex flex-col gap-0.5 items-stretch content-stretch"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{...items}
|
||||
</ul>
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContextMenuDivider() {
|
||||
return <hr className="border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
}
|
||||
|
||||
interface ContextMenuItemProps {
|
||||
children: React.ReactNode
|
||||
icon?: ActionIconProps['icon']
|
||||
onClick?: () => void
|
||||
hotkey?: string
|
||||
}
|
||||
|
||||
export function ContextMenuItem({
|
||||
children,
|
||||
icon,
|
||||
onClick,
|
||||
hotkey,
|
||||
}: ContextMenuItemProps) {
|
||||
return (
|
||||
<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"
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon && <ActionIcon icon={icon} bgClassName="!bg-transparent" />}
|
||||
<div className="flex-1">{children}</div>
|
||||
{hotkey && (
|
||||
<kbd className="px-1.5 py-0.5 rounded bg-primary/10 text-primary dark:bg-chalkboard-80 dark:text-chalkboard-40">
|
||||
{hotkey}
|
||||
</kbd>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContextMenuItemRefresh() {
|
||||
return (
|
||||
<ContextMenuItem
|
||||
icon="arrowRotateRight"
|
||||
onClick={() => globalThis?.window?.location.reload()}
|
||||
>
|
||||
Refresh
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
interface ContextMenuItemCopyProps {
|
||||
toBeCopiedContent?: string
|
||||
toBeCopiedLabel?: string
|
||||
}
|
||||
|
||||
export function ContextMenuItemCopy({
|
||||
toBeCopiedContent = globalThis.window?.getSelection()?.toString(),
|
||||
toBeCopiedLabel = 'selection',
|
||||
}: ContextMenuItemCopyProps) {
|
||||
return (
|
||||
<ContextMenuItem
|
||||
icon="clipboardPlus"
|
||||
onClick={() => {
|
||||
if (toBeCopiedContent) {
|
||||
globalThis?.navigator?.clipboard
|
||||
.writeText(toBeCopiedContent)
|
||||
.then(() => toast.success(`Copied ${toBeCopiedLabel} to clipboard`))
|
||||
.catch(() =>
|
||||
toast.error(`Failed to copy ${toBeCopiedLabel} to clipboard`)
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
@ -71,6 +71,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
bug: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.8209 5.99884C10.6403 5.73962 10.3399 5.57001 10 5.57001C9.65984 5.57001 9.35936 5.73984 9.17871 5.99935C9.43724 5.95129 9.71142 5.92578 10.0012 5.92578C10.29 5.92578 10.5633 5.95111 10.8209 5.99884ZM10 4.57001C8.9459 4.57001 8.08227 5.38548 8.00554 6.41997C7.58916 6.65398 7.23724 6.95989 6.95014 7.31304L5.85355 6.21645L5.14645 6.92356L6.40931 8.18642C6.20774 8.62503 6.08043 9.09624 6.0278 9.57001H5V10.57H6.01946C6.06396 11.1581 6.1867 11.8173 6.4071 12.4558L5.14645 13.7165L5.85355 14.4236L6.8408 13.4363C7.46354 14.555 8.47307 15.4258 10.0012 15.4258C11.529 15.4258 12.5378 14.5554 13.16 13.4371L14.1464 14.4236L14.8536 13.7165L13.5934 12.4563C13.8136 11.8177 13.9362 11.1583 13.9806 10.57H15V9.57001H13.9722C13.9197 9.0961 13.7925 8.62474 13.5911 8.18602L14.8536 6.92356L14.1464 6.21645L13.0505 7.31239C12.7633 6.95894 12.4112 6.65285 11.9944 6.41883C11.9171 5.38488 11.0537 4.57001 10 4.57001ZM10.5 14.3801V8.57001H9.5V14.3796C8.72105 14.2298 8.15885 13.7245 7.7428 12.9999C7.22316 12.095 7 10.937 7 10.07C7 8.46381 8.04281 6.92578 10.0012 6.92578C11.9589 6.92578 13 8.4629 13 10.07C13 10.9373 12.7773 12.0954 12.2582 13.0003C11.8422 13.7254 11.2799 14.2309 10.5 14.3801Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
checkmark: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
@ -18,6 +18,8 @@ import { useLspContext } from './LspProvider'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
|
||||
import { ContextMenu, ContextMenuItem } from './ContextMenu'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
|
||||
function getIndentationCSS(level: number) {
|
||||
return `calc(1rem * ${level + 1})`
|
||||
@ -125,6 +127,7 @@ const FileTreeItem = ({
|
||||
const navigate = useNavigate()
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
const isCurrentFile = fileOrDir.path === currentFile?.path
|
||||
const itemRef = useRef(null)
|
||||
|
||||
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
|
||||
const removeCurrentItemFromRenaming = useCallback(
|
||||
@ -185,7 +188,7 @@ const FileTreeItem = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="contents" ref={itemRef}>
|
||||
{fileOrDir.children === undefined ? (
|
||||
<li
|
||||
className={
|
||||
@ -321,7 +324,41 @@ const FileTreeItem = ({
|
||||
setIsOpen={setIsConfirmingDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<FileTreeContextMenu
|
||||
itemRef={itemRef}
|
||||
onRename={addCurrentItemToRenaming}
|
||||
onDelete={() => setIsConfirmingDelete(true)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface FileTreeContextMenuProps {
|
||||
itemRef: React.RefObject<HTMLElement>
|
||||
onRename: () => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
function FileTreeContextMenu({
|
||||
itemRef,
|
||||
onRename,
|
||||
onDelete,
|
||||
}: FileTreeContextMenuProps) {
|
||||
const platform = usePlatform()
|
||||
const metaKey = platform === 'macos' ? '⌘' : 'Ctrl'
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
menuTargetElement={itemRef}
|
||||
items={[
|
||||
<ContextMenuItem onClick={onRename} hotkey="Enter">
|
||||
Rename
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem onClick={onDelete} hotkey={metaKey + ' + Del'}>
|
||||
Delete
|
||||
</ContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||
import {
|
||||
WebGLRenderer,
|
||||
Scene,
|
||||
@ -12,21 +13,52 @@ import {
|
||||
Clock,
|
||||
Quaternion,
|
||||
ColorRepresentation,
|
||||
Vector2,
|
||||
Raycaster,
|
||||
Camera,
|
||||
Intersection,
|
||||
Object3D,
|
||||
} from 'three'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuDivider,
|
||||
ContextMenuItem,
|
||||
ContextMenuItemRefresh,
|
||||
} from './ContextMenu'
|
||||
|
||||
const CANVAS_SIZE = 80
|
||||
const FRUSTUM_SIZE = 0.5
|
||||
const AXIS_LENGTH = 0.35
|
||||
const AXIS_WIDTH = 0.02
|
||||
const AXIS_COLORS = {
|
||||
x: '#fa6668',
|
||||
y: '#11eb6b',
|
||||
z: '#6689ef',
|
||||
gray: '#c6c7c2',
|
||||
enum AxisColors {
|
||||
X = '#fa6668',
|
||||
Y = '#11eb6b',
|
||||
Z = '#6689ef',
|
||||
Gray = '#c6c7c2',
|
||||
}
|
||||
enum AxisNames {
|
||||
X = 'x',
|
||||
Y = 'y',
|
||||
Z = 'z',
|
||||
NEG_X = '-x',
|
||||
NEG_Y = '-y',
|
||||
NEG_Z = '-z',
|
||||
}
|
||||
const axisNamesSemantic: Record<AxisNames, string> = {
|
||||
[AxisNames.X]: 'Right',
|
||||
[AxisNames.Y]: 'Back',
|
||||
[AxisNames.Z]: 'Top',
|
||||
[AxisNames.NEG_X]: 'Left',
|
||||
[AxisNames.NEG_Y]: 'Front',
|
||||
[AxisNames.NEG_Z]: 'Bottom',
|
||||
}
|
||||
|
||||
export default function Gizmo() {
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||
const cameraPassiveUpdateTimer = useRef(0)
|
||||
const raycasterPassiveUpdateTimer = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return
|
||||
@ -41,35 +73,89 @@ export default function Gizmo() {
|
||||
const { gizmoAxes, gizmoAxisHeads } = createGizmo()
|
||||
scene.add(...gizmoAxes, ...gizmoAxisHeads)
|
||||
|
||||
const raycaster = new Raycaster()
|
||||
const { mouse, disposeMouseEvents } = initializeMouseEvents(
|
||||
canvas,
|
||||
raycasterIntersect,
|
||||
sceneInfra
|
||||
)
|
||||
const raycasterObjects = [...gizmoAxisHeads]
|
||||
|
||||
const clock = new Clock()
|
||||
const clientCamera = sceneInfra.camControls.camera
|
||||
let currentQuaternion = new Quaternion().copy(clientCamera.quaternion)
|
||||
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate)
|
||||
const delta = clock.getDelta()
|
||||
updateCameraOrientation(
|
||||
camera,
|
||||
currentQuaternion,
|
||||
sceneInfra.camControls.camera.quaternion,
|
||||
clock.getDelta()
|
||||
delta,
|
||||
cameraPassiveUpdateTimer
|
||||
)
|
||||
updateRayCaster(
|
||||
raycasterObjects,
|
||||
raycaster,
|
||||
mouse,
|
||||
camera,
|
||||
raycasterIntersect,
|
||||
delta,
|
||||
raycasterPassiveUpdateTimer
|
||||
)
|
||||
renderer.render(scene, camera)
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
animate()
|
||||
|
||||
return () => {
|
||||
renderer.dispose()
|
||||
disposeMouseEvents()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-none">
|
||||
<canvas ref={canvasRef} />
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
aria-label="View orientation gizmo"
|
||||
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto"
|
||||
>
|
||||
<canvas ref={canvasRef} />
|
||||
<ContextMenu
|
||||
menuTargetElement={wrapperRef}
|
||||
items={[
|
||||
...Object.entries(axisNamesSemantic).map(
|
||||
([axisName, axisSemantic]) => (
|
||||
<ContextMenuItem
|
||||
key={axisName}
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.updateCameraToAxis(
|
||||
axisName as AxisNames
|
||||
)
|
||||
}}
|
||||
>
|
||||
{axisSemantic} view
|
||||
</ContextMenuItem>
|
||||
)
|
||||
),
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition()
|
||||
}}
|
||||
>
|
||||
Reset view
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItemRefresh />,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const createCamera = () => {
|
||||
const createCamera = (): OrthographicCamera => {
|
||||
return new OrthographicCamera(
|
||||
-FRUSTUM_SIZE,
|
||||
FRUSTUM_SIZE,
|
||||
@ -82,21 +168,21 @@ const createCamera = () => {
|
||||
|
||||
const createGizmo = () => {
|
||||
const gizmoAxes = [
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.x, 0, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.X, 0, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Y, Math.PI / 2, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Z, -Math.PI / 2, 'y'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, -Math.PI / 2, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI / 2, 'y'),
|
||||
]
|
||||
|
||||
const gizmoAxisHeads = [
|
||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.x, 0, 'z'),
|
||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
|
||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
|
||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI, 'z'),
|
||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
|
||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
|
||||
createAxisHead(AxisNames.X, AxisColors.X, [AXIS_LENGTH, 0, 0]),
|
||||
createAxisHead(AxisNames.Y, AxisColors.Y, [0, AXIS_LENGTH, 0]),
|
||||
createAxisHead(AxisNames.Z, AxisColors.Z, [0, 0, AXIS_LENGTH]),
|
||||
createAxisHead(AxisNames.NEG_X, AxisColors.Gray, [-AXIS_LENGTH, 0, 0]),
|
||||
createAxisHead(AxisNames.NEG_Y, AxisColors.Gray, [0, -AXIS_LENGTH, 0]),
|
||||
createAxisHead(AxisNames.NEG_Z, AxisColors.Gray, [0, 0, -AXIS_LENGTH]),
|
||||
]
|
||||
|
||||
return { gizmoAxes, gizmoAxisHeads }
|
||||
@ -108,12 +194,9 @@ const createAxis = (
|
||||
color: ColorRepresentation,
|
||||
rotation = 0,
|
||||
axis = 'x'
|
||||
) => {
|
||||
const geometry = new BoxGeometry(length, width, width).translate(
|
||||
length / 2,
|
||||
0,
|
||||
0
|
||||
)
|
||||
): Mesh => {
|
||||
const geometry = new BoxGeometry(length, width, width)
|
||||
geometry.translate(length / 2, 0, 0)
|
||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
||||
const mesh = new Mesh(geometry, material)
|
||||
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
||||
@ -121,15 +204,17 @@ const createAxis = (
|
||||
}
|
||||
|
||||
const createAxisHead = (
|
||||
length: number,
|
||||
name: AxisNames,
|
||||
color: ColorRepresentation,
|
||||
rotation = 0,
|
||||
axis = 'x'
|
||||
) => {
|
||||
const geometry = new SphereGeometry(0.065, 16, 8).translate(length, 0, 0)
|
||||
position: number[]
|
||||
): Mesh => {
|
||||
const geometry = new SphereGeometry(0.065, 16, 8)
|
||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
||||
const mesh = new Mesh(geometry, material)
|
||||
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
||||
|
||||
mesh.position.set(position[0], position[1], position[2])
|
||||
mesh.updateMatrixWorld()
|
||||
mesh.name = name
|
||||
return mesh
|
||||
}
|
||||
|
||||
@ -137,10 +222,97 @@ const updateCameraOrientation = (
|
||||
camera: OrthographicCamera,
|
||||
currentQuaternion: Quaternion,
|
||||
targetQuaternion: Quaternion,
|
||||
deltaTime: number
|
||||
deltaTime: number,
|
||||
cameraPassiveUpdateTimer: MutableRefObject<number>
|
||||
) => {
|
||||
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
|
||||
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
|
||||
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
|
||||
camera.quaternion.copy(currentQuaternion)
|
||||
cameraPassiveUpdateTimer.current += deltaTime
|
||||
if (
|
||||
!quaternionsEqual(currentQuaternion, targetQuaternion) ||
|
||||
cameraPassiveUpdateTimer.current >= 5
|
||||
) {
|
||||
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
|
||||
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
|
||||
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
|
||||
camera.quaternion.copy(currentQuaternion)
|
||||
cameraPassiveUpdateTimer.current = 0
|
||||
}
|
||||
}
|
||||
|
||||
const quaternionsEqual = (
|
||||
q1: Quaternion,
|
||||
q2: Quaternion,
|
||||
tolerance: number = 0.001
|
||||
): boolean => {
|
||||
return (
|
||||
Math.abs(q1.x - q2.x) < tolerance &&
|
||||
Math.abs(q1.y - q2.y) < tolerance &&
|
||||
Math.abs(q1.z - q2.z) < tolerance &&
|
||||
Math.abs(q1.w - q2.w) < tolerance
|
||||
)
|
||||
}
|
||||
|
||||
const initializeMouseEvents = (
|
||||
canvas: HTMLCanvasElement,
|
||||
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
|
||||
sceneInfra: SceneInfra
|
||||
): { mouse: Vector2; disposeMouseEvents: () => void } => {
|
||||
const mouse = new Vector2()
|
||||
mouse.x = 1 // fix initial mouse position issue
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const { left, top, width, height } = canvas.getBoundingClientRect()
|
||||
mouse.x = ((event.clientX - left) / width) * 2 - 1
|
||||
mouse.y = ((event.clientY - top) / height) * -2 + 1
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (raycasterIntersect.current) {
|
||||
const axisName = raycasterIntersect.current.object.name as AxisNames
|
||||
sceneInfra.camControls.updateCameraToAxis(axisName)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('click', handleClick)
|
||||
|
||||
const disposeMouseEvents = () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('click', handleClick)
|
||||
}
|
||||
|
||||
return { mouse, disposeMouseEvents }
|
||||
}
|
||||
|
||||
const updateRayCaster = (
|
||||
objects: Object3D[],
|
||||
raycaster: Raycaster,
|
||||
mouse: Vector2,
|
||||
camera: Camera,
|
||||
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
|
||||
deltaTime: number,
|
||||
raycasterPassiveUpdateTimer: MutableRefObject<number>
|
||||
) => {
|
||||
raycasterPassiveUpdateTimer.current += deltaTime
|
||||
|
||||
// check if mouse is outside the canvas bounds and stop raycaster
|
||||
if (raycasterPassiveUpdateTimer.current < 2) {
|
||||
if (mouse.x < -1 || mouse.x > 1 || mouse.y < -1 || mouse.y > 1) {
|
||||
raycasterIntersect.current = null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
raycaster.setFromCamera(mouse, camera)
|
||||
const intersects = raycaster.intersectObjects(objects)
|
||||
|
||||
objects.forEach((object) => object.scale.set(1, 1, 1))
|
||||
if (intersects.length) {
|
||||
intersects[0].object.scale.set(1.5, 1.5, 1.5)
|
||||
raycasterIntersect.current = intersects[0] // filter first object
|
||||
} else {
|
||||
raycasterIntersect.current = null
|
||||
}
|
||||
if (raycasterPassiveUpdateTimer.current > 2) {
|
||||
raycasterPassiveUpdateTimer.current = 0
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ export function LowerRightControls(props: React.PropsWithChildren) {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<CustomIcon
|
||||
name="exclamationMark"
|
||||
name="bug"
|
||||
className={`w-5 h-5 ${linkOverrideClassName}`}
|
||||
/>
|
||||
<Tooltip position="top">Report a bug</Tooltip>
|
||||
|
@ -24,9 +24,9 @@ export function RefreshButton() {
|
||||
return (
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-10 dark:border-chalkboard-100"
|
||||
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
|
||||
>
|
||||
<CustomIcon name="arrowRotateRight" className="w-5 h-5" />
|
||||
<CustomIcon name="exclamationMark" className="w-5 h-5" />
|
||||
<Tooltip position="bottom-right">
|
||||
<span>Refresh and report</span>
|
||||
<br />
|
||||
|
@ -171,7 +171,9 @@ export const SettingsAuthProviderBase = ({
|
||||
})
|
||||
},
|
||||
'Execute AST': () => kclManager.executeCode(true, true),
|
||||
persistSettings: (context) =>
|
||||
},
|
||||
services: {
|
||||
'Persist settings': (context) =>
|
||||
saveSettings(context, loadedProject?.project?.path),
|
||||
},
|
||||
}
|
||||
|
@ -11,30 +11,8 @@
|
||||
--_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2));
|
||||
--_p-block: 4px;
|
||||
--_bg: var(--chalkboard-10);
|
||||
--_shadow-alpha: 5%;
|
||||
--_shadow-alpha: 8%;
|
||||
--_theme-alpha: 0.15;
|
||||
--_theme-outline: drop-shadow(
|
||||
0 1px 0
|
||||
oklch(
|
||||
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
|
||||
var(--_theme-alpha)
|
||||
)
|
||||
)
|
||||
drop-shadow(
|
||||
0 -1px 0 oklch(var(--primary-lightness) var(--primary-chroma)
|
||||
var(--primary-hue) / var(--_theme-alpha))
|
||||
)
|
||||
drop-shadow(
|
||||
1px 0 0
|
||||
oklch(
|
||||
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
|
||||
var(--_theme-alpha)
|
||||
)
|
||||
)
|
||||
drop-shadow(
|
||||
-1px 0 0 oklch(var(--primary-lightness) var(--primary-chroma)
|
||||
var(--primary-hue) / var(--_theme-alpha))
|
||||
);
|
||||
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
@ -61,16 +39,15 @@
|
||||
background: var(--_bg);
|
||||
@apply text-chalkboard-110;
|
||||
will-change: filter;
|
||||
filter: drop-shadow(0 1px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
drop-shadow(0 4px 8px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
var(--_theme-outline);
|
||||
filter: drop-shadow(0 1px 2px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
drop-shadow(0 4px 6px hsl(0 0% 0% / calc(var(--_shadow-alpha) / 2)));
|
||||
}
|
||||
|
||||
:global(.dark) .tooltip {
|
||||
--_bg: var(--chalkboard-110);
|
||||
--_bg: var(--chalkboard-90);
|
||||
--_theme-alpha: 40%;
|
||||
--_shadow-alpha: 16%;
|
||||
@apply text-chalkboard-10;
|
||||
filter: var(--_theme-outline);
|
||||
}
|
||||
|
||||
.tooltip:dir(rtl) {
|
||||
|
@ -7,7 +7,11 @@ import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||
import { addLineHighlight } from './highlightextension'
|
||||
import { setDiagnostics, Diagnostic } from '@codemirror/lint'
|
||||
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint'
|
||||
|
||||
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
|
||||
@ -91,11 +95,38 @@ export default class EditorManager {
|
||||
}
|
||||
}
|
||||
|
||||
clearDiagnostics(): void {
|
||||
if (!this.editorView) return
|
||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
|
||||
}
|
||||
|
||||
setDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this.editorView) return
|
||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
|
||||
}
|
||||
|
||||
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.editorView.dispatch(
|
||||
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
|
||||
)
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this._editorView) {
|
||||
undo(this._editorView)
|
||||
|
@ -382,9 +382,14 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
try {
|
||||
switch (notification.method) {
|
||||
case 'textDocument/publishDiagnostics':
|
||||
//const params = notification.params as PublishDiagnosticsParams
|
||||
console.log(
|
||||
'[lsp] [window/publishDiagnostics]',
|
||||
this.client.getName(),
|
||||
notification.params
|
||||
)
|
||||
const params = notification.params as PublishDiagnosticsParams
|
||||
// this is sometimes slower than our actual typing.
|
||||
//this.processDiagnostics(params)
|
||||
this.processDiagnostics(params)
|
||||
break
|
||||
case 'window/logMessage':
|
||||
console.log(
|
||||
|
@ -89,9 +89,10 @@ export class KclManager {
|
||||
return this._kclErrors
|
||||
}
|
||||
set kclErrors(kclErrors) {
|
||||
console.log('[lsp] not lsp, actually typescript: ', kclErrors)
|
||||
this._kclErrors = kclErrors
|
||||
let diagnostics = kclErrorsToDiagnostics(kclErrors)
|
||||
editorManager.setDiagnostics(diagnostics)
|
||||
editorManager.addDiagnostics(diagnostics)
|
||||
this._kclErrorsCallBack(kclErrors)
|
||||
}
|
||||
|
||||
@ -185,6 +186,11 @@ export class KclManager {
|
||||
const currentExecutionId = executionId || Date.now()
|
||||
this._cancelTokens.set(currentExecutionId, false)
|
||||
|
||||
// here we're going to clear diagnostics since we're the first
|
||||
// one in. We're the only location where diagnostics are cleared;
|
||||
// everything from here on out should be *appending*.
|
||||
editorManager.clearDiagnostics()
|
||||
|
||||
this.isExecuting = true
|
||||
await this.ensureWasmInit()
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
@ -234,6 +240,7 @@ export class KclManager {
|
||||
} = { updates: 'none' }
|
||||
) {
|
||||
await this.ensureWasmInit()
|
||||
|
||||
const newCode = recast(ast)
|
||||
const newAst = this.safeParse(newCode)
|
||||
if (!newAst) return
|
||||
@ -243,6 +250,11 @@ export class KclManager {
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
this._ast = { ...newAst }
|
||||
|
||||
// here we're going to clear diagnostics since we're the first
|
||||
// one in. We're the only location where diagnostics are cleared;
|
||||
// everything from here on out should be *appending*.
|
||||
editorManager.clearDiagnostics()
|
||||
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast: newAst,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
@ -365,13 +377,6 @@ export class KclManager {
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
||||
}
|
||||
exitEditMode() {
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
}
|
||||
defaultSelectionFilter() {
|
||||
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
|
||||
}
|
||||
@ -386,24 +391,11 @@ function defaultSelectionFilter(
|
||||
) as SketchGroup | ExtrudeGroup
|
||||
firstSketchOrExtrudeGroup &&
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_batch_req',
|
||||
batch_id: uuidv4(),
|
||||
responses: false,
|
||||
requests: [
|
||||
{
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: firstSketchOrExtrudeGroup.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_selection_filter',
|
||||
filter: ['face', 'edge', 'solid2d'],
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_selection_filter',
|
||||
filter: ['face', 'edge', 'solid2d', 'curve'],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -147,7 +147,6 @@ export enum ConnectionError {
|
||||
Unset = 0,
|
||||
LongLoadingTime,
|
||||
|
||||
LostVideoStream,
|
||||
ICENegotiate,
|
||||
DataChannelError,
|
||||
WebSocketError,
|
||||
@ -168,8 +167,6 @@ export const CONNECTION_ERROR_TEXT: Record<ConnectionError, string> = {
|
||||
[ConnectionError.Unset]: '',
|
||||
[ConnectionError.LongLoadingTime]:
|
||||
'Loading is taking longer than expected...',
|
||||
[ConnectionError.LostVideoStream]:
|
||||
'Lost connection to video stream... Reconnecting...',
|
||||
[ConnectionError.ICENegotiate]: 'ICE negotiation failed.',
|
||||
[ConnectionError.DataChannelError]: 'The data channel signaled an error.',
|
||||
[ConnectionError.WebSocketError]: 'The websocket signaled an error.',
|
||||
@ -315,8 +312,6 @@ class EngineConnection extends EventTarget {
|
||||
if (next.type === EngineConnectionStateType.Disconnecting) {
|
||||
const sub = next.value
|
||||
if (sub.type === DisconnectingType.Error) {
|
||||
console.log(sub)
|
||||
|
||||
// Record the last step we failed at.
|
||||
// (Check the current state that we're about to override that
|
||||
// it was a Connecting state.)
|
||||
@ -759,8 +754,6 @@ class EngineConnection extends EventTarget {
|
||||
// when assuming we're the only consumer or that all messages will
|
||||
// be carefully formatted here.
|
||||
|
||||
console.log(event)
|
||||
|
||||
if (typeof event.data !== 'string') {
|
||||
return
|
||||
}
|
||||
@ -781,7 +774,6 @@ class EngineConnection extends EventTarget {
|
||||
`Error in response to request ${message.request_id}:\n${errorsString}
|
||||
failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
)
|
||||
console.log(artifactThatFailed)
|
||||
} else {
|
||||
console.error(`Error from server:\n${errorsString}`)
|
||||
}
|
||||
@ -872,7 +864,6 @@ class EngineConnection extends EventTarget {
|
||||
this.pc
|
||||
?.createOffer()
|
||||
.then((offer: RTCSessionDescriptionInit) => {
|
||||
console.log(offer)
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Connecting,
|
||||
value: {
|
||||
@ -944,7 +935,6 @@ class EngineConnection extends EventTarget {
|
||||
|
||||
case 'trickle_ice':
|
||||
let candidate = resp.data?.candidate
|
||||
console.log('trickle_ice: using this candidate: ', candidate)
|
||||
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
|
||||
break
|
||||
|
||||
@ -1347,20 +1337,10 @@ export class EngineCommandManager extends EventTarget {
|
||||
this.engineConnection?.addEventListener(
|
||||
EngineConnectionEvents.NewTrack,
|
||||
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
|
||||
console.log('received track', mediaStream)
|
||||
|
||||
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
|
||||
if (this.engineConnection) {
|
||||
this.engineConnection.state = {
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: {
|
||||
type: DisconnectingType.Error,
|
||||
value: {
|
||||
error: ConnectionError.LostVideoStream,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
console.error(
|
||||
'video track mute: check webrtc internals -> inbound rtp'
|
||||
)
|
||||
})
|
||||
|
||||
setMediaStream(mediaStream)
|
||||
@ -1673,7 +1653,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
(command.cmd.type === 'highlight_set_entity' ||
|
||||
command.cmd.type === 'mouse_move' ||
|
||||
command.cmd.type === 'camera_drag_move' ||
|
||||
command.cmd.type === 'default_camera_look_at' ||
|
||||
command.cmd.type === ('default_camera_perspective_settings' as any))
|
||||
)
|
||||
) {
|
||||
@ -1688,7 +1667,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
command.type === 'modeling_cmd_req' &&
|
||||
command.cmd.type !== lastMessage
|
||||
) {
|
||||
console.log('sending command', command.cmd.type)
|
||||
lastMessage = command.cmd.type
|
||||
}
|
||||
if (command.type === 'modeling_cmd_batch_req') {
|
||||
@ -1702,7 +1680,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
if (
|
||||
(cmd.type === 'camera_drag_move' ||
|
||||
cmd.type === 'handle_mouse_drag_move' ||
|
||||
cmd.type === 'default_camera_look_at' ||
|
||||
cmd.type === ('default_camera_perspective_settings' as any)) &&
|
||||
this.engineConnection?.unreliableDataChannel &&
|
||||
!forceWebsocket
|
||||
|
@ -17,15 +17,17 @@ const prependRoutes =
|
||||
)
|
||||
}
|
||||
|
||||
type OnboardingPaths = {
|
||||
[K in keyof typeof onboardingPaths]: `/onboarding${(typeof onboardingPaths)[K]}`
|
||||
}
|
||||
|
||||
export const paths = {
|
||||
INDEX: '/',
|
||||
HOME: '/home',
|
||||
FILE: '/file',
|
||||
SETTINGS: '/settings',
|
||||
SIGN_IN: '/signin',
|
||||
ONBOARDING: prependRoutes(onboardingPaths)(
|
||||
'/onboarding'
|
||||
) as typeof onboardingPaths,
|
||||
ONBOARDING: prependRoutes(onboardingPaths)('/onboarding') as OnboardingPaths,
|
||||
} as const
|
||||
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
|
||||
|
||||
|
@ -11,98 +11,98 @@ import {
|
||||
|
||||
export const settingsMachine = createMachine(
|
||||
{
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IAbAFZN+AOwAWAIwAOYwE4AzGYBM+-ZosAaEAE9Eh62LP51ls+v0LMWt1awMAX3DnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BA9vQ0N1CwCxdVbdY1DnNwQzPp8zTTFje1D1QwtjSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmotNY3w7YysHRuNDTXV1bvdG7w-IKTcbaazWCzTEAxOZ4QiLJIrFL4dJZMAXUpXSrVDTGazPMQPR4GXRBAx-XoGfDWIadMx4n6EqEwuLwxLLVbrTbbNKYKBpLb8tAAUWgcAxMjk11uoHumn0+DEw2sJkMulCgWsFL6YnwfX0ELsYg61jMumZs1ZCXIACU4OgAATLWGiSSXKXYu4aLz4UwWBr6DqBYYUzSePXqUlBLxmyZmC2xeZs8gxB3UYjEJ2W+YSsoem5VL0IKy6z6EsJifQ2Czq0MTHzq8Yjfz6MQmBMu5NkABUuaxBZxCDD+DD5lJgUjxssFP0xl0I+0pm06uMmg8kSiIGwXPgpRZ83dFQHRYAtA0LCO2tZjGIHJNdB5fq5EK3dc1OsNGkrA+oO1bFoe0qFrKiAnlol7BDed5zo+FK+Doc7qhCNaVv4UwbkAA */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmsGxfCMPM08PQaDNU0cXRG1tLwedTaKxif7+UJTKIgGJzPCERZJFYpfDpLJgC6lK6VaqIEx6fBmCw2Do2IJ6MxdRDvTT4MRDdRGEzWbQ6ELTGGzOIIxLLVbrTbbNKYKBpLaitAAUWgcExMjk11uoBqVgM3jMYhsAIMrVs6ipPWChOeYhC9KMFhGHNh3IS5AASnB0AACZZw0SSS4KnF3PFafADTV1YZ2IxiH7dNpGfCaIzAgE+IzWMzBa1c+Y88gxZ3UYjEV3pvBysrem5VX0IFq0y3aTXWOp6JmU34IKMxuz0joGEYWsxp2IZu1kABUxexZdxtRG+EmQMZmne3dNBs0jKewLBsbCwI81n77vwtDAHHksDhBYHeDIEGYYEI2AAbowANZ3o8nzBnm3zMelpWqRAAFp62sJ4jEsZ4AT0UJGwjPFzH6cwNW0AwWXpbRImhbABXgUpvzwL0KgnCtgJMMCII8KCYLsA11EGOkXneDxmXMCk92hfCFlIQjFXLZUgLjddWhaFkgRCaxOhbEYzBnXwXkmOjAjjfduXfU9zzdOIeJ9fiEEA6ckwMClQ2BFpmJXMF9DjYI6hZfxmMw8IgA */
|
||||
id: 'Settings',
|
||||
predictableActionArguments: true,
|
||||
context: {} as ReturnType<typeof createSettings>,
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
entry: ['setThemeClass', 'setClientSideSceneUnits', 'persistSettings'],
|
||||
entry: ['setThemeClass', 'setClientSideSceneUnits'],
|
||||
|
||||
on: {
|
||||
'*': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: ['setSettingAtLevel', 'toastSuccess', 'persistSettings'],
|
||||
target: 'persisting settings',
|
||||
actions: ['setSettingAtLevel', 'toastSuccess'],
|
||||
},
|
||||
|
||||
'set.app.onboardingStatus': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: ['setSettingAtLevel', 'persistSettings'], // No toast
|
||||
target: 'persisting settings',
|
||||
|
||||
// No toast
|
||||
actions: ['setSettingAtLevel'],
|
||||
},
|
||||
'set.app.themeColor': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: ['setSettingAtLevel', 'persistSettings'], // No toast
|
||||
target: 'persisting settings',
|
||||
|
||||
// No toast
|
||||
actions: ['setSettingAtLevel'],
|
||||
},
|
||||
|
||||
'set.modeling.defaultUnit': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: [
|
||||
'setSettingAtLevel',
|
||||
'toastSuccess',
|
||||
'setClientSideSceneUnits',
|
||||
'Execute AST',
|
||||
'persistSettings',
|
||||
],
|
||||
},
|
||||
|
||||
'set.app.theme': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: [
|
||||
'setSettingAtLevel',
|
||||
'toastSuccess',
|
||||
'setThemeClass',
|
||||
'setEngineTheme',
|
||||
'persistSettings',
|
||||
'setClientTheme',
|
||||
],
|
||||
},
|
||||
|
||||
'set.modeling.highlightEdges': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'setSettingAtLevel',
|
||||
'toastSuccess',
|
||||
'setEngineEdges',
|
||||
'persistSettings',
|
||||
],
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
|
||||
},
|
||||
|
||||
'Reset settings': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: [
|
||||
'resetSettings',
|
||||
'setThemeClass',
|
||||
'setEngineTheme',
|
||||
'setClientSideSceneUnits',
|
||||
'Execute AST',
|
||||
'persistSettings',
|
||||
'setClientTheme',
|
||||
],
|
||||
},
|
||||
|
||||
'Set all settings': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: [
|
||||
'setAllSettings',
|
||||
'setThemeClass',
|
||||
'setEngineTheme',
|
||||
'setClientSideSceneUnits',
|
||||
'Execute AST',
|
||||
'persistSettings',
|
||||
'setClientTheme',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'persisting settings': {
|
||||
invoke: {
|
||||
src: 'Persist settings',
|
||||
id: 'persistSettings',
|
||||
onDone: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
|
||||
schema: {
|
||||
|
@ -3,7 +3,7 @@ import { Outlet, useNavigate } from 'react-router-dom'
|
||||
import Introduction from './Introduction'
|
||||
import Camera from './Camera'
|
||||
import Sketching from './Sketching'
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import Streaming from './Streaming'
|
||||
@ -94,17 +94,31 @@ export function useNextClick(newStatus: string) {
|
||||
export function useDismiss() {
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const {
|
||||
settings: { send },
|
||||
settings: { state, send },
|
||||
} = useSettingsAuthContext()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return useCallback(() => {
|
||||
const settingsCallback = useCallback(() => {
|
||||
send({
|
||||
type: 'set.app.onboardingStatus',
|
||||
data: { level: 'user', value: 'dismissed' },
|
||||
})
|
||||
navigate(filePath)
|
||||
}, [send, navigate, filePath])
|
||||
}, [send])
|
||||
|
||||
/**
|
||||
* A "listener" for the XState to return to "idle" state
|
||||
* when the user dismisses the onboarding, using the callback above
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
state.context.app.onboardingStatus.user === 'dismissed' &&
|
||||
state.matches('idle')
|
||||
) {
|
||||
navigate(filePath)
|
||||
}
|
||||
}, [filePath, navigate, state])
|
||||
|
||||
return settingsCallback
|
||||
}
|
||||
|
||||
// Get the 1-indexed step number of the current onboarding step
|
||||
|
@ -1,3 +1,8 @@
|
||||
# experimental = ["setup-scripts"]
|
||||
|
||||
# [script.test-server]
|
||||
# command = "just start-test-server"
|
||||
|
||||
# Each test can have at most 4 threads, but if its name contains "serial_test_", then it
|
||||
# also requires 4 threads.
|
||||
# This means such tests run one at a time, with 4 threads.
|
||||
@ -14,12 +19,20 @@ slow-timeout = { period = "50s", terminate-after = 5 }
|
||||
[[profile.default.overrides]]
|
||||
filter = "test(serial_test_)"
|
||||
test-group = "serial-integration"
|
||||
threads-required = 2
|
||||
threads-required = 4
|
||||
|
||||
# [[profile.default.scripts]]
|
||||
# filter = 'test(serial_test_)'
|
||||
# setup = 'test-server'
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "test(serial_test_)"
|
||||
test-group = "serial-integration"
|
||||
threads-required = 2
|
||||
threads-required = 4
|
||||
|
||||
# [[profile.default.scripts]]
|
||||
# filter = 'test(serial_test_)'
|
||||
# setup = 'test-server'
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "test(parser::parser_impl::snapshot_tests)"
|
||||
|
88
src/wasm-lib/Cargo.lock
generated
@ -297,9 +297,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bson"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d43b38e074cc0de2957f10947e376a1d88b9c4dbab340b590800cc1b2e066b2"
|
||||
checksum = "d8a88e82b9106923b5c4d6edfca9e7db958d4e98a478ec115022e81b9b38e2c8"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.13.1",
|
||||
@ -406,9 +406,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.4"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
||||
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -416,9 +416,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.2"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -430,9 +430,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.4"
|
||||
version = "4.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
|
||||
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@ -670,9 +670,9 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
|
||||
[[package]]
|
||||
name = "databake"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82175d72e69414ceafbe2b49686794d3a8bed846e0d50267355f83ea8fdd953a"
|
||||
checksum = "6a04fbfbecca8f0679c8c06fef907594adcc3e2052e11163a6d30535a1a5604d"
|
||||
dependencies = [
|
||||
"databake-derive",
|
||||
"proc-macro2",
|
||||
@ -681,9 +681,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "databake-derive"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
|
||||
checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1152,9 +1152,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.28"
|
||||
version = "0.14.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
|
||||
checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@ -1369,7 +1369,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.1.58"
|
||||
version = "0.1.59"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
@ -1434,11 +1434,24 @@ dependencies = [
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper",
|
||||
"kcl-lib",
|
||||
"pico-args",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.3.3"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0cbef813153197e60c0e96f59eea0b75f8418380f414b20250ee81b60e522c3"
|
||||
checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1451,7 +1464,7 @@ dependencies = [
|
||||
"format_serde_error",
|
||||
"futures",
|
||||
"http 0.2.12",
|
||||
"itertools 0.12.1",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"parse-display",
|
||||
@ -1815,6 +1828,12 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pico-args"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.5"
|
||||
@ -2037,9 +2056,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.4"
|
||||
version = "1.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -2378,9 +2397,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.20"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0218ceea14babe24a4a5836f86ade86c1effbc198164e619194cb5069187e29"
|
||||
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
|
||||
dependencies = [
|
||||
"bigdecimal",
|
||||
"bytes",
|
||||
@ -2395,9 +2414,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.20"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed5a1ccce8ff962e31a165d41f6e2a2dd1245099dc4d594f5574a86cd90f4d3"
|
||||
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2492,9 +2511,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.116"
|
||||
version = "1.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
dependencies = [
|
||||
"indexmap 2.2.5",
|
||||
"itoa",
|
||||
@ -2945,9 +2964,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.23.0"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0"
|
||||
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
@ -2975,9 +2994,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.13"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
|
||||
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@ -2996,9 +3015,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.13"
|
||||
version = "0.22.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
|
||||
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||
dependencies = [
|
||||
"indexmap 2.2.5",
|
||||
"serde",
|
||||
@ -3158,7 +3177,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "8.1.0"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#badbac08e61e65b312880aa64e9ece2976f1bbef"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"thiserror",
|
||||
@ -3170,7 +3189,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "8.1.0"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#badbac08e61e65b312880aa64e9ece2976f1bbef"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3444,6 +3463,7 @@ dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"futures",
|
||||
"gloo-utils",
|
||||
"hyper",
|
||||
"image",
|
||||
"js-sys",
|
||||
"kcl-lib",
|
||||
|
@ -10,11 +10,11 @@ rust-version = "1.73"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
|
||||
clap = "4.5.4"
|
||||
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
|
||||
clap = "4.5.7"
|
||||
gloo-utils = "0.2.0"
|
||||
kcl-lib = { path = "kcl" }
|
||||
kittycad = { workspace = true }
|
||||
kittycad.workspace = true
|
||||
serde_json = "1.0.116"
|
||||
tokio = { version = "1.38.0", features = ["sync"] }
|
||||
toml = "0.8.13"
|
||||
@ -24,6 +24,7 @@ wasm-bindgen-futures = "0.4.42"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
hyper = { version = "0.14.29", features = ["server", "http1"] }
|
||||
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
||||
kittycad = { workspace = true, default-features = true }
|
||||
pretty_assertions = "1.4.0"
|
||||
@ -64,10 +65,11 @@ members = [
|
||||
"derive-docs",
|
||||
"kcl",
|
||||
"kcl-macros",
|
||||
"kcl-test-server",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
kittycad = { version = "0.3.3", default-features = false, features = ["js", "requests"] }
|
||||
kittycad = { version = "0.3.5", default-features = false, features = ["js", "requests"] }
|
||||
kittycad-modeling-session = "0.1.4"
|
||||
|
||||
[[test]]
|
||||
|
@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "grackle"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "A new executor for KCL which compiles to Execution Plans"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
||||
kcl-lib = { path = "../kcl" }
|
||||
kittycad = { workspace = true }
|
||||
kittycad-execution-plan = { workspace = true }
|
||||
kittycad-execution-plan-traits = { workspace = true }
|
||||
kittycad-execution-plan-macros = { workspace = true }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
kittycad-modeling-session = { workspace = true }
|
||||
thiserror = "1.0.61"
|
||||
tokio = { version = "1.37.0", features = ["macros", "rt"] }
|
||||
twenty-twenty = "0.8.0"
|
||||
uuid = "1.8"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
serde_json = "1.0.116"
|
2
src/wasm-lib/justfile
Normal file
@ -0,0 +1,2 @@
|
||||
start-test-server:
|
||||
cargo build --quiet --bin kcl-test-server --workspace && ./target/debug/kcl-test-server
|
@ -11,7 +11,7 @@ repository = "https://github.com/KittyCAD/modeling-app"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
databake = "0.1.7"
|
||||
databake = "0.1.8"
|
||||
kcl-lib = { path = "../kcl" }
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
|
13
src/wasm-lib/kcl-test-server/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
hyper = { version = "0.14.29", features = ["server"] }
|
||||
kcl-lib = { path = "../kcl" }
|
||||
pico-args = "0.5.0"
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
210
src/wasm-lib/kcl-test-server/src/main.rs
Normal file
@ -0,0 +1,210 @@
|
||||
//! Executes KCL programs.
|
||||
//! The server reuses the same engine session for each KCL program it receives.
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use hyper::body::Bytes;
|
||||
use hyper::header::CONTENT_TYPE;
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use hyper::{Body, Error, Response, Server};
|
||||
use kcl_lib::executor::ExecutorContext;
|
||||
use kcl_lib::settings::types::UnitLength;
|
||||
use kcl_lib::test_server::RequestBody;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Parse the CLI arguments.
|
||||
let pargs = pico_args::Arguments::from_env();
|
||||
let args = ServerArgs::parse(pargs)?;
|
||||
// Run the actual server.
|
||||
start_server(args).await
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ServerArgs {
|
||||
/// What port this server should listen on.
|
||||
listen_on: SocketAddr,
|
||||
/// How many connections to establish with the engine.
|
||||
num_engine_conns: u8,
|
||||
}
|
||||
|
||||
impl ServerArgs {
|
||||
fn parse(mut pargs: pico_args::Arguments) -> Result<Self, pico_args::Error> {
|
||||
let args = ServerArgs {
|
||||
listen_on: pargs
|
||||
.opt_value_from_str("--listen-on")?
|
||||
.unwrap_or("0.0.0.0:3333".parse().unwrap()),
|
||||
num_engine_conns: pargs.opt_value_from_str("--num-engine-conns")?.unwrap_or(1),
|
||||
};
|
||||
println!("Config is {args:?}");
|
||||
Ok(args)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent from the server to each worker.
|
||||
struct WorkerReq {
|
||||
/// A KCL program, in UTF-8.
|
||||
body: Bytes,
|
||||
/// A channel to send the HTTP response back.
|
||||
resp: oneshot::Sender<Response<Body>>,
|
||||
}
|
||||
|
||||
/// Each worker has a connection to the engine, and accepts
|
||||
/// KCL programs. When it receives one (over the mpsc channel)
|
||||
/// it executes it and returns the result via a oneshot channel.
|
||||
fn start_worker(i: u8) -> mpsc::Sender<WorkerReq> {
|
||||
println!("Starting worker {i}");
|
||||
// Make a work queue for this worker.
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
tokio::task::spawn(async move {
|
||||
let state = ExecutorContext::new_for_unit_test(UnitLength::Mm).await.unwrap();
|
||||
println!("Worker {i} ready");
|
||||
while let Some(req) = rx.recv().await {
|
||||
let req: WorkerReq = req;
|
||||
let resp = snapshot_endpoint(req.body, state.clone()).await;
|
||||
if req.resp.send(resp).is_err() {
|
||||
println!("\tWorker {i} exiting");
|
||||
}
|
||||
}
|
||||
println!("\tWorker {i} exiting");
|
||||
});
|
||||
tx
|
||||
}
|
||||
|
||||
struct ServerState {
|
||||
workers: Vec<mpsc::Sender<WorkerReq>>,
|
||||
req_num: AtomicUsize,
|
||||
}
|
||||
|
||||
async fn start_server(args: ServerArgs) -> anyhow::Result<()> {
|
||||
let ServerArgs {
|
||||
listen_on,
|
||||
num_engine_conns,
|
||||
} = args;
|
||||
let workers: Vec<_> = (0..num_engine_conns).map(start_worker).collect();
|
||||
let state = Arc::new(ServerState {
|
||||
workers,
|
||||
req_num: 0.into(),
|
||||
});
|
||||
// In hyper, a `MakeService` is basically your server.
|
||||
// It makes a `Service` for each connection, which manages the connection.
|
||||
let make_service = make_service_fn(
|
||||
// This closure is run for each connection.
|
||||
move |_conn_info| {
|
||||
// eprintln!("Connected to a client");
|
||||
let state = state.clone();
|
||||
async move {
|
||||
// This is the `Service` which handles the connection.
|
||||
// `service_fn` converts a function which returns a Response
|
||||
// into a `Service`.
|
||||
Ok::<_, Error>(service_fn(move |req| {
|
||||
// eprintln!("Received a request");
|
||||
let state = state.clone();
|
||||
async move { handle_request(req, state).await }
|
||||
}))
|
||||
}
|
||||
},
|
||||
);
|
||||
let server = Server::bind(&listen_on).serve(make_service);
|
||||
println!("Listening on {listen_on}");
|
||||
println!("PID is {}", std::process::id());
|
||||
if let Err(e) = server.await {
|
||||
eprintln!("Server error: {e}");
|
||||
return Err(e.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_request(req: hyper::Request<Body>, state3: Arc<ServerState>) -> Result<Response<Body>, Error> {
|
||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||
|
||||
// Round robin requests between each available worker.
|
||||
let req_num = state3.req_num.fetch_add(1, Ordering::Relaxed);
|
||||
let worker_id = req_num % state3.workers.len();
|
||||
// println!("Sending request {req_num} to worker {worker_id}");
|
||||
let worker = state3.workers[worker_id].clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let req_sent = worker.send(WorkerReq { body, resp: tx }).await;
|
||||
req_sent.unwrap();
|
||||
let resp = rx.await.unwrap();
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// Execute a KCL program, then respond with a PNG snapshot.
|
||||
/// KCL errors (from engine or the executor) respond with HTTP Bad Gateway.
|
||||
/// Malformed requests are HTTP Bad Request.
|
||||
/// Successful requests contain a PNG as the body.
|
||||
async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body> {
|
||||
let body = match serde_json::from_slice::<RequestBody>(body.as_ref()) {
|
||||
Ok(bd) => bd,
|
||||
Err(e) => return bad_request(format!("Invalid request JSON: {e}")),
|
||||
};
|
||||
let RequestBody { kcl_program, test_name } = body;
|
||||
let parser = match kcl_lib::token::lexer(&kcl_program) {
|
||||
Ok(ts) => kcl_lib::parser::Parser::new(ts),
|
||||
Err(e) => return bad_request(format!("tokenization error: {e}")),
|
||||
};
|
||||
let program = match parser.ast() {
|
||||
Ok(pr) => pr,
|
||||
Err(e) => return bad_request(format!("Parse error: {e}")),
|
||||
};
|
||||
eprintln!("Executing {test_name}");
|
||||
if let Err(e) = state.reset_scene().await {
|
||||
return kcl_err(e);
|
||||
}
|
||||
// Let users know if the test is taking a long time.
|
||||
let (done_tx, done_rx) = oneshot::channel::<()>();
|
||||
let timer = time_until(done_rx);
|
||||
let snapshot = match state.execute_and_prepare_snapshot(program).await {
|
||||
Ok(sn) => sn,
|
||||
Err(e) => return kcl_err(e),
|
||||
};
|
||||
let _ = done_tx.send(());
|
||||
timer.abort();
|
||||
eprintln!("\tServing response");
|
||||
let png_bytes = snapshot.contents.0;
|
||||
let mut resp = Response::new(Body::from(png_bytes));
|
||||
resp.headers_mut().insert(CONTENT_TYPE, "image/png".parse().unwrap());
|
||||
resp
|
||||
}
|
||||
|
||||
fn bad_request(msg: String) -> Response<Body> {
|
||||
eprintln!("\tBad request");
|
||||
let mut resp = Response::new(Body::from(msg));
|
||||
*resp.status_mut() = hyper::StatusCode::BAD_REQUEST;
|
||||
resp
|
||||
}
|
||||
|
||||
fn bad_gateway(msg: String) -> Response<Body> {
|
||||
eprintln!("\tBad gateway");
|
||||
let mut resp = Response::new(Body::from(msg));
|
||||
*resp.status_mut() = hyper::StatusCode::BAD_GATEWAY;
|
||||
resp
|
||||
}
|
||||
|
||||
fn kcl_err(err: anyhow::Error) -> Response<Body> {
|
||||
eprintln!("\tBad KCL");
|
||||
bad_gateway(format!("{err}"))
|
||||
}
|
||||
|
||||
fn time_until(done: oneshot::Receiver<()>) -> JoinHandle<()> {
|
||||
tokio::task::spawn(async move {
|
||||
let period = 10;
|
||||
tokio::pin!(done);
|
||||
for i in 1..=3 {
|
||||
tokio::select! {
|
||||
biased;
|
||||
// If the test is done, no need for this timer anymore.
|
||||
_ = &mut done => return,
|
||||
_ = sleep(Duration::from_secs(period)) => {
|
||||
eprintln!("\tTest has taken {}s", period * i);
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.1.58"
|
||||
version = "0.1.59"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -16,9 +16,9 @@ async-recursion = "1.1.1"
|
||||
async-trait = "0.1.80"
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.38"
|
||||
clap = { version = "4.5.4", default-features = false, optional = true }
|
||||
clap = { version = "4.5.7", default-features = false, optional = true }
|
||||
dashmap = "5.5.3"
|
||||
databake = { version = "0.1.7", features = ["derive"] }
|
||||
databake = { version = "0.1.8", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.18", path = "../derive-docs" }
|
||||
form_urlencoded = "1.2.1"
|
||||
futures = { version = "0.3.30" }
|
||||
@ -35,7 +35,7 @@ serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
sha2 = "0.10.8"
|
||||
thiserror = "1.0.61"
|
||||
toml = "0.8.13"
|
||||
toml = "0.8.14"
|
||||
# TODO: change this to a cargo release once 8.1.1 comes out
|
||||
ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] }
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
@ -54,9 +54,9 @@ web-sys = { version = "0.3.69", features = ["console"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
approx = "0.5"
|
||||
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
|
||||
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
|
||||
tokio = { version = "1.38.0", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.23.0", features = ["rustls-tls-native-roots"] }
|
||||
tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"] }
|
||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||
|
||||
[features]
|
||||
|
@ -40,23 +40,54 @@ pub struct TcpRead {
|
||||
stream: futures::stream::SplitStream<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>>,
|
||||
}
|
||||
|
||||
/// Occurs when client couldn't read from the WebSocket to the engine.
|
||||
// #[derive(Debug)]
|
||||
pub enum WebSocketReadError {
|
||||
/// Could not read a message due to WebSocket errors.
|
||||
Read(tokio_tungstenite::tungstenite::Error),
|
||||
/// WebSocket message didn't contain a valid message that the KCL Executor could parse.
|
||||
Deser(anyhow::Error),
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for WebSocketReadError {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
Self::Deser(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl TcpRead {
|
||||
pub async fn read(&mut self) -> Result<WebSocketResponse> {
|
||||
pub async fn read(&mut self) -> std::result::Result<WebSocketResponse, WebSocketReadError> {
|
||||
let Some(msg) = self.stream.next().await else {
|
||||
anyhow::bail!("Failed to read from websocket");
|
||||
return Err(anyhow::anyhow!("Failed to read from WebSocket").into());
|
||||
};
|
||||
let msg: WebSocketResponse = match msg? {
|
||||
WsMsg::Text(text) => serde_json::from_str(&text)?,
|
||||
WsMsg::Binary(bin) => bson::from_slice(&bin)?,
|
||||
other => anyhow::bail!("Unexpected websocket message from server: {}", other),
|
||||
let msg = match msg {
|
||||
Ok(msg) => msg,
|
||||
Err(e) if matches!(e, tokio_tungstenite::tungstenite::Error::Protocol(_)) => {
|
||||
return Err(WebSocketReadError::Read(e))
|
||||
}
|
||||
Err(e) => return Err(anyhow::anyhow!("Error reading from engine's WebSocket: {e}").into()),
|
||||
};
|
||||
let msg: WebSocketResponse = match msg {
|
||||
WsMsg::Text(text) => serde_json::from_str(&text)
|
||||
.map_err(anyhow::Error::from)
|
||||
.map_err(WebSocketReadError::from)?,
|
||||
WsMsg::Binary(bin) => bson::from_slice(&bin)
|
||||
.map_err(anyhow::Error::from)
|
||||
.map_err(WebSocketReadError::from)?,
|
||||
other => return Err(anyhow::anyhow!("Unexpected WebSocket message from engine API: {other}").into()),
|
||||
};
|
||||
Ok(msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TcpReadHandle {
|
||||
handle: Arc<tokio::task::JoinHandle<Result<()>>>,
|
||||
handle: Arc<tokio::task::JoinHandle<Result<(), WebSocketReadError>>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TcpReadHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "TcpReadHandle")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TcpReadHandle {
|
||||
@ -150,14 +181,17 @@ impl EngineConnection {
|
||||
match tcp_read.read().await {
|
||||
Ok(ws_resp) => {
|
||||
for e in ws_resp.errors.iter().flatten() {
|
||||
println!("got error message: {e}");
|
||||
println!("got error message: {} {}", e.error_code, e.message);
|
||||
}
|
||||
if let Some(id) = ws_resp.request_id {
|
||||
responses_clone.insert(id, ws_resp.clone());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("got ws error: {:?}", e);
|
||||
match &e {
|
||||
WebSocketReadError::Read(e) => eprintln!("could not read from WS: {:?}", e),
|
||||
WebSocketReadError::Deser(e) => eprintln!("could not deserialize msg from WS: {:?}", e),
|
||||
}
|
||||
*socket_health_tcp_read.lock().unwrap() = SocketHealth::Inactive;
|
||||
return Err(e);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
use crate::executor::SourceRange;
|
||||
use crate::{executor::SourceRange, lsp::IntoDiagnostic};
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
|
||||
#[ts(export)]
|
||||
@ -42,19 +42,9 @@ pub struct KclErrorDetails {
|
||||
}
|
||||
|
||||
impl KclError {
|
||||
/// Get the error message, line and column from the error and input code.
|
||||
pub fn get_message_line_column(&self, input: &str) -> (String, Option<usize>, Option<usize>) {
|
||||
// Calculate the line and column of the error from the source range.
|
||||
let (line, column) = if let Some(range) = self.source_ranges().first() {
|
||||
let line = input[..range.0[0]].lines().count();
|
||||
let column = input[..range.0[0]].lines().last().map(|l| l.len()).unwrap_or_default();
|
||||
|
||||
(Some(line), Some(column))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
(format!("{}: {}", self.error_type(), self.message()), line, column)
|
||||
/// Get the error message.
|
||||
pub fn get_message(&self) -> String {
|
||||
format!("{}: {}", self.error_type(), self.message())
|
||||
}
|
||||
|
||||
pub fn error_type(&self) -> &'static str {
|
||||
@ -106,24 +96,6 @@ impl KclError {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let (message, _, _) = self.get_message_line_column(code);
|
||||
let source_ranges = self.source_ranges();
|
||||
|
||||
Diagnostic {
|
||||
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
source: Some("kcl".to_string()),
|
||||
message,
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
|
||||
let mut new = self.clone();
|
||||
match &mut new {
|
||||
@ -163,6 +135,26 @@ impl KclError {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for KclError {
|
||||
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let message = self.get_message();
|
||||
let source_ranges = self.source_ranges();
|
||||
|
||||
Diagnostic {
|
||||
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
source: Some("kcl".to_string()),
|
||||
message,
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is different than to_string() in that it will serialize the Error
|
||||
/// the struct as JSON so we can deserialize it on the js side.
|
||||
impl From<KclError> for String {
|
||||
|
@ -11,10 +11,11 @@ use serde_json::Value as JValue;
|
||||
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
|
||||
|
||||
use crate::{
|
||||
ast::types::{BodyItem, FunctionExpression, KclNone, Value},
|
||||
ast::types::{BodyItem, FunctionExpression, KclNone, Program, Value},
|
||||
engine::EngineManager,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
fs::FileManager,
|
||||
settings::types::UnitLength,
|
||||
std::{FunctionKind, StdLib},
|
||||
};
|
||||
|
||||
@ -975,6 +976,8 @@ impl Default for PipeInfo {
|
||||
}
|
||||
|
||||
/// The executor context.
|
||||
/// Cloning will return another handle to the same engine connection/session,
|
||||
/// as this uses `Arc` under the hood.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutorContext {
|
||||
pub engine: Arc<Box<dyn EngineManager>>,
|
||||
@ -990,7 +993,7 @@ pub struct ExecutorContext {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutorSettings {
|
||||
/// The unit to use in modeling dimensions.
|
||||
pub units: crate::settings::types::UnitLength,
|
||||
pub units: UnitLength,
|
||||
/// Highlight edges of 3D objects?
|
||||
pub highlight_edges: bool,
|
||||
/// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
|
||||
@ -1081,6 +1084,57 @@ impl ExecutorContext {
|
||||
})
|
||||
}
|
||||
|
||||
/// For executing unit tests.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn new_for_unit_test(units: UnitLength) -> Result<Self> {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||
|
||||
// Create the client.
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
// Set a local engine address if it's set.
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
let ctx = ExecutorContext::new(
|
||||
&client,
|
||||
ExecutorSettings {
|
||||
units,
|
||||
highlight_edges: true,
|
||||
enable_ssao: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
/// Clear everything in the scene.
|
||||
pub async fn reset_scene(&self) -> Result<()> {
|
||||
self.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::SceneClearAll {},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform the execution of a program.
|
||||
/// You can optionally pass in some initialization memory.
|
||||
/// Kurt uses this for partial execution.
|
||||
@ -1307,9 +1361,46 @@ impl ExecutorContext {
|
||||
}
|
||||
|
||||
/// Update the units for the executor.
|
||||
pub fn update_units(&mut self, units: crate::settings::types::UnitLength) {
|
||||
pub fn update_units(&mut self, units: UnitLength) {
|
||||
self.settings.units = units;
|
||||
}
|
||||
|
||||
/// Execute the program, then get a PNG screenshot.
|
||||
pub async fn execute_and_prepare_snapshot(&self, program: Program) -> Result<kittycad::types::TakeSnapshot> {
|
||||
let _ = self.run(program, None).await?;
|
||||
|
||||
// Zoom to fit.
|
||||
self.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
crate::executor::SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::ZoomToFit {
|
||||
object_ids: Default::default(),
|
||||
padding: 0.1,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Send a snapshot request to the engine.
|
||||
let resp = self
|
||||
.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
crate::executor::SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::TakeSnapshot {
|
||||
format: kittycad::types::ImageFormat::Png,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let kittycad::types::OkWebSocketResponseData::Modeling {
|
||||
modeling_response: kittycad::types::OkModelingCmdResponse::TakeSnapshot { data },
|
||||
} = resp
|
||||
else {
|
||||
anyhow::bail!("Unexpected response from engine: {:?}", resp);
|
||||
};
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// For each argument given,
|
||||
|
@ -11,10 +11,12 @@ pub mod engine;
|
||||
pub mod errors;
|
||||
pub mod executor;
|
||||
pub mod fs;
|
||||
pub mod lint;
|
||||
pub mod lsp;
|
||||
pub mod parser;
|
||||
pub mod settings;
|
||||
pub mod std;
|
||||
pub mod test_server;
|
||||
pub mod thread;
|
||||
pub mod token;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
64
src/wasm-lib/kcl/src/lint/ast_node.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use crate::ast::types;
|
||||
|
||||
/// The "Node" type wraps all the AST elements we're able to find in a KCL
|
||||
/// file. Tokens we walk through will be one of these.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Node<'a> {
|
||||
Program(&'a types::Program),
|
||||
|
||||
ExpressionStatement(&'a types::ExpressionStatement),
|
||||
VariableDeclaration(&'a types::VariableDeclaration),
|
||||
ReturnStatement(&'a types::ReturnStatement),
|
||||
|
||||
VariableDeclarator(&'a types::VariableDeclarator),
|
||||
|
||||
Literal(&'a types::Literal),
|
||||
Identifier(&'a types::Identifier),
|
||||
BinaryExpression(&'a types::BinaryExpression),
|
||||
FunctionExpression(&'a types::FunctionExpression),
|
||||
CallExpression(&'a types::CallExpression),
|
||||
PipeExpression(&'a types::PipeExpression),
|
||||
PipeSubstitution(&'a types::PipeSubstitution),
|
||||
ArrayExpression(&'a types::ArrayExpression),
|
||||
ObjectExpression(&'a types::ObjectExpression),
|
||||
MemberExpression(&'a types::MemberExpression),
|
||||
UnaryExpression(&'a types::UnaryExpression),
|
||||
|
||||
Parameter(&'a types::Parameter),
|
||||
|
||||
ObjectProperty(&'a types::ObjectProperty),
|
||||
|
||||
MemberObject(&'a types::MemberObject),
|
||||
LiteralIdentifier(&'a types::LiteralIdentifier),
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($node:ident, $t: ident) => {
|
||||
impl<'a> From<&'a types::$t> for Node<'a> {
|
||||
fn from(v: &'a types::$t) -> Self {
|
||||
Node::$t(v)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from!(Node, Program);
|
||||
impl_from!(Node, ExpressionStatement);
|
||||
impl_from!(Node, VariableDeclaration);
|
||||
impl_from!(Node, ReturnStatement);
|
||||
impl_from!(Node, VariableDeclarator);
|
||||
impl_from!(Node, Literal);
|
||||
impl_from!(Node, Identifier);
|
||||
impl_from!(Node, BinaryExpression);
|
||||
impl_from!(Node, FunctionExpression);
|
||||
impl_from!(Node, CallExpression);
|
||||
impl_from!(Node, PipeExpression);
|
||||
impl_from!(Node, PipeSubstitution);
|
||||
impl_from!(Node, ArrayExpression);
|
||||
impl_from!(Node, ObjectExpression);
|
||||
impl_from!(Node, MemberExpression);
|
||||
impl_from!(Node, UnaryExpression);
|
||||
impl_from!(Node, Parameter);
|
||||
impl_from!(Node, ObjectProperty);
|
||||
impl_from!(Node, MemberObject);
|
||||
impl_from!(Node, LiteralIdentifier);
|
233
src/wasm-lib/kcl/src/lint/ast_walk.rs
Normal file
@ -0,0 +1,233 @@
|
||||
use super::Node;
|
||||
use crate::ast::types::{
|
||||
BinaryPart, BodyItem, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, ObjectProperty,
|
||||
Parameter, Program, UnaryExpression, Value, VariableDeclarator,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
/// Walker is implemented by things that are able to walk an AST tree to
|
||||
/// produce lints. This trait is implemented automatically for a few of the
|
||||
/// common types, but can be manually implemented too.
|
||||
pub trait Walker<'a> {
|
||||
/// Walk will visit every element of the AST.
|
||||
fn walk(&self, n: Node<'a>) -> Result<bool>;
|
||||
}
|
||||
|
||||
impl<'a, FnT> Walker<'a> for FnT
|
||||
where
|
||||
FnT: Fn(Node<'a>) -> Result<bool>,
|
||||
{
|
||||
fn walk(&self, n: Node<'a>) -> Result<bool> {
|
||||
self(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the Walker against all [Node]s in a [Program].
|
||||
pub fn walk<'a, WalkT>(prog: &'a Program, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(prog.into())?;
|
||||
|
||||
for bi in &prog.body {
|
||||
walk_body_item(bi, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_variable_declarator<'a, WalkT>(node: &'a VariableDeclarator, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
f.walk((&node.id).into())?;
|
||||
walk_value(&node.init, f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_parameter<'a, WalkT>(node: &'a Parameter, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
f.walk((&node.identifier).into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_member_object<'a, WalkT>(node: &'a MemberObject, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_literal_identifier<'a, WalkT>(node: &'a LiteralIdentifier, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_member_expression<'a, WalkT>(node: &'a MemberExpression, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
|
||||
walk_member_object(&node.object, f)?;
|
||||
walk_literal_identifier(&node.property, f)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_binary_part<'a, WalkT>(node: &'a BinaryPart, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
match node {
|
||||
BinaryPart::Literal(lit) => f.walk(lit.as_ref().into())?,
|
||||
BinaryPart::Identifier(id) => f.walk(id.as_ref().into())?,
|
||||
BinaryPart::BinaryExpression(be) => f.walk(be.as_ref().into())?,
|
||||
BinaryPart::CallExpression(ce) => f.walk(ce.as_ref().into())?,
|
||||
BinaryPart::UnaryExpression(ue) => {
|
||||
walk_unary_expression(ue, f)?;
|
||||
true
|
||||
}
|
||||
BinaryPart::MemberExpression(me) => {
|
||||
walk_member_expression(me, f)?;
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_value<'a, WalkT>(node: &'a Value, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
match node {
|
||||
Value::Literal(lit) => {
|
||||
f.walk(lit.as_ref().into())?;
|
||||
}
|
||||
|
||||
Value::Identifier(id) => {
|
||||
// sometimes there's a bare Identifier without a Value::Identifier.
|
||||
f.walk(id.as_ref().into())?;
|
||||
}
|
||||
|
||||
Value::BinaryExpression(be) => {
|
||||
f.walk(be.as_ref().into())?;
|
||||
|
||||
walk_binary_part(&be.left, f)?;
|
||||
walk_binary_part(&be.right, f)?;
|
||||
}
|
||||
Value::FunctionExpression(fe) => {
|
||||
f.walk(fe.as_ref().into())?;
|
||||
|
||||
for arg in &fe.params {
|
||||
walk_parameter(arg, f)?;
|
||||
}
|
||||
walk(&fe.body, f)?;
|
||||
}
|
||||
Value::CallExpression(ce) => {
|
||||
f.walk(ce.as_ref().into())?;
|
||||
f.walk((&ce.callee).into())?;
|
||||
for e in &ce.arguments {
|
||||
walk_value::<WalkT>(e, f)?;
|
||||
}
|
||||
}
|
||||
Value::PipeExpression(pe) => {
|
||||
f.walk(pe.as_ref().into())?;
|
||||
|
||||
for e in &pe.body {
|
||||
walk_value::<WalkT>(e, f)?;
|
||||
}
|
||||
}
|
||||
Value::PipeSubstitution(ps) => {
|
||||
f.walk(ps.as_ref().into())?;
|
||||
}
|
||||
Value::ArrayExpression(ae) => {
|
||||
f.walk(ae.as_ref().into())?;
|
||||
for e in &ae.elements {
|
||||
walk_value::<WalkT>(e, f)?;
|
||||
}
|
||||
}
|
||||
Value::ObjectExpression(oe) => {
|
||||
walk_object_expression(oe, f)?;
|
||||
}
|
||||
Value::MemberExpression(me) => {
|
||||
walk_member_expression(me, f)?;
|
||||
}
|
||||
Value::UnaryExpression(ue) => {
|
||||
walk_unary_expression(ue, f)?;
|
||||
}
|
||||
_ => {
|
||||
println!("{:?}", node);
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walk through an [ObjectProperty].
|
||||
fn walk_object_property<'a, WalkT>(node: &'a ObjectProperty, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
walk_value(&node.value, f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walk through an [ObjectExpression].
|
||||
fn walk_object_expression<'a, WalkT>(node: &'a ObjectExpression, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
for prop in &node.properties {
|
||||
walk_object_property(prop, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// walk through an [UnaryExpression].
|
||||
fn walk_unary_expression<'a, WalkT>(node: &'a UnaryExpression, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
walk_binary_part(&node.argument, f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// walk through a [BodyItem].
|
||||
fn walk_body_item<'a, WalkT>(node: &'a BodyItem, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
// We don't walk a BodyItem since it's an enum itself.
|
||||
|
||||
match node {
|
||||
BodyItem::ExpressionStatement(xs) => {
|
||||
f.walk(xs.into())?;
|
||||
walk_value(&xs.expression, f)?;
|
||||
}
|
||||
BodyItem::VariableDeclaration(vd) => {
|
||||
f.walk(vd.into())?;
|
||||
for dec in &vd.declarations {
|
||||
walk_variable_declarator(dec, f)?;
|
||||
}
|
||||
}
|
||||
BodyItem::ReturnStatement(rs) => {
|
||||
f.walk(rs.into())?;
|
||||
walk_value(&rs.argument, f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
131
src/wasm-lib/kcl/src/lint/checks/camel_case.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use crate::{
|
||||
ast::types::VariableDeclarator,
|
||||
executor::SourceRange,
|
||||
lint::{
|
||||
rule::{def_finding, Discovered, Finding},
|
||||
Node,
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
def_finding!(
|
||||
Z0001,
|
||||
"Identifiers must be lowerCamelCase",
|
||||
"\
|
||||
By convention, variable names are lowerCamelCase, not snake_case, kebab-case,
|
||||
nor CammelCase. 🐪
|
||||
|
||||
For instance, a good identifier for the variable representing 'box height'
|
||||
would be 'boxHeight', not 'BOX_HEIGHT', 'box_height' nor 'BoxHeight'. For
|
||||
more information there's a pretty good Wikipedia page at
|
||||
|
||||
https://en.wikipedia.org/wiki/Camel_case
|
||||
"
|
||||
);
|
||||
|
||||
fn lint_lower_camel_case(decl: &VariableDeclarator) -> Result<Vec<Discovered>> {
|
||||
let mut findings = vec![];
|
||||
let ident = &decl.id;
|
||||
let name = &ident.name;
|
||||
|
||||
if !name.chars().next().unwrap().is_lowercase() {
|
||||
findings.push(Z0001.at(format!("found '{}'", name), SourceRange::new(ident.start, ident.end)));
|
||||
return Ok(findings);
|
||||
}
|
||||
|
||||
if name.contains('-') || name.contains('_') {
|
||||
findings.push(Z0001.at(format!("found '{}'", name), SourceRange::new(ident.start, ident.end)));
|
||||
return Ok(findings);
|
||||
}
|
||||
|
||||
Ok(findings)
|
||||
}
|
||||
|
||||
pub fn lint_variables(decl: Node) -> Result<Vec<Discovered>> {
|
||||
let Node::VariableDeclaration(decl) = decl else {
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
Ok(decl
|
||||
.declarations
|
||||
.iter()
|
||||
.flat_map(|v| lint_lower_camel_case(v).unwrap_or_default())
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{lint_variables, Z0001};
|
||||
use crate::lint::rule::{assert_finding, test_finding, test_no_finding};
|
||||
|
||||
#[test]
|
||||
fn z0001_const() {
|
||||
assert_finding!(lint_variables, Z0001, "const Thickness = 0.5");
|
||||
assert_finding!(lint_variables, Z0001, "const THICKNESS = 0.5");
|
||||
assert_finding!(lint_variables, Z0001, "const THICC_NES = 0.5");
|
||||
assert_finding!(lint_variables, Z0001, "const thicc_nes = 0.5");
|
||||
}
|
||||
|
||||
test_finding!(z0001_full_bad, lint_variables, Z0001, "\
|
||||
// Define constants
|
||||
const pipeLength = 40
|
||||
const pipeSmallDia = 10
|
||||
const pipeLargeDia = 20
|
||||
const thickness = 0.5
|
||||
|
||||
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
|
||||
const Part001 = startSketchOn('XY')
|
||||
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|
||||
|> line([thickness, 0], %)
|
||||
|> line([0, -1], %)
|
||||
|> angledLineToX({
|
||||
angle: 60,
|
||||
to: pipeSmallDia + thickness
|
||||
}, %)
|
||||
|> line([0, -pipeLength], %)
|
||||
|> angledLineToX({
|
||||
angle: -60,
|
||||
to: pipeLargeDia + thickness
|
||||
}, %)
|
||||
|> line([0, -1], %)
|
||||
|> line([-thickness, 0], %)
|
||||
|> line([0, 1], %)
|
||||
|> angledLineToX({ angle: 120, to: pipeSmallDia }, %)
|
||||
|> line([0, pipeLength], %)
|
||||
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis: 'y' }, %)
|
||||
");
|
||||
|
||||
test_no_finding!(z0001_full_good, lint_variables, Z0001, "\
|
||||
// Define constants
|
||||
const pipeLength = 40
|
||||
const pipeSmallDia = 10
|
||||
const pipeLargeDia = 20
|
||||
const thickness = 0.5
|
||||
|
||||
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
|
||||
const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|
||||
|> line([thickness, 0], %)
|
||||
|> line([0, -1], %)
|
||||
|> angledLineToX({
|
||||
angle: 60,
|
||||
to: pipeSmallDia + thickness
|
||||
}, %)
|
||||
|> line([0, -pipeLength], %)
|
||||
|> angledLineToX({
|
||||
angle: -60,
|
||||
to: pipeLargeDia + thickness
|
||||
}, %)
|
||||
|> line([0, -1], %)
|
||||
|> line([-thickness, 0], %)
|
||||
|> line([0, 1], %)
|
||||
|> angledLineToX({ angle: 120, to: pipeSmallDia }, %)
|
||||
|> line([0, pipeLength], %)
|
||||
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis: 'y' }, %)
|
||||
");
|
||||
}
|
4
src/wasm-lib/kcl/src/lint/checks/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod camel_case;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use camel_case::{lint_variables, Z0001};
|
9
src/wasm-lib/kcl/src/lint/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
mod ast_node;
|
||||
mod ast_walk;
|
||||
pub mod checks;
|
||||
mod rule;
|
||||
|
||||
pub use ast_node::Node;
|
||||
pub use ast_walk::walk;
|
||||
// pub(crate) use rule::{def_finding, finding};
|
||||
pub use rule::{lint, Discovered, Finding};
|
180
src/wasm-lib/kcl/src/lint/rule.rs
Normal file
@ -0,0 +1,180 @@
|
||||
use super::{walk, Node};
|
||||
use crate::{ast::types::Program, executor::SourceRange, lsp::IntoDiagnostic};
|
||||
use anyhow::Result;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
/// Check the provided AST for any found rule violations.
|
||||
///
|
||||
/// The Rule trait is automatically implemented for a few other types,
|
||||
/// but it can also be manually implemented as required.
|
||||
pub trait Rule<'a> {
|
||||
/// Check the AST at this specific node for any Finding(s).
|
||||
fn check(&self, node: Node<'a>) -> Result<Vec<Discovered>>;
|
||||
}
|
||||
|
||||
impl<'a, FnT> Rule<'a> for FnT
|
||||
where
|
||||
FnT: Fn(Node<'a>) -> Result<Vec<Discovered>>,
|
||||
{
|
||||
fn check(&self, n: Node<'a>) -> Result<Vec<Discovered>> {
|
||||
self(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Specific discovered lint rule Violation of a particular Finding.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Discovered {
|
||||
/// Zoo Lint Finding information.
|
||||
pub finding: Finding,
|
||||
|
||||
/// Further information about the specific finding.
|
||||
pub description: String,
|
||||
|
||||
/// Source code location.
|
||||
pub pos: SourceRange,
|
||||
|
||||
/// Is this discovered issue overridden by the programmer?
|
||||
pub overridden: bool,
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for Discovered {
|
||||
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let message = self.finding.title.to_owned();
|
||||
let source_range = self.pos;
|
||||
|
||||
Diagnostic {
|
||||
range: source_range.to_lsp_range(code),
|
||||
severity: Some(DiagnosticSeverity::INFORMATION),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
source: Some("lint".to_string()),
|
||||
message,
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract lint problem type.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Finding {
|
||||
/// Unique identifier for this particular issue.
|
||||
pub code: &'static str,
|
||||
|
||||
/// Short one-line description of this issue.
|
||||
pub title: &'static str,
|
||||
|
||||
/// Long human-readable description of this issue.
|
||||
pub description: &'static str,
|
||||
|
||||
/// Is this discovered issue experimental?
|
||||
pub experimental: bool,
|
||||
}
|
||||
|
||||
impl Finding {
|
||||
/// Create a new Discovered finding at the specific Position.
|
||||
pub fn at(&self, description: String, pos: SourceRange) -> Discovered {
|
||||
Discovered {
|
||||
description,
|
||||
finding: self.clone(),
|
||||
pos,
|
||||
overridden: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! def_finding {
|
||||
( $code:ident, $title:expr, $description:expr ) => {
|
||||
/// Generated Finding
|
||||
pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
|
||||
};
|
||||
}
|
||||
pub(crate) use def_finding;
|
||||
|
||||
macro_rules! finding {
|
||||
( $code:ident, $title:expr, $description:expr ) => {
|
||||
$crate::lint::rule::Finding {
|
||||
code: stringify!($code),
|
||||
title: $title,
|
||||
description: $description,
|
||||
experimental: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use finding;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
|
||||
|
||||
/// Check the provided Program for any Findings.
|
||||
pub fn lint<'a, RuleT>(prog: &'a Program, rule: RuleT) -> Result<Vec<Discovered>>
|
||||
where
|
||||
RuleT: Rule<'a>,
|
||||
{
|
||||
let v = Arc::new(Mutex::new(vec![]));
|
||||
walk(prog, &|node: Node<'a>| {
|
||||
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
||||
findings.append(&mut rule.check(node)?);
|
||||
Ok(true)
|
||||
})?;
|
||||
let x = v.lock().unwrap();
|
||||
Ok(x.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
macro_rules! assert_no_finding {
|
||||
( $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
let tokens = $crate::token::lexer($kcl).unwrap();
|
||||
let parser = $crate::parser::Parser::new(tokens);
|
||||
let prog = parser.ast().unwrap();
|
||||
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
|
||||
if discovered_finding.finding == $finding {
|
||||
assert!(false, "Finding {:?} was emitted", $finding.code);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_finding {
|
||||
( $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
let tokens = $crate::token::lexer($kcl).unwrap();
|
||||
let parser = $crate::parser::Parser::new(tokens);
|
||||
let prog = parser.ast().unwrap();
|
||||
|
||||
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
|
||||
if discovered_finding.finding == $finding {
|
||||
return;
|
||||
}
|
||||
}
|
||||
assert!(false, "Finding {:?} was not emitted", $finding.code);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_finding {
|
||||
( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
$crate::lint::rule::assert_finding!($check, $finding, $kcl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_no_finding {
|
||||
( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
$crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use assert_finding;
|
||||
pub(crate) use assert_no_finding;
|
||||
pub(crate) use test_finding;
|
||||
pub(crate) use test_no_finding;
|
||||
}
|
@ -36,9 +36,9 @@ use tower_lsp::{
|
||||
use super::backend::{InnerHandle, UpdateHandle};
|
||||
use crate::{
|
||||
ast::types::VariableKind,
|
||||
errors::KclError,
|
||||
executor::SourceRange,
|
||||
lsp::{backend::Backend as _, safemap::SafeMap},
|
||||
lint::{checks, lint},
|
||||
lsp::{backend::Backend as _, safemap::SafeMap, util::IntoDiagnostic},
|
||||
parser::PIPE_OPERATOR,
|
||||
};
|
||||
|
||||
@ -166,6 +166,7 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
}
|
||||
|
||||
async fn inner_on_change(&self, params: TextDocumentItem, force: bool) {
|
||||
self.clear_diagnostics_map(¶ms.uri).await;
|
||||
// We already updated the code map in the shared backend.
|
||||
|
||||
// Lets update the tokens.
|
||||
@ -251,14 +252,14 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
// Execute the code if we have an executor context.
|
||||
// This function automatically executes if we should & updates the diagnostics if we got
|
||||
// errors.
|
||||
let result = self.execute(¶ms, ast).await;
|
||||
if result.is_err() {
|
||||
// We return early because we got errors, and we don't want to clear the diagnostics.
|
||||
if self.execute(¶ms, ast.clone()).await.is_err() {
|
||||
// if there was an issue, let's bail and avoid trying to lint.
|
||||
return;
|
||||
}
|
||||
|
||||
// Lets update the diagnostics, since we got no errors.
|
||||
self.clear_diagnostics(¶ms.uri).await;
|
||||
for discovered_finding in lint(&ast, checks::lint_variables).into_iter().flatten() {
|
||||
self.add_to_diagnostics(¶ms, discovered_finding).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,30 +357,7 @@ impl Backend {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn add_to_diagnostics(&self, params: &TextDocumentItem, err: KclError) {
|
||||
let diagnostic = err.to_lsp_diagnostic(¶ms.text);
|
||||
// We got errors, update the diagnostics.
|
||||
self.diagnostics_map
|
||||
.insert(
|
||||
params.uri.to_string(),
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: vec![diagnostic.clone()],
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Publish the diagnostic.
|
||||
// If the client supports it.
|
||||
self.client
|
||||
.publish_diagnostics(params.uri.clone(), vec![diagnostic], None)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn clear_diagnostics(&self, uri: &url::Url) {
|
||||
async fn clear_diagnostics_map(&self, uri: &url::Url) {
|
||||
self.diagnostics_map
|
||||
.insert(
|
||||
uri.to_string(),
|
||||
@ -392,10 +370,43 @@ impl Backend {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Publish the diagnostic, we reset it here so the client knows the code compiles now.
|
||||
// If the client supports it.
|
||||
self.client.publish_diagnostics(uri.clone(), vec![], None).await;
|
||||
async fn add_to_diagnostics<DiagT: IntoDiagnostic + std::fmt::Debug>(
|
||||
&self,
|
||||
params: &TextDocumentItem,
|
||||
diagnostic: DiagT,
|
||||
) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostic))
|
||||
.await;
|
||||
|
||||
let diagnostic = diagnostic.to_lsp_diagnostic(¶ms.text);
|
||||
|
||||
let DocumentDiagnosticReport::Full(mut report) = self
|
||||
.diagnostics_map
|
||||
.get(params.uri.clone().as_str())
|
||||
.await
|
||||
.unwrap_or(DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: vec![],
|
||||
},
|
||||
}))
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
report.full_document_diagnostic_report.items.push(diagnostic);
|
||||
|
||||
self.diagnostics_map
|
||||
.insert(params.uri.to_string(), DocumentDiagnosticReport::Full(report.clone()))
|
||||
.await;
|
||||
|
||||
self.client
|
||||
.publish_diagnostics(params.uri.clone(), report.full_document_diagnostic_report.items, None)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn execute(&self, params: &TextDocumentItem, ast: crate::ast::types::Program) -> Result<()> {
|
||||
|
@ -7,3 +7,5 @@ mod safemap;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod util;
|
||||
|
||||
pub use util::IntoDiagnostic;
|
||||
|
@ -1498,6 +1498,53 @@ async fn test_kcl_lsp_diagnostic_has_errors() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_kcl_lsp_diagnostic_has_lints() {
|
||||
let server = kcl_lsp_server(false).await.unwrap();
|
||||
|
||||
// Send open file.
|
||||
server
|
||||
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentItem {
|
||||
uri: "file:///testlint.kcl".try_into().unwrap(),
|
||||
language_id: "kcl".to_string(),
|
||||
version: 1,
|
||||
text: r#"let THING = 10"#.to_string(),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
server.wait_on_handle().await;
|
||||
|
||||
// Send diagnostics request.
|
||||
let diagnostics = server
|
||||
.diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
|
||||
uri: "file:///testlint.kcl".try_into().unwrap(),
|
||||
},
|
||||
partial_result_params: Default::default(),
|
||||
work_done_progress_params: Default::default(),
|
||||
identifier: None,
|
||||
previous_result_id: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check the diagnostics.
|
||||
if let tower_lsp::lsp_types::DocumentDiagnosticReportResult::Report(diagnostics) = diagnostics {
|
||||
if let tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics) = diagnostics {
|
||||
assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 1);
|
||||
assert_eq!(
|
||||
diagnostics.full_document_diagnostic_report.items[0].message,
|
||||
"Identifiers must be lowerCamelCase"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected full diagnostics");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected diagnostics");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_copilot_lsp_set_editor_info() {
|
||||
let server = copilot_lsp_server().await.unwrap();
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Utility functions for working with ropes and positions.
|
||||
|
||||
use ropey::Rope;
|
||||
use tower_lsp::lsp_types::Position;
|
||||
use tower_lsp::lsp_types::{Diagnostic, Position};
|
||||
|
||||
pub fn position_to_offset(position: Position, rope: &Rope) -> Option<usize> {
|
||||
Some(rope.try_line_to_char(position.line as usize).ok()? + position.character as usize)
|
||||
@ -31,3 +31,10 @@ pub fn get_line_before(pos: Position, rope: &Rope) -> Option<String> {
|
||||
let line_start = offset - char_offset;
|
||||
Some(rope.slice(line_start..offset).to_string())
|
||||
}
|
||||
|
||||
/// Convert an object into a [lsp_types::Diagnostic] given the
|
||||
/// [TextDocumentItem]'s `.text` field.
|
||||
pub trait IntoDiagnostic {
|
||||
/// Convert the traited object to a [lsp_types::Diagnostic].
|
||||
fn to_lsp_diagnostic(&self, text: &str) -> Diagnostic;
|
||||
}
|
||||
|
126
src/wasm-lib/kcl/src/std/chamfer.rs
Normal file
@ -0,0 +1,126 @@
|
||||
//! Standard library chamfers.
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kittycad::types::ModelingCmd;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{ExtrudeGroup, MemoryItem},
|
||||
std::Args,
|
||||
};
|
||||
|
||||
pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
|
||||
|
||||
/// Data for chamfers.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChamferData {
|
||||
/// The radius of the chamfer.
|
||||
pub radius: f64,
|
||||
/// The tags of the paths you want to chamfer.
|
||||
pub tags: Vec<EdgeReference>,
|
||||
}
|
||||
|
||||
/// A string or a uuid.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Ord, PartialOrd, Eq, Hash)]
|
||||
#[ts(export)]
|
||||
#[serde(untagged)]
|
||||
pub enum EdgeReference {
|
||||
/// A uuid of an edge.
|
||||
Uuid(uuid::Uuid),
|
||||
/// A tag name of an edge.
|
||||
Tag(String),
|
||||
}
|
||||
|
||||
/// Create chamfers on tagged paths.
|
||||
pub async fn chamfer(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, extrude_group): (ChamferData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
|
||||
|
||||
let extrude_group = inner_chamfer(data, extrude_group, args).await?;
|
||||
Ok(MemoryItem::ExtrudeGroup(extrude_group))
|
||||
}
|
||||
|
||||
/// Create chamfers on tagged paths.
|
||||
///
|
||||
/// ```no_run
|
||||
/// const width = 20
|
||||
/// const length = 10
|
||||
/// const thickness = 1
|
||||
/// const chamferRadius = 2
|
||||
///
|
||||
/// const mountingPlateSketch = startSketchOn("XY")
|
||||
/// |> startProfileAt([-width/2, -length/2], %)
|
||||
/// |> lineTo([width/2, -length/2], %, 'edge1')
|
||||
/// |> lineTo([width/2, length/2], %, 'edge2')
|
||||
/// |> lineTo([-width/2, length/2], %, 'edge3')
|
||||
/// |> close(%, 'edge4')
|
||||
///
|
||||
/// const mountingPlate = extrude(thickness, mountingPlateSketch)
|
||||
/// |> chamfer({
|
||||
/// radius: chamferRadius,
|
||||
/// tags: [
|
||||
/// getNextAdjacentEdge('edge1', %),
|
||||
/// getNextAdjacentEdge('edge2', %),
|
||||
/// getNextAdjacentEdge('edge3', %),
|
||||
/// getNextAdjacentEdge('edge4', %)
|
||||
/// ],
|
||||
/// }, %)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "chamfer",
|
||||
}]
|
||||
async fn inner_chamfer(
|
||||
data: ChamferData,
|
||||
extrude_group: Box<ExtrudeGroup>,
|
||||
args: Args,
|
||||
) -> Result<Box<ExtrudeGroup>, KclError> {
|
||||
// Check if tags contains any duplicate values.
|
||||
let mut tags = data.tags.clone();
|
||||
tags.sort();
|
||||
tags.dedup();
|
||||
if tags.len() != data.tags.len() {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "Duplicate tags are not allowed.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
for tag in data.tags {
|
||||
let edge_id = match tag {
|
||||
EdgeReference::Uuid(uuid) => uuid,
|
||||
EdgeReference::Tag(tag) => {
|
||||
extrude_group
|
||||
.sketch_group_values
|
||||
.iter()
|
||||
.find(|p| p.get_name() == tag)
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("No edge found with tag: `{}`", tag),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?
|
||||
.get_base()
|
||||
.geo_meta
|
||||
.id
|
||||
}
|
||||
};
|
||||
|
||||
args.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid3DFilletEdge {
|
||||
edge_id,
|
||||
object_id: extrude_group.id,
|
||||
radius: data.radius,
|
||||
tolerance: DEFAULT_TOLERANCE, // We can let the user set this in the future.
|
||||
cut_type: Some(kittycad::types::CutType::Chamfer),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(extrude_group)
|
||||
}
|
@ -117,6 +117,7 @@ async fn inner_fillet(
|
||||
object_id: extrude_group.id,
|
||||
radius: data.radius,
|
||||
tolerance: DEFAULT_TOLERANCE, // We can let the user set this in the future.
|
||||
cut_type: Some(kittycad::types::CutType::Fillet),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
@ -275,7 +275,7 @@ pub async fn min(args: Args) -> Result<MemoryItem, KclError> {
|
||||
tags = ["math"],
|
||||
}]
|
||||
fn inner_min(args: Vec<f64>) -> f64 {
|
||||
let mut min = std::f64::MAX;
|
||||
let mut min = f64::MAX;
|
||||
for arg in args.iter() {
|
||||
if *arg < min {
|
||||
min = *arg;
|
||||
@ -312,7 +312,7 @@ pub async fn max(args: Args) -> Result<MemoryItem, KclError> {
|
||||
tags = ["math"],
|
||||
}]
|
||||
fn inner_max(args: Vec<f64>) -> f64 {
|
||||
let mut max = std::f64::MIN;
|
||||
let mut max = f64::MIN;
|
||||
for arg in args.iter() {
|
||||
if *arg > max {
|
||||
max = *arg;
|
||||
|
@ -1,5 +1,6 @@
|
||||
//! Functions implemented for language execution.
|
||||
|
||||
pub mod chamfer;
|
||||
pub mod extrude;
|
||||
pub mod fillet;
|
||||
pub mod helix;
|
||||
@ -10,6 +11,7 @@ pub mod patterns;
|
||||
pub mod revolve;
|
||||
pub mod segment;
|
||||
pub mod shapes;
|
||||
pub mod shell;
|
||||
pub mod sketch;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
@ -81,11 +83,13 @@ lazy_static! {
|
||||
Box::new(crate::std::patterns::PatternLinear3D),
|
||||
Box::new(crate::std::patterns::PatternCircular2D),
|
||||
Box::new(crate::std::patterns::PatternCircular3D),
|
||||
Box::new(crate::std::chamfer::Chamfer),
|
||||
Box::new(crate::std::fillet::Fillet),
|
||||
Box::new(crate::std::fillet::GetOppositeEdge),
|
||||
Box::new(crate::std::fillet::GetNextAdjacentEdge),
|
||||
Box::new(crate::std::fillet::GetPreviousAdjacentEdge),
|
||||
Box::new(crate::std::helix::Helix),
|
||||
Box::new(crate::std::shell::Shell),
|
||||
Box::new(crate::std::revolve::Revolve),
|
||||
Box::new(crate::std::revolve::GetEdge),
|
||||
Box::new(crate::std::import::Import),
|
||||
|
@ -200,6 +200,24 @@ pub async fn revolve(args: Args) -> Result<MemoryItem, KclError> {
|
||||
/// axis: getOppositeEdge('revolveAxis', box)
|
||||
/// }, %)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// const sketch001 = startSketchOn('XY')
|
||||
/// |> startProfileAt([10, 0], %)
|
||||
/// |> line([5, -5], %)
|
||||
/// |> line([5, 5], %)
|
||||
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
/// |> close(%)
|
||||
///
|
||||
/// const part001 = revolve({
|
||||
/// axis: {
|
||||
/// custom: {
|
||||
/// axis: [0.0, 1.0, 0.0],
|
||||
/// origin: [0.0, 0.0, 0.0]
|
||||
/// }
|
||||
/// }
|
||||
/// }, sketch001)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "revolve",
|
||||
}]
|
||||
|
140
src/wasm-lib/kcl/src/std/shell.rs
Normal file
@ -0,0 +1,140 @@
|
||||
//! Standard library shells.
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kittycad::types::ModelingCmd;
|
||||
use parse_display::{Display, FromStr};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{ExtrudeGroup, ExtrudeSurface, MemoryItem},
|
||||
std::{sketch::StartOrEnd, Args},
|
||||
};
|
||||
|
||||
/// A tag for a face.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case", untagged)]
|
||||
#[display("{0}")]
|
||||
pub enum FaceTag {
|
||||
StartOrEnd(StartOrEnd),
|
||||
/// A string tag for the face you want to sketch on.
|
||||
String(String),
|
||||
}
|
||||
|
||||
/// Data for shells.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ShellData {
|
||||
/// The thickness of the shell.
|
||||
pub thickness: f64,
|
||||
/// The faces you want removed.
|
||||
pub faces: Vec<FaceTag>,
|
||||
}
|
||||
|
||||
/// Create a shell.
|
||||
pub async fn shell(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, extrude_group): (ShellData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
|
||||
|
||||
let extrude_group = inner_shell(data, extrude_group, args).await?;
|
||||
Ok(MemoryItem::ExtrudeGroup(extrude_group))
|
||||
}
|
||||
|
||||
/// Shell a solid.
|
||||
///
|
||||
/// ```no_run
|
||||
/// const firstSketch = startSketchOn('XY')
|
||||
/// |> startProfileAt([-12, 12], %)
|
||||
/// |> line([24, 0], %)
|
||||
/// |> line([0, -24], %)
|
||||
/// |> line([-24, 0], %)
|
||||
/// |> close(%)
|
||||
/// |> extrude(6, %)
|
||||
///
|
||||
/// // Remove the end face for the extrusion.
|
||||
/// shell({
|
||||
/// faces: ['end'],
|
||||
/// thickness: 0.25,
|
||||
/// }, firstSketch)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "shell",
|
||||
}]
|
||||
async fn inner_shell(
|
||||
data: ShellData,
|
||||
extrude_group: Box<ExtrudeGroup>,
|
||||
args: Args,
|
||||
) -> Result<Box<ExtrudeGroup>, KclError> {
|
||||
if data.faces.is_empty() {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "Expected at least one face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
let mut face_ids = Vec::new();
|
||||
for tag in data.faces {
|
||||
let extrude_plane_id = match tag {
|
||||
FaceTag::String(ref s) => {
|
||||
if s.is_empty() {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "Expected a non-empty tag for the face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
extrude_group
|
||||
.value
|
||||
.iter()
|
||||
.find_map(|extrude_surface| match extrude_surface {
|
||||
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => {
|
||||
Some(extrude_plane.face_id)
|
||||
}
|
||||
ExtrudeSurface::ExtrudeArc(extrude_arc) if extrude_arc.name == *s => Some(extrude_arc.face_id),
|
||||
ExtrudeSurface::ExtrudePlane(_) | ExtrudeSurface::ExtrudeArc(_) => None,
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("Expected a face with the tag `{}`", tag),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?
|
||||
}
|
||||
FaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: "Expected a start face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?,
|
||||
FaceTag::StartOrEnd(StartOrEnd::End) => extrude_group.end_cap_id.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: "Expected an end face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?,
|
||||
};
|
||||
|
||||
face_ids.push(extrude_plane_id);
|
||||
}
|
||||
|
||||
if face_ids.is_empty() {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "Expected at least one valid face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
args.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid3DShellFace {
|
||||
face_ids,
|
||||
object_id: extrude_group.id,
|
||||
shell_thickness: data.thickness,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(extrude_group)
|
||||
}
|
8
src/wasm-lib/kcl/src/test_server.rs
Normal file
@ -0,0 +1,8 @@
|
||||
//! Types used to send data to the test server.
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct RequestBody {
|
||||
pub kcl_program: String,
|
||||
#[serde(default)]
|
||||
pub test_name: String,
|
||||
}
|
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_chamfer0.png
Normal file
After Width: | Height: | Size: 98 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_revolve6.png
Normal file
After Width: | Height: | Size: 220 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_shell0.png
Normal file
After Width: | Height: | Size: 157 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 289 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 141 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 210 KiB |
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 210 KiB |
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 183 KiB |
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 176 KiB |
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |