Compare commits
22 Commits
kurt-try-c
...
v0.22.1
Author | SHA1 | Date | |
---|---|---|---|
541400f4be | |||
39d249030d | |||
f8a69fac73 | |||
24f4bf160f | |||
8011594e24 | |||
0e09affb8f | |||
197a47346a | |||
9d083710e0 | |||
afa7c1dc4e | |||
c74b695a71 | |||
d0c244e05e | |||
a315b77f02 | |||
15c854ff18 | |||
acd3a5717d | |||
8a2555550f | |||
62e75c852a | |||
dd3601ea7b | |||
a5e7782d9a | |||
79b0b70688 | |||
1d134c1be0 | |||
1c58572234 | |||
ecee51e82b |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -180,9 +180,7 @@ jobs:
|
|||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
# TODO: re-enable for Windows builds, see https://github.com/tauri-apps/tauri/issues/9045
|
|
||||||
- name: Setup Rust cache
|
- name: Setup Rust cache
|
||||||
if: matrix.os != 'windows-latest'
|
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: './src-tauri -> target'
|
workspaces: './src-tauri -> target'
|
||||||
|
18
.github/workflows/playwright.yml
vendored
18
.github/workflows/playwright.yml
vendored
@ -46,12 +46,18 @@ jobs:
|
|||||||
- uses: KittyCAD/action-install-cli@main
|
- uses: KittyCAD/action-install-cli@main
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
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
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
- name: Download Wasm Cache
|
- name: Download Wasm Cache
|
||||||
id: download-wasm
|
id: download-wasm
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
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
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
@ -143,12 +149,20 @@ jobs:
|
|||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
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
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
- name: Download Wasm Cache
|
- name: Download Wasm Cache
|
||||||
id: download-wasm
|
id: download-wasm
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
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
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
File diff suppressed because one or more lines are too long
@ -56095,7 +56095,8 @@
|
|||||||
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)",
|
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)",
|
||||||
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)\nconst part002 = startSketchOn(part001, 'end')\n |> startProfileAt([4.5, -5], %)\n |> line([0, 5], %)\n |> line([5, 0], %)\n |> line([0, -5], %)\n |> close(%)\n |> extrude(5, %)",
|
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)\nconst part002 = startSketchOn(part001, 'end')\n |> startProfileAt([4.5, -5], %)\n |> line([0, 5], %)\n |> line([5, 0], %)\n |> line([0, -5], %)\n |> close(%)\n |> extrude(5, %)",
|
||||||
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %)\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({ angle: -90, axis: 'y' }, %)",
|
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %)\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({ angle: -90, axis: 'y' }, %)",
|
||||||
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %, 'revolveAxis')\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({\n angle: 90,\n axis: getOppositeEdge('revolveAxis', box)\n }, %)"
|
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %, 'revolveAxis')\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({\n angle: 90,\n axis: getOppositeEdge('revolveAxis', box)\n }, %)",
|
||||||
|
"const sketch001 = startSketchOn('XY')\n |> startProfileAt([10, 0], %)\n |> line([5, -5], %)\n |> line([5, 5], %)\n |> lineTo([profileStartX(%), profileStartY(%)], %)\n |> close(%)\n\nconst part001 = revolve({\n axis: {\n custom: {\n axis: [0.0, 1.0, 0.0],\n origin: [0.0, 0.0, 0.0]\n }\n }\n}, sketch001)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
TEST_SETTINGS_CORRUPTED,
|
TEST_SETTINGS_CORRUPTED,
|
||||||
TEST_SETTINGS_ONBOARDING_EXPORT,
|
TEST_SETTINGS_ONBOARDING_EXPORT,
|
||||||
TEST_SETTINGS_ONBOARDING_START,
|
TEST_SETTINGS_ONBOARDING_START,
|
||||||
|
TEST_CODE_GIZMO,
|
||||||
} from './storageStates'
|
} from './storageStates'
|
||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
import { LineInputsType } from 'lang/std/sketchcombos'
|
import { LineInputsType } from 'lang/std/sketchcombos'
|
||||||
@ -93,7 +94,7 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
// select a plane
|
// select a plane
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(u.codeLocator).toHaveText(
|
||||||
`const sketch001 = startSketchOn('XZ')`
|
`const sketch001 = startSketchOn('XZ')`
|
||||||
)
|
)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -102,29 +103,25 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
@ -154,8 +151,7 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Constrain' }).click()
|
await page.getByRole('button', { name: 'Constrain' }).click()
|
||||||
await page.getByRole('button', { name: 'Equal Length' }).click()
|
await page.getByRole('button', { name: 'Equal Length' }).click()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %, 'seg01')
|
|> line([${commonPoints.num1}, 0], %, 'seg01')
|
||||||
|> line([0, ${commonPoints.num1}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
@ -336,15 +332,15 @@ test('if you click the format button it formats your code', async ({
|
|||||||
// check no error to begin with
|
// check no error to begin with
|
||||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
|
|
||||||
await page.click('.cm-content')
|
await u.codeLocator.click()
|
||||||
await page.keyboard.type(`const sketch001 = startSketchOn('XY')
|
await page.keyboard.type(`const sketch001 = startSketchOn('XY')
|
||||||
|> startProfileAt([-10, -10], %)
|
|> startProfileAt([-10, -10], %)
|
||||||
|> line([20, 0], %)
|
|> line([20, 0], %)
|
||||||
|> line([0, 20], %)
|
|> line([0, 20], %)
|
||||||
|> line([-20, 0], %)
|
|> line([-20, 0], %)
|
||||||
|> close(%)`)
|
|> close(%)`)
|
||||||
await page.click('#code-pane button:first-child')
|
await page.locator('#code-pane button:first-child').click()
|
||||||
await page.click('button:has-text("Format code")')
|
await page.locator('button:has-text("Format code")').click()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XY')
|
.toHaveText(`const sketch001 = startSketchOn('XY')
|
||||||
@ -392,7 +388,7 @@ test('if you use the format keyboard binding it formats your code', async ({
|
|||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
// focus the editor
|
// focus the editor
|
||||||
await page.click('.cm-line')
|
await u.codeLocator.click()
|
||||||
|
|
||||||
// Hit alt+shift+f to format the code
|
// Hit alt+shift+f to format the code
|
||||||
await page.keyboard.press('Alt+Shift+KeyF')
|
await page.keyboard.press('Alt+Shift+KeyF')
|
||||||
@ -430,7 +426,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
|||||||
const topAng = 30
|
const topAng = 30
|
||||||
const bottomAng = 25
|
const bottomAng = 25
|
||||||
*/
|
*/
|
||||||
await page.click('.cm-content')
|
await u.codeLocator.click()
|
||||||
await page.keyboard.type('$ error')
|
await page.keyboard.type('$ error')
|
||||||
|
|
||||||
// press arrows to clear autocomplete
|
// press arrows to clear autocomplete
|
||||||
@ -527,7 +523,7 @@ fn squareHole = (l, w) => {
|
|||||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
|
|
||||||
// Click on the bottom of the code editor to add a new line
|
// Click on the bottom of the code editor to add a new line
|
||||||
await page.click('.cm-content')
|
await u.codeLocator.click()
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
@ -792,7 +788,7 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
// tests clicking on an option, selection the first option
|
// tests clicking on an option, selection the first option
|
||||||
// and arrowing down to an option
|
// and arrowing down to an option
|
||||||
|
|
||||||
await page.click('.cm-content')
|
await u.codeLocator.click()
|
||||||
await page.keyboard.type('const sketch001 = start')
|
await page.keyboard.type('const sketch001 = start')
|
||||||
|
|
||||||
// expect there to be six auto complete options
|
// expect there to be six auto complete options
|
||||||
@ -934,7 +930,7 @@ test('Project settings can be opened with keybinding from the editor', async ({
|
|||||||
.waitFor({ state: 'visible' })
|
.waitFor({ state: 'visible' })
|
||||||
|
|
||||||
// Put the cursor in the editor
|
// Put the cursor in the editor
|
||||||
await page.click('.cm-content')
|
await page.locator('.cm-content').click()
|
||||||
|
|
||||||
// Open the settings modal with the browser keyboard shortcut
|
// Open the settings modal with the browser keyboard shortcut
|
||||||
await page.keyboard.press('Meta+Shift+,')
|
await page.keyboard.press('Meta+Shift+,')
|
||||||
@ -983,7 +979,7 @@ test('Project and user settings can be reset', async ({ page }) => {
|
|||||||
.waitFor({ state: 'visible' })
|
.waitFor({ state: 'visible' })
|
||||||
|
|
||||||
// Put the cursor in the editor
|
// Put the cursor in the editor
|
||||||
await page.click('.cm-content')
|
await page.locator('.cm-content').click()
|
||||||
|
|
||||||
// Open the settings modal with the browser keyboard shortcut
|
// Open the settings modal with the browser keyboard shortcut
|
||||||
await page.keyboard.press('Meta+Shift+,')
|
await page.keyboard.press('Meta+Shift+,')
|
||||||
@ -1154,6 +1150,7 @@ test('Onboarding redirects and code updating', async ({ page }) => {
|
|||||||
await expect(page.locator('.cm-content')).toHaveText(/.+/)
|
await expect(page.locator('.cm-content')).toHaveText(/.+/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Testing selections', () => {
|
||||||
test('Selections work on fresh and edited sketch', async ({ page }) => {
|
test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||||
// tests mapping works on fresh sketch and edited sketch
|
// tests mapping works on fresh sketch and edited sketch
|
||||||
// tests using hovers which is the same as selections, because if
|
// tests using hovers which is the same as selections, because if
|
||||||
@ -1240,7 +1237,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
startXPx + PUR * 10,
|
startXPx + PUR * 10,
|
||||||
isSecondTime ? 295 : 500 - PUR * 20
|
isSecondTime ? 295 : 500 - PUR * 20
|
||||||
) // mouse onto another line
|
) // mouse onto another line
|
||||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||||
|
|
||||||
// now check clicking works including axis
|
// now check clicking works including axis
|
||||||
|
|
||||||
@ -1298,7 +1295,9 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(page.locator('.cm-cursor')).toHaveCount(1)
|
await expect(page.locator('.cm-cursor')).toHaveCount(1)
|
||||||
|
|
||||||
await page.keyboard.down(process.platform === 'linux' ? 'Control' : 'Meta')
|
await page.keyboard.down(
|
||||||
|
process.platform === 'linux' ? 'Control' : 'Meta'
|
||||||
|
)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||||
|
|
||||||
@ -1321,9 +1320,8 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
// select a line
|
// select a line, this verifies that sketches in the scene can be selected outside of sketch mode
|
||||||
// await topHorzSegmentClick()
|
await topHorzSegmentClick()
|
||||||
await page.getByText(commonPoints.startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
@ -1339,6 +1337,167 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await selectionSequence(true)
|
await selectionSequence(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Hovering over 3d features highlights code', async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`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, %)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
}, KCL_DEFAULT_LENGTH)
|
||||||
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
// wait for execution done
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
vantage: { x: 0, y: -1250, z: 580 },
|
||||||
|
center: { x: 0, y: 0, z: 0 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const extrusionTop: Coords2d = [800, 240]
|
||||||
|
const flatExtrusionFace: Coords2d = [960, 160]
|
||||||
|
const arc: Coords2d = [840, 160]
|
||||||
|
const close: Coords2d = [720, 200]
|
||||||
|
const nothing: Coords2d = [600, 200]
|
||||||
|
|
||||||
|
await page.mouse.move(nothing[0], nothing[1])
|
||||||
|
await page.mouse.click(nothing[0], nothing[1])
|
||||||
|
|
||||||
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
await page.waitForTimeout(200)
|
||||||
|
|
||||||
|
await page.mouse.move(extrusionTop[0], extrusionTop[1])
|
||||||
|
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||||
|
await page.mouse.move(nothing[0], nothing[1])
|
||||||
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
|
||||||
|
await page.mouse.move(arc[0], arc[1])
|
||||||
|
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||||
|
await page.mouse.move(nothing[0], nothing[1])
|
||||||
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
|
||||||
|
await page.mouse.move(close[0], close[1])
|
||||||
|
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||||
|
await page.mouse.move(nothing[0], nothing[1])
|
||||||
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
|
||||||
|
await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1])
|
||||||
|
await expect(page.getByTestId('hover-highlight')).toHaveCount(5) // multiple lines
|
||||||
|
await page.mouse.move(nothing[0], nothing[1])
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Testing selections (and hovers) work on sketches when NOT in sketch mode', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
pos: [694, 185],
|
||||||
|
expectedCode: "line([74.36, 130.4], %, 'seg01')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pos: [816, 244],
|
||||||
|
expectedCode: "angledLine([segAng('seg01', %), yo], %)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pos: [1107, 161],
|
||||||
|
expectedCode: 'tangentialArcTo([167.95, -28.85], %)',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
await page.addInitScript(
|
||||||
|
async ({ cases }) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const yo = 79
|
||||||
|
const part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([-7.54, -26.74], %)
|
||||||
|
|> ${cases[0].expectedCode}
|
||||||
|
|> line([-3.19, -138.43], %)
|
||||||
|
|> ${cases[1].expectedCode}
|
||||||
|
|> line([41.19, 28.97 + 5], %)
|
||||||
|
|> ${cases[2].expectedCode}`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ cases }
|
||||||
|
)
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
vantage: { x: -449, y: -7503, z: 99 },
|
||||||
|
center: { x: -449, y: 0, z: 99 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await u.waitForCmdReceive('default_camera_look_at')
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
// end setup, now test hover and selects
|
||||||
|
for (const { pos, expectedCode } of cases) {
|
||||||
|
// hover over segment, check it's content
|
||||||
|
await page.mouse.move(pos[0], pos[1], { steps: 5 })
|
||||||
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||||
|
await expect(page.getByTestId('hover-highlight').first()).toHaveText(
|
||||||
|
expectedCode
|
||||||
|
)
|
||||||
|
// hover over segment, click it and check the cursor has move to the right place
|
||||||
|
await page.mouse.click(pos[0], pos[1])
|
||||||
|
await expect(page.locator('.cm-activeLine')).toHaveText(
|
||||||
|
'|> ' + expectedCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test.describe('Command bar tests', () => {
|
test.describe('Command bar tests', () => {
|
||||||
test('Command bar works and can change a setting', async ({ page }) => {
|
test('Command bar works and can change a setting', async ({ page }) => {
|
||||||
// Brief boilerplate
|
// Brief boilerplate
|
||||||
@ -1399,7 +1558,7 @@ test.describe('Command bar tests', () => {
|
|||||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
|
||||||
// Put the cursor in the code editor
|
// Put the cursor in the code editor
|
||||||
await page.click('.cm-content')
|
await page.locator('.cm-content').click()
|
||||||
|
|
||||||
// Now try the same, but with the keyboard shortcut, check focus
|
// Now try the same, but with the keyboard shortcut, check focus
|
||||||
await page.keyboard.press('Meta+K')
|
await page.keyboard.press('Meta+K')
|
||||||
@ -1533,7 +1692,7 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
||||||
const { toSU, click00r, expectCodeToBe } = getMovementUtils({ center, page })
|
const { toSU, click00r } = getMovementUtils({ center, page })
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
@ -1550,25 +1709,25 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
let codeStr = "const sketch001 = startSketchOn('XY')"
|
let codeStr = "const sketch001 = startSketchOn('XY')"
|
||||||
|
|
||||||
await page.mouse.click(center.x, viewportSize.height * 0.55)
|
await page.mouse.click(center.x, viewportSize.height * 0.55)
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
await click00r(0, 0)
|
await click00r(0, 0)
|
||||||
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(50, 0)
|
await click00r(50, 0)
|
||||||
codeStr += ` |> line(${toSU([50, 0])}, %)`
|
codeStr += ` |> line(${toSU([50, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 50)
|
await click00r(0, 50)
|
||||||
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(-50, 0)
|
await click00r(-50, 0)
|
||||||
codeStr += ` |> line(${toSU([-50, 0])}, %)`
|
codeStr += ` |> line(${toSU([-50, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
// exit the sketch, reset relative clicker
|
// exit the sketch, reset relative clicker
|
||||||
click00r(undefined, undefined)
|
click00r(undefined, undefined)
|
||||||
@ -1586,24 +1745,24 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
await page.mouse.click(center.x + 30, center.y)
|
await page.mouse.click(center.x + 30, center.y)
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
codeStr += "const sketch002 = startSketchOn('XY')"
|
codeStr += "const sketch002 = startSketchOn('XY')"
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> startProfileAt(${toSU([30, 0])}, %)`
|
codeStr += ` |> startProfileAt(${toSU([30, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> line(${toSU([30 - 0.1 /* imprecision */, 0])}, %)`
|
codeStr += ` |> line(${toSU([30 + 0.1 /* imprecision */, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 30)
|
await click00r(0, 30)
|
||||||
codeStr += ` |> line(${toSU([0, 30])}, %)`
|
codeStr += ` |> line(${toSU([0, 30])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(-30, 0)
|
await click00r(-30, 0)
|
||||||
codeStr += ` |> line(${toSU([-30 + 0.1, 0])}, %)`
|
codeStr += ` |> line(${toSU([-30 - 0.1, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
click00r(undefined, undefined)
|
click00r(undefined, undefined)
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
@ -1653,98 +1812,6 @@ test('ProgramMemory can be serialised', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Hovering over 3d features highlights code', async ({ page }) => {
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
|
||||||
localStorage.setItem(
|
|
||||||
'persistCode',
|
|
||||||
`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, %)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
}, KCL_DEFAULT_LENGTH)
|
|
||||||
await page.setViewportSize({ width: 1000, height: 500 })
|
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
|
||||||
|
|
||||||
// wait for execution done
|
|
||||||
await u.openDebugPanel()
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
vantage: { x: 0, y: -1250, z: 580 },
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
|
|
||||||
const extrusionTop: Coords2d = [800, 240]
|
|
||||||
const flatExtrusionFace: Coords2d = [960, 160]
|
|
||||||
const arc: Coords2d = [840, 160]
|
|
||||||
const close: Coords2d = [720, 200]
|
|
||||||
const nothing: Coords2d = [600, 200]
|
|
||||||
|
|
||||||
await page.mouse.move(nothing[0], nothing[1])
|
|
||||||
await page.mouse.click(nothing[0], nothing[1])
|
|
||||||
|
|
||||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
|
||||||
await page.waitForTimeout(200)
|
|
||||||
|
|
||||||
await page.mouse.move(extrusionTop[0], extrusionTop[1])
|
|
||||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
|
||||||
await page.mouse.move(nothing[0], nothing[1])
|
|
||||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
|
||||||
|
|
||||||
await page.mouse.move(arc[0], arc[1])
|
|
||||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
|
||||||
await page.mouse.move(nothing[0], nothing[1])
|
|
||||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
|
||||||
|
|
||||||
await page.mouse.move(close[0], close[1])
|
|
||||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
|
||||||
await page.mouse.move(nothing[0], nothing[1])
|
|
||||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
|
||||||
|
|
||||||
await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1])
|
|
||||||
await expect(page.getByTestId('hover-highlight')).toHaveCount(5) // multiple lines
|
|
||||||
await page.mouse.move(nothing[0], nothing[1])
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
@ -3413,14 +3480,24 @@ test.describe('Testing segment overlays', () => {
|
|||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
await page.getByText('xLineTo(5 + 9 - 5, %)').click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
|
||||||
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
||||||
|
|
||||||
|
const clickUnconstrained = _clickUnconstrained(page)
|
||||||
|
const clickConstrained = _clickConstrained(page)
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
await u.sendCustomCmd({
|
await u.sendCustomCmd({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'default_camera_look_at',
|
type: 'default_camera_look_at',
|
||||||
vantage: { x: 0, y: -1250, z: 580 },
|
vantage: { x: 80, y: -1350, z: 510 },
|
||||||
center: { x: 0, y: 0, z: 0 },
|
center: { x: 80, y: 0, z: 510 },
|
||||||
up: { x: 0, y: 0, z: 1 },
|
up: { x: 0, y: 0, z: 1 },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -3435,27 +3512,6 @@ test.describe('Testing segment overlays', () => {
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await page.getByText('xLineTo(5 + 9 - 5, %)').click()
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
|
||||||
await page.waitForTimeout(500)
|
|
||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
|
||||||
|
|
||||||
const clickUnconstrained = _clickUnconstrained(page)
|
|
||||||
const clickConstrained = _clickConstrained(page)
|
|
||||||
|
|
||||||
// Drag the sketch into view
|
|
||||||
await page.mouse.move(600, 64)
|
|
||||||
await page.mouse.down({ button: 'middle' })
|
|
||||||
await page.mouse.move(600, 450, { steps: 10 })
|
|
||||||
await page.mouse.up({ button: 'middle' })
|
|
||||||
|
|
||||||
await page.mouse.move(600, 64)
|
|
||||||
await page.mouse.down({ button: 'middle' })
|
|
||||||
await page.mouse.move(600, 120, { steps: 10 })
|
|
||||||
await page.mouse.up({ button: 'middle' })
|
|
||||||
|
|
||||||
let ang = 0
|
let ang = 0
|
||||||
|
|
||||||
const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
|
const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
|
||||||
@ -3504,11 +3560,8 @@ test.describe('Testing segment overlays', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await page.mouse.move(700, 250)
|
await page.mouse.move(700, 250)
|
||||||
for (let i = 0; i < 5; i++) {
|
await page.mouse.wheel(0, 25)
|
||||||
await page.mouse.wheel(0, 100)
|
await page.waitForTimeout(100)
|
||||||
await page.waitForTimeout(25)
|
|
||||||
}
|
|
||||||
await page.waitForTimeout(200)
|
|
||||||
|
|
||||||
let lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`)
|
let lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`)
|
||||||
ang = await u.getAngle(`[data-overlay-index="2"]`)
|
ang = await u.getAngle(`[data-overlay-index="2"]`)
|
||||||
@ -3590,12 +3643,8 @@ const part001 = startSketchOn('XZ')
|
|||||||
const clickUnconstrained = _clickUnconstrained(page)
|
const clickUnconstrained = _clickUnconstrained(page)
|
||||||
|
|
||||||
await page.mouse.move(700, 250)
|
await page.mouse.move(700, 250)
|
||||||
for (let i = 0; i < 7; i++) {
|
await page.mouse.wheel(0, 25)
|
||||||
await page.mouse.wheel(0, 100)
|
await page.waitForTimeout(100)
|
||||||
await page.waitForTimeout(25)
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
|
|
||||||
let ang = 0
|
let ang = 0
|
||||||
|
|
||||||
@ -4781,6 +4830,232 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
|
|||||||
).not.toBeVisible()
|
).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Testing Gizmo', () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
testDescription: 'top view',
|
||||||
|
clickPosition: { x: 951, y: 385 },
|
||||||
|
expectedCameraPosition: { x: 800, y: -152, z: 4886.02 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: 'bottom view',
|
||||||
|
clickPosition: { x: 951, y: 429 },
|
||||||
|
expectedCameraPosition: { x: 800, y: -152, z: -4834.02 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: 'right view',
|
||||||
|
clickPosition: { x: 929, y: 417 },
|
||||||
|
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: 'left view',
|
||||||
|
clickPosition: { x: 974, y: 397 },
|
||||||
|
expectedCameraPosition: { x: -4060.02, y: -152, z: 26 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: 'back view',
|
||||||
|
clickPosition: { x: 967, y: 421 },
|
||||||
|
expectedCameraPosition: { x: 800, y: 4708.02, z: 26 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: 'front view',
|
||||||
|
clickPosition: { x: 935, y: 393 },
|
||||||
|
expectedCameraPosition: { x: 800, y: -5012.02, z: 26 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
for (const {
|
||||||
|
clickPosition,
|
||||||
|
expectedCameraPosition,
|
||||||
|
expectedCameraTarget,
|
||||||
|
testDescription,
|
||||||
|
} of cases) {
|
||||||
|
test(`check ${testDescription}`, async ({ page, browserName }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.addInitScript((TEST_CODE_GIZMO) => {
|
||||||
|
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
|
||||||
|
}, TEST_CODE_GIZMO)
|
||||||
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
// wait for execution done
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
vantage: {
|
||||||
|
x: 3000,
|
||||||
|
y: 3000,
|
||||||
|
z: 3000,
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
x: 800,
|
||||||
|
y: -152,
|
||||||
|
z: 26,
|
||||||
|
},
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await u.waitForCmdReceive('default_camera_get_settings')
|
||||||
|
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await page.mouse.move(clickPosition.x, clickPosition.y)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.mouse.click(clickPosition.x, clickPosition.y)
|
||||||
|
await page.mouse.move(0, 0)
|
||||||
|
await u.waitForCmdReceive('default_camera_look_at')
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await u.waitForCmdReceive('default_camera_get_settings')
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
// position
|
||||||
|
expect(page.getByTestId('cam-x-position')).toHaveValue(
|
||||||
|
expectedCameraPosition.x.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-y-position')).toHaveValue(
|
||||||
|
expectedCameraPosition.y.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-z-position')).toHaveValue(
|
||||||
|
expectedCameraPosition.z.toString()
|
||||||
|
),
|
||||||
|
// target
|
||||||
|
expect(page.getByTestId('cam-x-target')).toHaveValue(
|
||||||
|
expectedCameraTarget.x.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-y-target')).toHaveValue(
|
||||||
|
expectedCameraTarget.y.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-z-target')).toHaveValue(
|
||||||
|
expectedCameraTarget.z.toString()
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Context menu', async ({ page }) => {
|
||||||
|
const testCase = {
|
||||||
|
testDescription: 'Right view',
|
||||||
|
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test prelude taken from the above test
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.addInitScript((TEST_CODE_GIZMO) => {
|
||||||
|
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
|
||||||
|
}, TEST_CODE_GIZMO)
|
||||||
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
// wait for execution done
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
vantage: {
|
||||||
|
x: 3000,
|
||||||
|
y: 3000,
|
||||||
|
z: 3000,
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
x: 800,
|
||||||
|
y: -152,
|
||||||
|
z: 26,
|
||||||
|
},
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await u.waitForCmdReceive('default_camera_get_settings')
|
||||||
|
|
||||||
|
// Now find and select the correct
|
||||||
|
// view from the context menu
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
const gizmo = page.locator('[aria-label*=gizmo]')
|
||||||
|
await gizmo.click({ button: 'right' })
|
||||||
|
const buttonToTest = page.getByRole('button', {
|
||||||
|
name: testCase.testDescription,
|
||||||
|
})
|
||||||
|
await expect(buttonToTest).toBeVisible()
|
||||||
|
await buttonToTest.click()
|
||||||
|
|
||||||
|
// Now assert we've moved to the correct view
|
||||||
|
// Taken from the above test
|
||||||
|
await u.waitForCmdReceive('default_camera_look_at')
|
||||||
|
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await u.waitForCmdReceive('default_camera_get_settings')
|
||||||
|
await page.waitForTimeout(400)
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
// position
|
||||||
|
expect(page.getByTestId('cam-x-position')).toHaveValue(
|
||||||
|
testCase.expectedCameraPosition.x.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-y-position')).toHaveValue(
|
||||||
|
testCase.expectedCameraPosition.y.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-z-position')).toHaveValue(
|
||||||
|
testCase.expectedCameraPosition.z.toString()
|
||||||
|
),
|
||||||
|
// target
|
||||||
|
expect(page.getByTestId('cam-x-target')).toHaveValue(
|
||||||
|
testCase.expectedCameraTarget.x.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-y-target')).toHaveValue(
|
||||||
|
testCase.expectedCameraTarget.y.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-z-target')).toHaveValue(
|
||||||
|
testCase.expectedCameraTarget.z.toString()
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('Successful export shows a success toast', async ({ page }) => {
|
test('Successful export shows a success toast', async ({ page }) => {
|
||||||
// FYI this test doesn't work with only engine running locally
|
// FYI this test doesn't work with only engine running locally
|
||||||
// And you will need to have the KittyCAD CLI installed
|
// And you will need to have the KittyCAD CLI installed
|
||||||
|
@ -50,3 +50,25 @@ export const TEST_SETTINGS_CORRUPTED = {
|
|||||||
textWrapping: true,
|
textWrapping: true,
|
||||||
},
|
},
|
||||||
} satisfies Partial<SaveSettingsPayload>
|
} 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
|
// wait for 'Loading stream...' spinner
|
||||||
await page.getByTestId('loading-stream').waitFor()
|
await page.getByTestId('loading-stream').waitFor()
|
||||||
// wait for all spinners to be gone
|
// 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()
|
await page.getByTestId('start-sketch').waitFor()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeCurrentCode(page: Page) {
|
async function removeCurrentCode(page: Page) {
|
||||||
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
|
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.down(hotkey)
|
||||||
await page.keyboard.press('a')
|
await page.keyboard.press('a')
|
||||||
await page.keyboard.up(hotkey)
|
await page.keyboard.up(hotkey)
|
||||||
@ -28,12 +30,12 @@ async function removeCurrentCode(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendCustomCmd(page: Page, cmd: EngineCommand) {
|
async function sendCustomCmd(page: Page, cmd: EngineCommand) {
|
||||||
await page.fill('[data-testid="custom-cmd-input"]', JSON.stringify(cmd))
|
await page.getByTestId('custom-cmd-input').fill(JSON.stringify(cmd))
|
||||||
await page.click('[data-testid="custom-cmd-send-button"]')
|
await page.getByTestId('custom-cmd-send-button').click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearCommandLogs(page: Page) {
|
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) {
|
async function expectCmdLog(page: Page, locatorStr: string) {
|
||||||
@ -162,12 +164,7 @@ export const getMovementUtils = (opts: any) => {
|
|||||||
return ret.then(() => [last.x, last.y])
|
return ret.then(() => [last.x, last.y])
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectCodeToBe = async (str: string) => {
|
return { toSU, click00r }
|
||||||
await expect(opts.page.locator('.cm-content')).toHaveText(str)
|
|
||||||
await opts.page.waitForTimeout(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { toSU, click00r, expectCodeToBe }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUtils(page: Page) {
|
export async function getUtils(page: Page) {
|
||||||
@ -228,6 +225,7 @@ export async function getUtils(page: Page) {
|
|||||||
.locator(locator)
|
.locator(locator)
|
||||||
.boundingBox()
|
.boundingBox()
|
||||||
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
|
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
|
||||||
|
codeLocator: page.locator('.cm-content'),
|
||||||
doAndWaitForCmd: async (
|
doAndWaitForCmd: async (
|
||||||
fn: () => Promise<void>,
|
fn: () => Promise<void>,
|
||||||
commandType: string,
|
commandType: string,
|
||||||
|
@ -152,6 +152,7 @@ describe('ZMA (Tauri)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('signs out', async () => {
|
it('signs out', async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
||||||
await click(menuButton)
|
await click(menuButton)
|
||||||
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
|
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.22.0",
|
"version": "0.22.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.16.0",
|
"@codemirror/autocomplete": "^6.16.0",
|
||||||
|
111
src-tauri/Cargo.lock
generated
111
src-tauri/Cargo.lock
generated
@ -200,7 +200,7 @@ dependencies = [
|
|||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.8.13",
|
"toml 0.8.14",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -766,7 +766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
|
checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"toml 0.8.13",
|
"toml 0.8.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1373,7 +1373,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 0.8.13",
|
"toml 0.8.14",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg 0.52.0",
|
"winreg 0.52.0",
|
||||||
]
|
]
|
||||||
@ -2578,8 +2578,6 @@ dependencies = [
|
|||||||
"gltf-json",
|
"gltf-json",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"kittycad",
|
"kittycad",
|
||||||
"kittycad-execution-plan-macros",
|
|
||||||
"kittycad-execution-plan-traits",
|
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"parse-display",
|
"parse-display",
|
||||||
@ -2592,7 +2590,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"toml 0.8.13",
|
"toml 0.8.14",
|
||||||
"tower-lsp",
|
"tower-lsp",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"url",
|
"url",
|
||||||
@ -2655,28 +2653,6 @@ dependencies = [
|
|||||||
"uuid",
|
"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]]
|
[[package]]
|
||||||
name = "kuchikiki"
|
name = "kuchikiki"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@ -3353,9 +3329,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parse-display"
|
name = "parse-display"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06af5f9333eb47bd9ba8462d612e37a8328a5cb80b13f0af4de4c3b89f52dee5"
|
checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"parse-display-derive",
|
"parse-display-derive",
|
||||||
"regex",
|
"regex",
|
||||||
@ -3364,9 +3340,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parse-display-derive"
|
name = "parse-display-derive"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc9252f259500ee570c75adcc4e317fa6f57a1e47747d622e0bf838002a7b790"
|
checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3735,9 +3711,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.83"
|
version = "1.0.85"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
|
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@ -4299,6 +4275,19 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "rustls-native-certs"
|
name = "rustls-native-certs"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -5042,7 +5031,7 @@ dependencies = [
|
|||||||
"cfg-expr",
|
"cfg-expr",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml 0.8.13",
|
"toml 0.8.14",
|
||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5195,7 +5184,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"tauri-winres",
|
"tauri-winres",
|
||||||
"toml 0.8.13",
|
"toml 0.8.14",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5253,7 +5242,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"toml 0.8.13",
|
"toml 0.8.14",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5530,7 +5519,7 @@ dependencies = [
|
|||||||
"serde_with",
|
"serde_with",
|
||||||
"swift-rs",
|
"swift-rs",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml 0.8.13",
|
"toml 0.8.14",
|
||||||
"url",
|
"url",
|
||||||
"urlpattern",
|
"urlpattern",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@ -5664,9 +5653,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.37.0"
|
version = "1.38.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -5684,9 +5673,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.2.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -5715,18 +5704,29 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-rustls"
|
||||||
version = "0.21.0"
|
version = "0.26.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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 = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"rustls 0.22.4",
|
"rustls 0.23.7",
|
||||||
"rustls-native-certs",
|
"rustls-native-certs",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.25.0",
|
"tokio-rustls 0.26.0",
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5758,14 +5758,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.13"
|
version = "0.8.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
|
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"toml_edit 0.22.13",
|
"toml_edit 0.22.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5814,9 +5814,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.13"
|
version = "0.22.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
|
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.2.6",
|
"indexmap 2.2.6",
|
||||||
"serde",
|
"serde",
|
||||||
@ -6035,9 +6035,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.21.0"
|
version = "0.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
|
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -6046,11 +6046,10 @@ dependencies = [
|
|||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rustls 0.22.4",
|
"rustls 0.23.7",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"sha1",
|
"sha1",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"url",
|
|
||||||
"utf-8",
|
"utf-8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -74,5 +74,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"version": "0.22.0"
|
"version": "0.22.1"
|
||||||
}
|
}
|
||||||
|
@ -48,12 +48,14 @@ export type ReactCameraProperties =
|
|||||||
type: 'perspective'
|
type: 'perspective'
|
||||||
fov?: number
|
fov?: number
|
||||||
position: [number, number, number]
|
position: [number, number, number]
|
||||||
|
target: [number, number, number]
|
||||||
quaternion: [number, number, number, number]
|
quaternion: [number, number, number, number]
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'orthographic'
|
type: 'orthographic'
|
||||||
zoom?: number
|
zoom?: number
|
||||||
position: [number, number, number]
|
position: [number, number, number]
|
||||||
|
target: [number, number, number]
|
||||||
quaternion: [number, number, number, number]
|
quaternion: [number, number, number, number]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,7 +444,7 @@ export class CameraControls {
|
|||||||
this.handleEnd()
|
this.handleEnd()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.throttledEngCmd({
|
this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'default_camera_zoom',
|
type: 'default_camera_zoom',
|
||||||
@ -454,11 +456,11 @@ export class CameraControls {
|
|||||||
return
|
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 = this.pendingZoom ? this.pendingZoom : 1
|
||||||
this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed)
|
this.pendingZoom *= 1 + event.deltaY * 0.01
|
||||||
this.handleEnd()
|
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(
|
async tweenCameraToQuaternion(
|
||||||
targetQuaternion: Quaternion,
|
targetQuaternion: Quaternion,
|
||||||
targetPosition = new Vector3(),
|
targetPosition = new Vector3(),
|
||||||
@ -957,6 +1028,11 @@ export class CameraControls {
|
|||||||
roundOff(this.camera.position.y, 2),
|
roundOff(this.camera.position.y, 2),
|
||||||
roundOff(this.camera.position.z, 2),
|
roundOff(this.camera.position.z, 2),
|
||||||
],
|
],
|
||||||
|
target: [
|
||||||
|
roundOff(this.target.x, 2),
|
||||||
|
roundOff(this.target.y, 2),
|
||||||
|
roundOff(this.target.z, 2),
|
||||||
|
],
|
||||||
quaternion: [
|
quaternion: [
|
||||||
roundOff(this.camera.quaternion.x, 2),
|
roundOff(this.camera.quaternion.x, 2),
|
||||||
roundOff(this.camera.quaternion.y, 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' && (
|
{camSettings.type === 'perspective' && (
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@ -816,6 +825,71 @@ export const CamDebugSettings = () => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -51,14 +51,6 @@ function CommandBarSelectionInput({
|
|||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}, [selection, inputRef])
|
}, [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
|
// Fast-forward through this arg if it's marked as skippable
|
||||||
// and we have a valid selection already
|
// and we have a valid selection already
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
199
src/components/ContextMenu.tsx
Normal file
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>
|
||||||
|
)
|
||||||
|
}
|
@ -18,6 +18,8 @@ import { useLspContext } from './LspProvider'
|
|||||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
|
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
|
||||||
|
import { ContextMenu, ContextMenuItem } from './ContextMenu'
|
||||||
|
import usePlatform from 'hooks/usePlatform'
|
||||||
|
|
||||||
function getIndentationCSS(level: number) {
|
function getIndentationCSS(level: number) {
|
||||||
return `calc(1rem * ${level + 1})`
|
return `calc(1rem * ${level + 1})`
|
||||||
@ -125,6 +127,7 @@ const FileTreeItem = ({
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
const isCurrentFile = fileOrDir.path === currentFile?.path
|
const isCurrentFile = fileOrDir.path === currentFile?.path
|
||||||
|
const itemRef = useRef(null)
|
||||||
|
|
||||||
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
|
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
|
||||||
const removeCurrentItemFromRenaming = useCallback(
|
const removeCurrentItemFromRenaming = useCallback(
|
||||||
@ -185,7 +188,7 @@ const FileTreeItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="contents" ref={itemRef}>
|
||||||
{fileOrDir.children === undefined ? (
|
{fileOrDir.children === undefined ? (
|
||||||
<li
|
<li
|
||||||
className={
|
className={
|
||||||
@ -321,7 +324,41 @@ const FileTreeItem = ({
|
|||||||
setIsOpen={setIsConfirmingDelete}
|
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 { sceneInfra } from 'lib/singletons'
|
||||||
import { useEffect, useRef } from 'react'
|
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
WebGLRenderer,
|
WebGLRenderer,
|
||||||
Scene,
|
Scene,
|
||||||
@ -12,21 +13,52 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
ColorRepresentation,
|
ColorRepresentation,
|
||||||
|
Vector2,
|
||||||
|
Raycaster,
|
||||||
|
Camera,
|
||||||
|
Intersection,
|
||||||
|
Object3D,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuDivider,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuItemRefresh,
|
||||||
|
} from './ContextMenu'
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
const CANVAS_SIZE = 80
|
||||||
const FRUSTUM_SIZE = 0.5
|
const FRUSTUM_SIZE = 0.5
|
||||||
const AXIS_LENGTH = 0.35
|
const AXIS_LENGTH = 0.35
|
||||||
const AXIS_WIDTH = 0.02
|
const AXIS_WIDTH = 0.02
|
||||||
const AXIS_COLORS = {
|
enum AxisColors {
|
||||||
x: '#fa6668',
|
X = '#fa6668',
|
||||||
y: '#11eb6b',
|
Y = '#11eb6b',
|
||||||
z: '#6689ef',
|
Z = '#6689ef',
|
||||||
gray: '#c6c7c2',
|
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() {
|
export default function Gizmo() {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||||
|
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||||
|
const cameraPassiveUpdateTimer = useRef(0)
|
||||||
|
const raycasterPassiveUpdateTimer = useRef(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvasRef.current) return
|
if (!canvasRef.current) return
|
||||||
@ -41,35 +73,89 @@ export default function Gizmo() {
|
|||||||
const { gizmoAxes, gizmoAxisHeads } = createGizmo()
|
const { gizmoAxes, gizmoAxisHeads } = createGizmo()
|
||||||
scene.add(...gizmoAxes, ...gizmoAxisHeads)
|
scene.add(...gizmoAxes, ...gizmoAxisHeads)
|
||||||
|
|
||||||
|
const raycaster = new Raycaster()
|
||||||
|
const { mouse, disposeMouseEvents } = initializeMouseEvents(
|
||||||
|
canvas,
|
||||||
|
raycasterIntersect,
|
||||||
|
sceneInfra
|
||||||
|
)
|
||||||
|
const raycasterObjects = [...gizmoAxisHeads]
|
||||||
|
|
||||||
const clock = new Clock()
|
const clock = new Clock()
|
||||||
const clientCamera = sceneInfra.camControls.camera
|
const clientCamera = sceneInfra.camControls.camera
|
||||||
let currentQuaternion = new Quaternion().copy(clientCamera.quaternion)
|
let currentQuaternion = new Quaternion().copy(clientCamera.quaternion)
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
requestAnimationFrame(animate)
|
const delta = clock.getDelta()
|
||||||
updateCameraOrientation(
|
updateCameraOrientation(
|
||||||
camera,
|
camera,
|
||||||
currentQuaternion,
|
currentQuaternion,
|
||||||
sceneInfra.camControls.camera.quaternion,
|
sceneInfra.camControls.camera.quaternion,
|
||||||
clock.getDelta()
|
delta,
|
||||||
|
cameraPassiveUpdateTimer
|
||||||
|
)
|
||||||
|
updateRayCaster(
|
||||||
|
raycasterObjects,
|
||||||
|
raycaster,
|
||||||
|
mouse,
|
||||||
|
camera,
|
||||||
|
raycasterIntersect,
|
||||||
|
delta,
|
||||||
|
raycasterPassiveUpdateTimer
|
||||||
)
|
)
|
||||||
renderer.render(scene, camera)
|
renderer.render(scene, camera)
|
||||||
|
requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
animate()
|
animate()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
renderer.dispose()
|
renderer.dispose()
|
||||||
|
disposeMouseEvents()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-none">
|
<>
|
||||||
|
<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} />
|
<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>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCamera = () => {
|
const createCamera = (): OrthographicCamera => {
|
||||||
return new OrthographicCamera(
|
return new OrthographicCamera(
|
||||||
-FRUSTUM_SIZE,
|
-FRUSTUM_SIZE,
|
||||||
FRUSTUM_SIZE,
|
FRUSTUM_SIZE,
|
||||||
@ -82,21 +168,21 @@ const createCamera = () => {
|
|||||||
|
|
||||||
const createGizmo = () => {
|
const createGizmo = () => {
|
||||||
const gizmoAxes = [
|
const gizmoAxes = [
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.x, 0, 'z'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.X, 0, 'z'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Y, Math.PI / 2, 'z'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Z, -Math.PI / 2, 'y'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI, 'z'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI, 'z'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, -Math.PI / 2, 'z'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI / 2, 'y'),
|
||||||
]
|
]
|
||||||
|
|
||||||
const gizmoAxisHeads = [
|
const gizmoAxisHeads = [
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.x, 0, 'z'),
|
createAxisHead(AxisNames.X, AxisColors.X, [AXIS_LENGTH, 0, 0]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
|
createAxisHead(AxisNames.Y, AxisColors.Y, [0, AXIS_LENGTH, 0]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
|
createAxisHead(AxisNames.Z, AxisColors.Z, [0, 0, AXIS_LENGTH]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI, 'z'),
|
createAxisHead(AxisNames.NEG_X, AxisColors.Gray, [-AXIS_LENGTH, 0, 0]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
|
createAxisHead(AxisNames.NEG_Y, AxisColors.Gray, [0, -AXIS_LENGTH, 0]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
|
createAxisHead(AxisNames.NEG_Z, AxisColors.Gray, [0, 0, -AXIS_LENGTH]),
|
||||||
]
|
]
|
||||||
|
|
||||||
return { gizmoAxes, gizmoAxisHeads }
|
return { gizmoAxes, gizmoAxisHeads }
|
||||||
@ -108,12 +194,9 @@ const createAxis = (
|
|||||||
color: ColorRepresentation,
|
color: ColorRepresentation,
|
||||||
rotation = 0,
|
rotation = 0,
|
||||||
axis = 'x'
|
axis = 'x'
|
||||||
) => {
|
): Mesh => {
|
||||||
const geometry = new BoxGeometry(length, width, width).translate(
|
const geometry = new BoxGeometry(length, width, width)
|
||||||
length / 2,
|
geometry.translate(length / 2, 0, 0)
|
||||||
0,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
const material = new MeshBasicMaterial({ color: new Color(color) })
|
||||||
const mesh = new Mesh(geometry, material)
|
const mesh = new Mesh(geometry, material)
|
||||||
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
||||||
@ -121,15 +204,17 @@ const createAxis = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createAxisHead = (
|
const createAxisHead = (
|
||||||
length: number,
|
name: AxisNames,
|
||||||
color: ColorRepresentation,
|
color: ColorRepresentation,
|
||||||
rotation = 0,
|
position: number[]
|
||||||
axis = 'x'
|
): Mesh => {
|
||||||
) => {
|
const geometry = new SphereGeometry(0.065, 16, 8)
|
||||||
const geometry = new SphereGeometry(0.065, 16, 8).translate(length, 0, 0)
|
|
||||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
const material = new MeshBasicMaterial({ color: new Color(color) })
|
||||||
const mesh = new Mesh(geometry, material)
|
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
|
return mesh
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,10 +222,97 @@ const updateCameraOrientation = (
|
|||||||
camera: OrthographicCamera,
|
camera: OrthographicCamera,
|
||||||
currentQuaternion: Quaternion,
|
currentQuaternion: Quaternion,
|
||||||
targetQuaternion: Quaternion,
|
targetQuaternion: Quaternion,
|
||||||
deltaTime: number
|
deltaTime: number,
|
||||||
|
cameraPassiveUpdateTimer: MutableRefObject<number>
|
||||||
) => {
|
) => {
|
||||||
|
cameraPassiveUpdateTimer.current += deltaTime
|
||||||
|
if (
|
||||||
|
!quaternionsEqual(currentQuaternion, targetQuaternion) ||
|
||||||
|
cameraPassiveUpdateTimer.current >= 5
|
||||||
|
) {
|
||||||
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
|
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
|
||||||
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
|
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
|
||||||
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
|
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
|
||||||
camera.quaternion.copy(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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,13 +365,6 @@ export class KclManager {
|
|||||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
||||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, 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() {
|
||||||
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
|
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
|
||||||
}
|
}
|
||||||
@ -386,24 +379,11 @@ function defaultSelectionFilter(
|
|||||||
) as SketchGroup | ExtrudeGroup
|
) as SketchGroup | ExtrudeGroup
|
||||||
firstSketchOrExtrudeGroup &&
|
firstSketchOrExtrudeGroup &&
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_batch_req',
|
type: 'modeling_cmd_req',
|
||||||
batch_id: uuidv4(),
|
|
||||||
responses: false,
|
|
||||||
requests: [
|
|
||||||
{
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'edit_mode_enter',
|
|
||||||
target: firstSketchOrExtrudeGroup.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'set_selection_filter',
|
type: 'set_selection_filter',
|
||||||
filter: ['face', 'edge', 'solid2d'],
|
filter: ['face', 'edge', 'solid2d', 'curve'],
|
||||||
},
|
},
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,6 @@ export enum ConnectionError {
|
|||||||
Unset = 0,
|
Unset = 0,
|
||||||
LongLoadingTime,
|
LongLoadingTime,
|
||||||
|
|
||||||
LostVideoStream,
|
|
||||||
ICENegotiate,
|
ICENegotiate,
|
||||||
DataChannelError,
|
DataChannelError,
|
||||||
WebSocketError,
|
WebSocketError,
|
||||||
@ -168,8 +167,6 @@ export const CONNECTION_ERROR_TEXT: Record<ConnectionError, string> = {
|
|||||||
[ConnectionError.Unset]: '',
|
[ConnectionError.Unset]: '',
|
||||||
[ConnectionError.LongLoadingTime]:
|
[ConnectionError.LongLoadingTime]:
|
||||||
'Loading is taking longer than expected...',
|
'Loading is taking longer than expected...',
|
||||||
[ConnectionError.LostVideoStream]:
|
|
||||||
'Lost connection to video stream... Reconnecting...',
|
|
||||||
[ConnectionError.ICENegotiate]: 'ICE negotiation failed.',
|
[ConnectionError.ICENegotiate]: 'ICE negotiation failed.',
|
||||||
[ConnectionError.DataChannelError]: 'The data channel signaled an error.',
|
[ConnectionError.DataChannelError]: 'The data channel signaled an error.',
|
||||||
[ConnectionError.WebSocketError]: 'The websocket signaled an error.',
|
[ConnectionError.WebSocketError]: 'The websocket signaled an error.',
|
||||||
@ -315,8 +312,6 @@ class EngineConnection extends EventTarget {
|
|||||||
if (next.type === EngineConnectionStateType.Disconnecting) {
|
if (next.type === EngineConnectionStateType.Disconnecting) {
|
||||||
const sub = next.value
|
const sub = next.value
|
||||||
if (sub.type === DisconnectingType.Error) {
|
if (sub.type === DisconnectingType.Error) {
|
||||||
console.log(sub)
|
|
||||||
|
|
||||||
// Record the last step we failed at.
|
// Record the last step we failed at.
|
||||||
// (Check the current state that we're about to override that
|
// (Check the current state that we're about to override that
|
||||||
// it was a Connecting state.)
|
// 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
|
// when assuming we're the only consumer or that all messages will
|
||||||
// be carefully formatted here.
|
// be carefully formatted here.
|
||||||
|
|
||||||
console.log(event)
|
|
||||||
|
|
||||||
if (typeof event.data !== 'string') {
|
if (typeof event.data !== 'string') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -781,7 +774,6 @@ class EngineConnection extends EventTarget {
|
|||||||
`Error in response to request ${message.request_id}:\n${errorsString}
|
`Error in response to request ${message.request_id}:\n${errorsString}
|
||||||
failed cmd type was ${artifactThatFailed?.commandType}`
|
failed cmd type was ${artifactThatFailed?.commandType}`
|
||||||
)
|
)
|
||||||
console.log(artifactThatFailed)
|
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error from server:\n${errorsString}`)
|
console.error(`Error from server:\n${errorsString}`)
|
||||||
}
|
}
|
||||||
@ -872,7 +864,6 @@ class EngineConnection extends EventTarget {
|
|||||||
this.pc
|
this.pc
|
||||||
?.createOffer()
|
?.createOffer()
|
||||||
.then((offer: RTCSessionDescriptionInit) => {
|
.then((offer: RTCSessionDescriptionInit) => {
|
||||||
console.log(offer)
|
|
||||||
this.state = {
|
this.state = {
|
||||||
type: EngineConnectionStateType.Connecting,
|
type: EngineConnectionStateType.Connecting,
|
||||||
value: {
|
value: {
|
||||||
@ -944,7 +935,6 @@ class EngineConnection extends EventTarget {
|
|||||||
|
|
||||||
case 'trickle_ice':
|
case 'trickle_ice':
|
||||||
let candidate = resp.data?.candidate
|
let candidate = resp.data?.candidate
|
||||||
console.log('trickle_ice: using this candidate: ', candidate)
|
|
||||||
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
|
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -1347,20 +1337,10 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
this.engineConnection?.addEventListener(
|
this.engineConnection?.addEventListener(
|
||||||
EngineConnectionEvents.NewTrack,
|
EngineConnectionEvents.NewTrack,
|
||||||
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
|
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
|
||||||
console.log('received track', mediaStream)
|
|
||||||
|
|
||||||
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
|
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
|
||||||
if (this.engineConnection) {
|
console.error(
|
||||||
this.engineConnection.state = {
|
'video track mute: check webrtc internals -> inbound rtp'
|
||||||
type: EngineConnectionStateType.Disconnecting,
|
)
|
||||||
value: {
|
|
||||||
type: DisconnectingType.Error,
|
|
||||||
value: {
|
|
||||||
error: ConnectionError.LostVideoStream,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setMediaStream(mediaStream)
|
setMediaStream(mediaStream)
|
||||||
@ -1673,7 +1653,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
(command.cmd.type === 'highlight_set_entity' ||
|
(command.cmd.type === 'highlight_set_entity' ||
|
||||||
command.cmd.type === 'mouse_move' ||
|
command.cmd.type === 'mouse_move' ||
|
||||||
command.cmd.type === 'camera_drag_move' ||
|
command.cmd.type === 'camera_drag_move' ||
|
||||||
command.cmd.type === 'default_camera_look_at' ||
|
|
||||||
command.cmd.type === ('default_camera_perspective_settings' as any))
|
command.cmd.type === ('default_camera_perspective_settings' as any))
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@ -1688,7 +1667,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
command.type === 'modeling_cmd_req' &&
|
command.type === 'modeling_cmd_req' &&
|
||||||
command.cmd.type !== lastMessage
|
command.cmd.type !== lastMessage
|
||||||
) {
|
) {
|
||||||
console.log('sending command', command.cmd.type)
|
|
||||||
lastMessage = command.cmd.type
|
lastMessage = command.cmd.type
|
||||||
}
|
}
|
||||||
if (command.type === 'modeling_cmd_batch_req') {
|
if (command.type === 'modeling_cmd_batch_req') {
|
||||||
@ -1702,7 +1680,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
if (
|
if (
|
||||||
(cmd.type === 'camera_drag_move' ||
|
(cmd.type === 'camera_drag_move' ||
|
||||||
cmd.type === 'handle_mouse_drag_move' ||
|
cmd.type === 'handle_mouse_drag_move' ||
|
||||||
cmd.type === 'default_camera_look_at' ||
|
|
||||||
cmd.type === ('default_camera_perspective_settings' as any)) &&
|
cmd.type === ('default_camera_perspective_settings' as any)) &&
|
||||||
this.engineConnection?.unreliableDataChannel &&
|
this.engineConnection?.unreliableDataChannel &&
|
||||||
!forceWebsocket
|
!forceWebsocket
|
||||||
|
17
src/wasm-lib/Cargo.lock
generated
17
src/wasm-lib/Cargo.lock
generated
@ -1152,9 +1152,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.28"
|
version = "0.14.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
|
checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@ -2975,9 +2975,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.13"
|
version = "0.8.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
|
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
@ -2996,9 +2996,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.13"
|
version = "0.22.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
|
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.2.5",
|
"indexmap 2.2.5",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3158,7 +3158,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ts-rs"
|
name = "ts-rs"
|
||||||
version = "8.1.0"
|
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 = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@ -3170,7 +3170,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ts-rs-macros"
|
name = "ts-rs-macros"
|
||||||
version = "8.1.0"
|
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 = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3444,6 +3444,7 @@ dependencies = [
|
|||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"futures",
|
"futures",
|
||||||
"gloo-utils",
|
"gloo-utils",
|
||||||
|
"hyper",
|
||||||
"image",
|
"image",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"kcl-lib",
|
"kcl-lib",
|
||||||
|
@ -17,13 +17,14 @@ kcl-lib = { path = "kcl" }
|
|||||||
kittycad = { workspace = true }
|
kittycad = { workspace = true }
|
||||||
serde_json = "1.0.116"
|
serde_json = "1.0.116"
|
||||||
tokio = { version = "1.38.0", features = ["sync"] }
|
tokio = { version = "1.38.0", features = ["sync"] }
|
||||||
toml = "0.8.13"
|
toml = "0.8.14"
|
||||||
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
|
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
|
||||||
wasm-bindgen = "0.2.91"
|
wasm-bindgen = "0.2.91"
|
||||||
wasm-bindgen-futures = "0.4.42"
|
wasm-bindgen-futures = "0.4.42"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
hyper = { version = "0.14.29", features = ["server", "http1"] }
|
||||||
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
||||||
kittycad = { workspace = true, default-features = true }
|
kittycad = { workspace = true, default-features = true }
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
|
@ -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"
|
|
@ -35,7 +35,7 @@ serde = { version = "1.0.203", features = ["derive"] }
|
|||||||
serde_json = "1.0.116"
|
serde_json = "1.0.116"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
thiserror = "1.0.61"
|
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
|
# 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"] }
|
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"] }
|
url = { version = "2.5.0", features = ["serde"] }
|
||||||
|
@ -11,7 +11,7 @@ use serde_json::Value as JValue;
|
|||||||
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
|
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::types::{BodyItem, FunctionExpression, KclNone, Value},
|
ast::types::{BodyItem, FunctionExpression, KclNone, Program, Value},
|
||||||
engine::EngineManager,
|
engine::EngineManager,
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
fs::FileManager,
|
fs::FileManager,
|
||||||
@ -975,6 +975,8 @@ impl Default for PipeInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The executor context.
|
/// The executor context.
|
||||||
|
/// Cloning will return another handle to the same engine connection/session,
|
||||||
|
/// as this uses `Arc` under the hood.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ExecutorContext {
|
pub struct ExecutorContext {
|
||||||
pub engine: Arc<Box<dyn EngineManager>>,
|
pub engine: Arc<Box<dyn EngineManager>>,
|
||||||
@ -1310,6 +1312,43 @@ impl ExecutorContext {
|
|||||||
pub fn update_units(&mut self, units: crate::settings::types::UnitLength) {
|
pub fn update_units(&mut self, units: crate::settings::types::UnitLength) {
|
||||||
self.settings.units = units;
|
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,
|
/// For each argument given,
|
||||||
|
@ -200,6 +200,24 @@ pub async fn revolve(args: Args) -> Result<MemoryItem, KclError> {
|
|||||||
/// axis: getOppositeEdge('revolveAxis', box)
|
/// 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 {
|
#[stdlib {
|
||||||
name = "revolve",
|
name = "revolve",
|
||||||
}]
|
}]
|
||||||
|
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_revolve6.png
Normal file
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_revolve6.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 220 KiB |
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user