Improve warning in AssetManager, Create MaterialManager.copyMaterialProps, fix camera aspect when canvas has height 0, add undo for some buttons, changes/fixes for ui, handle InteractionPromptPlugin in CameraViewPlugin, support async onStop and onComplete in PopmotionPlugin, allow resetting geometry in BaseGroundPlugin, add pluginListeners, forPlugin in ThreeViewer, add SwitchNodeBasePlugin.snapIcons, fixes in InteractionPromptPlugin, add isEditor, LS_DEFAULT_LOGO in LoadingScreenPlugin, add isEditor in FrameFadePlugin.
1 год назад Improve warning in AssetManager, Create MaterialManager.copyMaterialProps, fix camera aspect when canvas has height 0, add undo for some buttons, changes/fixes for ui, handle InteractionPromptPlugin in CameraViewPlugin, support async onStop and onComplete in PopmotionPlugin, allow resetting geometry in BaseGroundPlugin, add pluginListeners, forPlugin in ThreeViewer, add SwitchNodeBasePlugin.snapIcons, fixes in InteractionPromptPlugin, add isEditor, LS_DEFAULT_LOGO in LoadingScreenPlugin, add isEditor in FrameFadePlugin.
1 год назад |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- 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} from '../../core'
- import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin'
- import {InteractionPromptPlugin} from '../interaction/InteractionPromptPlugin'
- import {getFittingDistance} from '../../three/utils/camera'
-
- export interface CameraViewPluginOptions{duration?: number, ease?: EasingFunctionType, interpolateMode?: 'spherical'|'linear'}
-
- /**
- * 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(options: CameraViewPluginOptions = {}) {
- 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)
-
- this.animDuration = options.duration ?? this.animDuration
- this.animEase = options.ease ?? this.animEase
- this.interpolateMode = options.interpolateMode ?? this.interpolateMode
- }
-
-
-
- @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() @uiDropdown('Interpolation', ['spherical', 'linear'].map((label:string)=>({label})))
- interpolateMode: 'spherical'|'linear' = 'spherical'
-
-
- // not used
- @serialize()
- // @uiSlider('RotationOffset', [0.2, 0.75], 0.01)
- rotationOffset = 0.25
-
- 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)
-
- // todo: move to PopmotionPlugin
- // todo: remove event listener
- viewer.addEventListener('preFrame', (_: any)=>{
- // 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', {sendArgs: false})
- public async resetToFirstView(duration = 100) {
- if (this.isDisabled()) return
- this._currentView = undefined
- await this.animateToView(0, duration)
- await timeout(2)
- }
-
- @uiButton('Add Current View')
- async addCurrentView() {
- if (this.isDisabled()) 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) {
- if (!this._cameraViews.includes(view)) this._cameraViews.push(view)
- view.addEventListener('setView', this._viewSetView as any)
- view.addEventListener('updateView', this._viewUpdateView as any)
- view.addEventListener('deleteView', this._viewDeleteView as any)
- view.addEventListener('animateView', this._viewAnimateView as any)
- this.uiConfig.uiRefresh?.()
- this.dispatchEvent({type: 'viewAdd', view})
- }
-
- protected _viewSetView = ({view, camera}: {view?: CameraView, camera?: ICamera}) => {
- if (!view) {
- this._viewer?.console.warn('Invalid view', view)
- return
- }
- this.setView(view, camera)
- }
-
- protected _viewUpdateView = ({view, camera}: {view: CameraView, camera?: ICamera}) => {
- if (!view) {
- this._viewer?.console.warn('Invalid view', view)
- return
- }
- const name = view.name
- this.getView(camera, view.isWorldSpace ?? true, view)
- view.name = name
- }
-
- protected _viewDeleteView = ({view}: {view: CameraView}) => {
- if (!view) {
- this._viewer?.console.warn('Invalid view', view)
- return
- }
- this.deleteView(view)
- }
-
- protected _viewAnimateView = async({view, camera, duration, easing, throwOnStop}: {view: CameraView, camera?: ICamera, duration?: number, easing?: Easing|EasingFunctionType, throwOnStop?: boolean}) => {
- 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)
- view.removeEventListener('setView', this._viewSetView as any)
- view.removeEventListener('updateView', this._viewUpdateView as any)
- view.removeEventListener('deleteView', this._viewDeleteView as any)
- view.removeEventListener('animateView', this._viewAnimateView as any)
- this.uiConfig.uiRefresh?.()
- this.dispatchEvent({type: 'viewDelete', view})
- }
-
- getView(camera?: ICamera, worldSpace = true, view?: CameraView) {
- camera = camera || this._viewer?.scene.mainCamera
- if (!camera) return view ?? new CameraView()
- return camera.getView(worldSpace, view)
- }
-
- setView(view: ICameraView, camera?: ICamera) {
- camera = camera || this._viewer?.scene.mainCamera
- if (!camera) return
- camera.setView(view)
- }
-
- private _currentView: CameraView | undefined
-
- @uiButton('Focus Next', {sendArgs: false}) 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', {sendArgs: false}) 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|string, 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] :
- typeof _view === 'string' ? this._cameraViews.find(v=>v.name === _view) :
- _view
- if (!view) {
- this._viewer?.console.warn('Invalid view', _view)
- return
- }
-
- const interactionPrompt = this._viewer?.getPlugin(InteractionPromptPlugin)
- if (interactionPrompt && interactionPrompt.animationRunning) {
- await interactionPrompt.stopAnimation({reset: true})
- }
-
- this._currentView = view
- this._animating = true
-
- this._viewer?.scene.mainCamera.setInteractions(false, CameraViewPlugin.PluginType) // todo: also for seekOnScroll
-
- 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._viewer?.scene.mainCamera.setInteractions(true, CameraViewPlugin.PluginType)
- this._animating = false
-
- this._viewer?.setDirty()
-
- this.dispatchEvent({type: 'viewChange', view})
-
- await timeout(10)
- }
-
- @uiButton('Animate All Views')
- async animateAllViews() {
- if (this.isDisabled()) 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._cameraViews.forEach(v=>this.addView(v))
- 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, 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 cameraZ = getFittingDistance(this._viewer.scene.mainCamera, bbox)
- const center = bbox.getCenter(new Vector3()) // world position
- 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.isDisabled()) 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.isDisabled()) 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.isDisabled()) 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.isDisabled() || !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.isDisabled()) 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
- }
|