Compare commits

...

4 Commits

Author SHA1 Message Date
f310283899 Try 16 core machine for playwright ubuntu 2024-07-02 11:37:10 -04:00
e42a891df8 Add nix flake and direnv config (#2694) 2024-06-18 11:32:08 -04:00
98200565bf Add a dismiss button to the command bar (#2647)
* Remove tab hotkey from selection input

* Add dismiss button to the command bar

* update Cargo.lock

* tweak close button styles

* Switch from padding to margin for positioning without messing up focus outline

* Revert "update Cargo.lock"

This reverts commit 862a6897ba.

* Restore Cargo.lock I hate VSCode sometimes

* Update Cargo.lock in src-tauri, fix clippy

---------

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2024-06-18 09:06:46 -04:00
570fd827ed fix zoom issues with sketch mode (#2664)
* cam stuff start

* more progress

* mostly done

* fix snapshot tests

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

* fix

* fix ubuntu

* more tweaks fixes

* add test

* more FE fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-18 16:08:41 +10:00
28 changed files with 805 additions and 446 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake .

View File

@ -35,7 +35,7 @@ jobs:
playwright-ubuntu: playwright-ubuntu:
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-latest-8-cores runs-on: ubuntu-latest-16-cores
needs: check-rust-changes needs: check-rust-changes
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

1
.gitignore vendored
View File

@ -17,6 +17,7 @@
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
.direnv
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*

View File

@ -38,9 +38,9 @@ document.addEventListener('mousemove', (e) =>
const deg = (Math.PI * 2) / 360 const deg = (Math.PI * 2) / 360
const commonPoints = { const commonPoints = {
startAt: '[9.06, -12.22]', startAt: '[7.19, -9.7]',
num1: 9.14, num1: 7.25,
num2: 18.2, num2: 14.44,
// num1: 9.64, // num1: 9.64,
// num2: 19.19, // num2: 19.19,
} }
@ -99,7 +99,7 @@ test('Basic sketch', async ({ page }) => {
) )
await u.closeDebugPanel() await u.closeDebugPanel()
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
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)
@ -118,13 +118,13 @@ test('Basic sketch', async ({ page }) => {
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ') await expect(u.codeLocator).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 + 0.01}], %)`)
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(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ') await expect(u.codeLocator).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 + 0.01}], %)
|> line([-${commonPoints.num2}, 0], %)`) |> line([-${commonPoints.num2}, 0], %)`)
// deselect line tool // deselect line tool
@ -154,31 +154,113 @@ test('Basic sketch', async ({ page }) => {
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ') await expect(u.codeLocator).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 + 0.01}], %)
|> angledLine([180, segLen('seg01', %)], %)`) |> angledLine([180, segLen('seg01', %)], %)`)
}) })
test('Can moving camera', async ({ page, context }) => { test.describe('Testing Camera Movement', () => {
test.skip(process.platform === 'darwin', 'Can moving camera') test('Can moving camera', async ({ page, context }) => {
const u = await getUtils(page) test.skip(process.platform === 'darwin', 'Can moving camera')
await page.setViewportSize({ width: 1200, height: 500 }) const u = await getUtils(page)
await page.goto('/') await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await page.goto('/')
await u.openAndClearDebugPanel() await u.waitForAuthSkipAppStart()
await u.closeKclCodePanel() await u.openAndClearDebugPanel()
await u.closeKclCodePanel()
const camPos: [number, number, number] = [0, 85, 85] const camPos: [number, number, number] = [0, 85, 85]
const bakeInRetries = async ( const bakeInRetries = async (
mouseActions: any, mouseActions: any,
xyz: [number, number, number], xyz: [number, number, number],
cnt = 0 cnt = 0
) => { ) => {
// hack that we're implemented our own retry instead of using retries built into playwright. // hack that we're implemented our own retry instead of using retries built into playwright.
// however each of these camera drags can be flaky, because of udp // however each of these camera drags can be flaky, because of udp
// and so putting them together means only one needs to fail to make this test extra flaky. // and so putting them together means only one needs to fail to make this test extra flaky.
// this way we can retry within the test // this way we can retry within the test
// We could break them out into separate tests, but the longest past of the test is waiting // We could break them out into separate tests, but the longest past of the test is waiting
// for the stream to start, so it can be good to bundle related things together. // for the stream to start, so it can be good to bundle related things together.
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
// rotate
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// const yo = page.getByTestId('cam-x-position').inputValue()
await u.doAndWaitForImageDiff(async () => {
await mouseActions()
await u.openAndClearDebugPanel()
await u.closeDebugPanel()
await page.waitForTimeout(100)
}, 300)
await u.openAndClearDebugPanel()
await page.getByTestId('cam-x-position').isVisible()
const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(),
page.getByTestId('cam-z-position').inputValue(),
])
const xError = Math.abs(Number(vals[0]) + xyz[0])
const yError = Math.abs(Number(vals[1]) + xyz[1])
const zError = Math.abs(Number(vals[2]) + xyz[2])
let shouldRetry = false
if (xError > 5 || yError > 5 || zError > 5) {
if (cnt > 2) {
console.log('xVal', vals[0], 'xError', xError)
console.log('yVal', vals[1], 'yError', yError)
console.log('zVal', vals[2], 'zError', zError)
throw new Error('Camera position not as expected')
}
shouldRetry = true
}
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(100)
if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1)
}
await bakeInRetries(async () => {
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(600, 303)
await page.mouse.up({ button: 'right' })
}, [4, -10.5, -120])
await bakeInRetries(async () => {
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200, { steps: 2 })
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
}, [-19, -85, -85])
const camCommand: EngineCommand = { const camCommand: EngineCommand = {
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -202,122 +284,182 @@ test('Can moving camera', async ({ page, context }) => {
await u.sendCustomCmd(updateCamCommand) await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100) await page.waitForTimeout(100)
// rotate await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100) await page.waitForTimeout(200)
// const yo = page.getByTestId('cam-x-position').inputValue()
// zoom
await u.doAndWaitForImageDiff(async () => { await u.doAndWaitForImageDiff(async () => {
await mouseActions() await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openAndClearDebugPanel() await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
await page.waitForTimeout(100)
}, 300) }, 300)
// zoom with scroll
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await page.getByTestId('cam-x-position').isVisible() // TODO, it appears we don't get the cam setting back from the engine when the interaction is zoom into `backInRetries` once the information is sent back on zoom
// await expect(Math.abs(Number(await page.getByTestId('cam-x-position').inputValue()) + 12)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-y-position').inputValue()) - 85)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-z-position').inputValue()) - 85)).toBeLessThan(1.5)
const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(),
page.getByTestId('cam-z-position').inputValue(),
])
const xError = Math.abs(Number(vals[0]) + xyz[0])
const yError = Math.abs(Number(vals[1]) + xyz[1])
const zError = Math.abs(Number(vals[2]) + xyz[2])
let shouldRetry = false
if (xError > 5 || yError > 5 || zError > 5) {
if (cnt > 2) {
console.log('xVal', vals[0], 'xError', xError)
console.log('yVal', vals[1], 'yError', yError)
console.log('zVal', vals[2], 'zError', zError)
throw new Error('Camera position not as expected')
}
shouldRetry = true
}
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(100)
if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1)
}
await bakeInRetries(async () => {
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(600, 303)
await page.mouse.up({ button: 'right' })
}, [4, -10.5, -120])
await bakeInRetries(async () => { await bakeInRetries(async () => {
await page.mouse.move(700, 400)
await page.mouse.wheel(0, -100)
}, [1, -68, -68])
})
test('Zoom should be consistent when exiting or entering sketches', async ({
page,
}) => {
// start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place
// than zoom and pan outside of sketch mode and enter again and it should not change from where it is
// than again for sketching
test.skip(process.platform !== 'darwin', 'Zoom should be consistent')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 325)
let code = `const sketch001 = startSketchOn('XY')`
await expect(u.codeLocator).toHaveText(code)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
// move the camera slightly
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.mouse.move(600, 200) await page.mouse.move(700, 300)
await page.mouse.down({ button: 'right' }) await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200, { steps: 2 }) await page.mouse.move(800, 200)
await page.mouse.up({ button: 'right' }) await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
}, [-19, -85, -85])
const camCommand: EngineCommand = { let y = 350,
type: 'modeling_cmd_req', x = 948
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
await u.clearCommandLogs() await u.canvasLocator.click({ position: { x: 783, y } })
await u.closeDebugPanel() code += `\n |> startProfileAt([8.12, -12.98], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y } })
code += `\n |> line([11.18, 0], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y: 275 } })
code += `\n |> line([0, 6.99], %)`
// await expect(u.codeLocator).toHaveText(code)
await page.getByRole('button', { name: 'Start Sketch' }).click() // click the line button
await page.waitForTimeout(200) await page.getByRole('button', { name: 'Line' }).click()
// zoom const hoverOverNothing = async () => {
await u.doAndWaitForImageDiff(async () => { // await u.canvasLocator.hover({position: {x: 700, y: 325}})
await page.keyboard.down('Control') await page.mouse.move(700, 325)
await page.mouse.move(700, 400) await page.waitForTimeout(100)
await page.mouse.down({ button: 'right' }) await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move(700, 300) }
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await u.closeDebugPanel() await page.waitForTimeout(100)
}, 300) // hover over horizontal line
await u.canvasLocator.hover({ position: { x: 800, y } })
await expect(page.getByTestId('hover-highlight')).toBeVisible()
// zoom with scroll await hoverOverNothing()
await u.openAndClearDebugPanel() // hover over vertical line
// TODO, it appears we don't get the cam setting back from the engine when the interaction is zoom into `backInRetries` once the information is sent back on zoom await u.canvasLocator.hover({ position: { x, y: 325 } })
// await expect(Math.abs(Number(await page.getByTestId('cam-x-position').inputValue()) + 12)).toBeLessThan(1.5) await expect(page.getByTestId('hover-highlight')).toBeVisible()
// await expect(Math.abs(Number(await page.getByTestId('cam-y-position').inputValue()) - 85)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-z-position').inputValue()) - 85)).toBeLessThan(1.5)
await page.getByRole('button', { name: 'Exit Sketch' }).click() await hoverOverNothing()
await bakeInRetries(async () => { // click exit sketch
await page.mouse.move(700, 400) await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.mouse.wheel(0, -100) await page.waitForTimeout(400)
}, [1, -68, -68])
await hoverOverNothing()
await page.waitForTimeout(100)
// hover over horizontal line
await page.mouse.move(858, y, { steps: 5 })
await expect(page.getByTestId('hover-highlight')).toBeVisible()
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(x, 325)
await expect(page.getByTestId('hover-highlight')).toBeVisible()
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(857, y)
await expect(page.getByTestId('hover-highlight')).toBeVisible()
// now click it
await page.mouse.click(857, y)
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
x = 975
y = 468
await page.waitForTimeout(100)
await page.mouse.move(x, 419, { steps: 5 })
await expect(page.getByTestId('hover-highlight')).toBeVisible()
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await hoverOverNothing()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
await page.mouse.move(x, 419)
await expect(page.getByTestId('hover-highlight')).toBeVisible()
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
})
}) })
test('if you click the format button it formats your code', async ({ test('if you click the format button it formats your code', async ({
@ -744,7 +886,7 @@ const sketchOnPlaneAndBackSideTest = async (
} }
const code = `const sketch001 = startSketchOn('${plane}') const code = `const sketch001 = startSketchOn('${plane}')
|> startProfileAt([1.14, -1.54], %)` |> startProfileAt([0.9, -1.22], %)`
await u.openDebugPanel() await u.openDebugPanel()
@ -1245,28 +1387,25 @@ test.describe('Testing selections', () => {
.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 + 0.01}], %)`)
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(page.locator('.cm-content'))
.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 + 0.01}], %)
|> line([-${commonPoints.num2}, 0], %)`) |> line([-${commonPoints.num2}, 0], %)`)
// deselect line tool // deselect line tool
await page.getByRole('button', { name: 'Line' }).click() await page.getByRole('button', { name: 'Line' }).click()
await u.closeDebugPanel() await u.closeDebugPanel()
const selectionSequence = async (isSecondTime = false) => { const selectionSequence = async () => {
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.move( await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
startXPx + PUR * 15,
isSecondTime ? 430 : 500 - PUR * 10
)
await expect(page.getByTestId('hover-highlight')).toBeVisible() await expect(page.getByTestId('hover-highlight')).toBeVisible()
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience // bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
@ -1276,10 +1415,7 @@ test.describe('Testing selections', () => {
// check mousing off, than mousing onto another line // check mousing off, than mousing onto another line
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move( await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
startXPx + PUR * 10,
isSecondTime ? 295 : 500 - PUR * 20
) // mouse onto another line
await expect(page.getByTestId('hover-highlight').first()).toBeVisible() await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
// now check clicking works including axis // now check clicking works including axis
@ -1376,8 +1512,33 @@ test.describe('Testing selections', () => {
await page.waitForTimeout(300) // wait for animation await page.waitForTimeout(300) // wait for animation
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: 0, y: -1378.01, 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)
await emptySpaceClick()
await u.closeDebugPanel()
// hover again and check it works // hover again and check it works
await selectionSequence(true) await selectionSequence()
}) })
test('Hovering over 3d features highlights code', async ({ page }) => { test('Hovering over 3d features highlights code', async ({ page }) => {
@ -1761,6 +1922,7 @@ test('Can add multiple sketches', async ({ page }) => {
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(50, 0) await click00r(50, 0)
await page.waitForTimeout(100)
codeStr += ` |> line(${toSU([50, 0])}, %)` codeStr += ` |> line(${toSU([50, 0])}, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
@ -1785,26 +1947,26 @@ test('Can add multiple sketches', async ({ page }) => {
// when exiting the sketch above the camera is still looking down at XY, // when exiting the sketch above the camera is still looking down at XY,
// so selecting the plane again is a bit easier. // so selecting the plane again is a bit easier.
await page.mouse.click(center.x + 30, center.y) await page.mouse.click(center.x + 200, center.y + 100)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
codeStr += "const sketch002 = startSketchOn('XY')" codeStr += "const sketch002 = startSketchOn('XY')"
await expect(u.codeLocator).toHaveText(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([1.53, 0], %)`
await expect(u.codeLocator).toHaveText(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([1.53, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 30) await click00r(0, 30)
codeStr += ` |> line(${toSU([0, 30])}, %)` codeStr += ` |> line([0, -1.53], %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(-30, 0) await click00r(-30, 0)
codeStr += ` |> line(${toSU([-30 - 0.1, 0])}, %)` codeStr += ` |> line([-1.53, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
click00r(undefined, undefined) click00r(undefined, undefined)
@ -1812,7 +1974,6 @@ test('Can add multiple sketches', async ({ page }) => {
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([100, 100, 100]) await u.updateCamPosition([100, 100, 100])
await page.waitForTimeout(250)
await u.clearCommandLogs() await u.clearCommandLogs()
}) })
@ -2217,9 +2378,9 @@ const doSnapAtDifferentScales = async (
await u.openDebugPanel() await u.openDebugPanel()
const code = `const sketch001 = startSketchOn('-XZ') const code = `const sketch001 = startSketchOn('-XZ')
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %) |> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|> line([${roundOff(scale * 175.36)}, 0], %) |> line([${roundOff(scale * 139.19)}, 0], %)
|> line([0, -${roundOff(scale * 175.36) + fudge}], %) |> line([0, -${roundOff(scale * 139.2)}], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)` |> close(%)`
@ -2249,6 +2410,7 @@ const doSnapAtDifferentScales = async (
const pointC = [900, 400] const pointC = [900, 400]
// draw three lines // draw three lines
await page.waitForTimeout(500)
await page.mouse.click(pointA[0], pointA[1]) await page.mouse.click(pointA[0], pointA[1])
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
@ -2384,11 +2546,8 @@ test('Sketch on face', async ({ page }) => {
await page.getByText('startProfileAt([-12.94, 6.6], %)').click() await page.getByText('startProfileAt([-12.94, 6.6], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Edit Sketch' }).click()
() => page.getByRole('button', { name: 'Edit Sketch' }).click(), await page.waitForTimeout(400)
'default_camera_get_settings',
true
)
await page.waitForTimeout(150) await page.waitForTimeout(150)
await page.setViewportSize({ width: 1200, height: 1200 }) await page.setViewportSize({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
@ -4658,13 +4817,15 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
await expect( await expect(
page.getByRole('button', { name: 'Exit Sketch' }) page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible() ).not.toBeVisible()
await page.waitForTimeout(400)
// Extrude // Extrude
await page.mouse.click(750, 150) await page.mouse.click(750, 150)
await expect(extrudeButton).not.toBeDisabled() await expect(extrudeButton).not.toBeDisabled()
await page.keyboard.press('e') await page.keyboard.press('e')
await page.mouse.move(730, 230, { steps: 5 }) await page.waitForTimeout(100)
await page.mouse.click(730, 230) await page.mouse.move(900, 200, { steps: 5 })
await page.mouse.click(900, 200)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Continue' }).click() await page.getByRole('button', { name: 'Continue' }).click()
await page.getByRole('button', { name: 'Submit command' }).click() await page.getByRole('button', { name: 'Submit command' }).click()
@ -4768,7 +4929,7 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
) )
await u.closeDebugPanel() await u.closeDebugPanel()
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
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)
@ -4847,15 +5008,15 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
.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([-11.64, 11.11], %)`) |> line([-9.16, 8.81], %)`)
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(page.locator('.cm-content'))
.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([-11.64, 11.11], %) |> line([-9.16, 8.81], %)
|> line([-6.56, 0], %)`) |> line([-5.28, 0], %)`)
// Unequip line tool // Unequip line tool
await page.keyboard.press('Escape') await page.keyboard.press('Escape')

View File

@ -405,17 +405,16 @@ test('Draft segments should look right', async ({ page, context }) => {
// 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( let code = `const sketch001 = startSketchOn('XZ')`
`const sketch001 = startSketchOn('XZ')` await expect(page.locator('.cm-content')).toHaveText(code)
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation await page.waitForTimeout(700) // TODO detect animation ending, or disable animation
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')) code += `
.toHaveText(`const sketch001 = startSketchOn('XZ') |> startProfileAt([7.19, -9.7], %)`
|> startProfileAt([9.06, -12.22], %)`) await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -427,10 +426,9 @@ test('Draft segments should look right', async ({ page, context }) => {
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')) code += `
.toHaveText(`const sketch001 = startSketchOn('XZ') |> line([7.25, 0], %)`
|> startProfileAt([9.06, -12.22], %) await expect(page.locator('.cm-content')).toHaveText(code)
|> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -513,17 +511,16 @@ test.describe('Client side scene scale should match engine scale', () => {
// 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( let code = `const sketch001 = startSketchOn('XZ')`
`const sketch001 = startSketchOn('XZ')` await expect(page.locator('.cm-content')).toHaveText(code)
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
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')) code += `
.toHaveText(`const sketch001 = startSketchOn('XZ') |> startProfileAt([7.19, -9.7], %)`
|> startProfileAt([9.06, -12.22], %)`) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -531,21 +528,18 @@ test.describe('Client side scene scale should match engine scale', () => {
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')) code += `
.toHaveText(`const sketch001 = startSketchOn('XZ') |> line([7.25, 0], %)`
|> startProfileAt([9.06, -12.22], %) await expect(u.codeLocator).toHaveText(code)
|> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content')) code += `
.toHaveText(`const sketch001 = startSketchOn('XZ') |> tangentialArcTo([21.7, -2.44], %)`
|> startProfileAt([9.06, -12.22], %) await expect(u.codeLocator).toHaveText(code)
|> line([9.14, 0], %)
|> tangentialArcTo([27.34, -3.08], %)`)
// click tangential arc tool again to unequip it // click tangential arc tool again to unequip it
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -616,17 +610,16 @@ test.describe('Client side scene scale should match engine scale', () => {
// 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( let code = `const sketch001 = startSketchOn('XZ')`
`const sketch001 = startSketchOn('XZ')` await expect(u.codeLocator).toHaveText(code)
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
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')) code += `
.toHaveText(`const sketch001 = startSketchOn('XZ') |> startProfileAt([182.59, -246.32], %)`
|> startProfileAt([230.03, -310.32], %)`) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -634,21 +627,18 @@ test.describe('Client side scene scale should match engine scale', () => {
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')) code += `
.toHaveText(`const sketch001 = startSketchOn('XZ') |> line([184.3, 0], %)`
|> startProfileAt([230.03, -310.32], %) await expect(u.codeLocator).toHaveText(code)
|> line([232.2, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content')) code += `
.toHaveText(`const sketch001 = startSketchOn('XZ') |> tangentialArcTo([551.2, -62.01], %)`
|> startProfileAt([230.03, -310.32], %) await expect(u.codeLocator).toHaveText(code)
|> line([232.2, 0], %)
|> tangentialArcTo([694.43, -78.12], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -132,8 +132,8 @@ export const getMovementUtils = (opts: any) => {
// NOTE: these pretty much can't be perfect because of screen scaling. // NOTE: these pretty much can't be perfect because of screen scaling.
// Handle on a case-by-case. // Handle on a case-by-case.
const toU = (x: number, y: number) => [ const toU = (x: number, y: number) => [
kcRound(x * 0.0854), kcRound(x * 0.0678),
kcRound(-y * 0.0854), // Y is inverted in our coordinate system kcRound(-y * 0.0678), // Y is inverted in our coordinate system
] ]
// Turn the array into a string with specific formatting // Turn the array into a string with specific formatting
@ -226,6 +226,7 @@ export async function getUtils(page: Page) {
.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'), codeLocator: page.locator('.cm-content'),
canvasLocator: page.getByTestId('client-side-scene'),
doAndWaitForCmd: async ( doAndWaitForCmd: async (
fn: () => Promise<void>, fn: () => Promise<void>,
commandType: string, commandType: string,

62
flake.lock generated Normal file
View File

@ -0,0 +1,62 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1718470082,
"narHash": "sha256-u2F0MMYE+Efc+ocruTbtU/wWHuYHWcJafp5zJ++n/YE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3027ba73dfef68eb555fc2fa97aed4e999e74f97",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1718681902,
"narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "16c8ad83297c278eebe740dea5491c1708960dd1",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

70
flake.nix Normal file
View File

@ -0,0 +1,70 @@
{
description = "modeling-app development environment";
# Flake inputs
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
rust-overlay.url = "github:oxalica/rust-overlay"; # A helper for Rust + Nix
};
# Flake outputs
outputs = { self, nixpkgs, rust-overlay }:
let
# Overlays enable you to customize the Nixpkgs attribute set
overlays = [
# Makes a `rust-bin` attribute available in Nixpkgs
(import rust-overlay)
# Provides a `rustToolchain` attribute for Nixpkgs that we can use to
# create a Rust environment
(self: super: {
rustToolchain = super. rust-bin.stable.latest.default.override {
targets = [ "wasm32-unknown-unknown" ];
extensions = [ "rustfmt" "llvm-tools-preview" ];
};
})
];
# Systems supported
allSystems = [
"x86_64-linux" # 64-bit Intel/AMD Linux
"aarch64-linux" # 64-bit ARM Linux
"x86_64-darwin" # 64-bit Intel macOS
"aarch64-darwin" # 64-bit ARM macOS
];
# Helper to provide system-specific attributes
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
pkgs = import nixpkgs { inherit overlays system; };
});
in
{
# Development environment output
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
# The Nix packages provided in the environment
packages = (with pkgs; [
# The package provided by our custom overlay. Includes cargo, Clippy, cargo-fmt,
# rustdoc, rustfmt, and other tools.
rustToolchain
cargo-llvm-cov
cargo-nextest
just
postgresql.lib
openssl
pkg-config
nodejs_22
]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [
libiconv
darwin.apple_sdk.frameworks.Security
]);
TARGET_CC = "${pkgs.stdenv.cc}/bin/${pkgs.stdenv.cc.targetPrefix}cc";
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
};
});
};
}

View File

@ -127,7 +127,7 @@ export function App() {
/> />
<ModalContainer /> <ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} /> <ModelingSidebar paneOpacity={paneOpacity} />
<Stream className="absolute inset-0 z-0" /> <Stream />
{/* <CamToggle /> */} {/* <CamToggle /> */}
<LowerRightControls> <LowerRightControls>
<Gizmo /> <Gizmo />

View File

@ -174,41 +174,6 @@ export class CameraControls {
} }
} }
throttledUpdateEngineFov = throttle(
(vals: {
position: Vector3
quaternion: Quaternion
zoom: number
fov: number
target: Vector3
}) => {
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_perspective_settings',
...convertThreeCamValuesToEngineCam({
...vals,
isPerspective: true,
}),
fov_y: vals.fov,
...calculateNearFarFromFOV(vals.fov),
},
}
this.engineCommandManager.sendSceneCommand(cmd)
this.lastPerspectiveCmd = cmd
this.lastPerspectiveCmdTime = Date.now()
if (this.lastPerspectiveCmdTimeoutId !== null) {
clearTimeout(this.lastPerspectiveCmdTimeoutId)
}
this.lastPerspectiveCmdTimeoutId = setTimeout(
this.sendLastPerspectiveReliableChannel,
lastCmdDelay
) as any as number
},
1000 / 30
)
constructor( constructor(
isOrtho = false, isOrtho = false,
domElement: HTMLCanvasElement, domElement: HTMLCanvasElement,
@ -534,26 +499,28 @@ export class CameraControls {
direction.normalize() direction.normalize()
this.camera.position.copy(this.target).addScaledVector(direction, distance) this.camera.position.copy(this.target).addScaledVector(direction, distance)
} }
usePerspectiveCamera = () => { usePerspectiveCamera = async () => {
this._usePerspectiveCamera() this._usePerspectiveCamera()
this.engineCommandManager.sendSceneCommand({ if (this.syncDirection === 'clientToEngine') {
type: 'modeling_cmd_req', await this.engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(), type: 'modeling_cmd_req',
cmd: { cmd_id: uuidv4(),
type: 'default_camera_set_perspective', cmd: {
parameters: { type: 'default_camera_set_perspective',
fov_y: parameters: {
this.camera instanceof PerspectiveCamera ? this.camera.fov : 45, fov_y:
...calculateNearFarFromFOV(this.lastPerspectiveFov), this.camera instanceof PerspectiveCamera ? this.camera.fov : 45,
...calculateNearFarFromFOV(this.lastPerspectiveFov),
},
}, },
}, })
}) }
this.onCameraChange() this.onCameraChange()
this.update() this.update()
return this.camera return this.camera
} }
dollyZoom = (newFov: number) => { dollyZoom = async (newFov: number, splitEngineCalls = false) => {
if (!(this.camera instanceof PerspectiveCamera)) { if (!(this.camera instanceof PerspectiveCamera)) {
console.warn('Dolly zoom is only applicable to perspective cameras.') console.warn('Dolly zoom is only applicable to perspective cameras.')
return return
@ -604,13 +571,52 @@ export class CameraControls {
this.camera.near = z_near this.camera.near = z_near
this.camera.far = z_far this.camera.far = z_far
this.throttledUpdateEngineFov({ if (splitEngineCalls) {
fov: newFov, await this.engineCommandManager.sendSceneCommand({
position: newPosition, type: 'modeling_cmd_req',
quaternion: this.camera.quaternion, cmd_id: uuidv4(),
zoom: this.camera.zoom, cmd: {
target: this.target, type: 'default_camera_look_at',
}) ...convertThreeCamValuesToEngineCam({
isPerspective: true,
position: newPosition,
quaternion: this.camera.quaternion,
zoom: this.camera.zoom,
target: this.target,
}),
},
})
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
parameters: {
fov_y: newFov,
z_near: 0.01,
z_far: 1000,
},
},
})
} else {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_perspective_settings',
...convertThreeCamValuesToEngineCam({
isPerspective: true,
position: newPosition,
quaternion: this.camera.quaternion,
zoom: this.camera.zoom,
target: this.target,
}),
fov_y: newFov,
z_near: 0.01,
z_far: 1000,
},
})
}
} }
update = (forceUpdate = false) => { update = (forceUpdate = false) => {
@ -1015,6 +1021,29 @@ export class CameraControls {
.onComplete(onComplete) .onComplete(onComplete)
.start() .start()
}) })
snapToPerspectiveBeforeHandingBackControlToEngine = async (
targetCamUp = new Vector3(0, 0, 1)
) => {
if (this.syncDirection === 'engineToClient') {
console.warn(
'animate To Perspective not design to work with engineToClient syncDirection.'
)
}
this.isFovAnimationInProgress = true
const targetFov = this.fovBeforeOrtho // Target FOV for perspective
this.lastPerspectiveFov = 4
let currentFov = 4
const initialCameraUp = this.camera.up.clone()
this.usePerspectiveCamera()
const tempVec = new Vector3()
currentFov = this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov)
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, 1)
this.camera.up.copy(currentUp)
await this.dollyZoom(currentFov, true)
this.isFovAnimationInProgress = false
}
get reactCameraProperties(): ReactCameraProperties { get reactCameraProperties(): ReactCameraProperties {
return { return {
@ -1087,7 +1116,7 @@ function calculateNearFarFromFOV(fov: number) {
// const nearFarRatio = (fov - 3) / (45 - 3) // const nearFarRatio = (fov - 3) / (45 - 3)
// const z_near = 0.1 + nearFarRatio * (5 - 0.1) // const z_near = 0.1 + nearFarRatio * (5 - 0.1)
// const z_far = 1000 + nearFarRatio * (100000 - 1000) // const z_far = 1000 + nearFarRatio * (100000 - 1000)
return { z_near: 0.1, z_far: 1000 } return { z_near: 0.01, z_far: 1000 }
} }
function convertThreeCamValuesToEngineCam({ function convertThreeCamValuesToEngineCam({
@ -1106,11 +1135,6 @@ function convertThreeCamValuesToEngineCam({
// leaving for now since it's working but maybe revisit later // leaving for now since it's working but maybe revisit later
const euler = new Euler().setFromQuaternion(quaternion, 'XYZ') const euler = new Euler().setFromQuaternion(quaternion, 'XYZ')
const lookAtVector = new Vector3(0, 0, -1)
.applyEuler(euler)
.normalize()
.add(position)
const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize() const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize()
if (isPerspective) { if (isPerspective) {
return { return {
@ -1119,6 +1143,10 @@ function convertThreeCamValuesToEngineCam({
vantage: position, vantage: position,
} }
} }
const lookAtVector = new Vector3(0, 0, -1)
.applyEuler(euler)
.normalize()
.add(position)
const fudgeFactor2 = zoom * 0.9979224466814468 - 0.03473692325839295 const fudgeFactor2 = zoom * 0.9979224466814468 - 0.03473692325839295
const zoomFactor = (-ZOOM_MAGIC_NUMBER + fudgeFactor2) / zoom const zoomFactor = (-ZOOM_MAGIC_NUMBER + fudgeFactor2) / zoom
const direction = lookAtVector.clone().sub(position).normalize() const direction = lookAtVector.clone().sub(position).normalize()

View File

@ -136,6 +136,7 @@ export const ClientSideScene = ({
<div <div
ref={canvasRef} ref={canvasRef}
style={{ cursor: cursor }} style={{ cursor: cursor }}
data-testid="client-side-scene"
className={`absolute inset-0 h-full w-full transition-all duration-300 ${ className={`absolute inset-0 h-full w-full transition-all duration-300 ${
hideClient ? 'opacity-0' : 'opacity-100' hideClient ? 'opacity-0' : 'opacity-100'
} ${hideServer ? 'bg-chalkboard-10 dark:bg-chalkboard-100' : ''} ${ } ${hideServer ? 'bg-chalkboard-10 dark:bg-chalkboard-100' : ''} ${

View File

@ -1329,13 +1329,6 @@ export class SceneEntities {
to, to,
}) })
} }
async animateAfterSketch() {
// if (isReducedMotion()) {
// sceneInfra.camControls.usePerspectiveCamera()
// return
// }
await sceneInfra.camControls.animateToPerspective()
}
removeSketchGrid() { removeSketchGrid() {
if (this.axisGroup) this.scene.remove(this.axisGroup) if (this.axisGroup) this.scene.remove(this.axisGroup)
} }
@ -1399,114 +1392,135 @@ export class SceneEntities {
selected.material.color = defaultPlaneColor(type) selected.material.color = defaultPlaneColor(type)
}, },
onClick: async (args) => { onClick: async (args) => {
const checkExtrudeFaceClick = async (): Promise< const { streamDimensions } = useStore.getState()
['face' | 'plane' | 'other', string] const { entity_id, ...rest } = await sendSelectEventToEngine(
> => { args?.mouseEvent,
const { streamDimensions } = useStore.getState() document.getElementById('video-stream') as HTMLVideoElement,
const { entity_id } = await sendSelectEventToEngine( streamDimensions
args?.mouseEvent, )
document.getElementById('video-stream') as HTMLVideoElement, let _entity_id = entity_id
streamDimensions console.log('things', _entity_id, rest)
) if (!_entity_id) return
if (!entity_id) return ['other', ''] if (
if ( engineCommandManager.defaultPlanes?.xy === _entity_id ||
engineCommandManager.defaultPlanes?.xy === entity_id || engineCommandManager.defaultPlanes?.xz === _entity_id ||
engineCommandManager.defaultPlanes?.xz === entity_id || engineCommandManager.defaultPlanes?.yz === _entity_id ||
engineCommandManager.defaultPlanes?.yz === entity_id engineCommandManager.defaultPlanes?.negXy === _entity_id ||
) { engineCommandManager.defaultPlanes?.negXz === _entity_id ||
return ['plane', entity_id] engineCommandManager.defaultPlanes?.negYz === _entity_id
) {
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
[engineCommandManager.defaultPlanes.xy]: 'XY',
[engineCommandManager.defaultPlanes.xz]: 'XZ',
[engineCommandManager.defaultPlanes.yz]: 'YZ',
[engineCommandManager.defaultPlanes.negXy]: '-XY',
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
} }
const artifact = this.engineCommandManager.artifactMap[entity_id] // TODO can we get this information from rust land when it creates the default planes?
// If we clicked on an extrude wall, we climb up the parent Id // maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
// to get the sketch profile's face ID. If we clicked on an endcap, let zAxis: [number, number, number] = [0, 0, 1]
// we already have it. let yAxis: [number, number, number] = [0, 1, 0]
const targetId =
'additionalData' in artifact &&
artifact.additionalData?.type === 'cap'
? entity_id
: artifact.parentId
// tsc cannot infer that target can have extrusions // get unit vector from camera position to target
// from the commandType (why?) so we need to cast it const camVector = sceneInfra.camControls.camera.position
const target = this.engineCommandManager.artifactMap?.[ .clone()
targetId || '' .sub(sceneInfra.camControls.target)
] as ArtifactMapCommand & { extrusions?: string[] }
// TODO: We get the first extrusion command ID, if (engineCommandManager.defaultPlanes?.xy === _entity_id) {
// which is fine while backend systems only support one extrusion. console.log('XY')
// but we need to more robustly handle resolving to the correct extrusion zAxis = [0, 0, 1]
// if there are multiple. yAxis = [0, 1, 0]
const extrusions = if (camVector.z < 0) {
this.engineCommandManager.artifactMap?.[ zAxis = [0, 0, -1]
target?.extrusions?.[0] || '' _entity_id = engineCommandManager.defaultPlanes?.negXy || ''
] }
} else if (engineCommandManager.defaultPlanes?.yz === _entity_id) {
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info') console.log('YZ')
return ['other', entity_id] zAxis = [1, 0, 0]
yAxis = [0, 0, 1]
const faceInfo = await getFaceDetails(entity_id) if (camVector.x < 0) {
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis) zAxis = [-1, 0, 0]
return ['other', entity_id] _entity_id = engineCommandManager.defaultPlanes?.negYz || ''
const { z_axis, y_axis, origin } = faceInfo }
const sketchPathToNode = getNodePathFromSourceRange( } else if (engineCommandManager.defaultPlanes?.xz === _entity_id) {
kclManager.ast, console.log('XZ')
artifact.range zAxis = [0, 1, 0]
) yAxis = [0, 0, 1]
const extrudePathToNode = extrusions?.range _entity_id = engineCommandManager.defaultPlanes?.negXz || ''
? getNodePathFromSourceRange(kclManager.ast, extrusions.range) if (camVector.y < 0) {
: [] zAxis = [0, -1, 0]
_entity_id = engineCommandManager.defaultPlanes?.xz || ''
}
}
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'Select default plane', type: 'Select default plane',
data: { data: {
type: 'extrudeFace', type: 'defaultPlane',
zAxis: [z_axis.x, z_axis.y, z_axis.z], planeId: _entity_id,
yAxis: [y_axis.x, y_axis.y, y_axis.z], plane: defaultPlaneStrMap[_entity_id],
position: [origin.x, origin.y, origin.z].map( zAxis,
(num) => num / sceneInfra._baseUnitMultiplier yAxis,
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap:
artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info
: 'none',
faceId: entity_id,
}, },
}) })
return ['face', entity_id] return
} }
const artifact = this.engineCommandManager.artifactMap[_entity_id]
// If we clicked on an extrude wall, we climb up the parent Id
// to get the sketch profile's face ID. If we clicked on an endcap,
// we already have it.
const targetId =
'additionalData' in artifact &&
artifact.additionalData?.type === 'cap'
? _entity_id
: artifact.parentId
const faceResult = await checkExtrudeFaceClick() // tsc cannot infer that target can have extrusions
if (faceResult[0] === 'face') return // from the commandType (why?) so we need to cast it
const target = this.engineCommandManager.artifactMap?.[
targetId || ''
] as ArtifactMapCommand & { extrusions?: string[] }
// TODO: We get the first extrusion command ID,
// which is fine while backend systems only support one extrusion.
// but we need to more robustly handle resolving to the correct extrusion
// if there are multiple.
const extrusions =
this.engineCommandManager.artifactMap?.[target?.extrusions?.[0] || '']
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info') return
const faceInfo = await getFaceDetails(_entity_id)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis) return
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
)
const extrudePathToNode = extrusions?.range
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
: []
if (!args || !args.intersects?.[0]) return
if (args.mouseEvent.which !== 1) return
const { intersects } = args
const type = intersects?.[0].object.name || ''
const posNorm = Number(intersects?.[0]?.normal?.z) > 0
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
let zAxis: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
let yAxis: [number, number, number] = [0, 1, 0]
if (type === YZ_PLANE) {
planeString = posNorm ? 'YZ' : '-YZ'
zAxis = posNorm ? [1, 0, 0] : [-1, 0, 0]
yAxis = [0, 0, 1]
} else if (type === XZ_PLANE) {
planeString = posNorm ? '-XZ' : 'XZ'
zAxis = posNorm ? [0, 1, 0] : [0, -1, 0]
yAxis = [0, 0, 1]
}
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'Select default plane', type: 'Select default plane',
data: { data: {
type: 'defaultPlane', type: 'extrudeFace',
plane: planeString, zAxis: [z_axis.x, z_axis.y, z_axis.z],
zAxis, yAxis: [y_axis.x, y_axis.y, y_axis.z],
yAxis, position: [origin.x, origin.y, origin.z].map(
planeId: faceResult[1], (num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap:
artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info
: 'none',
faceId: _entity_id,
}, },
}) })
return
}, },
}) })
} }

View File

@ -6,6 +6,8 @@ import CommandComboBox from '../CommandComboBox'
import CommandBarReview from './CommandBarReview' import CommandBarReview from './CommandBarReview'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
export const CommandBar = () => { export const CommandBar = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
@ -103,7 +105,7 @@ export const CommandBar = () => {
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<WrapperComponent.Panel <WrapperComponent.Panel
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70" className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded rounded-tl-none shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
as="div" as="div"
data-testid="command-bar" data-testid="command-bar"
> >
@ -116,6 +118,19 @@ export const CommandBar = () => {
<CommandBarReview stepBack={stepBack} /> <CommandBarReview stepBack={stepBack} />
) )
)} )}
<button
onClick={() => commandBarSend({ type: 'Close' })}
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
>
<CustomIcon
name="close"
className="w-5 h-5 rounded-sm bg-destroy-10 text-destroy-80 dark:bg-destroy-80 dark:text-destroy-10 group-hover:brightness-110"
/>
<Tooltip position="bottom" delay={500}>
Cancel{' '}
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
</Tooltip>
</button>
</WrapperComponent.Panel> </WrapperComponent.Panel>
</Transition.Child> </Transition.Child>
</WrapperComponent> </WrapperComponent>

View File

@ -7,10 +7,8 @@ import {
getSelectionType, getSelectionType,
getSelectionTypeDisplayText, getSelectionTypeDisplayText,
} from 'lib/selections' } from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { StateFrom } from 'xstate' import { StateFrom } from 'xstate'
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) => const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
@ -41,12 +39,6 @@ function CommandBarSelectionInput({
canSubmitSelectionArg(selectionsByType, arg) canSubmitSelectionArg(selectionsByType, arg)
) )
useHotkeys('tab', () => onSubmit(selection), {
enableOnFormTags: true,
enableOnContentEditable: true,
keyup: true,
})
useEffect(() => { useEffect(() => {
inputRef.current?.focus() inputRef.current?.focus()
}, [selection, inputRef]) }, [selection, inputRef])

View File

@ -74,8 +74,8 @@ const CustomIconMap = {
bug: ( bug: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M10.8209 5.99884C10.6403 5.73962 10.3399 5.57001 10 5.57001C9.65984 5.57001 9.35936 5.73984 9.17871 5.99935C9.43724 5.95129 9.71142 5.92578 10.0012 5.92578C10.29 5.92578 10.5633 5.95111 10.8209 5.99884ZM10 4.57001C8.9459 4.57001 8.08227 5.38548 8.00554 6.41997C7.58916 6.65398 7.23724 6.95989 6.95014 7.31304L5.85355 6.21645L5.14645 6.92356L6.40931 8.18642C6.20774 8.62503 6.08043 9.09624 6.0278 9.57001H5V10.57H6.01946C6.06396 11.1581 6.1867 11.8173 6.4071 12.4558L5.14645 13.7165L5.85355 14.4236L6.8408 13.4363C7.46354 14.555 8.47307 15.4258 10.0012 15.4258C11.529 15.4258 12.5378 14.5554 13.16 13.4371L14.1464 14.4236L14.8536 13.7165L13.5934 12.4563C13.8136 11.8177 13.9362 11.1583 13.9806 10.57H15V9.57001H13.9722C13.9197 9.0961 13.7925 8.62474 13.5911 8.18602L14.8536 6.92356L14.1464 6.21645L13.0505 7.31239C12.7633 6.95894 12.4112 6.65285 11.9944 6.41883C11.9171 5.38488 11.0537 4.57001 10 4.57001ZM10.5 14.3801V8.57001H9.5V14.3796C8.72105 14.2298 8.15885 13.7245 7.7428 12.9999C7.22316 12.095 7 10.937 7 10.07C7 8.46381 8.04281 6.92578 10.0012 6.92578C11.9589 6.92578 13 8.4629 13 10.07C13 10.9373 12.7773 12.0954 12.2582 13.0003C11.8422 13.7254 11.2799 14.2309 10.5 14.3801Z" d="M10.8209 5.99884C10.6403 5.73962 10.3399 5.57001 10 5.57001C9.65984 5.57001 9.35936 5.73984 9.17871 5.99935C9.43724 5.95129 9.71142 5.92578 10.0012 5.92578C10.29 5.92578 10.5633 5.95111 10.8209 5.99884ZM10 4.57001C8.9459 4.57001 8.08227 5.38548 8.00554 6.41997C7.58916 6.65398 7.23724 6.95989 6.95014 7.31304L5.85355 6.21645L5.14645 6.92356L6.40931 8.18642C6.20774 8.62503 6.08043 9.09624 6.0278 9.57001H5V10.57H6.01946C6.06396 11.1581 6.1867 11.8173 6.4071 12.4558L5.14645 13.7165L5.85355 14.4236L6.8408 13.4363C7.46354 14.555 8.47307 15.4258 10.0012 15.4258C11.529 15.4258 12.5378 14.5554 13.16 13.4371L14.1464 14.4236L14.8536 13.7165L13.5934 12.4563C13.8136 11.8177 13.9362 11.1583 13.9806 10.57H15V9.57001H13.9722C13.9197 9.0961 13.7925 8.62474 13.5911 8.18602L14.8536 6.92356L14.1464 6.21645L13.0505 7.31239C12.7633 6.95894 12.4112 6.65285 11.9944 6.41883C11.9171 5.38488 11.0537 4.57001 10 4.57001ZM10.5 14.3801V8.57001H9.5V14.3796C8.72105 14.2298 8.15885 13.7245 7.7428 12.9999C7.22316 12.095 7 10.937 7 10.07C7 8.46381 8.04281 6.92578 10.0012 6.92578C11.9589 6.92578 13 8.4629 13 10.07C13 10.9373 12.7773 12.0954 12.2582 13.0003C11.8422 13.7254 11.2799 14.2309 10.5 14.3801Z"
fill="currentColor" fill="currentColor"
/> />

View File

@ -76,6 +76,7 @@ import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards' import { getVarNameModal } from 'hooks/useToolbarGuards'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { uuidv4 } from 'lib/utils'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -141,7 +142,41 @@ export const ModelingMachineProvider = ({
{ {
actions: { actions: {
'sketch exit execute': () => { 'sketch exit execute': () => {
kclManager.executeCode(true) ;(async () => {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
sceneInfra.camControls.syncDirection = 'engineToClient'
const settings: Models['CameraSettings_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
)?.data?.data?.settings
if (settings.up.z !== 1) {
// workaround for gimbal lock situation
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: settings.center,
vantage: {
...settings.pos,
y:
settings.pos.y +
(settings.center.z - settings.pos.z > 0 ? 2 : -2),
},
up: { x: 0, y: 0, z: 1 },
},
})
}
kclManager.executeCode(true)
})()
}, },
'Set mouse state': assign({ 'Set mouse state': assign({
mouseState: (_, event) => event.data, mouseState: (_, event) => event.data,
@ -464,7 +499,7 @@ export const ModelingMachineProvider = ({
engineCommandManager, engineCommandManager,
data.faceId data.faceId
) )
sceneInfra.camControls.syncDirection = 'clientToEngine'
return { return {
sketchPathToNode: pathToNewSketchNode, sketchPathToNode: pathToNewSketchNode,
zAxis: data.zAxis, zAxis: data.zAxis,
@ -478,8 +513,10 @@ export const ModelingMachineProvider = ({
) )
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
const quat = await getSketchQuaternion(pathToNode, data.zAxis) await letEngineAnimateAndSyncCamAfter(
await sceneInfra.camControls.tweenCameraToQuaternion(quat) engineCommandManager,
data.planeId
)
return { return {
sketchPathToNode: pathToNode, sketchPathToNode: pathToNode,
zAxis: data.zAxis, zAxis: data.zAxis,

View File

@ -126,8 +126,8 @@ export const Stream = ({ className = '' }: { className?: string }) => {
return ( return (
<div <div
id="stream" className="absolute inset-0 z-0"
className={className} data-testid="stream"
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
@ -142,7 +142,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
onMouseMoveCapture={handleMouseMove} onMouseMoveCapture={handleMouseMove}
className="w-full cursor-pointer h-full" className="w-full cursor-pointer h-full"
disablePictureInPicture disablePictureInPicture
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
id="video-stream" id="video-stream"
/> />
<ClientSideScene <ClientSideScene

View File

@ -249,3 +249,10 @@ code {
.cm-ghostText * { .cm-ghostText * {
color: rgb(120, 120, 120, 0.8) !important; color: rgb(120, 120, 120, 0.8) !important;
} }
@layer components {
kbd.hotkey {
@apply font-mono text-xs inline-block px-1 py-0.5 rounded-sm;
@apply bg-chalkboard-20 dark:bg-chalkboard-90;
}
}

View File

@ -364,18 +364,55 @@ export class KclManager {
return this?.engineCommandManager?.defaultPlanes return this?.engineCommandManager?.defaultPlanes
} }
showPlanes() { showPlanes(all = false) {
if (!this.defaultPlanes) return if (!this.defaultPlanes) return Promise.all([])
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false) const thePromises = [
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false) this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false),
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false) this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false),
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false),
]
if (all) {
thePromises.push(
this.engineCommandManager.setPlaneHidden(
this.defaultPlanes.negXy,
false
)
)
thePromises.push(
this.engineCommandManager.setPlaneHidden(
this.defaultPlanes.negYz,
false
)
)
thePromises.push(
this.engineCommandManager.setPlaneHidden(
this.defaultPlanes.negXz,
false
)
)
}
return Promise.all(thePromises)
} }
hidePlanes() { hidePlanes(all = false) {
if (!this.defaultPlanes) return if (!this.defaultPlanes) return Promise.all([])
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true) const thePromises = [
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true) this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true),
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true) this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true),
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true),
]
if (all) {
thePromises.push(
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negXy, true)
)
thePromises.push(
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negYz, true)
)
thePromises.push(
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negXz, true)
)
}
return Promise.all(thePromises)
} }
defaultSelectionFilter() { defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager) defaultSelectionFilter(this.programMemory, this.engineCommandManager)

View File

@ -540,7 +540,7 @@ function codeToIdSelections(
.filter(Boolean) as any .filter(Boolean) as any
} }
export function sendSelectEventToEngine( export async function sendSelectEventToEngine(
e: MouseEvent | React.MouseEvent<HTMLDivElement, MouseEvent>, e: MouseEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
el: HTMLVideoElement, el: HTMLVideoElement,
streamDimensions: { streamWidth: number; streamHeight: number } streamDimensions: { streamWidth: number; streamHeight: number }
@ -551,7 +551,7 @@ export function sendSelectEventToEngine(
el, el,
...streamDimensions, ...streamDimensions,
}) })
const result: Promise<Models['SelectWithPoint_type']> = engineCommandManager const result: Models['SelectWithPoint_type'] = await engineCommandManager
.sendSceneCommand({ .sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {

File diff suppressed because one or more lines are too long