| - [PickingPlugin](#pickingplugin) - Adds support for selecting objects in the viewer with user interactions and selection widgets | - [PickingPlugin](#pickingplugin) - Adds support for selecting objects in the viewer with user interactions and selection widgets | ||||
| - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | ||||
| - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | ||||
| - [CameraViewPlugin](#cameraviewplugin) - Add support for saving, loading, animating, looping between camera views | |||||
| - [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas | - [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas | ||||
| - [GeometryUVPreviewPlugin](#geometryuvpreviewplugin) - Preview UVs of any geometry in a UI panel over the canvas | - [GeometryUVPreviewPlugin](#geometryuvpreviewplugin) - Preview UVs of any geometry in a UI panel over the canvas | ||||
| - [FrameFadePlugin](#framefadeplugin) - Post-render pass to smoothly fade to a new rendered frame over time | - [FrameFadePlugin](#framefadeplugin) - Post-render pass to smoothly fade to a new rendered frame over time | ||||
| [`camera.deactivateMain`](https://threepipe.org/docs/classes/PerspectiveCamera2.html#deactivateMain) - Deactivate the camera as the main camera. | [`camera.deactivateMain`](https://threepipe.org/docs/classes/PerspectiveCamera2.html#deactivateMain) - Deactivate the camera as the main camera. | ||||
| See also [CameraViewPlugin](#cameraviewplugin) for camera focus animation. | |||||
| ## AssetManager | ## AssetManager | ||||
| Source Code: [src/assetmanager/AssetManager.ts](./src/assetmanager/AssetManager.ts) | Source Code: [src/assetmanager/AssetManager.ts](./src/assetmanager/AssetManager.ts) | ||||
| Note: The animation is started when the animate or animateAsync function is called. | Note: The animation is started when the animate or animateAsync function is called. | ||||
| ## CameraViewPlugin | |||||
| [//]: # (todo: image) | |||||
| Example: https://threepipe.org/examples/#camera-view-plugin/ | |||||
| Source Code: [src/plugins/animation/CameraViewPlugin.ts](./src/plugins/ui/RenderTargetPreviewPlugin.ts) | |||||
| API Reference: [CameraViewPlugin](https://threepipe.org/docs/classes/CameraViewPlugin.html) | |||||
| CameraViewPlugin adds support to save and load camera views, which can then be animated to. | |||||
| It uses PopmotionPlugin internally to animate any camera to a saved view or to loop through all the saved views. | |||||
| It also provides a UI to manage the views. | |||||
| ```typescript | |||||
| import {CameraViewPlugin, ThreeViewer, CameraView, Vector3, Quaternion, EasingFunctions, timeout} from 'threepipe' | |||||
| const viewer = new ThreeViewer({...}) | |||||
| const cameraViewPlugin = viewer.addPluginSync(new CameraViewPlugin()) | |||||
| const intialView = cameraViewPlugin.getView() | |||||
| // or = viewer.scene.mainCamera.getView() | |||||
| // create a new view | |||||
| const view = new CameraView( | |||||
| 'My View', // name | |||||
| new Vector3(0, 0, 10), // position | |||||
| new Vector3(0, 0, 0), // target | |||||
| new Quaternion(0, 0, 0, 1), // quaternion rotation | |||||
| 1 // zoom | |||||
| ) | |||||
| // or clone a view | |||||
| const view2 = intialView.clone() | |||||
| view2.position.add(new Vector3(0, 5, 0)) // move up 5 units | |||||
| // animate the main camera to a view | |||||
| await cameraViewPlugin.animateToView( | |||||
| view, | |||||
| 2000, // in ms, = 2sec | |||||
| EasingFunctions.easeInOut, | |||||
| ).catch(()=>console.log('Animation stopped')) | |||||
| // stop any/all animations | |||||
| cameraViewPlugin.stopAllAnimations() | |||||
| // add views to the plugin | |||||
| cameraViewPlugin.addView(view) | |||||
| cameraViewPlugin.addView(view2) | |||||
| cameraViewPlugin.addView(intialView) | |||||
| cameraViewPlugin.addCurrentView() // adds the current view of the main camera | |||||
| // loop through all the views once | |||||
| cameraViewPlugin.animDuration = 2000 // default duration | |||||
| cameraViewPlugin.animEase = EasingFunctions.easeInOutSine // default easing | |||||
| await cameraViewPlugin.animateAllViews() | |||||
| // loop through all the views forever | |||||
| cameraViewPlugin.viewLooping = true | |||||
| await timeout(10000) // wait for some time | |||||
| // stop looping | |||||
| cameraViewPlugin.viewLooping = false | |||||
| ``` | |||||
| ## RenderTargetPreviewPlugin | ## RenderTargetPreviewPlugin | ||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Camera View Plugin</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, CameraView, CameraViewPlugin, EasingFunctions, ThreeViewer, Vector3} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| }) | |||||
| const cameraViewPlugin = viewer.addPluginSync(CameraViewPlugin) | |||||
| console.log(cameraViewPlugin) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| // Get the current camera view and save it in a variable | |||||
| const initialView = cameraViewPlugin.getView() | |||||
| const topView = new CameraView( | |||||
| 'topView', | |||||
| new Vector3(0, 6, 0), | |||||
| initialView.target, | |||||
| ) | |||||
| const leftView = new CameraView( | |||||
| 'leftView', | |||||
| new Vector3(-6, 0, 0), | |||||
| initialView.target, | |||||
| ) | |||||
| const rightView = new CameraView( | |||||
| 'leftView', | |||||
| new Vector3(6, 0, 0), | |||||
| initialView.target, | |||||
| ) | |||||
| createSimpleButtons({ | |||||
| ['Top View']: async() => cameraViewPlugin.animateToView(topView, 1000, EasingFunctions.easeInOutSine), | |||||
| ['Left View']: async() => cameraViewPlugin.animateToView(leftView, 1000, EasingFunctions.easeInOutSine), | |||||
| ['Right View']: async() => cameraViewPlugin.animateToView(rightView, 1000, EasingFunctions.easeInOutSine), | |||||
| ['Pan right/left']: async(btn) => { | |||||
| btn.disabled = true | |||||
| const currentView = cameraViewPlugin.getView() | |||||
| await cameraViewPlugin.animateToView(new CameraView( | |||||
| 'view', | |||||
| currentView.position, | |||||
| new Vector3(4, 0, 0).sub(currentView.target), | |||||
| )) | |||||
| btn.disabled = false | |||||
| }, | |||||
| ['Move up/down']: async(btn) => { | |||||
| btn.disabled = true | |||||
| const currentView = cameraViewPlugin.getView() | |||||
| await cameraViewPlugin.animateToView(new CameraView( | |||||
| 'view', | |||||
| new Vector3(currentView.position.x, 5 - currentView.position.y, currentView.position.z), | |||||
| currentView.target, | |||||
| )) | |||||
| btn.disabled = false | |||||
| }, | |||||
| ['Reset']: async() => cameraViewPlugin.animateToView(initialView, 1000, EasingFunctions.easeInOutSine), | |||||
| }) | |||||
| const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | |||||
| ui.appendChild(viewer.scene.mainCamera.uiConfig) | |||||
| const uiC = ui.setupPluginUi(CameraViewPlugin)! | |||||
| uiC.expanded = true | |||||
| uiC.uiRefresh?.() | |||||
| } | |||||
| init().then(_testFinish) |
| import { | import { | ||||
| _testFinish, | _testFinish, | ||||
| CameraViewPlugin, | |||||
| DepthBufferPlugin, | DepthBufferPlugin, | ||||
| DropzonePlugin, | DropzonePlugin, | ||||
| FrameFadePlugin, | FrameFadePlugin, | ||||
| await viewer.addPlugins([ | await viewer.addPlugins([ | ||||
| new ProgressivePlugin(), | new ProgressivePlugin(), | ||||
| new GLTFAnimationPlugin(), | new GLTFAnimationPlugin(), | ||||
| new CameraViewPlugin(), | |||||
| new ViewerUiConfigPlugin(), | new ViewerUiConfigPlugin(), | ||||
| // new SceneUiConfigPlugin(), // this is already in ViewerUiPlugin | // new SceneUiConfigPlugin(), // this is already in ViewerUiPlugin | ||||
| new DepthBufferPlugin(HalfFloatType, true, true), | new DepthBufferPlugin(HalfFloatType, true, true), | ||||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | ||||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ||||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin], | ['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin], | ||||
| ['Animation']: [GLTFAnimationPlugin], | |||||
| ['Animation']: [GLTFAnimationPlugin, CameraViewPlugin], | |||||
| ['Debug']: [RenderTargetPreviewPlugin], | ['Debug']: [RenderTargetPreviewPlugin], | ||||
| }) | }) | ||||
| viewer.scene.addObject(new HemisphereLight(0xffffff, 0x444444, 5)) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | ||||
| // const result = await viewer.load<IObject3D>('https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Blender-Exporter@master/polly/project_polly.gltf', { | // const result = await viewer.load<IObject3D>('https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Blender-Exporter@master/polly/project_polly.gltf', { |
| import {IObject3D, IObject3DEvent, IObject3DEventTypes, IObject3DUserData, IObjectSetDirtyOptions} from './IObject' | import {IObject3D, IObject3DEvent, IObject3DEventTypes, IObject3DUserData, IObjectSetDirtyOptions} from './IObject' | ||||
| import {IShaderPropertiesUpdater} from '../materials' | import {IShaderPropertiesUpdater} from '../materials' | ||||
| import {ICameraControls, TControlsCtor} from './camera/ICameraControls' | import {ICameraControls, TControlsCtor} from './camera/ICameraControls' | ||||
| import {CameraView, ICameraView} from './camera/CameraView' | |||||
| /** | /** | ||||
| * Available modes for {@link ICamera.controlsMode} property. | * Available modes for {@link ICamera.controlsMode} property. | ||||
| readonly isCamera: true | readonly isCamera: true | ||||
| setDirty(options?: ICameraSetDirtyOptions): void; | setDirty(options?: ICameraSetDirtyOptions): void; | ||||
| near: number; | |||||
| far: number; | |||||
| readonly isMainCamera: boolean; | readonly isMainCamera: boolean; | ||||
| readonly isPerspectiveCamera?: boolean; | |||||
| readonly isOrthographicCamera?: boolean; | |||||
| activateMain(options?: Partial<ICameraEvent>, _internal?: boolean, _refresh?: boolean): void; | activateMain(options?: Partial<ICameraEvent>, _internal?: boolean, _refresh?: boolean): void; | ||||
| deactivateMain(options?: Partial<ICameraEvent>, _internal?: boolean, _refresh?: boolean): void; | deactivateMain(options?: Partial<ICameraEvent>, _internal?: boolean, _refresh?: boolean): void; | ||||
| */ | */ | ||||
| position: Vector3, | position: Vector3, | ||||
| // todo: make disable/enable functions with key like in FrameFadePlugin | |||||
| interactionsEnabled: boolean; | interactionsEnabled: boolean; | ||||
| readonly canUserInteract: boolean; | |||||
| /** | |||||
| * Check whether user can interact with this camera. | |||||
| * Interactions can be enabled/disabled in a variety of ways, | |||||
| * like {@link interactionsEnabled}, {@link controlsMode}, {@link isMainCamera} property | |||||
| */ | |||||
| readonly canUserInteract: boolean; | |||||
| zoom: number; | zoom: number; | ||||
| /** | /** | ||||
| controlsMode?: TCameraControlsMode; // todo add more. | controlsMode?: TCameraControlsMode; // todo add more. | ||||
| // controlsEnabled: boolean; // use controlsMode = '' instead | // controlsEnabled: boolean; // use controlsMode = '' instead | ||||
| /** | |||||
| * Automatically managed when {@link autoNearFar} is `true`. See also {@link minNearPlane} | |||||
| */ | |||||
| near: number; | |||||
| /** | |||||
| * Automatically managed when {@link autoNearFar} is `true`. See also {@link maxFarPlane} | |||||
| */ | |||||
| far: number; | |||||
| // also in userData | // also in userData | ||||
| autoNearFar: boolean // default = true | autoNearFar: boolean // default = true | ||||
| minNearPlane: number // default = 0.2 | minNearPlane: number // default = 0.2 | ||||
| */ | */ | ||||
| isActiveCamera: boolean; | isActiveCamera: boolean; | ||||
| setControlsCtor(key: string, ctor: TControlsCtor, replace?: boolean): void; | setControlsCtor(key: string, ctor: TControlsCtor, replace?: boolean): void; | ||||
| removeControlsCtor(key: string): void; | removeControlsCtor(key: string): void; | ||||
| refreshCameraControls(setDirty?: boolean): void | refreshCameraControls(setDirty?: boolean): void | ||||
| updateProjectionMatrix(): void | updateProjectionMatrix(): void | ||||
| fov?: number | fov?: number | ||||
| getView<T extends ICameraView = CameraView>(worldSpace?: boolean, cameraView?: T): T | |||||
| setView(view: ICameraView): void | |||||
| /** | |||||
| * Set camera view from another camera. | |||||
| * @param camera | |||||
| * @param distanceFromTarget - default = 4 | |||||
| * @param worldSpace - default = true | |||||
| */ | |||||
| setViewFromCamera(camera: ICamera|Camera, distanceFromTarget?: number, worldSpace?: boolean): void | |||||
| /** | |||||
| * Dispatches the `setView` event which triggers the main camera to set its view to this camera's view. | |||||
| * @param eventOptions | |||||
| */ | |||||
| setViewToMain(eventOptions: Partial<ICameraEvent>): void | |||||
| // region inherited type fixes | // region inherited type fixes | ||||
| // re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936 | // re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936 | ||||
| getObjectById<T extends IObject3D = IObject3D>(id: number): T | undefined | getObjectById<T extends IObject3D = IObject3D>(id: number): T | undefined | ||||
| getObjectByName<T extends IObject3D = IObject3D>(name: string): T | undefined | getObjectByName<T extends IObject3D = IObject3D>(name: string): T | undefined | ||||
| getObjectByProperty<T extends IObject3D = IObject3D>(name: string, value: string): T | undefined | getObjectByProperty<T extends IObject3D = IObject3D>(name: string, value: string): T | undefined | ||||
| copy(source: this, recursive?: boolean, distanceFromTarget?: number, ...args: any[]): this | |||||
| copy(source: this, recursive?: boolean, distanceFromTarget?: number, worldSpace?: boolean, ...args: any[]): this | |||||
| clone(recursive?: boolean): this | clone(recursive?: boolean): this | ||||
| add(...object: IObject3D[]): this | add(...object: IObject3D[]): this | ||||
| remove(...object: IObject3D[]): this | remove(...object: IObject3D[]): this |
| import {Event, EventDispatcher, Quaternion, Vector3} from 'three' | |||||
| import {onChange, serializable, serialize} from 'ts-browser-helpers' | |||||
| import {IUiConfigContainer, uiButton, uiInput, uiNumber, UiObjectConfig, uiPanelContainer, uiVector} from 'uiconfig.js' | |||||
| import {ICamera} from '../ICamera' | |||||
| export interface ICameraView{ | |||||
| name: string | |||||
| position: Vector3 | |||||
| target: Vector3 | |||||
| quaternion: Quaternion | |||||
| zoom: number | |||||
| animate(camera?: ICamera, duration?: number): void | |||||
| set(camera?: ICamera): void | |||||
| } | |||||
| @serializable('CameraView') | |||||
| @uiPanelContainer('Camera View') | |||||
| export class CameraView extends EventDispatcher<Event, 'setView'|'animateView'> implements ICameraView, IUiConfigContainer { | |||||
| @onChange(CameraView.prototype._nameChanged) | |||||
| @serialize() @uiInput() name = 'Camera View' | |||||
| @serialize() @uiVector() position = new Vector3() | |||||
| @serialize() @uiVector() target = new Vector3() | |||||
| @serialize() @uiVector() quaternion = new Quaternion() | |||||
| @serialize() @uiNumber() zoom = 1 | |||||
| @uiButton() set = (camera?: ICamera) => this.dispatchEvent({type: 'setView', camera, view: this}) | |||||
| @uiButton() animate = (camera?: ICamera, duration?: number) => this.dispatchEvent({type: 'animateView', camera, duration, view: this}) | |||||
| constructor(name?: string, position?: Vector3, target?: Vector3, quaternion?: Quaternion, zoom?: number) { | |||||
| super() | |||||
| if (name !== undefined) this.name = name | |||||
| if (position) this.position.copy(position) | |||||
| if (target) this.target.copy(target) | |||||
| if (quaternion) this.quaternion.copy(quaternion) | |||||
| if (zoom !== undefined) this.zoom = zoom | |||||
| } | |||||
| private _nameChanged() { | |||||
| if (this.uiConfig) { | |||||
| this.uiConfig.label = this.name | |||||
| this.uiConfig.uiRefresh?.() | |||||
| } | |||||
| } | |||||
| clone() { | |||||
| return new CameraView(this.name, this.position, this.target, this.quaternion, this.zoom) | |||||
| } | |||||
| uiConfig?: UiObjectConfig | |||||
| // uiConfig = generateUiFolder(this.name, this) | |||||
| } |
| import {iCameraCommons} from '../object/iCameraCommons' | import {iCameraCommons} from '../object/iCameraCommons' | ||||
| import {bindToValue} from '../../three/utils/decorators' | import {bindToValue} from '../../three/utils/decorators' | ||||
| import {makeICameraCommonUiConfig} from '../object/IObjectUi' | import {makeICameraCommonUiConfig} from '../object/IObjectUi' | ||||
| import {CameraView, ICameraView} from './CameraView' | |||||
| // todo: maybe change domElement to some wrapper/base class of viewer | // todo: maybe change domElement to some wrapper/base class of viewer | ||||
| export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | ||||
| @serialize() focus: number | @serialize() focus: number | ||||
| @onChange3(PerspectiveCamera2.prototype.setDirty) | @onChange3(PerspectiveCamera2.prototype.setDirty) | ||||
| // @uiSlider('Zoom', [0.001, 20], 0.001) | |||||
| @uiSlider('FoV Zoom', [0.001, 10], 0.001) | |||||
| @serialize() zoom: number | @serialize() zoom: number | ||||
| @uiVector('Position') | @uiVector('Position') | ||||
| setDirty(options?: ICameraSetDirtyOptions|Event): void { | setDirty(options?: ICameraSetDirtyOptions|Event): void { | ||||
| if (!this._positionWorld) return // class not initialized | if (!this._positionWorld) return // class not initialized | ||||
| if (options?.key === 'fov') this.updateProjectionMatrix() | |||||
| if (options?.key === 'fov' || options?.key === 'zoom') this.updateProjectionMatrix() | |||||
| this.getWorldPosition(this._positionWorld) | this.getWorldPosition(this._positionWorld) | ||||
| // endregion | // endregion | ||||
| // region camera views | |||||
| getView<T extends ICameraView = CameraView>(worldSpace = true, _view?: 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 | |||||
| } | |||||
| } | |||||
| return view as T | |||||
| } | |||||
| setView(view: ICameraView) { | |||||
| 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() | |||||
| } | |||||
| setViewFromCamera(camera: Camera|ICamera, distanceFromTarget?: number, worldSpace = true) { | |||||
| // 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(eventOptions: Partial<ICameraEvent>) { | |||||
| this.dispatchEvent({type: 'setView', ...eventOptions, camera: this, bubbleToParent: true}) | |||||
| } | |||||
| // endregion | |||||
| // region utils/others | // region utils/others | ||||
| // for shader prop updater | // for shader prop updater | ||||
| } | } | ||||
| /** | /** | ||||
| * @deprecated | |||||
| * @deprecated - use setDirty directly | |||||
| * @param setDirty | * @param setDirty | ||||
| */ | */ | ||||
| targetUpdated(setDirty = true): void { | targetUpdated(setDirty = true): void { | ||||
| if (setDirty) this.setDirty() | if (setDirty) this.setDirty() | ||||
| } | } | ||||
| // setCameraOptions<T extends Partial<IPerspectiveCameraOptions | IOrthographicCameraOptions>>(value: T, setDirty = true): void { | // setCameraOptions<T extends Partial<IPerspectiveCameraOptions | IOrthographicCameraOptions>>(value: T, setDirty = true): void { | ||||
| // const ops: any = {...value} | // const ops: any = {...value} | ||||
| // | // | ||||
| getObjectById: <T extends IObject3D = IObject3D>(id: number) => T | undefined | getObjectById: <T extends IObject3D = IObject3D>(id: number) => T | undefined | ||||
| getObjectByName: <T extends IObject3D = IObject3D>(name: string) => T | undefined | getObjectByName: <T extends IObject3D = IObject3D>(name: string) => T | undefined | ||||
| getObjectByProperty: <T extends IObject3D = IObject3D>(name: string, value: string) => T | undefined | getObjectByProperty: <T extends IObject3D = IObject3D>(name: string, value: string) => T | undefined | ||||
| copy: (source: ICamera|Camera, recursive?: boolean, distanceFromTarget?: number) => this | |||||
| copy: (source: ICamera|Camera, recursive?: boolean, distanceFromTarget?: number, worldSpace?: boolean) => this | |||||
| clone: (recursive?: boolean) => this | clone: (recursive?: boolean) => this | ||||
| add: (...object: IObject3D[]) => this | add: (...object: IObject3D[]) => this | ||||
| remove: (...object: IObject3D[]) => this | remove: (...object: IObject3D[]) => this |
| export {PerspectiveCamera2} from './camera/PerspectiveCamera2' | export {PerspectiveCamera2} from './camera/PerspectiveCamera2' | ||||
| export {CameraView, type ICameraView} from './camera/CameraView' | |||||
| export {ExtendedShaderMaterial} from './material/ExtendedShaderMaterial' | export {ExtendedShaderMaterial} from './material/ExtendedShaderMaterial' | ||||
| export {PhysicalMaterial, type PhysicalMaterialEventTypes, MeshStandardMaterial2} from './material/PhysicalMaterial' | export {PhysicalMaterial, type PhysicalMaterialEventTypes, MeshStandardMaterial2} from './material/PhysicalMaterial' | ||||
| export {ShaderMaterial2} from './material/ShaderMaterial2' | export {ShaderMaterial2} from './material/ShaderMaterial2' |
| import {ICamera} from '../ICamera' | import {ICamera} from '../ICamera' | ||||
| import {Vector3} from 'three' | import {Vector3} from 'three' | ||||
| export function makeICameraCommonUiConfig(this: IObject3D, config: UiObjectConfig): UiObjectConfig[] { | |||||
| export function makeICameraCommonUiConfig(this: ICamera, config: UiObjectConfig): UiObjectConfig[] { | |||||
| return [ | return [ | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Set View', | label: 'Set View', | ||||
| value: ()=>{ | value: ()=>{ | ||||
| // todo: call setView on the camera, which will dispatch the event | |||||
| (this as ICamera).dispatchEvent({type: 'setView', ui: true, camera: this as ICamera}) | |||||
| config.uiRefresh?.(true, 'postFrame') | |||||
| console.log('set view', this) | |||||
| this.setViewToMain({ui: true}) | |||||
| config.uiRefresh?.(true, 'postFrame') // config is parent config | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Activate main', | label: 'Activate main', | ||||
| hidden: ()=>(this as ICamera)?.isMainCamera, | |||||
| hidden: ()=>this?.isMainCamera, | |||||
| value: ()=>{ | value: ()=>{ | ||||
| // todo: call activateMain on the camera, which will dispatch the event | |||||
| (this as ICamera).dispatchEvent({type: 'activateMain', ui: true, camera: this as ICamera}) | |||||
| this.activateMain({ui: true}) | |||||
| config.uiRefresh?.(true, 'postFrame') | config.uiRefresh?.(true, 'postFrame') | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Deactivate main', | label: 'Deactivate main', | ||||
| hidden: ()=>!(this as ICamera)?.isMainCamera, | |||||
| hidden: ()=>!this?.isMainCamera, | |||||
| value: ()=>{ | value: ()=>{ | ||||
| // todo: call activateMain on the camera, which will dispatch the event | |||||
| (this as ICamera).dispatchEvent({type: 'activateMain', ui: true, camera: undefined}) | |||||
| this.deactivateMain({ui: true}) | |||||
| config.uiRefresh?.(true, 'postFrame') | config.uiRefresh?.(true, 'postFrame') | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| type: 'checkbox', | type: 'checkbox', | ||||
| label: 'Auto LookAt Target', | label: 'Auto LookAt Target', | ||||
| getValue: ()=>(this as ICamera).userData.autoLookAtTarget ?? false, | |||||
| getValue: ()=>this.userData.autoLookAtTarget ?? false, | |||||
| setValue: (v)=>{ | setValue: (v)=>{ | ||||
| (this as ICamera).userData.autoLookAtTarget = v | |||||
| this.userData.autoLookAtTarget = v | |||||
| config.uiRefresh?.(true, 'postFrame') | config.uiRefresh?.(true, 'postFrame') | ||||
| }, | }, | ||||
| }, | }, |
| iObjectCommons.setDirty.call(this, {refreshScene: false, ...options}) | iObjectCommons.setDirty.call(this, {refreshScene: false, ...options}) | ||||
| }, | }, | ||||
| activateMain: function(this: ICamera, options: Partial<ICameraEvent> = {}, _internal = false, _refresh = true): void { | activateMain: function(this: ICamera, options: Partial<ICameraEvent> = {}, _internal = false, _refresh = true): void { | ||||
| if (!_internal) 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 (!_internal) { | |||||
| if (options.camera === null) return this.deactivateMain(options, _internal, _refresh) | |||||
| 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 | if (this.userData.__isMainCamera) return | ||||
| this.userData.__isMainCamera = true | this.userData.__isMainCamera = true | ||||
| this.userData.__lastScale = this.scale.clone() | this.userData.__lastScale = this.scale.clone() | ||||
| upgradeCamera: upgradeCamera, | upgradeCamera: upgradeCamera, | ||||
| copy: (superCopy: ICamera['copy']): ICamera['copy'] => | copy: (superCopy: ICamera['copy']): ICamera['copy'] => | ||||
| function(this: ICamera, camera: ICamera | Camera, recursive?, distanceFromTarget?, ...args): ICamera { | |||||
| function(this: ICamera, camera: ICamera | Camera, recursive?, distanceFromTarget?, worldSpace?, ...args): ICamera { | |||||
| if (!camera.isCamera) { | if (!camera.isCamera) { | ||||
| console.error('ICamera.copy: camera is not a Camera', camera) | console.error('ICamera.copy: camera is not a Camera', camera) | ||||
| return this | return this | ||||
| const minDistance = (this.controls as any)?.minDistance ?? distanceFromTarget ?? 4 | const minDistance = (this.controls as any)?.minDistance ?? distanceFromTarget ?? 4 | ||||
| camera.getWorldDirection(this.target).multiplyScalar(minDistance).add(this.getWorldPosition(new Vector3())) | 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.updateMatrixWorld(true) | ||||
| this.updateProjectionMatrix() | this.updateProjectionMatrix() | ||||
| this.refreshAspect(true) | |||||
| this.refreshAspect(false) | |||||
| this.setDirty() | |||||
| return this | return this | ||||
| }, | }, | ||||
| if (!this.userData.__autoBubbleToParentEvents) this.userData.__autoBubbleToParentEvents = ['select'] | if (!this.userData.__autoBubbleToParentEvents) this.userData.__autoBubbleToParentEvents = ['select'] | ||||
| // Event bubbling. todo: set bubbleToParent in these events when dispatched from child and remove from here? | // Event bubbling. todo: set bubbleToParent in these events when dispatched from child and remove from here? | ||||
| if (this.isCamera) this.userData.__autoBubbleToParentEvents.push('activateMain', 'setView') | |||||
| if (this.isLight) this.assetType = 'light' | if (this.isLight) this.assetType = 'light' | ||||
| else if (this.isCamera) this.assetType = 'camera' | else if (this.isCamera) this.assetType = 'camera' |
| import {Object3D, Vector3} from 'three' | |||||
| import {Easing} from 'popmotion' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {Box3B} from '../../three' | |||||
| import {onChange, serialize, timeout} from 'ts-browser-helpers' | |||||
| import {generateUiConfig, uiButton, uiDropdown, uiInput, UiObjectConfig, uiSlider, uiToggle} from 'uiconfig.js' | |||||
| import {EasingFunctions, EasingFunctionType} from '../../utils' | |||||
| import {CameraView, ICamera, ICameraView, PerspectiveCamera2} from '../../core' | |||||
| import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin' | |||||
| /** | |||||
| * Camera View Plugin | |||||
| * | |||||
| * Provides API to save, interact and animate and loop between with multiple camera states/views using the {@link PopmotionPlugin}. | |||||
| * | |||||
| */ | |||||
| export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewChange'|'viewAdd'|'viewDelete'> { | |||||
| static readonly PluginType = 'CameraViews' | |||||
| enabled = true | |||||
| // get dirty() { // todo: issue with recorder convergeMode? | |||||
| // return this._animating | |||||
| // } | |||||
| constructor() { | |||||
| super() | |||||
| this.addCurrentView = this.addCurrentView.bind(this) | |||||
| this.resetToFirstView = this.resetToFirstView.bind(this) | |||||
| this.animateAllViews = this.animateAllViews.bind(this) | |||||
| // this.recordAllViews = this.recordAllViews.bind(this) | |||||
| // this._wheel = this._wheel.bind(this) | |||||
| // this._pointerMove = this._pointerMove.bind(this) | |||||
| // this._postFrame = this._postFrame.bind(this) | |||||
| } | |||||
| @serialize('cameraViews') | |||||
| private _cameraViews: CameraView[] = [] | |||||
| get cameraViews(): CameraView[] { | |||||
| return this._cameraViews | |||||
| } | |||||
| get camViews(): CameraView[] { | |||||
| return this._cameraViews | |||||
| } | |||||
| @onChange(CameraViewPlugin.prototype._animationLoop) | |||||
| /** | |||||
| * Loop all views indefinitely. | |||||
| */ | |||||
| @serialize() @uiToggle('Loop All Views') viewLooping = false | |||||
| /** | |||||
| * Pauses time between view changes when animating all views or looping. | |||||
| */ | |||||
| @serialize() @uiInput('View Pause Time') viewPauseTime = 200 | |||||
| /** | |||||
| * {@link EasingFunctions} | |||||
| */ | |||||
| @serialize() @uiDropdown('Ease', Object.keys(EasingFunctions).map((label:string)=>({label}))) animEase: EasingFunctionType = 'easeInOutSine' // ms | |||||
| @serialize() @uiSlider('Duration', [10, 10000], 10) animDuration = 1000 // ms | |||||
| @serialize() @uiSlider('RotationOffset', [0.2, 0.75], 0.01) rotationOffset = 0.25 | |||||
| @serialize() @uiDropdown('Interpolation', ['spherical', 'linear'].map((label:string)=>({label}))) | |||||
| interpolateMode: 'spherical'|'linear' = 'spherical' | |||||
| private _animating = false | |||||
| get animating(): boolean { | |||||
| return this._animating | |||||
| } | |||||
| dependencies = [PopmotionPlugin] | |||||
| // private _updaters: {u: ((timestamp: number) => void), time: number}[] = [] | |||||
| // private _lastFrameTime = 0 // for post frame | |||||
| onAdded(viewer: ThreeViewer): void { | |||||
| super.onAdded(viewer) | |||||
| let interactionsDisabled = false // we need this because interactionsEnabled is also set in PickingPlugin | |||||
| // todo: move to PopmotionPlugin | |||||
| // todo: remove event listener | |||||
| viewer.addEventListener('preFrame', (_: any)=>{ | |||||
| if (/* this.seekOnScroll || */ this._animating) { | |||||
| if (this._viewer!.scene.mainCamera.interactionsEnabled) { | |||||
| this._viewer!.scene.mainCamera.interactionsEnabled = false | |||||
| interactionsDisabled = true | |||||
| // console.log(interactionsDisabled) | |||||
| } | |||||
| } else if (interactionsDisabled) { | |||||
| this._viewer!.scene.mainCamera.interactionsEnabled = true | |||||
| interactionsDisabled = false | |||||
| // console.log(interactionsDisabled) | |||||
| } | |||||
| // console.log(ev.deltaTime) | |||||
| // this._updaters.forEach(u=>{ | |||||
| // let dt = ev.deltaTime | |||||
| // if (u.time + dt < 0) dt = -u.time | |||||
| // u.time += dt | |||||
| // if (Math.abs(dt) > 0.001) | |||||
| // u.u(dt) | |||||
| // }) | |||||
| }) | |||||
| // viewer.addEventListener('postFrame', this._postFrame) | |||||
| // window.addEventListener('wheel', this._wheel) | |||||
| // window.addEventListener('pointermove', this._pointerMove) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer): void { | |||||
| // viewer.removeEventListener('postFrame', this._postFrame) | |||||
| // window.removeEventListener('wheel', this._wheel) | |||||
| // window.removeEventListener('pointermove', this._pointerMove) | |||||
| return super.onRemove(viewer) | |||||
| } | |||||
| @uiButton('Reset To First View') | |||||
| public async resetToFirstView(duration = 100) { | |||||
| if (!this.enabled) return | |||||
| this._currentView = undefined | |||||
| await this.animateToView(0, duration) | |||||
| await timeout(2) | |||||
| } | |||||
| @uiButton('Add Current View') | |||||
| async addCurrentView() { | |||||
| if (!this.enabled) return | |||||
| const camera = this._viewer?.scene.mainCamera | |||||
| if (!camera) return | |||||
| const view = this.getView(camera) | |||||
| this.addView(view) | |||||
| view.name = 'View ' + this._cameraViews.length | |||||
| return view | |||||
| } | |||||
| addView(view: CameraView) { | |||||
| this._cameraViews.push(view) | |||||
| view.addEventListener('setView', this._viewSetView) | |||||
| view.addEventListener('animateView', this._viewAnimateView) | |||||
| this.uiConfig.uiRefresh?.() | |||||
| this.dispatchEvent({type: 'viewAdd', view}) | |||||
| } | |||||
| protected _viewSetView = ({view, camera}: {view: CameraView, camera?: ICamera}&any) => { | |||||
| if (!view) { | |||||
| this._viewer?.console.warn('Invalid view', view) | |||||
| return | |||||
| } | |||||
| this.setView(view, camera) | |||||
| } | |||||
| protected _viewAnimateView = async({view, camera, duration, easing, throwOnStop}: {view: CameraView, camera?: ICamera, duration?: number, easing?: Easing|EasingFunctionType, throwOnStop?: boolean}&any) => { | |||||
| if (!view) { | |||||
| this._viewer?.console.warn('Invalid view', view) | |||||
| return | |||||
| } | |||||
| return this.animateToView(view, duration || this.animDuration, easing || this.animEase, camera, throwOnStop) | |||||
| } | |||||
| deleteView(view: CameraView) { | |||||
| const i = this._cameraViews.indexOf(view) | |||||
| if (i >= 0) | |||||
| this._cameraViews.splice(i, 1) | |||||
| this.uiConfig.uiRefresh?.() | |||||
| this.dispatchEvent({type: 'viewDelete', view}) | |||||
| } | |||||
| getView(camera?: ICamera, worldSpace = true) { | |||||
| camera = camera || this._viewer?.scene.mainCamera | |||||
| if (!camera) return new CameraView() | |||||
| return camera.getView(worldSpace) | |||||
| } | |||||
| setView(view: ICameraView, camera?: ICamera) { | |||||
| camera = camera || this._viewer?.scene.mainCamera | |||||
| if (!camera) return | |||||
| camera.setView(view) | |||||
| } | |||||
| private _currentView: CameraView | undefined | |||||
| @uiButton('Focus Next') focusNext = (wrap = true)=>{ | |||||
| if (this._animating) return | |||||
| if (this._cameraViews.length < 2) return | |||||
| let index = this._cameraViews.findIndex(v=>v === this._currentView) | |||||
| if (index < 0) index = -1 // first view | |||||
| index = index + 1 | |||||
| if (!wrap) index = Math.min(index, this._cameraViews.length - 1) | |||||
| else index = index % this._cameraViews.length | |||||
| this.animateToView(index) | |||||
| } | |||||
| @uiButton('Focus Previous') focusPrevious = (wrap = true)=> { | |||||
| if (this._animating) return | |||||
| if (this._cameraViews.length < 2 || !this._currentView) return | |||||
| let index = this._cameraViews.findIndex(v=>v === this._currentView) | |||||
| if (index < 0) index = 0 // last view | |||||
| index = index - 1 | |||||
| if (!wrap) index = Math.max(index, 0) | |||||
| else index = (index + this._cameraViews.length) % this._cameraViews.length | |||||
| this.animateToView(index) | |||||
| } | |||||
| private _popAnimations: AnimationResult[] = [] | |||||
| async animateToView(_view: CameraView|number, duration?: number, easing?: Easing|EasingFunctionType, camera?: ICamera, throwOnStop = false) { | |||||
| camera = camera || this._viewer?.scene.mainCamera | |||||
| if (!camera) return | |||||
| // if (this._currentView === view) return // todo: also check if the camera is at the correct position and orientation, till then use resetToFirstView to reset current view | |||||
| if (this._animating) { | |||||
| this._popAnimations.forEach(a=>a?.stop && a.stop()) // don't call stopAllAnimations here, as it sets viewLooping to false and changes config. | |||||
| this._popAnimations = [] | |||||
| let i = 0 | |||||
| while (this._animating) { | |||||
| await timeout(100) | |||||
| if (i++ > 20) { // 2s timeout | |||||
| break | |||||
| } | |||||
| } | |||||
| if (this._animating) { | |||||
| console.warn('Unable to stop all animations, maybe because of viewLooping?') | |||||
| return | |||||
| } | |||||
| } | |||||
| const view = typeof _view === 'number' ? this._cameraViews[_view] : _view | |||||
| this._currentView = view | |||||
| this._animating = true | |||||
| this.dispatchEvent({type: 'startViewChange', view}) | |||||
| const popmotion = this._viewer?.getPlugin(PopmotionPlugin) | |||||
| if (!popmotion) throw new Error('PopmotionPlugin not found') | |||||
| if (duration === undefined) duration = this.animDuration | |||||
| const ease: any = (typeof easing === 'function' ? easing : EasingFunctions[easing || this.animEase]) as (x: number) => number | |||||
| // const ease = (x:number)=>x | |||||
| // const driver = this._driver | |||||
| this._popAnimations = [] | |||||
| await popmotion.animateCameraAsync(camera, view, this.interpolateMode === 'spherical', {ease, duration}, this._popAnimations) | |||||
| .catch((e)=>{ | |||||
| // console.error(e) | |||||
| if (throwOnStop) throw e | |||||
| }) | |||||
| this._animating = false | |||||
| this.dispatchEvent({type: 'viewChange', view}) | |||||
| await timeout(10) | |||||
| } | |||||
| @uiButton('Animate All Views') | |||||
| async animateAllViews() { | |||||
| if (!this.enabled) return | |||||
| if (this.viewLooping || this._cameraViews.length < 2) return | |||||
| while (this._viewQueue.length > 0) this._viewQueue.pop() | |||||
| this._viewQueue.push(...this._cameraViews) | |||||
| this._viewQueue.push(this._viewQueue.shift()!) | |||||
| this._infiniteLooping = false | |||||
| await this._animationLoop() | |||||
| this._infiniteLooping = true | |||||
| } | |||||
| @uiButton('Stop All Animations') | |||||
| async stopAllAnimations() { | |||||
| this.viewLooping = false | |||||
| this._popAnimations.forEach(a => a?.stop?.()) | |||||
| this._popAnimations = [] | |||||
| while (this._animating || this._animationLooping) { | |||||
| await timeout(100) | |||||
| } | |||||
| } | |||||
| fromJSON(data: any, meta?: any): this | null { | |||||
| this._cameraViews.forEach(v=>this.deleteView(v)) // deserialize pushes to the existing array | |||||
| if (super.fromJSON(data, meta)) { | |||||
| this.uiConfig.uiRefresh?.() | |||||
| return this | |||||
| } | |||||
| return null | |||||
| } | |||||
| public async animateToObject(selected?: Object3D, distanceMultiplier = 4, duration?: number, ease?: Easing|EasingFunctionType, distanceBounds = {min: 0.5, max: 5.0}) { | |||||
| if (!this._viewer) return | |||||
| const bbox = new Box3B().expandByObject(selected || this._viewer.scene.modelRoot.modelObject, false, true) | |||||
| const center = bbox.getCenter(new Vector3()) | |||||
| const size = bbox.getSize(new Vector3()) | |||||
| const radius = size.length() / 2 | |||||
| await this.animateToTarget(Math.min(distanceBounds.max, Math.max(distanceBounds.min, radius * distanceMultiplier)), center, duration, ease) | |||||
| } | |||||
| public async animateToFitObject(selected?: Object3D, distanceMultiplier = 1.5, duration = 1000, ease?: Easing|EasingFunctionType, distanceBounds = {min: 0.5, max: 50.0}) { | |||||
| if (!this._viewer) return | |||||
| const bbox = new Box3B().expandByObject(selected || this._viewer.scene.modelRoot, false, true) | |||||
| const center = bbox.getCenter(new Vector3()) // world position | |||||
| const size = bbox.getSize(new Vector3()) | |||||
| const cam = this._viewer.scene.mainCamera | |||||
| let cameraZ = 1 | |||||
| if (cam.isPerspectiveCamera) { | |||||
| // get the max side of the bounding box (fits to width OR height as needed ) | |||||
| const fov = (cam as PerspectiveCamera2).fov * (Math.PI / 180) | |||||
| const fovh = 2 * Math.atan(Math.tan(fov / 2) * cam.aspect) | |||||
| const dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)) | |||||
| const dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)) | |||||
| cameraZ = Math.max(dx, dy) | |||||
| } | |||||
| await this.animateToTarget(Math.min(distanceBounds.max, Math.max(distanceBounds.min, cameraZ * distanceMultiplier)), center, duration, ease) | |||||
| } | |||||
| /** | |||||
| * | |||||
| * @param distanceFromTarget - in world units | |||||
| * @param center - target (center) of the view in world coordinates | |||||
| * @param duration - in milliseconds | |||||
| * @param ease | |||||
| */ | |||||
| public async animateToTarget(distanceFromTarget: number, center: Vector3, duration?: number, ease?: Easing|EasingFunctionType) { | |||||
| const view = this.getView() // world space | |||||
| view.target.copy(center) | |||||
| const direction = new Vector3().subVectors(view.target, view.position).normalize() | |||||
| view.position.copy(direction.multiplyScalar(-distanceFromTarget).add(view.target)) | |||||
| await this.animateToView(view, duration, ease) | |||||
| } | |||||
| uiConfig: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: 'Camera Views', | |||||
| // expanded: true, | |||||
| children: [ | |||||
| ()=>[...this._cameraViews.map(view => view.uiConfig)], | |||||
| ...generateUiConfig(this), | |||||
| ], | |||||
| } | |||||
| get animationLooping(): boolean { | |||||
| return this._animationLooping | |||||
| } | |||||
| private _viewQueue: CameraView[] = [] | |||||
| private _animationLooping = false | |||||
| private _infiniteLooping = true | |||||
| private async _animationLoop() { | |||||
| if (this._animationLooping) return | |||||
| this._animationLooping = true | |||||
| while (this.viewLooping || !this._infiniteLooping) { | |||||
| if (!this.enabled) break | |||||
| if (this._cameraViews.length < 1) break | |||||
| if (this._viewQueue.length === 0) { | |||||
| if (this._infiniteLooping) this._viewQueue.push(...this._cameraViews) | |||||
| else break | |||||
| } | |||||
| await this.animateToView(this._viewQueue.shift()!) | |||||
| await timeout(2 + this.viewPauseTime) // ms delay | |||||
| } | |||||
| this._animationLooping = false | |||||
| } | |||||
| // region deprecated | |||||
| /** | |||||
| * @deprecated - renamed to {@link getView} or {@link ICamera.getView} | |||||
| * @param camera | |||||
| * @param worldSpace | |||||
| */ | |||||
| getCurrentCameraView(camera?: ICamera, worldSpace = true) { | |||||
| return this.getView(camera, worldSpace) | |||||
| } | |||||
| /** | |||||
| * @deprecated - renamed to {@link setView} or {@link ICamera.setView} | |||||
| * @param view | |||||
| */ | |||||
| setCurrentCameraView(view: CameraView) { | |||||
| return this.setView(view) | |||||
| } | |||||
| /** | |||||
| * @deprecated - use {@link animateToView} instead | |||||
| * @param view | |||||
| */ | |||||
| async focusView(view: CameraView) { | |||||
| return this.animateToView(view) | |||||
| } | |||||
| // endregion | |||||
| // region to be ported to other plugins | |||||
| // /** | |||||
| // * For slight rotation of camera when seekOnScroll is enabled | |||||
| // */ | |||||
| // private _pointerMove(ev: PointerEvent) { | |||||
| // if (!this.enabled) return | |||||
| // if (!this._animating && this.seekOnScroll) { | |||||
| // const cam = this._viewer?.scene.mainCamera | |||||
| // if (!cam) return | |||||
| // const s = new Spherical() | |||||
| // const p = cam.position | |||||
| // const t = cam.target | |||||
| // const q = new Quaternion().setFromUnitVectors(cam.cameraObject.up, new Vector3(0, 1, 0)) | |||||
| // const qi = q.clone().invert() | |||||
| // const offset = p.clone().sub(t) | |||||
| // offset.applyQuaternion(q) | |||||
| // s.setFromVector3(offset) | |||||
| // s.theta += this.rotationOffset * ev.movementX / this._viewer!.canvas!.clientWidth | |||||
| // s.phi += this.rotationOffset * ev.movementY / this._viewer!.canvas!.clientHeight | |||||
| // s.makeSafe() | |||||
| // offset.setFromSpherical(s) | |||||
| // offset.applyQuaternion(qi) | |||||
| // p.copy(t).add(offset) | |||||
| // cam.setDirty() | |||||
| // } | |||||
| // } | |||||
| // // @uiToggle() @serialize() | |||||
| // animateOnScroll = false // buggy | |||||
| // | |||||
| // @uiToggle() @serialize() | |||||
| // seekOnScroll = false | |||||
| // private _scrollAnimationState = 0 | |||||
| // scrollAnimationDamping = 0.1 | |||||
| // private _wheel(ev: any | WheelEvent) { | |||||
| // if (!this.enabled) return | |||||
| // if (this.seekOnScroll && !this._animating) { | |||||
| // // if (ev.deltaY > 0) this.focusNext(false) | |||||
| // // else this.focusPrevious(false) | |||||
| // } else if (Math.abs(ev.deltaY) > 0.001) { | |||||
| // this._scrollAnimationState = -1. * Math.sign(ev.deltaY) | |||||
| // } | |||||
| // } | |||||
| // private _driver: Driver = (update)=>{ | |||||
| // return { | |||||
| // start: ()=>this._updaters.push({u:update, time:0}), | |||||
| // stop: ()=> this._updaters.splice(this._updaters.findIndex(u=>u.u === update), 1), | |||||
| // } | |||||
| // } | |||||
| // private _fadeDisabled = false | |||||
| // todo: same code used in PopmotionPlugin, merge somehow | |||||
| // private _postFrame() { | |||||
| // if (!this._viewer) return | |||||
| // if (!this.enabled || !this._animating) { | |||||
| // this._lastFrameTime = 0 | |||||
| // if (this._fadeDisabled) { | |||||
| // this._viewer.getPluginByType<FrameFadePlugin>('FrameFade')?.enable(CameraViewPlugin.PluginType) | |||||
| // this._fadeDisabled = false | |||||
| // } | |||||
| // // console.log('not anim') | |||||
| // return | |||||
| // } | |||||
| // const time = now() / 1000.0 | |||||
| // if (this._lastFrameTime < 1) this._lastFrameTime = time - 1.0 / 60.0 | |||||
| // let delta = time - this._lastFrameTime | |||||
| // this._lastFrameTime = time | |||||
| // delta = delta * (this.animateOnScroll ? this._scrollAnimationState : 1) | |||||
| // | |||||
| // const d = this._viewer.getPluginByType<ProgressivePlugin>('Progressive')?.postFrameConvergedRecordingDelta() | |||||
| // if (d && d > 0) delta = d | |||||
| // if (d === 0) return // not converged yet. | |||||
| // // if d < 0: not recording, do nothing | |||||
| // | |||||
| // delta *= 1000 | |||||
| // | |||||
| // // delta = 16.666 | |||||
| // | |||||
| // // console.log(delta) | |||||
| // // console.log(dt) | |||||
| // // | |||||
| // | |||||
| // if (delta <= 0) return | |||||
| // | |||||
| // this._updaters.forEach(u=>{ | |||||
| // let dt = delta | |||||
| // if (u.time + dt < 0) dt = -u.time | |||||
| // u.time += dt | |||||
| // if (Math.abs(dt) > 0.001) | |||||
| // u.u(dt) | |||||
| // }) | |||||
| // if (this._scrollAnimationState < 0.001) this._scrollAnimationState = 0 | |||||
| // else this._scrollAnimationState *= 1.0 - this.scrollAnimationDamping | |||||
| // | |||||
| // if (!this._fadeDisabled) { | |||||
| // const ff = this._viewer.getPluginByType<FrameFadePlugin>('FrameFade') | |||||
| // if (ff) { | |||||
| // ff.disable(CameraViewPlugin.PluginType) | |||||
| // this._fadeDisabled = true | |||||
| // } | |||||
| // } | |||||
| // } | |||||
| // @uiButton('Record All Views') | |||||
| // public async recordAllViews(onStart?: ()=>void, downloadOnEnd = true) { | |||||
| // if (!this.enabled) return | |||||
| // const recorder = this._viewer?.getPluginByType<CanvasRecorderPlugin>('CanvasRecorder') | |||||
| // if (!recorder || !recorder.enabled) return | |||||
| // if (this._cameraViews.length < 1) return | |||||
| // await this.resetToFirstView() | |||||
| // if (recorder.isRecording()) { | |||||
| // console.error('CanvasRecorderPlugin is already recording') | |||||
| // return | |||||
| // } | |||||
| // return new Promise<Blob|undefined>((resolve, reject) => { | |||||
| // const listener2 = ()=>{ | |||||
| // recorder.removeEventListener('start', listenerStart) | |||||
| // recorder.removeEventListener('stop', listener2) | |||||
| // recorder.removeEventListener('error', listenerError) | |||||
| // } | |||||
| // const listenerStart = async() => { | |||||
| // listener2() | |||||
| // onStart?.() | |||||
| // await this.animateAllViews() | |||||
| // const blob = await recorder.stopRecording() | |||||
| // if (downloadOnEnd) { | |||||
| // const name = await this._viewer?.prompt('Canvas Recorder: Save file as', 'recording.mp4') | |||||
| // if (name !== null && blob) await this._downloadBlob(blob, name || 'recording.mp4') | |||||
| // } | |||||
| // resolve(blob) | |||||
| // } | |||||
| // const listenerError = async() => { | |||||
| // listener2() | |||||
| // reject() | |||||
| // } | |||||
| // recorder.addEventListener('start', listenerStart) | |||||
| // recorder.addEventListener('stop', listener2) | |||||
| // recorder.addEventListener('error', listenerError) | |||||
| // if (!recorder.startRecording()) { | |||||
| // console.error('cannot start recording') | |||||
| // return | |||||
| // } | |||||
| // }) | |||||
| // } | |||||
| // private async _downloadBlob(blob: Blob, name: string) { | |||||
| // const tr = this._viewer?.getPluginByType<FileTransferPlugin>('FileTransferPlugin') | |||||
| // if (!tr) { | |||||
| // this._viewer?.console.error('FileTransferPlugin required to export/download file') | |||||
| // return | |||||
| // } | |||||
| // await tr.exportFile(blob, name) | |||||
| // } | |||||
| // endregion | |||||
| } |
| import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | ||||
| import type {ProgressivePlugin} from '../pipeline/ProgressivePlugin' | import type {ProgressivePlugin} from '../pipeline/ProgressivePlugin' | ||||
| import {generateUUID} from '../../three' | import {generateUUID} from '../../three' | ||||
| import {makeSetterFor} from '../../utils' | |||||
| import {animateCameraToViewLinear, animateCameraToViewSpherical, EasingFunctions, makeSetterFor} from '../../utils' | |||||
| import {ICamera, ICameraView} from '../../core' | |||||
| export interface AnimationResult{ | export interface AnimationResult{ | ||||
| id: string | id: string | ||||
| dependencies = [] | dependencies = [] | ||||
| private _fadeDisabled = false | private _fadeDisabled = false | ||||
| /** | /** | ||||
| * Disable the frame fade plugin while animation is running | * Disable the frame fade plugin while animation is running | ||||
| */ | */ | ||||
| return this.animations[uuid] | return this.animations[uuid] | ||||
| } | } | ||||
| async animateAsync<V>(options: AnimationOptions<V>& {target?: any, key?: string}): Promise<string> { | |||||
| return this.animate(options).promise | |||||
| async animateAsync<V>(options: AnimationOptions<V>& {target?: any, key?: string}, animations?: AnimationResult[]): Promise<string> { | |||||
| const anim = this.animate(options) | |||||
| if (animations) animations.push(anim) | |||||
| return anim.promise | |||||
| } | } | ||||
| async animateTargetAsync<T>(target: T, key: keyof T, options: AnimationOptions<T[keyof T]>): Promise<string> { | |||||
| return this.animate({...options, target, key: key as string}).promise | |||||
| async animateTargetAsync<T>(target: T, key: keyof T, options: AnimationOptions<T[keyof T]>, animations?: AnimationResult[]): Promise<string> { | |||||
| const anim = this.animate({...options, target, key: key as string}) | |||||
| if (animations) animations.push(anim) | |||||
| return anim.promise | |||||
| } | } | ||||
| // todo : animateObject/animateTarget | |||||
| animateCamera(camera: ICamera, view: ICameraView, spherical = true, options?: Partial<AnimationOptions<any>>) { | |||||
| const anim = spherical ? | |||||
| animateCameraToViewSpherical(camera, view) : | |||||
| animateCameraToViewLinear(camera, view) | |||||
| return this.animate({ | |||||
| ease: EasingFunctions.linear, | |||||
| duration: 1000, | |||||
| ...anim, ...options, | |||||
| }) | |||||
| } | |||||
| async animateCameraAsync(camera: ICamera, view: ICameraView, spherical = true, options?: Partial<AnimationOptions<any>>, animations?: AnimationResult[]) { | |||||
| const anim = this.animateCamera(camera, view, spherical, options) | |||||
| if (animations) animations.push(anim) | |||||
| return anim.promise | |||||
| } | |||||
| } | } |
| // animation | // animation | ||||
| export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | ||||
| export {PopmotionPlugin} from './animation/PopmotionPlugin' | export {PopmotionPlugin} from './animation/PopmotionPlugin' | ||||
| export {CameraViewPlugin} from './animation/CameraViewPlugin' |
| export {dataTextureFromColor, dataTextureFromVec4, halfFloatToRgbe} from './conversion' | export {dataTextureFromColor, dataTextureFromVec4, halfFloatToRgbe} from './conversion' | ||||
| export {uniform, matDefine} from './decorators' | export {uniform, matDefine} from './decorators' | ||||
| export {getEncodingComponents, getTexelEncoding, getTexelDecoding, getTexelDecoding2, getTexelDecodingFunction, getTexelEncodingFunction, getTextureColorSpaceFromMap} from './encoding' | export {getEncodingComponents, getTexelEncoding, getTexelDecoding, getTexelDecoding2, getTexelDecodingFunction, getTexelEncodingFunction, getTextureColorSpaceFromMap} from './encoding' | ||||
| export {generateUUID, toIndexedGeometry, isInScene} from './misc' | |||||
| export {generateUUID, toIndexedGeometry, isInScene, localToWorldQuaternion, worldToLocalQuaternion} from './misc' | |||||
| export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDataUrl, texImageToCanvas} from './texture' | export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDataUrl, texImageToCanvas} from './texture' | ||||
| export {threeConstMappings} from './const-mappings' | export {threeConstMappings} from './const-mappings' | ||||
| export {ObjectPicker} from './ObjectPicker' | export {ObjectPicker} from './ObjectPicker' |
| import {BufferGeometry, MathUtils} from 'three' | |||||
| import {BufferGeometry, MathUtils, Object3D, Quaternion} from 'three' | |||||
| import {mergeVertices} from 'three/examples/jsm/utils/BufferGeometryUtils.js' | import {mergeVertices} from 'three/examples/jsm/utils/BufferGeometryUtils.js' | ||||
| import {IGeometry, IMaterial, IObject3D, IScene, ITexture} from '../../core' | import {IGeometry, IMaterial, IObject3D, IScene, ITexture} from '../../core' | ||||
| } | } | ||||
| return false | return false | ||||
| } | } | ||||
| /** | |||||
| * Convert a world-space quaternion to local-space quaternion. | |||||
| * https://github.com/mrdoob/three.js/pull/20243 | |||||
| * @param object | |||||
| * @param quaternion | |||||
| * @param _q | |||||
| */ | |||||
| export function worldToLocalQuaternion(object: Object3D, quaternion: Quaternion, _q = new Quaternion()) { | |||||
| return quaternion.premultiply(object.getWorldQuaternion(_q).invert()) | |||||
| } | |||||
| /** | |||||
| * Convert a local-space quaternion to world-space quaternion. | |||||
| * https://github.com/mrdoob/three.js/pull/20243 | |||||
| * @param object | |||||
| * @param quaternion | |||||
| * @param _q | |||||
| */ | |||||
| export function localToWorldQuaternion(object: Object3D, quaternion: Quaternion, _q = new Quaternion()) { | |||||
| return quaternion.premultiply(object.getWorldQuaternion(_q)) | |||||
| } |
| linear, | linear, | ||||
| } from 'popmotion' | } from 'popmotion' | ||||
| import {timeout} from 'ts-browser-helpers' | import {timeout} from 'ts-browser-helpers' | ||||
| import {MathUtils} from 'three' | |||||
| export {animate} | export {animate} | ||||
| export type {AnimationOptions, KeyframeOptions, Easing} | export type {AnimationOptions, KeyframeOptions, Easing} | ||||
| }) | }) | ||||
| } | } | ||||
| export function lerpAngle(a: number, b: number, t: number) { | |||||
| const d = b - a | |||||
| if (d >= Math.PI) { | |||||
| return a + (d - Math.PI * 2) * t | |||||
| } else if (d <= -Math.PI) { | |||||
| return a + (d + Math.PI * 2) * t | |||||
| } else { | |||||
| return a + d * t | |||||
| } | |||||
| } | |||||
| export const lerp = MathUtils.lerp |
| import {Quaternion, Spherical, Vector3} from 'three' | |||||
| import {worldToLocalQuaternion} from '../three' | |||||
| import {CameraView, ICamera, ICameraView} from '../core' | |||||
| import {AnimationOptions} from 'popmotion' | |||||
| import {lerp, lerpAngle} from './animation' | |||||
| export function sphericalFromCameraView(view: Pick<CameraView, 'position'|'target'>): Spherical { | |||||
| const pos = view.position.clone() | |||||
| pos.sub(view.target) | |||||
| const spherical = new Spherical().setFromVector3(pos) | |||||
| spherical.makeSafe() // todo: is it needed? | |||||
| return spherical | |||||
| } | |||||
| export function animateCameraToViewSpherical(camera: ICamera, view: ICameraView): AnimationOptions<number> { | |||||
| // similar to orbit controls | |||||
| const parent = camera.parent | |||||
| const target = camera.target.clone() | |||||
| const position = camera.getWorldPosition(new Vector3()) | |||||
| const init = { | |||||
| position, target, zoom: camera.zoom, | |||||
| spherical: sphericalFromCameraView({position, target}), | |||||
| } | |||||
| const current = { | |||||
| position: new Vector3(), | |||||
| target: new Vector3(), | |||||
| zoom: 1, | |||||
| spherical: new Spherical(), | |||||
| } | |||||
| const final = { | |||||
| position: view.position, | |||||
| target: view.target, | |||||
| zoom: view.zoom, | |||||
| spherical: sphericalFromCameraView(view), | |||||
| } | |||||
| function setter() { | |||||
| camera.position.copy(parent ? parent.worldToLocal(current.position) : current.position) | |||||
| camera.target.copy(current.target) // always in world space | |||||
| camera.zoom = current.zoom | |||||
| // lookAt in setDirty updates the quaternion | |||||
| camera.setDirty() // because it has min change distance in setter | |||||
| } | |||||
| return { | |||||
| from: 0, | |||||
| to: 1, | |||||
| onUpdate: (v) => { | |||||
| current.spherical.phi = lerpAngle(init.spherical.phi, final.spherical.phi, v) | |||||
| current.spherical.theta = lerpAngle(init.spherical.theta, final.spherical.theta, v) | |||||
| current.spherical.radius = lerp(init.spherical.radius, final.spherical.radius, v) | |||||
| current.target.copy(init.target).lerp(final.target, v) | |||||
| current.position.setFromSpherical(current.spherical) | |||||
| current.position.add(current.target) | |||||
| current.zoom = lerp(init.zoom, final.zoom, v) | |||||
| setter() | |||||
| }, | |||||
| onComplete: () => { | |||||
| current.position.copy(final.position) | |||||
| current.target.copy(final.target) | |||||
| current.zoom = final.zoom | |||||
| setter() | |||||
| }, | |||||
| onStop: () => { | |||||
| throw new Error('Animation Stopped') | |||||
| }, | |||||
| } | |||||
| } | |||||
| export function animateCameraToViewLinear(camera: ICamera, view: ICameraView): AnimationOptions<number> { | |||||
| // similar to orbit controls | |||||
| // so camera.up is the orbit axis | |||||
| const parent = camera.parent | |||||
| const target = camera.target.clone() | |||||
| const position = camera.getWorldPosition(new Vector3()) | |||||
| const quaternion = camera.getWorldQuaternion(new Quaternion()) | |||||
| const init = { | |||||
| position, target, quaternion, zoom: camera.zoom, | |||||
| } | |||||
| const current = { | |||||
| position: new Vector3(), | |||||
| target: new Vector3(), | |||||
| quaternion: new Quaternion(), | |||||
| zoom: 1, | |||||
| } | |||||
| const final = view | |||||
| function setter() { | |||||
| camera.position.copy(parent ? parent.worldToLocal(current.position) : current.position) | |||||
| camera.target.copy(current.target) // always in world space | |||||
| camera.quaternion.copy(parent ? worldToLocalQuaternion(parent, current.quaternion, camera.quaternion) : current.quaternion) | |||||
| camera.zoom = current.zoom | |||||
| camera.setDirty() // because it has min change distance in setter | |||||
| } | |||||
| return { | |||||
| from: 0, | |||||
| to: 1, | |||||
| onUpdate: (v) => { | |||||
| current.position.lerpVectors(init.position, final.position, v) | |||||
| current.target.lerpVectors(init.target, final.target, v) | |||||
| current.quaternion.slerpQuaternions(init.quaternion, final.quaternion, v) | |||||
| current.zoom = lerp(init.zoom, final.zoom, v) | |||||
| setter() | |||||
| }, | |||||
| onComplete: () => { | |||||
| current.position.copy(final.position) | |||||
| current.target.copy(final.target) | |||||
| current.quaternion.copy(final.quaternion) | |||||
| current.zoom = final.zoom | |||||
| setter() | |||||
| }, | |||||
| onStop: () => { | |||||
| throw new Error('Animation Stopped') | |||||
| }, | |||||
| } | |||||
| } | |||||
| export {ThreeSerialization, type SerializationMetaType, type SerializationResourcesType, MetaImporter, metaToResources, getEmptyMeta, metaFromResources, convertArrayBufferToStringsInMeta, convertStringsToArrayBuffersInMeta, copyMaterialUserData, copyObject3DUserData, copyUserData, copyTextureUserData, jsonToBlob, serializeTextureInExtras} from './serialization' | export {ThreeSerialization, type SerializationMetaType, type SerializationResourcesType, MetaImporter, metaToResources, getEmptyMeta, metaFromResources, convertArrayBufferToStringsInMeta, convertStringsToArrayBuffersInMeta, copyMaterialUserData, copyObject3DUserData, copyUserData, copyTextureUserData, jsonToBlob, serializeTextureInExtras} from './serialization' | ||||
| export {shaderReplaceString} from './shader-helpers' | export {shaderReplaceString} from './shader-helpers' | ||||
| export {makeGLBFile} from './gltf' | export {makeGLBFile} from './gltf' | ||||
| export {animateAsync, animateTarget, EasingFunctions, makeSetterFor, animate} from './animation' | |||||
| export {animateCameraToViewLinear, animateCameraToViewSpherical, sphericalFromCameraView} from './camera-anim' | |||||
| export {animateAsync, animateTarget, EasingFunctions, makeSetterFor, animate, lerp, lerpAngle} from './animation' | |||||
| export type {Easing, KeyframeOptions, AnimationOptions, EasingFunctionType, AnimateResult} from './animation' | export type {Easing, KeyframeOptions, AnimationOptions, EasingFunctionType, AnimateResult} from './animation' | ||||
| import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin' | import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin' | ||||
| import {uiConfig, uiFolderContainer, UiObjectConfig} from 'uiconfig.js' | import {uiConfig, uiFolderContainer, UiObjectConfig} from 'uiconfig.js' | ||||
| import {IRenderTarget} from '../rendering' | import {IRenderTarget} from '../rendering' | ||||
| import type {ProgressivePlugin} from '../plugins' | |||||
| import {TonemapPlugin} from '../plugins' | import {TonemapPlugin} from '../plugins' | ||||
| import {VERSION} from './version' | import {VERSION} from './version' | ||||
| this.addEventListener('postFrame', () => { // todo: move inside RootScene. | this.addEventListener('postFrame', () => { // todo: move inside RootScene. | ||||
| const cam = this._scene.mainCamera | const cam = this._scene.mainCamera | ||||
| if (cam && cam.canUserInteract) { | if (cam && cam.canUserInteract) { | ||||
| // todo | |||||
| // const d = this.getPluginByType<ProgressivePlugin>('Progressive')?.postFrameConvergedRecordingDelta() | |||||
| // // if (d && d > 0) delta = d | |||||
| // if (d !== undefined && d === 0) return // not converged yet. | |||||
| // // if d < 0 or undefined: not recording, do nothing | |||||
| const d = this.getPlugin<ProgressivePlugin>('ProgressivePlugin')?.postFrameConvergedRecordingDelta() | |||||
| // if (d && d > 0) delta = d | |||||
| if (d !== undefined && d === 0) return // not converged yet. | |||||
| // if d < 0 or undefined: not recording, do nothing | |||||
| cam.controls?.update() | cam.controls?.update() | ||||
| } | } | ||||
| this.console.warn('Cannot find camera', event) | this.console.warn('Cannot find camera', event) | ||||
| return | return | ||||
| } | } | ||||
| this._scene.mainCamera.copy(event.camera) | |||||
| const worldPos = event.camera.getWorldPosition(this._scene.mainCamera.position) | |||||
| // camera.getWorldQuaternion(this.quaternion) // todo: do if autoLookAtTarget is false | |||||
| if (this._scene.mainCamera.parent) { | |||||
| this._scene.mainCamera.position.copy(this._scene.mainCamera.parent.worldToLocal(worldPos)) | |||||
| // this.quaternion.premultiply(this.parent.quaternion.clone().invert()) | |||||
| } | |||||
| this._scene.mainCamera.setDirty() | |||||
| const camera = this._scene.mainCamera | |||||
| camera.setViewFromCamera(event.camera) // default is worldSpace | |||||
| } else if (event.type === 'activateMain') | } else if (event.type === 'activateMain') | ||||
| this._scene.mainCamera = event.camera || undefined // event.camera should have been upgraded when added to the scene. | this._scene.mainCamera = event.camera || undefined // event.camera should have been upgraded when added to the scene. | ||||
| } | } |