Files
speckle-server/packages/viewer/src/modules/extensions/CameraController.ts
T
Alexandru Popovici f3974dd9d0 Alex/text updates (#5012)
* feat(viewer-lib): Text updates:
- Update to latest troika-three-text version
- Started working on the new TextBatch whoich will actually batch texts
- Augmented BatchedText type from troika
- Renamed old SpeckleText to TextLabel

* feat)viewer-lib): Copied over the batched version for the speckle text into our text material since troika won't export it

* feat(viewer-lib): First draft on text batching

* feat(viewer-lib): WIP on TextBatch and SpeckleText

* feat(viewer-lib): SpeckleText batch now has working TAS and BAS. Overloaded getBatches to also take an array of geometry types and added GeometryType.TEXT in places where required alongisde MESH

* feat(viewer-lib): Text batch has correctly transformed texts

* feat(viewer-lib): Patched troika BatchedText to allow per text opacity. Draw ranges for text batches are now functional

* feat(viewer-lib): Fixed an issue with the BAS not correctly reporting bounds. Had to override two methods completely in our SpeckleText extension of the BatchedText just so that we don't do stupid things and still get good performance when dealing with a huge number of texts

* feat(viewer-lib): Added text batch object count limit. Default is 5k. Implemented proper material caching and cloning inside SpeckleText. Overriden the default updateBounds function so that we don't waste tens of millisecons per frame!!! pointlessly

* feat(viewer-lib): Implemented TextBatchObject along with individual text batch object transform manipulation at batch level.

* chore(viewer-lib): Updated the pipelines to not render text geometries twice

* feat(viewer-lib): Implemented RTE for batched text rendering. As with the rest of the geometry types, RTE is automaic and will only be used when needed

* feat(viewer-lib): Integrated remaining text v3 features: alignments and maxWidth

* feat(viwer-lib): Implemented billboarding and RTE billboarding for text.

* feat(viewer-lib): Text batches now report correct object materials and can be filtered properly

* fix(viewer-lib): Some Fixes:
- The need for text RTE is now correctly being computed on the right text dimensions
- Sequential update ranges now correctly apply materials to all of them

* fix(viewer-lib): RTE text box is now correctly transformed. The text batch object only uses the TAS for intersecting since it's BAS is redundant.

* feat(viewer-lib): Text batches now correctly use gradient/ramp textures along with sample indices for colored filtering.

* feat(viewer-lib): Implemented raycasting for billboarded text batches in the most simple and robust way I was capable of. Lacks TAS speedup but it's a compromise we have to make and one which we probably will never regret

* feat(viewer-lib): Good progress on reworking TextLabel, which replaces the old multi purpose SpeckleText, which we use internally for measurements. More precise rendering, no more rogue margins between text and background. Regular billboarding now also works, along with non-billboarded rendering

* feat(viewer-lib): Finally a unified billboarding solution in SpeckleBasicMaterial. Supporting both world and screen billboarding; SpeckleTextMaterial now extends SpeckleBasicMaterial; TextLabel now has proper control over size and margins. No more weird offsets. Added background margins to the text params which work in both world and screen space.

* feat(viewer-lib): Implemented raycasting for all billboarding types. Spent quite some time on the screen billboarding one because of a stupid mistake

* chore(viewer-lib): Added (vibed) type declaration file for troika's Text class and fixed compiler errors for TextLabel

* chore(viewer-lib): Renamed SpeckleText to SpeckleBatchedText and fixed all compiler errors. Updated type definition file

* feat(viewer-lib): Integrated TextLabel with measurements. Simplified a lot of code

* fix(viewer-lib): Some updates and fixes to text and measurements integration
- Screen space billboarding now also takes an NDC offset alongside the size.
- Added auto margin calculation for TextLabel background so it's always centered regardless of anchor-ing
- DPR is automatically factored in for TextLabel
- Some changes to sizes and margins for measurements

* fix(viewer-lib): Bunch of fixes and tweaks

* fix(viewer-lib): Area measurement's area plane no longer overdraws on top of the area value text label via simple stenciling

* fix(viewer-lib): Fixed CI build

* fix(viewer-lib): Fixed CI build

* feat(viewer-lib): Slightly reduces the size and h margin of text gizmos for measurements

* fix(viewer-lib): Fixed incorrect text transformation when neither RTE nor billboarded

* chore(viewer-lib): Added review suggestions
2025-07-15 14:48:13 +03:00

739 lines
22 KiB
TypeScript

import { Extension } from './Extension.js'
import {
Box3,
Camera,
MathUtils,
OrthographicCamera,
PerspectiveCamera,
Quaternion,
Sphere,
Vector2,
Vector3
} from 'three'
import { CameraProjection, type CameraEventPayload } from '../objects/SpeckleCamera.js'
import { CameraEvent, type SpeckleCamera } from '../objects/SpeckleCamera.js'
import { FlyControls, FlyControlsOptions } from './controls/FlyControls.js'
import { SpeckleControls } from './controls/SpeckleControls.js'
import Logger from '../utils/Logger.js'
import { GeometryType } from '../batching/Batch.js'
import { IViewer, SpeckleView, UpdateFlags } from '../../IViewer.js'
import {
SmoothOrbitControlsOptions,
SmoothOrbitControls
} from './controls/SmoothOrbitControls.js'
export enum NearPlaneCalculation {
EMPIRIC,
ACCURATE
}
export type CanonicalView =
| 'front'
| 'back'
| 'up'
| 'top'
| 'down'
| 'bottom'
| 'right'
| 'left'
| '3d'
| '3D'
export type InlineView = {
position: Vector3
target: Vector3
}
export type PolarView = {
azimuth: number
polar: number
radius?: number
origin?: Vector3
}
export type CameraControllerOptions = SmoothOrbitControlsOptions &
FlyControlsOptions & { nearPlaneCalculation?: NearPlaneCalculation }
export function isPerspectiveCamera(camera: Camera): camera is PerspectiveCamera {
return (camera as PerspectiveCamera).isPerspectiveCamera
}
export function isOrthographicCamera(camera: Camera): camera is OrthographicCamera {
return (camera as OrthographicCamera).isOrthographicCamera
}
export function computeOrthographicSize(
distance: number,
fov: number,
aspect: number
): Vector2 {
const height = Math.tan(MathUtils.DEG2RAD * (fov / 2)) * 2.0 * distance
const width = height * aspect
return new Vector2(width, height)
}
export const DefaultOrbitControlsOptions: Required<CameraControllerOptions> = {
enableOrbit: true,
enableZoom: true,
enablePan: true,
orbitSensitivity: 1,
zoomSensitivity: 1,
panSensitivity: 1,
inputSensitivity: 1,
minimumRadius: 0,
maximumRadius: Infinity,
minimumPolarAngle: 0,
maximumPolarAngle: Math.PI,
minimumAzimuthalAngle: -Infinity,
maximumAzimuthalAngle: Infinity,
minimumFieldOfView: 40,
maximumFieldOfView: 60,
touchAction: 'none',
infiniteZoom: true,
zoomToCursor: true,
orbitAroundCursor: true,
showOrbitPoint: true,
lookSpeed: 1,
moveSpeed: 1,
damperDecay: 30,
enableLook: true,
relativeUpDown: false,
nearPlaneCalculation: NearPlaneCalculation.ACCURATE
}
export class CameraController extends Extension implements SpeckleCamera {
protected _renderingCamera: PerspectiveCamera | OrthographicCamera
protected perspectiveCamera: PerspectiveCamera
protected orthographicCamera: OrthographicCamera
protected _lastCameraChanged: boolean = false
protected _options: Required<CameraControllerOptions> = DefaultOrbitControlsOptions
protected _activeControls: SpeckleControls
protected _orbitControls: SmoothOrbitControls
protected _flyControls: FlyControls
get renderingCamera(): PerspectiveCamera | OrthographicCamera {
return this._renderingCamera
}
set renderingCamera(value: PerspectiveCamera | OrthographicCamera) {
this._renderingCamera = value
}
public get enabled() {
return this._activeControls.enabled
}
public set enabled(val) {
this.controls.enabled = val
}
public get fieldOfView(): number {
return this.perspectiveCamera.fov
}
public set fieldOfView(value: number) {
this.perspectiveCamera.fov = value
this.perspectiveCamera.updateProjectionMatrix()
}
public get aspect(): number {
return this.perspectiveCamera.aspect
}
public get controls(): SpeckleControls {
return this._activeControls
}
public get options(): Required<CameraControllerOptions> {
return this._options
}
public set options(value: CameraControllerOptions) {
Object.assign(this._options, value)
this._orbitControls.options = value
this._flyControls.options = value
}
public constructor(viewer: IViewer) {
super(viewer)
/** Create the default perspective camera */
this.perspectiveCamera = new PerspectiveCamera(
55,
window.innerWidth / window.innerHeight
)
const aspect =
this.viewer.getContainer().offsetWidth / this.viewer.getContainer().offsetHeight
/** Create the default orthographic camera */
const fustrumSize = 50
this.orthographicCamera = new OrthographicCamera(
(-fustrumSize * aspect) / 2,
(fustrumSize * aspect) / 2,
fustrumSize / 2,
-fustrumSize / 2,
0.001,
10000
)
/** Perspective camera as default on startup */
this.renderingCamera = this.perspectiveCamera
this._flyControls = new FlyControls(
this._renderingCamera,
this.viewer.getContainer(),
this.viewer.World,
this._options
)
this._flyControls.enabled = false
this._flyControls.setDamperDecayTime(30)
this._flyControls.up = new Vector3(0, 0, 1)
this._orbitControls = new SmoothOrbitControls(
this.perspectiveCamera,
this.viewer.getContainer(),
this.viewer.World,
this.viewer.getRenderer(),
this._options
)
this._orbitControls.enabled = true
this.viewer.getRenderer().speckleCamera = this
this._activeControls = this._orbitControls
this.default()
}
public default() {
if (this._activeControls instanceof SmoothOrbitControls) {
this._activeControls.up = new Vector3(0, 0, 1)
this._activeControls.setOrbit(2.356, 0.955)
this._activeControls.jumpToGoal()
}
}
public on<T extends CameraEvent>(
eventType: T,
listener: (arg: CameraEventPayload[T]) => void
): void {
super.on(eventType, listener)
}
public getTarget(): Vector3 {
return this._activeControls.getTarget()
}
public getPosition(): Vector3 {
return this._activeControls.getPosition()
}
public toggleControls() {
const oldControls: SpeckleControls = this._activeControls
let newControls: SpeckleControls | undefined = undefined
if (this._activeControls instanceof SmoothOrbitControls) {
newControls = this._flyControls
} else if (this._activeControls instanceof FlyControls) {
newControls = this._orbitControls
}
if (!newControls) throw new Error('Not controls found!')
oldControls.enabled = false
newControls.enabled = true
newControls.fromPositionAndTarget(
oldControls.getCurrentPosition(),
oldControls.getCurrentTarget()
)
newControls.jumpToGoal()
this._activeControls = newControls
this.viewer.requestRender()
}
public setCameraView(
objectIds: string[] | undefined,
transition: boolean | undefined,
fit?: number
): void
public setCameraView(
view: CanonicalView | SpeckleView | InlineView | PolarView,
transition: boolean | undefined,
fit?: number
): void
public setCameraView(
bounds: Box3,
transition: boolean | undefined,
fit?: number
): void
public setCameraView(
arg0:
| string[]
| CanonicalView
| SpeckleView
| InlineView
| PolarView
| Box3
| undefined,
arg1 = true,
arg2 = 1.2
): void {
if (!arg0) {
this.zoomExtents(arg2, arg1)
} else if (Array.isArray(arg0)) {
this.zoom(arg0, arg2, arg1)
} else if (this.isBox3(arg0)) {
this.zoomToBox(arg0, arg2, arg1)
} else {
this.setView(arg0, arg1)
}
this.emit(CameraEvent.Dynamic)
}
public onEarlyUpdate(_delta?: number) {
const changed = this._activeControls.update(_delta)
if (changed !== this._lastCameraChanged) {
this.emit(changed ? CameraEvent.Dynamic : CameraEvent.Stationary)
}
this.emit(CameraEvent.FrameUpdate, changed)
this._lastCameraChanged = changed
if (changed) {
this.updateCameraPlanes()
}
}
public onLateUpdate(): void {
this.emit(CameraEvent.LateFrameUpdate, this._lastCameraChanged)
}
public onResize() {
const aspect =
this.viewer.getContainer().offsetWidth / this.viewer.getContainer().offsetHeight
this.perspectiveCamera.aspect = aspect
this.perspectiveCamera.updateProjectionMatrix()
const distance = this._activeControls
.getPosition()
.distanceTo(this._activeControls.getTarget())
const orthographicSize = computeOrthographicSize(
distance,
this.perspectiveCamera.fov,
aspect
)
this.orthographicCamera.zoom = 1
this.orthographicCamera.left = orthographicSize.x / -2
this.orthographicCamera.right = orthographicSize.x / 2
this.orthographicCamera.top = orthographicSize.y / 2
this.orthographicCamera.bottom = orthographicSize.y / -2
this.orthographicCamera.updateProjectionMatrix()
}
public setPerspectiveCameraOn() {
if (this._renderingCamera === this.perspectiveCamera) return
this.renderingCamera = this.perspectiveCamera
this.setupPerspectiveCamera()
this.viewer.requestRender(UpdateFlags.RENDER_RESET)
}
public setOrthoCameraOn(): void {
if (this._renderingCamera === this.orthographicCamera) return
this.renderingCamera = this.orthographicCamera
this.setupOrthoCamera()
this.viewer.requestRender(UpdateFlags.RENDER_RESET)
}
public toggleCameras(): void {
if (this._renderingCamera === this.perspectiveCamera) this.setOrthoCameraOn()
else this.setPerspectiveCameraOn()
}
protected setupOrthoCamera() {
this.controls.targetCamera = this.orthographicCamera
this.enableRotations()
this.updateCameraPlanes(this.viewer.getRenderer().sceneBox)
this.emit(CameraEvent.ProjectionChanged, CameraProjection.ORTHOGRAPHIC)
}
protected setupPerspectiveCamera() {
this.controls.targetCamera = this.perspectiveCamera
this.enableRotations()
this.updateCameraPlanes(this.viewer.getRenderer().sceneBox)
this.emit(CameraEvent.ProjectionChanged, CameraProjection.PERSPECTIVE)
}
public disableRotations() {
this.options = { enableOrbit: false, enableLook: false }
}
public enableRotations() {
this.options = { enableOrbit: true, enableLook: true }
}
public updateCameraPlanes(targetVolume?: Box3, offsetScale: number = 1) {
const renderer = this.viewer.getRenderer()
if (!renderer.renderingCamera) return
if (!targetVolume) targetVolume = this.viewer.getRenderer().sceneBox
let nearPlane = this.computeNearCameraPlaneEmpiric(targetVolume, offsetScale)
if (this._options.nearPlaneCalculation === NearPlaneCalculation.ACCURATE)
nearPlane = this.computeNearCameraPlaneAccurate(
targetVolume,
offsetScale,
nearPlane
)
if (nearPlane) {
renderer.renderingCamera.near = nearPlane
renderer.renderingCamera.updateProjectionMatrix()
}
this.updateFarCameraPlane()
}
protected computeNearCameraPlaneEmpiric(
targetVolume?: Box3,
offsetScale: number = 1
): number | undefined {
if (!targetVolume) return
if (targetVolume.isEmpty()) {
Logger.warn('Cannot set camera planes for empty volume')
return
}
const size = targetVolume.getSize(new Vector3())
const maxSize = Math.max(size.x, size.y, size.z)
const camFov =
this._renderingCamera === this.perspectiveCamera ? this.fieldOfView : 55
const camAspect =
this._renderingCamera === this.perspectiveCamera ? this.aspect : 1.2
const fitHeightDistance = maxSize / (2 * Math.atan((Math.PI * camFov) / 360))
const fitWidthDistance = fitHeightDistance / camAspect
const distance = offsetScale * Math.max(fitHeightDistance, fitWidthDistance)
return this.perspectiveCamera ? distance / 100 : 0.001
}
protected computeNearCameraPlaneAccurate(
targetVolume?: Box3,
offsetScale: number = 1,
fallback?: number
): number | undefined {
const minDist = this.getClosestGeometryDistance(fallback)
this._flyControls.minDist = minDist
this._orbitControls.minDist = minDist
if (minDist === Number.POSITIVE_INFINITY) {
return this.computeNearCameraPlaneEmpiric(targetVolume, offsetScale)
}
const camFov =
this._renderingCamera === this.perspectiveCamera ? this.fieldOfView : 55
const camAspect =
this._renderingCamera === this.perspectiveCamera ? this.aspect : 1.2
const nearPlane =
Math.max(minDist, 0) /
Math.sqrt(
1 +
Math.pow(Math.tan(((camFov / 180) * Math.PI) / 2), 2) *
(Math.pow(camAspect, 2) + 1)
)
// console.log(minDist, nearPlane)
return nearPlane
}
protected updateFarCameraPlane() {
const renderer = this.viewer.getRenderer()
if (!renderer.renderingCamera) return
const v = new Vector3()
const box = renderer.sceneBox
const camPos = new Vector3().copy(renderer.renderingCamera.position)
let d = 0
v.set(box.min.x, box.min.y, box.min.z) // 000
d = Math.max(camPos.distanceTo(v), d)
v.set(box.min.x, box.min.y, box.max.z) // 001
d = Math.max(camPos.distanceTo(v), d)
v.set(box.min.x, box.max.y, box.min.z) // 010
d = Math.max(camPos.distanceTo(v), d)
v.set(box.min.x, box.max.y, box.max.z) // 011
d = Math.max(camPos.distanceTo(v), d)
v.set(box.max.x, box.min.y, box.min.z) // 100
d = Math.max(camPos.distanceTo(v), d)
v.set(box.max.x, box.min.y, box.max.z) // 101
d = Math.max(camPos.distanceTo(v), d)
v.set(box.max.x, box.max.y, box.min.z) // 110
d = Math.max(camPos.distanceTo(v), d)
v.set(box.max.x, box.max.y, box.max.z) // 111
d = Math.max(camPos.distanceTo(v), d)
renderer.renderingCamera.far = d * 2
renderer.renderingCamera.updateProjectionMatrix()
}
protected getClosestGeometryDistance(fallback?: number): number {
const cameraPosition = this._activeControls.getCurrentPosition()
const cameraTarget = this._activeControls.getCurrentTarget()
const cameraDir = new Vector3().subVectors(cameraTarget, cameraPosition).normalize()
const batches = this.viewer
.getRenderer()
.batcher.getBatches(undefined, [GeometryType.MESH, GeometryType.TEXT])
let minDist = Number.POSITIVE_INFINITY
for (let b = 0; b < batches.length; b++) {
const result = batches[b].mesh.TAS.closestPointToPointHalfplane(
cameraPosition,
cameraDir,
fallback
)
if (!result) continue
minDist = Math.min(minDist, result.distance)
}
return minDist
}
protected zoom(objectIds?: string[], fit?: number, transition?: boolean) {
if (!objectIds) {
this.zoomExtents(fit, transition)
return
}
this.zoomToBox(this.viewer.getRenderer().boxFromObjects(objectIds), fit, transition)
}
protected zoomExtents(fit = 1.2, transition = true) {
if (this.viewer.getRenderer().clippingVolume.isEmpty()) {
const box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
this.zoomToBox(box, fit, transition)
return
}
const box = new Box3().fromOBB(this.viewer.getRenderer().clippingVolume)
/** This is for special cases like when the stream will only have one point
* which three will not consider it's size when computing the bounding box
* resulting in a zero size bounding box. That's why we make sure the bounding
* box is never zero in size
*/
if (box.min.equals(box.max)) {
box.expandByVector(new Vector3(1, 1, 1))
}
this.zoomToBox(box, fit, transition)
}
protected zoomToBox(box: Box3, fit = 1.2, transition = true) {
if (box.max.x === Infinity || box.max.x === -Infinity) {
box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
}
const targetSphere = new Sphere()
box.getBoundingSphere(targetSphere)
targetSphere.radius = this.fitToRadius(targetSphere.radius) * fit
this._activeControls.fitToSphere(targetSphere)
if (!transition) {
this._activeControls.jumpToGoal()
}
this.updateCameraPlanes(box, fit)
}
protected fitToRadius(radius: number) {
// https://stackoverflow.com/a/44849975
const vFOV = this.perspectiveCamera.getEffectiveFOV() * MathUtils.DEG2RAD
const hFOV = Math.atan(Math.tan(vFOV * 0.5) * this.perspectiveCamera.aspect) * 2
const fov = 1 < this.perspectiveCamera.aspect ? vFOV : hFOV
return radius / Math.sin(fov * 0.5)
}
protected isSpeckleView(
view: CanonicalView | SpeckleView | InlineView | PolarView
): view is SpeckleView {
return (view as SpeckleView).name !== undefined
}
protected isCanonicalView(
view: CanonicalView | SpeckleView | InlineView | PolarView
): view is CanonicalView {
return typeof (view as CanonicalView) === 'string'
}
protected isInlineView(
view: CanonicalView | SpeckleView | InlineView | PolarView
): view is InlineView {
return (
(view as InlineView).position !== undefined &&
(view as InlineView).target !== undefined
)
}
protected isPolarView(
view: CanonicalView | SpeckleView | InlineView | PolarView
): view is PolarView {
return (
(view as PolarView).azimuth !== undefined &&
(view as PolarView).polar !== undefined
)
}
protected isBox3(
view: CanonicalView | SpeckleView | InlineView | PolarView | Box3
): view is Box3 {
return view instanceof Box3
}
protected setView(
view: CanonicalView | SpeckleView | InlineView | PolarView,
transition = true
): void {
if (this.isSpeckleView(view)) {
this.setViewSpeckle(view, transition)
}
if (this.isCanonicalView(view)) {
this.setViewCanonical(view, transition)
}
if (this.isInlineView(view)) {
this.setViewInline(view, transition)
}
if (this.isPolarView(view)) {
this.setViewPolar(view, transition)
}
}
protected setViewSpeckle(view: SpeckleView, transition = true) {
this._activeControls.fromPositionAndTarget(
new Vector3(view.origin.x, view.origin.y, view.origin.z),
new Vector3(view.target.x, view.target.y, view.target.z)
)
if (!transition) this._activeControls.jumpToGoal()
this.enableRotations()
}
/**
* Rotates camera to some canonical views
* @param {string} side Can be any of front, back, up (top), down (bottom), right, left.
* @param {Number} fit [description]
* @param {Boolean} transition [description]
* @return {[type]} [description]
*/
protected setViewCanonical(side: string, transition = true) {
const targetSphere = new Sphere()
this.viewer.World.worldBox.getBoundingSphere(targetSphere)
const distance = this.fitToRadius(targetSphere.radius)
const canonicalPosition = new Vector3().copy(
this.viewer.World.worldBox.getCenter(new Vector3())
)
const canonicalTarget = new Vector3().copy(canonicalPosition)
const controlerBasis = new Quaternion().setFromUnitVectors(
new Vector3(0, 1, 0),
this._activeControls.up
)
switch (side) {
case 'front':
this._activeControls.fromPositionAndTarget(
canonicalPosition.add(
new Vector3(0, 0, 1)
.applyQuaternion(controlerBasis)
.multiplyScalar(distance)
),
canonicalTarget
)
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
break
case 'back':
this._activeControls.fromPositionAndTarget(
canonicalPosition.add(
new Vector3(0, 0, -1)
.applyQuaternion(controlerBasis)
.multiplyScalar(distance)
),
canonicalTarget
)
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
break
case 'up':
case 'top':
this._activeControls.fromPositionAndTarget(
canonicalPosition.add(
new Vector3(0, 1, 0)
.applyQuaternion(controlerBasis)
.multiplyScalar(distance)
),
canonicalTarget
)
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
break
case 'down':
case 'bottom':
this._activeControls.fromPositionAndTarget(
canonicalPosition.add(
new Vector3(0, -1, 0)
.applyQuaternion(controlerBasis)
.multiplyScalar(distance)
),
canonicalTarget
)
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
break
case 'right':
this._activeControls.fromPositionAndTarget(
canonicalPosition.add(
new Vector3(1, 0, 0)
.applyQuaternion(controlerBasis)
.multiplyScalar(distance)
),
canonicalTarget
)
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
break
case 'left':
this._activeControls.fromPositionAndTarget(
canonicalPosition.add(
new Vector3(-1, 0, 0)
.applyQuaternion(controlerBasis)
.multiplyScalar(distance)
),
canonicalTarget
)
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
break
case '3d':
this._activeControls.fromPositionAndTarget(
new Vector3().copy(this.viewer.World.worldBox.max),
canonicalTarget
)
this.zoomExtents()
break
default: {
this.enableRotations()
break
}
}
if (!transition) this._activeControls.jumpToGoal()
}
protected setViewInline(view: InlineView, transition = true) {
this._activeControls.fromPositionAndTarget(view.position, view.target)
if (!transition) this._activeControls.jumpToGoal()
this.enableRotations()
}
private setViewPolar(view: PolarView, transition = true) {
;(this._activeControls as SmoothOrbitControls).adjustOrbit(
view.azimuth,
view.polar,
view.radius ? view.radius : 0
)
if (!transition) this._activeControls.jumpToGoal()
this.enableRotations()
}
}