UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

270 lines (246 loc) 13.3 kB
import {iObjectCommons} from './iObjectCommons' import {Camera, IUniform, Vector3} from 'three' import type {ICamera, ICameraEventMap, ICameraSetDirtyOptions} from '../ICamera' import {CameraView, ICameraView} from '../camera/CameraView' export const iCameraCommons = { setDirty: function(this: ICamera, options?: ICameraSetDirtyOptions): void { if (!this._positionWorld) return // not initialized yet // noinspection SuspiciousTypeOfGuard it can be string when called from bindToValue const isStr = typeof options === 'string' const changeKey = isStr ? options as string : options?.change ?? options?.key // todo use both? let projectionUpdated = false if (options?.projectionUpdated !== false && (!changeKey || ['zoom', 'fov', 'left', 'right', 'top', 'bottom', 'aspect', 'frustumSize', 'view', 'coordinateSystem', 'projection', 'activateMain', 'deactivateMain', 'near', 'far', 'nearFar', 'deserialize'].includes(changeKey)) ) { this.updateProjectionMatrix() projectionUpdated = true } projectionUpdated = projectionUpdated || options?.projectionUpdated || false if (isStr) options = undefined this.getWorldPosition(this._positionWorld) // console.log('target', target, this._controls, this._camera) // noinspection PointlessBooleanExpressionJS if (this.controls && this.controls.target && this.controls.enabled !== false && this.target !== this.controls.target) { this.controls.target.copy(this.target) // this.controls.update() // this should be done automatically postFrame } // if (!this.controls || !this.controls.enabled) { else if (this.userData.autoLookAtTarget) { this.lookAt(this.target) } // todo refresh target on rotation change if autoLookAtTarget is false? (calculate distanceToTarget from the current/prev target and position this.dispatchEvent({...options, type: 'update', bubbleToParent: false, camera: this}) // does not bubble this.dispatchEvent({...options, type: 'cameraUpdate', projectionUpdated, bubbleToParent: true, camera: this}) // this sets dirty in the viewer iObjectCommons.setDirty.call(this, {refreshScene: false, ...options, projectionUpdated}) }, activateMain: function(this: ICamera, options: Omit<ICameraEventMap['activateMain'], 'bubbleToParent'> = {}, _internal = false, _refresh = true, canvas?: HTMLCanvasElement): void { if (!_internal) { if (options.camera === null) return this.deactivateMain(options, _internal, _refresh) // if (!canvas) // so that viewer can update the canvas ref set on the camera return this.dispatchEvent({ type: 'activateMain', ...options, camera: this, bubbleToParent: true, }) } // this will be used by RootScene to deactivate other cameras and activate this one if (this.userData.__isMainCamera) return this.userData.__isMainCamera = true this.userData.__lastScale = this.scale.clone() this.scale.divide(this.getWorldScale(new Vector3())) // make unit scale, for near far and all if (canvas && this.setCanvas) this.setCanvas(canvas, _refresh) else if (_refresh) { this.refreshCameraControls(false) this.refreshAspect(false) } this.setDirty({change: 'activateMain', ...options}) // console.log({...this._camera.modelObject.position}) }, deactivateMain: function(this: ICamera, options: Omit<ICameraEventMap['activateMain'], 'bubbleToParent'> = {}, _internal = false, _refresh = true, clearCanvas = false): void { if (!_internal) return this.dispatchEvent({ type: 'activateMain', ...options, camera: undefined, bubbleToParent: true, }) // this will be used by RootScene to deactivate other cameras and activate this one if (!this.userData.__isMainCamera) return this.userData.__isMainCamera = false // or delete? if (this.userData.__lastScale) { this.scale.copy(this.userData.__lastScale) delete this.userData.__lastScale } if (clearCanvas) this.setCanvas(undefined, _refresh) else if (_refresh) this.refreshCameraControls(false) if (_refresh) { this.refreshCameraControls(false) } this.setDirty({change: 'deactivateMain', ...options}) }, refreshUi: function(this: ICamera) { // todo this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) }, refreshTarget: function(this: ICamera, distanceFromTarget = 4, setDirty = true) { if (this.controls?.enabled && this.controls.target) { if (this.controls.target !== this.target) this.target.copy(this.controls.target) } else { // this.cameraObject.updateWorldMatrix(true, false) this.getWorldDirection(this.target) // .transformDirection(this.cameraObject.matrixWorldInverse) // .multiplyScalar(distanceFromTarget).add(this._position) .multiplyScalar(distanceFromTarget).add(this.getWorldPosition(new Vector3())) // if (this.cameraObject.parent) this.cameraObject.parent.worldToLocal(this._target) } if (setDirty) this.setDirty({change: 'target'}) }, refreshAspect: function(this: ICamera, setDirty = true) { if (this.autoAspect) { if (!this._canvas) { console.warn('ICamera: cannot calculate aspect ratio without canvas/container') } else { let aspect = this._canvas.clientWidth / this._canvas.clientHeight if (!isFinite(aspect)) aspect = 1 this.aspect = aspect this.refreshFrustum && this.refreshFrustum(false) } } if (setDirty) this.setDirty({change: 'aspect'}) }, updateShaderProperties: function(this: ICamera, material: {defines: Record<string, string | number | undefined>; uniforms: {[p: string]: IUniform}}) { material.uniforms.cameraPositionWorld?.value?.copy(this._positionWorld) material.uniforms.cameraNearFar?.value?.set(this.near, this.far) if (material.uniforms.projection) material.uniforms.projection.value = this.projectionMatrix // todo: rename to projectionMatrix2? material.defines.PERSPECTIVE_CAMERA = this.type === 'PerspectiveCamera' ? '1' : '0' material.defines.ORTHOGRAPHIC_CAMERA = this.type === 'OrthographicCamera' ? '1' : '0' // todo return this }, upgradeCamera: upgradeCamera, copy: (superCopy: ICamera['copy']): ICamera['copy'] => function(this: ICamera, camera: ICamera | Camera, recursive?, distanceFromTarget?, worldSpace?, ...args): ICamera { if (!camera.isCamera) { console.error('ICamera.copy: camera is not a Camera', camera) return this } superCopy.call(this, camera, recursive, ...args) // moved to setView in ThreeViewer // const worldPos = camera.getWorldPosition(this.position) // camera.getWorldQuaternion(this.quaternion) // if (this.parent) { // this.position.copy(this.parent.worldToLocal(worldPos)) // this.quaternion.premultiply(this.parent.quaternion.clone().invert()) // } if ((<ICamera>camera).target?.isVector3) this.target.copy((<ICamera>camera).target) else { const minDistance = (this.controls as any)?.minDistance ?? distanceFromTarget ?? 4 camera.getWorldDirection(this.target).multiplyScalar(minDistance).add(this.getWorldPosition(new Vector3())) } if (worldSpace) { // default = false const worldPos = camera.getWorldPosition(this.position) // this.getWorldQuaternion(this.quaternion) // todo: do if autoLookAtTarget is false // todo up vector if (this.parent) { this.position.copy(this.parent.worldToLocal(worldPos)) // this.quaternion.premultiply(this.parent.quaternion.clone().invert()) } } this.updateMatrixWorld(true) this.updateProjectionMatrix() this.refreshAspect(false) this.setDirty() return this }, getView: function<T extends ICameraView = CameraView>(this: ICamera, worldSpace = true, _view?: T): T { const up = new Vector3() this.updateWorldMatrix(true, false) const matrix = this.matrixWorld up.x = matrix.elements[4] up.y = matrix.elements[5] up.z = matrix.elements[6] up.normalize() const view = _view || new CameraView() view.name = this.name view.position.copy(this.position) view.target.copy(this.target) view.quaternion.copy(this.quaternion) view.zoom = this.zoom // view.up.copy(up) const parent = this.parent if (parent) { if (worldSpace) { view.position.applyMatrix4(parent.matrixWorld) this.getWorldQuaternion(view.quaternion) // target, up is already in world space } else { up.transformDirection(parent.matrixWorld.clone().invert()) // pos is already in local space // target should always be in world space } } view.isWorldSpace = worldSpace return view as T }, setView: function<T extends ICameraView = CameraView>(this: ICamera, view: T): void { this.position.copy(view.position) this.target.copy(view.target) // this.up.copy(view.up) this.quaternion.copy(view.quaternion) this.zoom = view.zoom this.setDirty() }, // todo rename to setFromCamera? setViewFromCamera: function(this: ICamera, camera: Camera|ICamera, distanceFromTarget?: number, worldSpace = true): void { // todo: getView, setView can also be used, do we need copy? as that will copy all the properties this.copy(camera, undefined, distanceFromTarget, worldSpace) }, setViewToMain: function(this: ICamera, eventOptions: Omit<ICameraEventMap['setView'], 'camera'|'bubbleToParent'>): void { this.dispatchEvent({type: 'setView', ...eventOptions, camera: this, bubbleToParent: true}) }, setNearFar(camera: ICamera, near: number, far: number, setDirty: boolean, source?: string) { const changed = Math.abs(camera.near - near) + Math.abs(camera.far - far) > 0.001 camera.near = near camera.far = far if (setDirty && changed && camera.setDirty) { camera.setDirty({change: 'nearFar', source}) return true } else { camera.updateProjectionMatrix() } return false }, defaultMinNear: 0.1, defaultMaxFar: 1000, } function upgradeCamera(this: ICamera) { if (!this.isCamera) { console.error('Object is not a camera', this) return } if (this.userData.__cameraSetup) return this.userData.__cameraSetup = true iObjectCommons.upgradeObject3D.call(this) this.copy = iCameraCommons.copy(this.copy) if (!this.target) this.target = new Vector3() if (!this._positionWorld) this._positionWorld = new Vector3() if (!this.refreshTarget) this.refreshTarget = iCameraCommons.refreshTarget if (!this.refreshAspect) this.refreshAspect = iCameraCommons.refreshAspect if (!this.updateShaderProperties) this.updateShaderProperties = iCameraCommons.updateShaderProperties if (!this.activateMain) this.activateMain = iCameraCommons.activateMain if (!this.deactivateMain) this.deactivateMain = iCameraCommons.deactivateMain if (!this.refreshUi) this.refreshUi = iCameraCommons.refreshUi if (!this.setDirty) this.setDirty = iCameraCommons.setDirty // if (!this.controlsMode) this.controlsMode = '' if (!this.getView) this.getView = iCameraCommons.getView if (!this.setView) this.setView = iCameraCommons.setView if (!this.setViewFromCamera) this.setViewFromCamera = iCameraCommons.setViewFromCamera if (!this.setViewToMain) this.setViewToMain = iCameraCommons.setViewToMain if (!this.setCanvas) this.setCanvas = ()=>notSupported('setCanvas') if (!this.setControlsCtor) this.setControlsCtor = ()=>notSupported('setControlsCtor') if (!this.removeControlsCtor) this.removeControlsCtor = ()=>notSupported('removeControlsCtor') if (!this.refreshCameraControls) this.refreshCameraControls = ()=>notSupported('refreshCameraControls') if (!this.setInteractions) this.setInteractions = ()=>notSupported('setInteractions') if (!this.dispose) this.dispose = ()=>notSupported('dispose') this.assetType = 'camera' // todo uiconfig, anything else? } function notSupported(n: string) { console.warn(`ICamera.${n} is not supported on this object. Please use objects of PerspectiveCamera2 or OrthographicCamera2 classes.`) }