d7f0325192
* Removed the concept of Providers entirely. Now extentions specify explicity extention types or archypes in their inject lists. Removed the concept of core-extensions entirely. All extensions are now equal. The concept of CameraProvider was replaced by SpeckleCamera which the SpeckleRenderer now uses and relie on the default camera controller extension to seed it. * Fixed some compile errors * Fixed compile errors. Had to make Extension concrete, but meh, it's fine I guess * fix viewer types * Removed generic arguments since they're no longer needed --------- Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
596 lines
18 KiB
TypeScript
596 lines
18 KiB
TypeScript
import * as THREE from 'three'
|
|
import CameraControls from 'camera-controls'
|
|
import { Extension } from './Extension'
|
|
import { SpeckleCameraControls } from '../objects/SpeckleCameraControls'
|
|
import { Box3, OrthographicCamera, PerspectiveCamera, Sphere, Vector3 } from 'three'
|
|
import { KeyboardKeyHold, HOLD_EVENT_TYPE } from 'hold-event'
|
|
import { CameraProjection } from '../objects/SpeckleCamera'
|
|
import { CameraEvent, SpeckleCamera } from '../objects/SpeckleCamera'
|
|
import Logger from 'js-logger'
|
|
import { IViewer, SpeckleView } from '../../IViewer'
|
|
|
|
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 class CameraController extends Extension implements SpeckleCamera {
|
|
protected _renderingCamera: PerspectiveCamera | OrthographicCamera = null
|
|
protected perspectiveCamera: PerspectiveCamera = null
|
|
protected orthographicCamera: OrthographicCamera = null
|
|
protected _controls: SpeckleCameraControls = null
|
|
|
|
get renderingCamera(): PerspectiveCamera | OrthographicCamera {
|
|
return this._renderingCamera
|
|
}
|
|
|
|
set renderingCamera(value: PerspectiveCamera | OrthographicCamera) {
|
|
this._renderingCamera = value
|
|
}
|
|
|
|
public get enabled() {
|
|
return this._controls.enabled
|
|
}
|
|
|
|
public set enabled(val) {
|
|
this._controls.enabled = val
|
|
}
|
|
|
|
public get fieldOfView() {
|
|
return this.perspectiveCamera.fov
|
|
}
|
|
|
|
public set fieldOfView(value: number) {
|
|
this.perspectiveCamera.fov = value
|
|
this.perspectiveCamera.updateProjectionMatrix()
|
|
}
|
|
|
|
public get aspect() {
|
|
return this.perspectiveCamera.aspect
|
|
}
|
|
|
|
public get controls() {
|
|
return this._controls
|
|
}
|
|
|
|
public constructor(viewer: IViewer) {
|
|
super(viewer)
|
|
/** Create the default perspective camera */
|
|
this.perspectiveCamera = new PerspectiveCamera(
|
|
55,
|
|
window.innerWidth / window.innerHeight
|
|
)
|
|
this.perspectiveCamera.up.set(0, 0, 1)
|
|
this.perspectiveCamera.position.set(1, 1, 1)
|
|
this.perspectiveCamera.updateProjectionMatrix()
|
|
|
|
const aspect =
|
|
this.viewer.getContainer().offsetWidth / this.viewer.getContainer().offsetHeight
|
|
|
|
/** Create the defaultorthographic camera */
|
|
const fustrumSize = 50
|
|
this.orthographicCamera = new OrthographicCamera(
|
|
(-fustrumSize * aspect) / 2,
|
|
(fustrumSize * aspect) / 2,
|
|
fustrumSize / 2,
|
|
-fustrumSize / 2,
|
|
0.001,
|
|
10000
|
|
)
|
|
this.orthographicCamera.up.set(0, 0, 1)
|
|
this.orthographicCamera.position.set(100, 100, 100)
|
|
this.orthographicCamera.updateProjectionMatrix()
|
|
|
|
/** Perspective camera as default on startup */
|
|
this.renderingCamera = this.perspectiveCamera
|
|
|
|
/** Setup the controls library (urgh...) */
|
|
CameraControls.install({ THREE })
|
|
SpeckleCameraControls.install()
|
|
this._controls = new SpeckleCameraControls(
|
|
this.perspectiveCamera,
|
|
this.viewer.getContainer()
|
|
)
|
|
this._controls.maxPolarAngle = Math.PI / 2
|
|
this._controls.restThreshold = 0.001
|
|
this.setupWASDControls()
|
|
|
|
this._controls.addEventListener('rest', () => {
|
|
this.emit(CameraEvent.Stationary)
|
|
})
|
|
this._controls.addEventListener('controlstart', () => {
|
|
this.emit(CameraEvent.Dynamic)
|
|
})
|
|
|
|
this._controls.addEventListener('controlend', () => {
|
|
if (this._controls.hasRested) this.emit(CameraEvent.Stationary)
|
|
})
|
|
|
|
this._controls.addEventListener('control', () => {
|
|
this.emit(CameraEvent.Dynamic)
|
|
})
|
|
|
|
this.viewer.getRenderer().speckleCamera = this
|
|
}
|
|
|
|
setCameraView(objectIds: string[], transition: boolean, fit?: number): void
|
|
setCameraView(
|
|
view: CanonicalView | SpeckleView | InlineView | PolarView,
|
|
transition: boolean
|
|
): void
|
|
setCameraView(bounds: Box3, transition: boolean): void
|
|
setCameraView(
|
|
arg0: string[] | CanonicalView | SpeckleView | InlineView | PolarView | Box3,
|
|
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(deltaTime: number) {
|
|
const changed = this._controls.update(deltaTime)
|
|
this.emit(CameraEvent.FrameUpdate, changed)
|
|
}
|
|
|
|
public onResize() {
|
|
this.perspectiveCamera.aspect =
|
|
this.viewer.getContainer().offsetWidth / this.viewer.getContainer().offsetHeight
|
|
this.perspectiveCamera.updateProjectionMatrix()
|
|
|
|
const lineOfSight = new Vector3()
|
|
this.perspectiveCamera.getWorldDirection(lineOfSight)
|
|
const target = new Vector3()
|
|
this._controls.getTarget(target)
|
|
const distance = target.clone().sub(this.perspectiveCamera.position)
|
|
const depth = distance.dot(lineOfSight)
|
|
const dims = {
|
|
x: this.viewer.getContainer().offsetWidth,
|
|
y: this.viewer.getContainer().offsetHeight
|
|
}
|
|
const aspect = dims.x / dims.y
|
|
const fov = this.perspectiveCamera.fov
|
|
const height = depth * 2 * Math.atan((fov * (Math.PI / 180)) / 2)
|
|
const width = height * aspect
|
|
|
|
this.orthographicCamera.zoom = 1
|
|
this.orthographicCamera.left = width / -2
|
|
this.orthographicCamera.right = width / 2
|
|
this.orthographicCamera.top = height / 2
|
|
this.orthographicCamera.bottom = height / -2
|
|
this.orthographicCamera.updateProjectionMatrix()
|
|
}
|
|
|
|
public setPerspectiveCameraOn() {
|
|
if (this._renderingCamera === this.perspectiveCamera) return
|
|
this.renderingCamera = this.perspectiveCamera
|
|
this.setupPerspectiveCamera()
|
|
this.viewer.requestRender()
|
|
}
|
|
|
|
public setOrthoCameraOn() {
|
|
if (this._renderingCamera === this.orthographicCamera) return
|
|
this.renderingCamera = this.orthographicCamera
|
|
this.setupOrthoCamera()
|
|
this.viewer.requestRender()
|
|
}
|
|
|
|
public toggleCameras() {
|
|
if (this._renderingCamera === this.perspectiveCamera) this.setOrthoCameraOn()
|
|
else this.setPerspectiveCameraOn()
|
|
}
|
|
|
|
protected setupOrthoCamera() {
|
|
this._controls.mouseButtons.wheel = CameraControls.ACTION.ZOOM
|
|
|
|
const lineOfSight = new Vector3()
|
|
this.perspectiveCamera.getWorldDirection(lineOfSight)
|
|
const target = new Vector3().copy(this.viewer.World.worldOrigin)
|
|
const distance = target.clone().sub(this.perspectiveCamera.position)
|
|
const depth = distance.length()
|
|
const dims = {
|
|
x: this.viewer.getContainer().offsetWidth,
|
|
y: this.viewer.getContainer().offsetHeight
|
|
}
|
|
const aspect = dims.x / dims.y
|
|
const fov = this.perspectiveCamera.fov
|
|
const height = depth * 2 * Math.atan((fov * (Math.PI / 180)) / 2)
|
|
const width = height * aspect
|
|
|
|
this.orthographicCamera.zoom = 1
|
|
this.orthographicCamera.left = width / -2
|
|
this.orthographicCamera.right = width / 2
|
|
this.orthographicCamera.top = height / 2
|
|
this.orthographicCamera.bottom = height / -2
|
|
this.orthographicCamera.far = this.perspectiveCamera.far
|
|
this.orthographicCamera.near = 0.0001
|
|
this.orthographicCamera.updateProjectionMatrix()
|
|
this.orthographicCamera.position.copy(this.perspectiveCamera.position)
|
|
this.orthographicCamera.quaternion.copy(this.perspectiveCamera.quaternion)
|
|
this.orthographicCamera.updateProjectionMatrix()
|
|
|
|
this._controls.camera = this.orthographicCamera
|
|
this.setCameraPlanes(this.viewer.getRenderer().sceneBox)
|
|
this.emit(CameraEvent.ProjectionChanged, CameraProjection.ORTHOGRAPHIC)
|
|
}
|
|
|
|
protected setupPerspectiveCamera() {
|
|
this._controls.mouseButtons.wheel = CameraControls.ACTION.DOLLY
|
|
this.perspectiveCamera.position.copy(this.perspectiveCamera.position)
|
|
this.perspectiveCamera.quaternion.copy(this.perspectiveCamera.quaternion)
|
|
this.perspectiveCamera.updateProjectionMatrix()
|
|
this._controls.camera = this.perspectiveCamera
|
|
this._controls.zoomTo(1)
|
|
this.enableRotations()
|
|
this.setCameraPlanes(this.viewer.getRenderer().sceneBox)
|
|
this.emit(CameraEvent.ProjectionChanged, CameraProjection.PERSPECTIVE)
|
|
}
|
|
|
|
public disableRotations() {
|
|
this._controls.mouseButtons.left = CameraControls.ACTION.TRUCK
|
|
}
|
|
|
|
public enableRotations() {
|
|
this._controls.mouseButtons.left = CameraControls.ACTION.ROTATE
|
|
}
|
|
|
|
protected setupWASDControls() {
|
|
const KEYCODE = { W: 87, A: 65, S: 83, D: 68 }
|
|
|
|
const wKey = new KeyboardKeyHold(KEYCODE.W, 16.666)
|
|
const aKey = new KeyboardKeyHold(KEYCODE.A, 16.666)
|
|
const sKey = new KeyboardKeyHold(KEYCODE.S, 16.666)
|
|
const dKey = new KeyboardKeyHold(KEYCODE.D, 16.666)
|
|
const isTruckingGroup = new Array(4)
|
|
|
|
const setTrucking = (index, value) => {
|
|
isTruckingGroup[index] = value
|
|
if (isTruckingGroup.every((element) => element === false)) {
|
|
this._controls.isTrucking = false
|
|
this._controls['dispatchEvent']({ type: 'rest' })
|
|
} else this._controls.isTrucking = true
|
|
}
|
|
|
|
aKey.addEventListener(
|
|
HOLD_EVENT_TYPE.HOLD_START,
|
|
function () {
|
|
this.controls.dispatchEvent({ type: 'controlstart' })
|
|
}.bind(this)
|
|
)
|
|
aKey.addEventListener(
|
|
'holding',
|
|
function (event) {
|
|
if (this.viewer.mouseOverRenderer === false) return
|
|
setTrucking(0, true)
|
|
this.controls.truck(-0.01 * event.deltaTime, 0, false)
|
|
return
|
|
}.bind(this)
|
|
)
|
|
aKey.addEventListener(
|
|
HOLD_EVENT_TYPE.HOLD_END,
|
|
function () {
|
|
setTrucking(0, false)
|
|
}.bind(this)
|
|
)
|
|
|
|
dKey.addEventListener(
|
|
HOLD_EVENT_TYPE.HOLD_START,
|
|
function () {
|
|
this.controls.dispatchEvent({ type: 'controlstart' })
|
|
}.bind(this)
|
|
)
|
|
dKey.addEventListener(
|
|
'holding',
|
|
function (event) {
|
|
if (this.viewer.mouseOverRenderer === false) return
|
|
setTrucking(1, true)
|
|
this.controls.truck(0.01 * event.deltaTime, 0, false)
|
|
return
|
|
}.bind(this)
|
|
)
|
|
dKey.addEventListener(
|
|
HOLD_EVENT_TYPE.HOLD_END,
|
|
function () {
|
|
setTrucking(1, false)
|
|
}.bind(this)
|
|
)
|
|
|
|
wKey.addEventListener(
|
|
HOLD_EVENT_TYPE.HOLD_START,
|
|
function () {
|
|
this.controls.dispatchEvent({ type: 'controlstart' })
|
|
}.bind(this)
|
|
)
|
|
wKey.addEventListener(
|
|
'holding',
|
|
function (event) {
|
|
if (this.viewer.mouseOverRenderer === false) return
|
|
setTrucking(2, true)
|
|
this.controls.forward(0.01 * event.deltaTime, false)
|
|
return
|
|
}.bind(this)
|
|
)
|
|
wKey.addEventListener(
|
|
HOLD_EVENT_TYPE.HOLD_END,
|
|
function () {
|
|
setTrucking(2, false)
|
|
}.bind(this)
|
|
)
|
|
|
|
sKey.addEventListener(
|
|
HOLD_EVENT_TYPE.HOLD_START,
|
|
function () {
|
|
this.controls.dispatchEvent({ type: 'controlstart' })
|
|
}.bind(this)
|
|
)
|
|
sKey.addEventListener(
|
|
'holding',
|
|
function (event) {
|
|
if (this.viewer.mouseOverRenderer === false) return
|
|
setTrucking(3, true)
|
|
this.controls.forward(-0.01 * event.deltaTime, false)
|
|
return
|
|
}.bind(this)
|
|
)
|
|
sKey.addEventListener(
|
|
HOLD_EVENT_TYPE.HOLD_END,
|
|
function () {
|
|
setTrucking(3, false)
|
|
}.bind(this)
|
|
)
|
|
}
|
|
|
|
public setCameraPlanes(targetVolume: Box3, offsetScale: number = 1) {
|
|
if (targetVolume.isEmpty()) {
|
|
Logger.error('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)
|
|
|
|
this._controls.minDistance = distance / 100
|
|
this._controls.maxDistance = distance * 100
|
|
|
|
this._renderingCamera.near = distance / 100
|
|
this._renderingCamera.far = distance * 100
|
|
this._renderingCamera.updateProjectionMatrix()
|
|
}
|
|
|
|
protected zoom(objectIds?: string[], fit?: number, transition?: boolean) {
|
|
if (!objectIds) {
|
|
this.zoomExtents(fit, transition)
|
|
return
|
|
}
|
|
this.zoomToBox(this.viewer.getRenderer().boxFromObjects(objectIds), fit, transition)
|
|
}
|
|
|
|
private 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 = 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)
|
|
// this.viewer.controls.setBoundary( box )
|
|
}
|
|
|
|
private zoomToBox(box, 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 target = new Sphere()
|
|
box.getBoundingSphere(target)
|
|
target.radius = target.radius * fit
|
|
this._controls.fitToSphere(target, transition)
|
|
|
|
this.setCameraPlanes(box, fit)
|
|
|
|
if (this._renderingCamera === this.orthographicCamera) {
|
|
// fit the camera inside, so we don't have clipping plane issues.
|
|
// WIP implementation
|
|
const camPos = this._renderingCamera.position
|
|
let dist = target.distanceToPoint(camPos)
|
|
if (dist < 0) {
|
|
dist *= -1
|
|
this._controls.setPosition(camPos.x + dist, camPos.y + dist, camPos.z + dist)
|
|
}
|
|
}
|
|
}
|
|
|
|
private isSpeckleView(
|
|
view: CanonicalView | SpeckleView | InlineView | PolarView
|
|
): view is SpeckleView {
|
|
return (view as SpeckleView).name !== undefined
|
|
}
|
|
|
|
private isCanonicalView(
|
|
view: CanonicalView | SpeckleView | InlineView | PolarView
|
|
): view is CanonicalView {
|
|
return typeof (view as CanonicalView) === 'string'
|
|
}
|
|
|
|
private isInlineView(
|
|
view: CanonicalView | SpeckleView | InlineView | PolarView
|
|
): view is InlineView {
|
|
return (
|
|
(view as InlineView).position !== undefined &&
|
|
(view as InlineView).target !== undefined
|
|
)
|
|
}
|
|
|
|
private isPolarView(
|
|
view: CanonicalView | SpeckleView | InlineView | PolarView
|
|
): view is PolarView {
|
|
return (
|
|
(view as PolarView).azimuth !== undefined &&
|
|
(view as PolarView).polar !== undefined
|
|
)
|
|
}
|
|
|
|
private isBox3(view: unknown): view is Box3 {
|
|
return view['isBox3']
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
private setViewSpeckle(view: SpeckleView, transition = true) {
|
|
this._controls.setLookAt(
|
|
view.view.origin['x'],
|
|
view.view.origin['y'],
|
|
view.view.origin['z'],
|
|
view.view.target['x'],
|
|
view.view.target['y'],
|
|
view.view.target['z'],
|
|
transition
|
|
)
|
|
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]
|
|
*/
|
|
private setViewCanonical(side: string, transition = true) {
|
|
const DEG90 = Math.PI * 0.5
|
|
const DEG180 = Math.PI
|
|
|
|
switch (side) {
|
|
case 'front':
|
|
this.zoomExtents()
|
|
this._controls.rotateTo(0, DEG90, transition)
|
|
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
|
|
break
|
|
|
|
case 'back':
|
|
this.zoomExtents()
|
|
this._controls.rotateTo(DEG180, DEG90, transition)
|
|
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
|
|
break
|
|
|
|
case 'up':
|
|
case 'top':
|
|
this.zoomExtents()
|
|
this._controls.rotateTo(0, 0, transition)
|
|
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
|
|
break
|
|
|
|
case 'down':
|
|
case 'bottom':
|
|
this.zoomExtents()
|
|
this._controls.rotateTo(0, DEG180, transition)
|
|
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
|
|
break
|
|
|
|
case 'right':
|
|
this.zoomExtents()
|
|
this._controls.rotateTo(DEG90, DEG90, transition)
|
|
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
|
|
break
|
|
|
|
case 'left':
|
|
this.zoomExtents()
|
|
this._controls.rotateTo(-DEG90, DEG90, transition)
|
|
if (this._renderingCamera === this.orthographicCamera) this.disableRotations()
|
|
break
|
|
|
|
case '3d':
|
|
case '3D':
|
|
default: {
|
|
let box
|
|
if (this.viewer.getRenderer().allObjects.children.length === 0)
|
|
box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
|
|
else box = new Box3().setFromObject(this.viewer.getRenderer().allObjects)
|
|
if (box.max.x === Infinity || box.max.x === -Infinity) {
|
|
box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
|
|
}
|
|
this._controls.setPosition(box.max.x, box.max.y, box.max.z, transition)
|
|
this.zoomExtents()
|
|
this.enableRotations()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
private setViewInline(view: InlineView, transition = true) {
|
|
this._controls.setLookAt(
|
|
view.position.x,
|
|
view.position.y,
|
|
view.position.z,
|
|
view.target.x,
|
|
view.target.y,
|
|
view.target.z,
|
|
transition
|
|
)
|
|
this.enableRotations()
|
|
}
|
|
|
|
private setViewPolar(view: PolarView, transition = true) {
|
|
this._controls.rotate(view.azimuth, view.polar, transition)
|
|
this.enableRotations()
|
|
}
|
|
}
|