Skip to content

Commit 7d03382

Browse files
authored
Nickmccleery/fix top bottom default views (#7860)
* First pass; this works but feels a little messy to me. * Tidy up top and bottom, using current camera setup. * Add some warnings on world coord system. * Unify methods for top/bottom view. * Use result type to satisfy the linter gods who do not like throws. * Tweak test to accommodate alternate engine command. * Reinstate tests... how on earth did I do that? * Scrap the DIY result type and push some hardware-y variable naming. * Wee comment. * Remove dupe log.
1 parent 4cf81b3 commit 7d03382

File tree

2 files changed

+119
-8
lines changed

2 files changed

+119
-8
lines changed

e2e/playwright/testing-gizmo.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ test.describe('Testing Gizmo', () => {
9898
await page.waitForTimeout(100)
9999
await page.mouse.click(clickPosition.x, clickPosition.y)
100100
await page.mouse.move(0, 0)
101-
await u.waitForCmdReceive('default_camera_look_at')
101+
102+
// We use different camera commands for top/bottom cf. all others.
103+
if (['top view', 'bottom view'].includes(testDescription)) {
104+
await u.waitForCmdReceive('default_camera_set_view')
105+
} else {
106+
await u.waitForCmdReceive('default_camera_look_at')
107+
}
102108
await u.clearCommandLogs()
103109

104110
await u.sendCustomCmd({

src/clientSideScene/CameraControls.ts

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import Hammer from 'hammerjs'
21
import type {
32
CameraDragInteractionType_type,
43
CameraViewState_type,
54
} from '@kittycad/lib/dist/types/src/models'
5+
import { isArray } from '@src/lib/utils'
6+
67
import type { EngineStreamActor } from '@src/machines/engineStreamMachine'
78
import * as TWEEN from '@tweenjs/tween.js'
9+
import Hammer from 'hammerjs'
810
import {
911
Euler,
1012
MathUtils,
@@ -34,7 +36,9 @@ import type {
3436
} from '@src/lang/std/engineConnection'
3537
import type { MouseGuard } from '@src/lib/cameraControls'
3638
import { cameraMouseDragGuards } from '@src/lib/cameraControls'
39+
import type { SettingsType } from '@src/lib/settings/initialSettings'
3740
import { reportRejection } from '@src/lib/trap'
41+
import { err } from '@src/lib/trap'
3842
import {
3943
getNormalisedCoordinates,
4044
isReducedMotion,
@@ -44,12 +48,18 @@ import {
4448
uuidv4,
4549
} from '@src/lib/utils'
4650
import { deg2Rad } from '@src/lib/utils2d'
47-
import type { SettingsType } from '@src/lib/settings/initialSettings'
4851
import { degToRad } from 'three/src/math/MathUtils'
4952

5053
const ORTHOGRAPHIC_CAMERA_SIZE = 20
5154
const FRAMES_TO_ANIMATE_IN = 30
5255
const ORTHOGRAPHIC_MAGIC_FOV = 4
56+
const EXPECTED_WORLD_COORD_SYSTEM = 'right_handed_up_z'
57+
58+
// Partial declaration; the front/back/left/right are handled differently.
59+
enum StandardView {
60+
TOP = 'top',
61+
BOTTOM = 'bottom',
62+
}
5363

5464
const tempQuaternion = new Quaternion() // just used for maths
5565

@@ -919,28 +929,123 @@ export class CameraControls {
919929
})
920930
}
921931

932+
async getCameraView(): Promise<CameraViewState_type | Error> {
933+
const response = await this.engineCommandManager.sendSceneCommand({
934+
type: 'modeling_cmd_req',
935+
cmd_id: uuidv4(),
936+
cmd: { type: 'default_camera_get_view' },
937+
})
938+
939+
// Check valid response from the engine.
940+
const singleResponse = isArray(response) ? response[0] : response
941+
const noValidResponse =
942+
!singleResponse?.success || !('resp' in singleResponse)
943+
944+
if (noValidResponse) {
945+
return new Error('Failed to get camera view state: no valid response.')
946+
}
947+
948+
// Check we actually have the 'modeling_response' field.
949+
const data = singleResponse.resp.data
950+
const noModelingResponse = !('modeling_response' in data)
951+
952+
if (noModelingResponse) {
953+
return new Error(
954+
'Failed to get camera view state: no `modeling_response`.'
955+
)
956+
}
957+
958+
// Check that we have the expected response type and the nested data.
959+
const modelingResponse = data.modeling_response
960+
const noData = !('data' in modelingResponse)
961+
const wrongResponseType =
962+
modelingResponse.type !== 'default_camera_get_view'
963+
964+
if (noData || wrongResponseType) {
965+
return new Error('Failed to get camera view state: invalid response.')
966+
}
967+
968+
return modelingResponse.data.view
969+
}
970+
971+
async setCameraViewAlongZ(direction: StandardView) {
972+
// Sets the camera view along the Z axis, giving us a top-down or bottom-up view.
973+
// The approach first retrieves current camera setup, then alters pivot params,
974+
// ultimately preserving other camera settings, e.g., ortho vs. perspective projection.
975+
976+
// Note: this assumes right handed, Z-up, X positive to the right.
977+
const Z_AXIS_QUATERNIONS = {
978+
[StandardView.TOP]: { x: 0, y: 0, z: 0, w: 1 },
979+
[StandardView.BOTTOM]: { x: 1, y: 0, z: 0, w: 0 },
980+
} as const
981+
982+
const cameraView = await this.getCameraView()
983+
984+
if (err(cameraView)) {
985+
return
986+
}
987+
988+
// Handle unexpected world coordinate system; should delete this eventually.
989+
if (cameraView.world_coord_system !== EXPECTED_WORLD_COORD_SYSTEM) {
990+
console.warn(
991+
`Camera is not in the expected ${EXPECTED_WORLD_COORD_SYSTEM} world coordinate system.
992+
Resulting view may not match expectations.`
993+
)
994+
}
995+
996+
const cameraViewTarget: CameraViewState_type = {
997+
...cameraView,
998+
pivot_rotation: Z_AXIS_QUATERNIONS[direction],
999+
pivot_position: {
1000+
x: this.target.x,
1001+
y: this.target.y,
1002+
z: this.target.z,
1003+
},
1004+
}
1005+
1006+
await this.engineCommandManager.sendSceneCommand({
1007+
type: 'modeling_cmd_req',
1008+
cmd_id: uuidv4(),
1009+
cmd: {
1010+
type: 'default_camera_set_view',
1011+
view: cameraViewTarget,
1012+
},
1013+
})
1014+
1015+
await this.engineCommandManager.sendSceneCommand({
1016+
type: 'modeling_cmd_req',
1017+
cmd_id: uuidv4(),
1018+
cmd: {
1019+
type: 'default_camera_get_settings',
1020+
},
1021+
})
1022+
}
1023+
9221024
async updateCameraToAxis(
9231025
axis: 'x' | 'y' | 'z' | '-x' | '-y' | '-z'
9241026
): Promise<void> {
1027+
// TODO: We currently use both `default_camera_look_at` and `default_camera_set_view`
1028+
// (via `setCameraViewAlongZ`). We should unify these during future camera work.
1029+
9251030
const distance = this.camera.position.distanceTo(this.target)
9261031

9271032
const vantage = this.target.clone()
928-
let up = { x: 0, y: 0, z: 1 }
1033+
const up = { x: 0, y: 0, z: 1 }
9291034

9301035
if (axis === 'x') {
9311036
vantage.x += distance
9321037
} else if (axis === 'y') {
9331038
vantage.y += distance
9341039
} else if (axis === 'z') {
935-
vantage.z += distance
936-
up = { x: -1, y: 0, z: 0 }
1040+
await this.setCameraViewAlongZ(StandardView.TOP)
1041+
return
9371042
} else if (axis === '-x') {
9381043
vantage.x -= distance
9391044
} else if (axis === '-y') {
9401045
vantage.y -= distance
9411046
} else if (axis === '-z') {
942-
vantage.z -= distance
943-
up = { x: -1, y: 0, z: 0 }
1047+
await this.setCameraViewAlongZ(StandardView.BOTTOM)
1048+
return
9441049
}
9451050

9461051
await this.engineCommandManager.sendSceneCommand({

0 commit comments

Comments
 (0)