solve a couple of scene scale bugs (#1496)
* solve a couple of scene scale bugs * Some cam fixes (#1520) * rotate and zoom basics working * intergrate mouse guards, and add pan * implement orthographic camera again * implement switch to perspective camera again * migrate dollyzoom * make pan robust for differnt FOV and orthographic cam * tween to quaternion and default plane selection working with quirks * fix pan It the up and right was derived from the camera's up, which is a static [0,0,1] not the camera's current cameras real up, which aligns itself as best to [0,0,1] but is not that especially when looking straight up or down, and the pan felt very awkward in these vertical look sintuations * fix raycastRing to use new camera * fix tween to quaternion for camera lock situations And get all playwright tests passing * fix up CamToggle, even thought this component is not setup properly to use react properties from our scene class * add animation to cameras back in * first big clean up of sceneInfra * move more cam stuff out of sceneInfra * clean up mouse guard logic * clean up camera change callbacks * fix some sitations where animation to xy doesn't work great * needs to take the target into consideration * last bits of clean up * more clean up * make vitest happ * fix up remaining interaction guards * make scrolling less sensative for trackpads * remove debug cube * fix snapshot tests
This commit is contained in:
@ -1,12 +1,10 @@
|
||||
import {
|
||||
AmbientLight,
|
||||
Color,
|
||||
Euler,
|
||||
GridHelper,
|
||||
LineBasicMaterial,
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
Quaternion,
|
||||
Scene,
|
||||
Vector3,
|
||||
WebGLRenderer,
|
||||
@ -20,32 +18,26 @@ import {
|
||||
Intersection,
|
||||
Object3D,
|
||||
Object3DEventMap,
|
||||
BoxGeometry,
|
||||
} from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { deg2Rad } from 'lib/utils2d'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { MouseGuard, cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import { Axis } from 'lib/selections'
|
||||
import { BaseUnit, SETTINGS_PERSIST_KEY } from 'machines/settingsMachine'
|
||||
import { CameraControls } from './CameraControls'
|
||||
|
||||
type SendType = ReturnType<typeof useModelingContext>['send']
|
||||
|
||||
// 63.5 is definitely a bit of a magic number, play with it until it looked right
|
||||
// if it were 64, that would feel like it's something in the engine where a random
|
||||
// power of 2 is used, but it's the 0.5 seems to make things look much more correct
|
||||
const ZOOM_MAGIC_NUMBER = 63.5
|
||||
const FRAMES_TO_ANIMATE_IN = 30
|
||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||
export const ZOOM_MAGIC_NUMBER = 63.5
|
||||
|
||||
export const INTERSECTION_PLANE_LAYER = 1
|
||||
export const SKETCH_LAYER = 2
|
||||
const DEBUG_SHOW_INTERSECTION_PLANE = false
|
||||
export const DEBUG_SHOW_INTERSECTION_PLANE = false
|
||||
export const DEBUG_SHOW_BOTH_SCENES = false
|
||||
|
||||
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||
@ -57,100 +49,6 @@ export const AXIS_GROUP = 'axisGroup'
|
||||
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
||||
export const ARROWHEAD = 'arrowhead'
|
||||
|
||||
const tempQuaternion = new Quaternion() // just used for maths
|
||||
|
||||
interface ThreeCamValues {
|
||||
position: Vector3
|
||||
quaternion: Quaternion
|
||||
zoom: number
|
||||
isPerspective: boolean
|
||||
target: Vector3
|
||||
}
|
||||
|
||||
const lastCmdDelay = 50
|
||||
|
||||
let lastCmd: EngineCommand | null = null
|
||||
let lastCmdTime: number = Date.now()
|
||||
let lastCmdTimeoutId: number | null = null
|
||||
|
||||
const sendLastReliableChannel = () => {
|
||||
if (lastCmd && Date.now() - lastCmdTime >= lastCmdDelay) {
|
||||
engineCommandManager.sendSceneCommand(lastCmd, true)
|
||||
lastCmdTime = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => {
|
||||
const cmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
...convertThreeCamValuesToEngineCam(threeValues),
|
||||
},
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(cmd)
|
||||
lastCmd = cmd
|
||||
lastCmdTime = Date.now()
|
||||
|
||||
if (lastCmdTimeoutId !== null) {
|
||||
clearTimeout(lastCmdTimeoutId)
|
||||
}
|
||||
lastCmdTimeoutId = setTimeout(
|
||||
sendLastReliableChannel,
|
||||
lastCmdDelay
|
||||
) as any as number
|
||||
}, 1000 / 30)
|
||||
|
||||
let lastPerspectiveCmd: EngineCommand | null = null
|
||||
let lastPerspectiveCmdTime: number = Date.now()
|
||||
let lastPerspectiveCmdTimeoutId: number | null = null
|
||||
|
||||
const sendLastPerspectiveReliableChannel = () => {
|
||||
if (
|
||||
lastPerspectiveCmd &&
|
||||
Date.now() - lastPerspectiveCmdTime >= lastCmdDelay
|
||||
) {
|
||||
engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true)
|
||||
lastPerspectiveCmdTime = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
const 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),
|
||||
},
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(cmd)
|
||||
lastPerspectiveCmd = cmd
|
||||
lastPerspectiveCmdTime = Date.now()
|
||||
if (lastPerspectiveCmdTimeoutId !== null) {
|
||||
clearTimeout(lastPerspectiveCmdTimeoutId)
|
||||
}
|
||||
lastPerspectiveCmdTimeoutId = setTimeout(
|
||||
sendLastPerspectiveReliableChannel,
|
||||
lastCmdDelay
|
||||
) as any as number
|
||||
},
|
||||
1000 / 15
|
||||
)
|
||||
|
||||
interface BaseCallbackArgs2 {
|
||||
object: any
|
||||
event: any
|
||||
@ -178,34 +76,18 @@ interface onMoveCallbackArgs {
|
||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||
}
|
||||
|
||||
export type ReactCameraProperties =
|
||||
| {
|
||||
type: 'perspective'
|
||||
fov?: number
|
||||
position: [number, number, number]
|
||||
quaternion: [number, number, number, number]
|
||||
}
|
||||
| {
|
||||
type: 'orthographic'
|
||||
zoom?: number
|
||||
position: [number, number, number]
|
||||
quaternion: [number, number, number, number]
|
||||
}
|
||||
|
||||
// This singleton class is responsible for all of the under the hood setup for the client side scene.
|
||||
// That is the cameras and switching between them, raycasters for click mouse events and their abstractions (onClick etc), setting up controls.
|
||||
// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
|
||||
class SceneInfra {
|
||||
static instance: SceneInfra
|
||||
scene: Scene
|
||||
camera: PerspectiveCamera | OrthographicCamera
|
||||
renderer: WebGLRenderer
|
||||
controls: OrbitControls
|
||||
camControls: CameraControls
|
||||
isPerspective = true
|
||||
fov = 45
|
||||
fovBeforeAnimate = 45
|
||||
isFovAnimationInProgress = false
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
|
||||
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {}
|
||||
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
|
||||
@ -256,55 +138,18 @@ class SceneInfra {
|
||||
selectedObject: null | any = null
|
||||
mouseDownVector: null | Vector2 = null
|
||||
|
||||
// reacts hooks into some of this singleton's properties
|
||||
reactCameraProperties: ReactCameraProperties = {
|
||||
type: 'perspective',
|
||||
fov: 12,
|
||||
position: [0, 0, 0],
|
||||
quaternion: [0, 0, 0, 1],
|
||||
}
|
||||
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
|
||||
setReactCameraPropertiesCallback = (
|
||||
cb: (a: ReactCameraProperties) => void
|
||||
) => {
|
||||
this.reactCameraPropertiesCallback = cb
|
||||
}
|
||||
setCam = (camProps: ReactCameraProperties) => {
|
||||
if (
|
||||
camProps.type === 'perspective' &&
|
||||
this.camera instanceof OrthographicCamera
|
||||
) {
|
||||
this.usePerspectiveCamera()
|
||||
} else if (
|
||||
camProps.type === 'orthographic' &&
|
||||
this.camera instanceof PerspectiveCamera
|
||||
) {
|
||||
this.useOrthographicCamera()
|
||||
}
|
||||
this.camera.position.set(...camProps.position)
|
||||
this.camera.quaternion.set(...camProps.quaternion)
|
||||
if (
|
||||
camProps.type === 'perspective' &&
|
||||
this.camera instanceof PerspectiveCamera
|
||||
) {
|
||||
// not sure what to do here, calling dollyZoom here is buggy because it updates the position
|
||||
// at the same time
|
||||
} else if (
|
||||
camProps.type === 'orthographic' &&
|
||||
this.camera instanceof OrthographicCamera
|
||||
) {
|
||||
this.camera.zoom = camProps.zoom || 1
|
||||
}
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// SCENE
|
||||
this.scene = new Scene()
|
||||
this.scene.background = new Color(0x000000)
|
||||
this.scene.background = null
|
||||
|
||||
// RENDERER
|
||||
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
|
||||
// CAMERA
|
||||
const camHeightDistanceRatio = 0.5
|
||||
const baseUnit: BaseUnit =
|
||||
@ -315,25 +160,19 @@ class SceneInfra {
|
||||
const ang = Math.atan(camHeightDistanceRatio)
|
||||
const x = Math.cos(ang) * length
|
||||
const y = Math.sin(ang) * length
|
||||
this.camera = this.createPerspectiveCamera()
|
||||
this.camera.position.set(0, -x, y)
|
||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
|
||||
// RENDERER
|
||||
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
this.camControls = new CameraControls(false, this.renderer.domElement)
|
||||
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
||||
this.camControls.camera.position.set(0, -x, y)
|
||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
|
||||
// RAYCASTERS
|
||||
this.raycaster.layers.enable(SKETCH_LAYER)
|
||||
this.raycaster.layers.disable(0)
|
||||
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
|
||||
// CONTROLS
|
||||
this.controls = this.setupOrbitControls()
|
||||
|
||||
// GRID
|
||||
const size = 100
|
||||
const divisions = 10
|
||||
@ -353,415 +192,40 @@ class SceneInfra {
|
||||
|
||||
SceneInfra.instance = this
|
||||
}
|
||||
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
|
||||
() => {}
|
||||
setIsCamMovingCallback(cb: (isMoving: boolean, isTween: boolean) => void) {
|
||||
this._isCamMovingCallback = cb
|
||||
}
|
||||
private _onCamChange: () => void = () => {}
|
||||
setOnCamChange(cb: () => void) {
|
||||
this._onCamChange = cb
|
||||
}
|
||||
setInteractionGuards = (guard: MouseGuard) => {
|
||||
this.interactionGuards = guard
|
||||
// setMouseGuards is oun patch-package patch to orbit controls
|
||||
// see patches/three+0.160.0.patch
|
||||
;(this.controls as any).setMouseGuards(guard)
|
||||
}
|
||||
private createPerspectiveCamera = () => {
|
||||
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
|
||||
this.camera = new PerspectiveCamera(
|
||||
this.fov,
|
||||
window.innerWidth / window.innerHeight,
|
||||
z_near,
|
||||
z_far
|
||||
)
|
||||
this.camera.up.set(0, 0, 1)
|
||||
this.camera.layers.enable(SKETCH_LAYER)
|
||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
|
||||
return this.camera
|
||||
}
|
||||
setupOrbitControls = (target?: [number, number, number]): OrbitControls => {
|
||||
if (this.controls) this.controls.dispose()
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
||||
if (target) {
|
||||
// if we're swapping from perspective to orthographic,
|
||||
// we'll need to recreate the orbit controls
|
||||
// and most likely want the target to be the same
|
||||
this.controls.target.set(...target)
|
||||
}
|
||||
this.controls.update()
|
||||
this.controls.addEventListener('change', this.onCameraChange)
|
||||
// debounce is needed because the start and end events are fired too often for zoom on scroll
|
||||
let debounceTimer = 0
|
||||
const handleStart = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
this._isCamMovingCallback(true, false)
|
||||
}
|
||||
const handleEnd = () => {
|
||||
debounceTimer = setTimeout(() => {
|
||||
this._isCamMovingCallback(false, false)
|
||||
}, 400) as any as number
|
||||
}
|
||||
this.controls.addEventListener('start', handleStart)
|
||||
this.controls.addEventListener('end', handleEnd)
|
||||
|
||||
// setMouseGuards is oun patch-package patch to orbit controls
|
||||
// see patches/three+0.160.0.patch
|
||||
;(this.controls as any).setMouseGuards(this.interactionGuards)
|
||||
return this.controls
|
||||
}
|
||||
onStreamStart = () => this.onCameraChange()
|
||||
|
||||
deferReactUpdate = throttle((a: ReactCameraProperties) => {
|
||||
this.reactCameraPropertiesCallback(a)
|
||||
}, 200)
|
||||
|
||||
onCameraChange = () => {
|
||||
const scale = getSceneScale(this.camera, this.controls.target)
|
||||
const scale = getSceneScale(
|
||||
this.camControls.camera,
|
||||
this.camControls.target
|
||||
)
|
||||
const planesGroup = this.scene.getObjectByName(DEFAULT_PLANES)
|
||||
const axisGroup = this.scene
|
||||
.getObjectByName(AXIS_GROUP)
|
||||
?.getObjectByName('gridHelper')
|
||||
planesGroup && planesGroup.scale.set(scale, scale, scale)
|
||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||
|
||||
throttledUpdateEngineCamera({
|
||||
quaternion: this.camera.quaternion,
|
||||
position: this.camera.position,
|
||||
zoom: this.camera.zoom,
|
||||
isPerspective: this.isPerspective,
|
||||
target: this.controls.target,
|
||||
})
|
||||
this.deferReactUpdate({
|
||||
type:
|
||||
this.camera instanceof PerspectiveCamera
|
||||
? 'perspective'
|
||||
: 'orthographic',
|
||||
[this.camera instanceof PerspectiveCamera ? 'fov' : 'zoom']:
|
||||
this.camera instanceof PerspectiveCamera
|
||||
? this.camera.fov
|
||||
: this.camera.zoom,
|
||||
position: [
|
||||
roundOff(this.camera.position.x, 2),
|
||||
roundOff(this.camera.position.y, 2),
|
||||
roundOff(this.camera.position.z, 2),
|
||||
],
|
||||
quaternion: [
|
||||
roundOff(this.camera.quaternion.x, 2),
|
||||
roundOff(this.camera.quaternion.y, 2),
|
||||
roundOff(this.camera.quaternion.z, 2),
|
||||
roundOff(this.camera.quaternion.w, 2),
|
||||
],
|
||||
})
|
||||
this._onCamChange()
|
||||
}
|
||||
|
||||
onWindowResize = () => {
|
||||
if (this.camera instanceof PerspectiveCamera) {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight
|
||||
} else if (this.camera instanceof OrthographicCamera) {
|
||||
const aspect = window.innerWidth / window.innerHeight
|
||||
this.camera.left = -ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
||||
this.camera.right = ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
||||
this.camera.top = ORTHOGRAPHIC_CAMERA_SIZE
|
||||
this.camera.bottom = -ORTHOGRAPHIC_CAMERA_SIZE
|
||||
}
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
}
|
||||
|
||||
animate = () => {
|
||||
requestAnimationFrame(this.animate)
|
||||
TWEEN.update() // This will update all tweens during the animation loop
|
||||
if (!this.isFovAnimationInProgress)
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
async tweenCameraToQuaternion(
|
||||
targetQuaternion: Quaternion,
|
||||
duration = 500,
|
||||
toOrthographic = true
|
||||
): Promise<void> {
|
||||
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||
let _duration = duration
|
||||
if (isVertical) {
|
||||
_duration = duration * 0.6
|
||||
await this._tweenCameraToQuaternion(new Quaternion(), _duration, false)
|
||||
if (!this.isFovAnimationInProgress) {
|
||||
// console.log('animation frame', this.cameraControls.camera)
|
||||
this.camControls.update()
|
||||
this.renderer.render(this.scene, this.camControls.camera)
|
||||
}
|
||||
await this._tweenCameraToQuaternion(
|
||||
targetQuaternion,
|
||||
_duration,
|
||||
toOrthographic
|
||||
)
|
||||
}
|
||||
_tweenCameraToQuaternion(
|
||||
targetQuaternion: Quaternion,
|
||||
duration = 500,
|
||||
toOrthographic = false
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const camera = this.camera
|
||||
this._isCamMovingCallback(true, true)
|
||||
const initialQuaternion = camera.quaternion.clone()
|
||||
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||
let tweenEnd = isVertical ? 0.99 : 1
|
||||
const controlsTarget = this.controls.target.clone()
|
||||
const initialDistance = controlsTarget.distanceTo(camera.position.clone())
|
||||
|
||||
const cameraAtTime = (animationProgress: number /* 0 - 1 */) => {
|
||||
const currentQ = tempQuaternion.slerpQuaternions(
|
||||
initialQuaternion,
|
||||
targetQuaternion,
|
||||
animationProgress
|
||||
)
|
||||
if (this.camera instanceof PerspectiveCamera)
|
||||
// changing the camera position back when it's orthographic doesn't do anything
|
||||
// and it messes up animating back to perspective later
|
||||
this.camera.position
|
||||
.set(0, 0, 1)
|
||||
.applyQuaternion(currentQ)
|
||||
.multiplyScalar(initialDistance)
|
||||
.add(controlsTarget)
|
||||
|
||||
this.camera.up.set(0, 1, 0).applyQuaternion(currentQ).normalize()
|
||||
this.camera.quaternion.copy(currentQ)
|
||||
this.controls.target.copy(controlsTarget)
|
||||
this.controls.update()
|
||||
this.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
const onComplete = async () => {
|
||||
if (isReducedMotion() && toOrthographic) {
|
||||
cameraAtTime(0.99)
|
||||
this.useOrthographicCamera()
|
||||
} else if (toOrthographic) {
|
||||
await this.animateToOrthographic()
|
||||
}
|
||||
if (isVertical) cameraAtTime(1)
|
||||
this.camera.up.set(0, 0, 1)
|
||||
this.controls.enableRotate = false
|
||||
this._isCamMovingCallback(false, true)
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (isReducedMotion()) {
|
||||
onComplete()
|
||||
return
|
||||
}
|
||||
|
||||
new TWEEN.Tween({ t: 0 })
|
||||
.to({ t: tweenEnd }, duration)
|
||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||
.onUpdate(({ t }) => cameraAtTime(t))
|
||||
.onComplete(onComplete)
|
||||
.start()
|
||||
})
|
||||
}
|
||||
|
||||
animateToOrthographic = () =>
|
||||
new Promise((resolve) => {
|
||||
this.isFovAnimationInProgress = true
|
||||
let currentFov = this.fov
|
||||
this.fovBeforeAnimate = this.fov
|
||||
|
||||
const targetFov = 4
|
||||
const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN
|
||||
let frameWaitOnFinish = 10
|
||||
|
||||
const animateFovChange = () => {
|
||||
if (this.camera instanceof PerspectiveCamera) {
|
||||
if (this.camera.fov > targetFov) {
|
||||
// Decrease the FOV
|
||||
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.dollyZoom(currentFov)
|
||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||
} else if (frameWaitOnFinish > 0) {
|
||||
frameWaitOnFinish--
|
||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||
} else {
|
||||
// Once the target FOV is reached, switch to the orthographic camera
|
||||
// Needs to wait a couple frames after the FOV animation is complete
|
||||
this.useOrthographicCamera()
|
||||
this.isFovAnimationInProgress = false
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animateFovChange() // Start the animation
|
||||
})
|
||||
|
||||
animateToPerspective = () =>
|
||||
new Promise((resolve) => {
|
||||
this.isFovAnimationInProgress = true
|
||||
// Immediately set the camera to perspective with a very low FOV
|
||||
this.fov = 4
|
||||
let currentFov = 4
|
||||
this.camera.updateProjectionMatrix()
|
||||
const targetFov = this.fovBeforeAnimate // Target FOV for perspective
|
||||
const fovAnimationStep = (targetFov - currentFov) / FRAMES_TO_ANIMATE_IN
|
||||
this.usePerspectiveCamera()
|
||||
|
||||
const animateFovChange = () => {
|
||||
if (this.camera instanceof OrthographicCamera) return
|
||||
if (this.camera.fov < targetFov) {
|
||||
// Increase the FOV
|
||||
currentFov = Math.min(currentFov + fovAnimationStep, targetFov)
|
||||
// this.camera.fov = currentFov
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.dollyZoom(currentFov)
|
||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||
} else {
|
||||
// Set the flag to false as the FOV animation is complete
|
||||
this.isFovAnimationInProgress = false
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
animateFovChange() // Start the animation
|
||||
})
|
||||
dispose = () => {
|
||||
// Dispose of scene resources, renderer, and controls
|
||||
this.renderer.dispose()
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
// Dispose of any other resources like geometries, materials, textures
|
||||
}
|
||||
|
||||
useOrthographicCamera = () => {
|
||||
this.isPerspective = false
|
||||
const { x: px, y: py, z: pz } = this.camera.position
|
||||
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
||||
const { x: tx, y: ty, z: tz } = this.controls.target
|
||||
const aspect = window.innerWidth / window.innerHeight
|
||||
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
|
||||
this.camera = new OrthographicCamera(
|
||||
-ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
||||
ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
||||
ORTHOGRAPHIC_CAMERA_SIZE,
|
||||
-ORTHOGRAPHIC_CAMERA_SIZE,
|
||||
z_near,
|
||||
z_far
|
||||
)
|
||||
this.camera.up.set(0, 0, 1)
|
||||
this.camera.layers.enable(SKETCH_LAYER)
|
||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
this.camera.position.set(px, py, pz)
|
||||
const distance = this.camera.position.distanceTo(new Vector3(tx, ty, tz))
|
||||
const fovFactor = 45 / this.fov
|
||||
this.camera.zoom = (ZOOM_MAGIC_NUMBER * fovFactor * 0.8) / distance
|
||||
|
||||
this.setupOrbitControls([tx, ty, tz])
|
||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.controls.update()
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_set_orthographic',
|
||||
},
|
||||
})
|
||||
}
|
||||
usePerspectiveCamera = () => {
|
||||
this.isPerspective = true
|
||||
const { x: px, y: py, z: pz } = this.camera.position
|
||||
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
||||
const { x: tx, y: ty, z: tz } = this.controls.target
|
||||
const zoom = this.camera.zoom
|
||||
this.camera = this.createPerspectiveCamera()
|
||||
|
||||
this.camera.position.set(px, py, pz)
|
||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||
const zoomFudgeFactor = 2280
|
||||
const distance = zoomFudgeFactor / (zoom * this.fov)
|
||||
const direction = new Vector3().subVectors(
|
||||
this.camera.position,
|
||||
this.controls.target
|
||||
)
|
||||
direction.normalize()
|
||||
this.camera.position
|
||||
.copy(this.controls.target)
|
||||
.addScaledVector(direction, distance)
|
||||
|
||||
this.setupOrbitControls([tx, ty, tz])
|
||||
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_set_perspective',
|
||||
parameters: {
|
||||
fov_y: this.camera.fov,
|
||||
...calculateNearFarFromFOV(this.fov),
|
||||
},
|
||||
},
|
||||
})
|
||||
this.onCameraChange()
|
||||
return this.camera
|
||||
}
|
||||
|
||||
dollyZoom = (newFov: number) => {
|
||||
if (!(this.camera instanceof PerspectiveCamera)) {
|
||||
console.warn('Dolly zoom is only applicable to perspective cameras.')
|
||||
return
|
||||
}
|
||||
this.fov = newFov
|
||||
|
||||
// Calculate the direction vector from the camera towards the controls target
|
||||
const direction = new Vector3()
|
||||
.subVectors(this.controls.target, this.camera.position)
|
||||
.normalize()
|
||||
|
||||
// Calculate the distance to the controls target before changing the FOV
|
||||
const distanceBefore = this.camera.position.distanceTo(this.controls.target)
|
||||
|
||||
// Calculate the scale factor for the new FOV compared to the old one
|
||||
// This needs to be calculated before updating the camera's FOV
|
||||
const oldFov = this.camera.fov
|
||||
|
||||
const viewHeightFactor = (fov: number) => {
|
||||
/* *
|
||||
/|
|
||||
/ |
|
||||
/ |
|
||||
/ |
|
||||
/ | viewHeight/2
|
||||
/ |
|
||||
/ |
|
||||
/↙️fov/2 |
|
||||
/________|
|
||||
\ |
|
||||
\._._._.|
|
||||
*/
|
||||
return Math.tan(deg2Rad(fov / 2))
|
||||
}
|
||||
const scaleFactor = viewHeightFactor(oldFov) / viewHeightFactor(newFov)
|
||||
|
||||
this.camera.fov = newFov
|
||||
this.camera.updateProjectionMatrix()
|
||||
|
||||
const distanceAfter = distanceBefore * scaleFactor
|
||||
|
||||
const newPosition = this.controls.target
|
||||
.clone()
|
||||
.add(direction.multiplyScalar(-distanceAfter))
|
||||
this.camera.position.copy(newPosition)
|
||||
|
||||
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
|
||||
this.camera.near = z_near
|
||||
this.camera.far = z_far
|
||||
|
||||
throttledUpdateEngineFov({
|
||||
fov: newFov,
|
||||
position: newPosition,
|
||||
quaternion: this.camera.quaternion,
|
||||
zoom: this.camera.zoom,
|
||||
target: this.controls.target,
|
||||
})
|
||||
}
|
||||
getPlaneIntersectPoint = (): {
|
||||
intersection2d?: Vector2
|
||||
intersectPoint: Vector3
|
||||
@ -769,7 +233,7 @@ class SceneInfra {
|
||||
} | null => {
|
||||
this.planeRaycaster.setFromCamera(
|
||||
this.currentMouseVector,
|
||||
sceneInfra.camera
|
||||
sceneInfra.camControls.camera
|
||||
)
|
||||
const planeIntersects = this.planeRaycaster.intersectObjects(
|
||||
this.scene.children,
|
||||
@ -907,7 +371,7 @@ class SceneInfra {
|
||||
}
|
||||
|
||||
// Check the center point
|
||||
this.raycaster.setFromCamera(mouseDownVector, this.camera)
|
||||
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
|
||||
updateClosestIntersection(
|
||||
this.raycaster.intersectObjects(this.scene.children, true)
|
||||
)
|
||||
@ -922,7 +386,7 @@ class SceneInfra {
|
||||
mouseDownVector.x + offsetX,
|
||||
mouseDownVector.y - offsetY
|
||||
)
|
||||
this.raycaster.setFromCamera(ringVector, this.camera)
|
||||
this.raycaster.setFromCamera(ringVector, this.camControls.camera)
|
||||
updateClosestIntersection(
|
||||
this.raycaster.intersectObjects(this.scene.children, true)
|
||||
)
|
||||
@ -1015,7 +479,10 @@ class SceneInfra {
|
||||
}
|
||||
})
|
||||
planesGroup.layers.enable(SKETCH_LAYER)
|
||||
const sceneScale = getSceneScale(this.camera, this.controls.target)
|
||||
const sceneScale = getSceneScale(
|
||||
this.camControls.camera,
|
||||
this.camControls.target
|
||||
)
|
||||
planesGroup.scale.set(sceneScale, sceneScale, sceneScale)
|
||||
this.scene.add(planesGroup)
|
||||
}
|
||||
@ -1050,52 +517,6 @@ class SceneInfra {
|
||||
|
||||
export const sceneInfra = new SceneInfra()
|
||||
|
||||
function convertThreeCamValuesToEngineCam({
|
||||
target,
|
||||
position,
|
||||
quaternion,
|
||||
zoom,
|
||||
isPerspective,
|
||||
}: ThreeCamValues): {
|
||||
center: Vector3
|
||||
up: Vector3
|
||||
vantage: Vector3
|
||||
} {
|
||||
// Something to consider is that the orbit controls have a target,
|
||||
// we're kind of deriving the target/lookAtVector here when it might not be needed
|
||||
// leaving for now since it's working but maybe revisit later
|
||||
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()
|
||||
if (isPerspective) {
|
||||
return {
|
||||
center: target,
|
||||
up: upVector,
|
||||
vantage: position,
|
||||
}
|
||||
}
|
||||
const zoomFactor = -ZOOM_MAGIC_NUMBER / zoom
|
||||
const direction = lookAtVector.clone().sub(position).normalize()
|
||||
const newVantage = position.clone().add(direction.multiplyScalar(zoomFactor))
|
||||
return {
|
||||
center: lookAtVector,
|
||||
up: upVector,
|
||||
vantage: newVantage,
|
||||
}
|
||||
}
|
||||
|
||||
function calculateNearFarFromFOV(fov: number) {
|
||||
const nearFarRatio = (fov - 3) / (45 - 3)
|
||||
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
||||
const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||
return { z_near: 0.1, z_far }
|
||||
}
|
||||
|
||||
export function getSceneScale(
|
||||
camera: PerspectiveCamera | OrthographicCamera,
|
||||
target: Vector3
|
||||
@ -1131,12 +552,6 @@ function baseUnitTomm(baseUnit: BaseUnit) {
|
||||
}
|
||||
}
|
||||
|
||||
export function isQuaternionVertical(q: Quaternion) {
|
||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||
// no x or y components means it's vertical
|
||||
return compareVec2Epsilon2([v.x, v.y], [0, 0])
|
||||
}
|
||||
|
||||
export type DefaultPlane =
|
||||
| 'xy-default-plane'
|
||||
| 'xz-default-plane'
|
||||
|
Reference in New Issue
Block a user