Implement dual camera sync direction (#1597)

* implement dual camer sync direction

The existance of the client side scene requires two cameras to stay in sync, really these need to be a master-slave relationship, intitial this was implemented with the client side scene taking the lead and sending updates to the server using the  endpoint (as it didn't require an new endpoints), but even though we added a sequence property to this endpoint and sent it over udp, it was still an abuse of this endpoint as the engine didn't have this endpoint setup with a fload of messages and low-latency in mind.

Now we have migrated back to sending mouse events to the engine instead, but with the engine replying with camera details on drag_end etc so that we can keep the client camera in sync.

The client side camera still does take the master role in sketch mode as it makes sense to keep the low latency benfits of the local camera for the locallay rendered assets in sketch mode, moving the camera in this mode already did hide the engine camera while the camera is moving so as to avoid ghoasting so this works well.

The camera controls now work by syncing in either direction depending on what's appropiate

* fmt

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

* update default plane extrude numbers

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

* trigger-ci

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Kurt Hutten
2024-03-02 08:20:50 +11:00
committed by GitHub
parent f9df7ff885
commit b0e6140e9f
22 changed files with 224 additions and 90 deletions

View File

@ -16,9 +16,9 @@ document.addEventListener('mousemove', (e) =>
*/ */
const commonPoints = { const commonPoints = {
startAt: '[0.93, -1.26]', startAt: '[9.06, -12.22]',
num1: 0.95, num1: 9.14,
num2: 1.88, num2: 18.2,
} }
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
@ -102,13 +102,13 @@ test('Basic sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 - 0.01}], %)`) |> line([0, ${commonPoints.num1}], %)`)
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 part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 - 0.01}], %) |> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)`) |> line([-${commonPoints.num2}, 0], %)`)
// deselect line tool // deselect line tool
@ -133,7 +133,7 @@ test('Basic sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %) |> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %)
|> line([0, ${commonPoints.num1 - 0.01}], %) |> line([0, ${commonPoints.num1}], %)
|> angledLine([180, segLen('seg01', %)], %)`) |> angledLine([180, segLen('seg01', %)], %)`)
}) })
@ -492,13 +492,13 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 - 0.01}], %)`) |> line([0, ${commonPoints.num1}], %)`)
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 part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 - 0.01}], %) |> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)`) |> line([-${commonPoints.num2}, 0], %)`)
// deselect line tool // deselect line tool
@ -771,12 +771,12 @@ test('Can add multiple sketches', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 - 0.01}], %)`) |> line([0, ${commonPoints.num1}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ') const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 - 0.01}], %) |> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)` |> line([-${commonPoints.num2}, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch) await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)

View File

@ -384,13 +384,13 @@ test('extrude on each default plane should be stable', async ({
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}') const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([0.70, 0.44], %) |> startProfileAt([7.00, 4.40], %)
|> line([0.66, -0.02], %) |> line([6.60, -0.20], %)
|> line([0.28, 0.50], %) |> line([2.80, 5.00], %)
|> line([-0.56, 0.44], %) |> line([-5.60, 4.40], %)
|> line([-0.54, -0.38], %) |> line([-5.40, -3.80], %)
|> close(%) |> close(%)
|> extrude(1.00, %) |> extrude(10.00, %)
` `
await context.addInitScript(async (code) => { await context.addInitScript(async (code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
@ -484,7 +484,7 @@ test('Draft segments should look right', async ({ page, context }) => {
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(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([0.93, -1.26], %)`) |> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -498,8 +498,8 @@ test('Draft segments should look right', async ({ page, context }) => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([0.93, -1.26], %) |> startProfileAt([9.06, -12.22], %)
|> line([0.95, 0], %)`) |> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -562,7 +562,7 @@ test('Client side scene scale should match engine scale inch', async ({
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(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([0.93, -1.26], %)`) |> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -572,8 +572,8 @@ test('Client side scene scale should match engine scale inch', async ({
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([0.93, -1.26], %) |> startProfileAt([9.06, -12.22], %)
|> line([0.95, 0], %)`) |> 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)
@ -582,9 +582,13 @@ test('Client side scene scale should match engine scale inch', async ({
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([0.93, -1.26], %) |> startProfileAt([9.06, -12.22], %)
|> line([0.95, 0], %) |> line([9.14, 0], %)
|> tangentialArcTo([2.82, -0.32], %)`) |> tangentialArcTo([27.34, -3.08], %)`)
// click tangential arc tool again to unequip it
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch // screen shot should show the sketch
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
@ -658,7 +662,7 @@ test('Client side scene scale should match engine scale mm', async ({
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(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([0.93, -1.26], %)`) |> startProfileAt([230.03, -310.33], %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -668,8 +672,8 @@ test('Client side scene scale should match engine scale mm', async ({
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([0.93, -1.26], %) |> startProfileAt([230.03, -310.33], %)
|> line([0.95, 0], %)`) |> 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)
@ -678,9 +682,12 @@ test('Client side scene scale should match engine scale mm', async ({
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([0.93, -1.26], %) |> startProfileAt([230.03, -310.33], %)
|> line([0.95, 0], %) |> line([232.2, 0], %)
|> tangentialArcTo([2.82, -0.32], %)`) |> tangentialArcTo([694.43, -78.12], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch // screen shot should show the sketch
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -16,7 +16,11 @@ import {
SKETCH_LAYER, SKETCH_LAYER,
ZOOM_MAGIC_NUMBER, ZOOM_MAGIC_NUMBER,
} from './sceneInfra' } from './sceneInfra'
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection' import {
EngineCommand,
Subscription,
engineCommandManager,
} from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { deg2Rad } from 'lib/utils2d' import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils' import { isReducedMotion, roundOff, throttle } from 'lib/utils'
@ -28,6 +32,12 @@ const FRAMES_TO_ANIMATE_IN = 30
const tempQuaternion = new Quaternion() // just used for maths const tempQuaternion = new Quaternion() // just used for maths
type interactionType = 'pan' | 'rotate' | 'zoom'
const throttledEngCmd = throttle((cmd: EngineCommand) => {
engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 15)
interface ThreeCamValues { interface ThreeCamValues {
position: Vector3 position: Vector3
quaternion: Quaternion quaternion: Quaternion
@ -110,10 +120,11 @@ const throttledUpdateEngineFov = throttle(
lastCmdDelay lastCmdDelay
) as any as number ) as any as number
}, },
1000 / 15 1000 / 30
) )
export class CameraControls { export class CameraControls {
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
camera: PerspectiveCamera | OrthographicCamera camera: PerspectiveCamera | OrthographicCamera
target: Vector3 target: Vector3
domElement: HTMLCanvasElement domElement: HTMLCanvasElement
@ -221,6 +232,45 @@ export class CameraControls {
this.update() this.update()
this._usePerspectiveCamera() this._usePerspectiveCamera()
const cb: Subscription<
'default_camera_zoom' | 'camera_drag_end' | 'default_camera_get_settings'
>['callback'] = ({ data, type }) => {
const camSettings = data.settings
this.camera.position.set(
camSettings.pos.x,
camSettings.pos.y,
camSettings.pos.z
)
this.target.set(
camSettings.center.x,
camSettings.center.y,
camSettings.center.z
)
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
this.camera.fov = camSettings.fov_y
} else if (
this.camera instanceof OrthographicCamera &&
camSettings.ortho_scale
) {
this.camera.zoom = camSettings.ortho_scale
}
this.onCameraChange()
}
setTimeout(() => {
engineCommandManager.subscribeTo({
event: 'camera_drag_end',
callback: cb,
})
engineCommandManager.subscribeTo({
event: 'default_camera_zoom',
callback: cb,
})
engineCommandManager.subscribeTo({
event: 'default_camera_get_settings',
callback: cb,
})
})
} }
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void = private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
@ -254,7 +304,21 @@ export class CameraControls {
onMouseDown = (event: MouseEvent) => { onMouseDown = (event: MouseEvent) => {
this.isDragging = true this.isDragging = true
this.mouseDownPosition.set(event.clientX, event.clientY) this.mouseDownPosition.set(event.clientX, event.clientY)
let interaction = this.getInteractionType(event)
if (interaction === 'none') return
this.handleStart() this.handleStart()
if (this.syncDirection === 'engineToClient') {
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x: event.clientX, y: event.clientY },
},
cmd_id: uuidv4(),
})
}
} }
onMouseMove = (event: MouseEvent) => { onMouseMove = (event: MouseEvent) => {
@ -265,36 +329,34 @@ export class CameraControls {
.sub(this.mouseDownPosition) .sub(this.mouseDownPosition)
this.mouseDownPosition.copy(this.mouseNewPosition) this.mouseDownPosition.copy(this.mouseNewPosition)
let state: 'pan' | 'rotate' | 'zoom' = 'pan' const interaction = this.getInteractionType(event)
if (interaction === 'none') return
if (this.interactionGuards.pan.callback(event as any)) { if (this.syncDirection === 'engineToClient') {
if (this.enablePan === false) return throttledEngCmd({
// handleMouseDownPan(event) type: 'modeling_cmd_req',
state = 'pan' cmd: {
} else if (this.interactionGuards.rotate.callback(event as any)) { type: 'camera_drag_move',
if (this.enableRotate === false) return interaction,
// handleMouseDownRotate(event) window: { x: event.clientX, y: event.clientY },
state = 'rotate' },
} else if (this.interactionGuards.zoom.dragCallback(event as any)) { cmd_id: uuidv4(),
if (this.enableZoom === false) return })
// handleMouseDownDolly(event)
state = 'zoom'
} else {
return return
} }
// Implement camera movement logic here based on deltaMove // Implement camera movement logic here based on deltaMove
// For example, for rotating the camera around the target: // For example, for rotating the camera around the target:
if (state === 'rotate') { if (interaction === 'rotate') {
this.pendingRotation = this.pendingRotation this.pendingRotation = this.pendingRotation
? this.pendingRotation ? this.pendingRotation
: new Vector2() : new Vector2()
this.pendingRotation.x += deltaMove.x this.pendingRotation.x += deltaMove.x
this.pendingRotation.y += deltaMove.y this.pendingRotation.y += deltaMove.y
} else if (state === 'zoom') { } else if (interaction === 'zoom') {
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1 this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + deltaMove.y * 0.01 this.pendingZoom *= 1 + deltaMove.y * 0.01
} else if (state === 'pan') { } else if (interaction === 'pan') {
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2() this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
let distance = this.camera.position.distanceTo(this.target) let distance = this.camera.position.distanceTo(this.target)
if (this.camera instanceof OrthographicCamera) { if (this.camera instanceof OrthographicCamera) {
@ -311,11 +373,45 @@ export class CameraControls {
onMouseUp = (event: MouseEvent) => { onMouseUp = (event: MouseEvent) => {
this.isDragging = false this.isDragging = false
this.handleEnd() this.handleEnd()
if (this.syncDirection === 'engineToClient') {
const interaction = this.getInteractionType(event)
if (interaction === 'none') return
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_end',
interaction,
window: { x: event.clientX, y: event.clientY },
},
cmd_id: uuidv4(),
})
}
} }
onMouseWheel = (event: WheelEvent) => { onMouseWheel = (event: WheelEvent) => {
// Assume trackpad if the deltas are small and integers // Assume trackpad if the deltas are small and integers
this.handleStart() this.handleStart()
if (this.syncDirection === 'engineToClient') {
const interactions = this.interactionGuards.zoom.scrollCallback(
event as any
)
if (!interactions) {
this.handleEnd()
return
}
throttledEngCmd({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: -event.deltaY * 0.4,
},
cmd_id: uuidv4(),
})
this.handleEnd()
return
}
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0 const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
@ -637,6 +733,10 @@ export class CameraControls {
duration = 500, duration = 500,
toOrthographic = true toOrthographic = true
): Promise<void> { ): Promise<void> {
if (this.syncDirection === 'engineToClient')
console.warn(
'tweenCameraToQuaternion not design to work with engineToClient syncDirection.'
)
const isVertical = isQuaternionVertical(targetQuaternion) const isVertical = isQuaternionVertical(targetQuaternion)
let remainingDuration = duration let remainingDuration = duration
if (isVertical) { if (isVertical) {
@ -719,6 +819,10 @@ export class CameraControls {
animateToOrthographic = () => animateToOrthographic = () =>
new Promise((resolve) => { new Promise((resolve) => {
if (this.syncDirection === 'engineToClient')
console.warn(
'animate To Orthographic not design to work with engineToClient syncDirection.'
)
this.isFovAnimationInProgress = true this.isFovAnimationInProgress = true
let currentFov = this.lastPerspectiveFov let currentFov = this.lastPerspectiveFov
this.fovBeforeOrtho = currentFov this.fovBeforeOrtho = currentFov
@ -752,6 +856,10 @@ export class CameraControls {
}) })
animateToPerspective = () => animateToPerspective = () =>
new Promise((resolve) => { new Promise((resolve) => {
if (this.syncDirection === 'engineToClient')
console.warn(
'animate To Perspective not design to work with engineToClient syncDirection.'
)
this.isFovAnimationInProgress = true this.isFovAnimationInProgress = true
// Immediately set the camera to perspective with a very low FOV // Immediately set the camera to perspective with a very low FOV
const targetFov = this.fovBeforeOrtho // Target FOV for perspective const targetFov = this.fovBeforeOrtho // Target FOV for perspective
@ -798,13 +906,14 @@ export class CameraControls {
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
} }
throttledUpdateEngineCamera({ if (this.syncDirection === 'clientToEngine')
quaternion: this.camera.quaternion, throttledUpdateEngineCamera({
position: this.camera.position, quaternion: this.camera.quaternion,
zoom: this.camera.zoom, position: this.camera.position,
isPerspective: this.isPerspective, zoom: this.camera.zoom,
target: this.target, isPerspective: this.isPerspective,
}) target: this.target,
})
this.deferReactUpdate({ this.deferReactUpdate({
type: this.isPerspective ? 'perspective' : 'orthographic', type: this.isPerspective ? 'perspective' : 'orthographic',
[this.isPerspective ? 'fov' : 'zoom']: [this.isPerspective ? 'fov' : 'zoom']:
@ -825,9 +934,18 @@ export class CameraControls {
}) })
Object.values(this._camChangeCallbacks).forEach((cb) => cb()) Object.values(this._camChangeCallbacks).forEach((cb) => cb())
} }
getInteractionType = (event: any) =>
_getInteractionType(
this.interactionGuards,
event,
this.enablePan,
this.enableRotate,
this.enableZoom
)
} }
// currently duplicated, delete one // Pure function helpers
function calculateNearFarFromFOV(fov: number) { 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)
@ -835,7 +953,6 @@ function calculateNearFarFromFOV(fov: number) {
return { z_near: 0.1, z_far } return { z_near: 0.1, z_far }
} }
// currently duplicated, delete one
function convertThreeCamValuesToEngineCam({ function convertThreeCamValuesToEngineCam({
target, target,
position, position,
@ -876,8 +993,6 @@ function convertThreeCamValuesToEngineCam({
} }
} }
// Pure function helpers
function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion { function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
// Direction from position to target, normalized. // Direction from position to target, normalized.
let direction = new Vector3().subVectors(target, position).normalize() let direction = new Vector3().subVectors(target, position).normalize()
@ -896,3 +1011,17 @@ function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
return quaternion return quaternion
} }
function _getInteractionType(
interactionGuards: MouseGuard,
event: any,
enablePan: boolean,
enableRotate: boolean,
enableZoom: boolean
): interactionType | 'none' {
let state: interactionType | 'none' = 'none'
if (enablePan && interactionGuards.pan.callback(event)) return 'pan'
if (enableRotate && interactionGuards.rotate.callback(event)) return 'rotate'
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
return state
}

View File

@ -1,15 +1,8 @@
import { import { MouseEventHandler, useEffect, useRef, useState } from 'react'
MouseEventHandler,
WheelEventHandler,
useEffect,
useRef,
useState,
} from 'react'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore' import { useStore } from '../useStore'
import { getNormalisedCoordinates, throttle } from '../lib/utils' import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
@ -36,7 +29,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
streamDimensions: s.streamDimensions, streamDimensions: s.streamDimensions,
})) }))
const { settings } = useGlobalStateContext() const { settings } = useGlobalStateContext()
const cameraControls = settings?.context?.cameraControls
const { state } = useModelingContext() const { state } = useModelingContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { overallState } = useNetworkStatus() const { overallState } = useNetworkStatus()
@ -68,19 +60,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
setClickCoords({ x, y }) setClickCoords({ x, y })
} }
const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: e.deltaY * 0.4,
},
cmd_id: uuidv4(),
})
}, Math.round(1000 / fps))
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({ const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
clientX, clientX,
clientY, clientY,
@ -159,7 +138,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
muted muted
autoPlay autoPlay
controls={false} controls={false}
onWheel={handleScroll}
onPlay={() => setIsLoading(false)} onPlay={() => setIsLoading(false)}
onMouseMoveCapture={handleMouseMove} onMouseMoveCapture={handleMouseMove}
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`} className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}

View File

@ -796,7 +796,7 @@ interface UnreliableSubscription<T extends UnreliableResponses['type']> {
callback: (data: Extract<UnreliableResponses, { type: T }>) => void callback: (data: Extract<UnreliableResponses, { type: T }>) => void
} }
interface Subscription<T extends ModelTypes> { export interface Subscription<T extends ModelTypes> {
event: T event: T
callback: ( callback: (
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }> data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
@ -926,6 +926,15 @@ export class EngineCommandManager {
}, },
}) })
sceneInfra.camControls.onCameraChange() sceneInfra.camControls.onCameraChange()
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
this.initPlanes().then(() => { this.initPlanes().then(() => {
this.resolveReady() this.resolveReady()

View File

@ -40,9 +40,9 @@ export interface MouseGuard {
} }
const butName = (e: React.MouseEvent) => ({ const butName = (e: React.MouseEvent) => ({
middle: !!(e.buttons & 4), middle: !!(e.buttons & 4) || e.button === 1,
right: !!(e.buttons & 2), right: !!(e.buttons & 2) || e.button === 2,
left: !!(e.buttons & 1), left: !!(e.buttons & 1) || e.button === 0,
}) })
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = { export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {

View File

@ -119,7 +119,7 @@ export type MoveDesc = { line: number; snippet: string }
export const modelingMachine = createMachine( export const modelingMachine = createMachine(
{ {
/** @xstate-layout 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-McBNlhGQjynZHZSmibF5HsGoKFmKgKi6KcCAA */ /** @xstate-layout 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-McBNlhGQjynZHZSmibF5HsGoKFmKgKi6KcCAA */
id: 'Modeling', id: 'Modeling',
tsTypes: {} as import('./modelingMachine.typegen').Typegen0, tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
@ -481,6 +481,7 @@ export const modelingMachine = createMachine(
'animate after sketch', 'animate after sketch',
'tear down client sketch', 'tear down client sketch',
'remove sketch grid', 'remove sketch grid',
'engineToClient cam sync direction',
], ],
entry: ['add axis n grid', 'conditionally equip line tool'], entry: ['add axis n grid', 'conditionally equip line tool'],
@ -514,6 +515,8 @@ export const modelingMachine = createMachine(
internal: true, internal: true,
}, },
}, },
entry: 'clientToEngine cam sync direction',
}, },
'animating to existing sketch': { 'animating to existing sketch': {
@ -524,6 +527,8 @@ export const modelingMachine = createMachine(
onDone: 'Sketch', onDone: 'Sketch',
}, },
], ],
entry: 'clientToEngine cam sync direction',
}, },
}, },
@ -845,6 +850,12 @@ export const modelingMachine = createMachine(
// (note the orbit controls are always active though) // (note the orbit controls are always active though)
sceneInfra.resetMouseListeners() sceneInfra.resetMouseListeners()
}, },
'clientToEngine cam sync direction': () => {
sceneInfra.camControls.syncDirection = 'clientToEngine'
},
'engineToClient cam sync direction': () => {
sceneInfra.camControls.syncDirection = 'engineToClient'
},
}, },
// end actions // end actions
} }