| const popmotion = viewer.addPluginSync(new PopmotionPlugin()) | const popmotion = viewer.addPluginSync(new PopmotionPlugin()) | ||||
| // Move the object cube 1 unit up. | // Move the object cube 1 unit up. | ||||
| const anim = popmotion.animate({ | |||||
| const anim = popmotion.animateTarget(cube, 'position', { | |||||
| to: cube.position.clone().add(new Vector3(0,1,0)), | |||||
| duration: 500, // ms | |||||
| onComplete: () => isMovedUp = true, | |||||
| onStop: () => throw(new Error('Animation stopped')), | |||||
| }) | |||||
| // Alternatively, set the property directly in onUpdate. | |||||
| const anim1 = popmotion.animate({ | |||||
| from: cube.position.y, | from: cube.position.y, | ||||
| to: cube.position.y + 1, | to: cube.position.y + 1, | ||||
| duration: 500, // ms | duration: 500, // ms | ||||
| cube.position.setY(v) | cube.position.setY(v) | ||||
| cube.setDirty() | cube.setDirty() | ||||
| }, | }, | ||||
| onComplete: () => isMovedUp = !isMovedUp, | |||||
| onComplete: () => isMovedUp = true, | |||||
| onStop: () => throw(new Error('Animation stopped')), | onStop: () => throw(new Error('Animation stopped')), | ||||
| }) | }) | ||||
| import {_testFinish, BoxGeometry, Color, Mesh, PhysicalMaterial, PopmotionPlugin, ThreeViewer} from 'threepipe' | |||||
| import {_testFinish, BoxGeometry, Color, Mesh, PhysicalMaterial, PopmotionPlugin, ThreeViewer, Vector3} from 'threepipe' | |||||
| import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | ||||
| async function init() { | async function init() { | ||||
| createSimpleButtons({ | createSimpleButtons({ | ||||
| ['Move Up/Down']: async(btn) => { | ['Move Up/Down']: async(btn) => { | ||||
| btn.disabled = true | btn.disabled = true | ||||
| await popmotion.animateAsync({ | |||||
| from: cube.position.y, | |||||
| to: cube.position.y + (isMovedUp ? -1 : 1), | |||||
| await popmotion.animateTargetAsync(cube, 'position', { | |||||
| to: cube.position.clone().add(new Vector3(0, isMovedUp ? -1 : 1, 0)), | |||||
| duration: 500, // ms | duration: 500, // ms | ||||
| onUpdate: (v) => { | |||||
| cube.position.setY(v) | |||||
| cube.setDirty() | |||||
| }, | |||||
| onComplete: () => isMovedUp = !isMovedUp, | onComplete: () => isMovedUp = !isMovedUp, | ||||
| }) | |||||
| }) // setDirty is automatically called on the cube since it's the target | |||||
| btn.disabled = false | btn.disabled = false | ||||
| }, | }, | ||||
| ['Rotate +90deg']: async(btn) => { | ['Rotate +90deg']: async(btn) => { |
| { | { | ||||
| "name": "@threepipe/plugins-extra-importers", | "name": "@threepipe/plugins-extra-importers", | ||||
| "version": "0.1.0", | |||||
| "version": "0.1.1", | |||||
| "lockfileVersion": 2, | "lockfileVersion": 2, | ||||
| "requires": true, | "requires": true, | ||||
| "packages": { | "packages": { | ||||
| "": { | "": { | ||||
| "name": "@threepipe/plugins-extra-importers", | "name": "@threepipe/plugins-extra-importers", | ||||
| "version": "0.1.0", | |||||
| "version": "0.1.1", | |||||
| "license": "Apache-2.0", | "license": "Apache-2.0", | ||||
| "dependencies": { | "dependencies": { | ||||
| "threepipe": "file:./../../src/" | "threepipe": "file:./../../src/" |
| } | } | ||||
| public findMaterialsByName(name: string|RegExp, regex = false): IMaterial[] { | public findMaterialsByName(name: string|RegExp, regex = false): IMaterial[] { | ||||
| return this._materials.filter(v=>typeof name !== 'string' || regex ? v.name.match(name) !== null : v.name === name) | |||||
| return this._materials.filter(v=> | |||||
| typeof name !== 'string' || regex ? | |||||
| v.name.match(typeof name === 'string' ? '^' + name + '$' : name) !== null : | |||||
| v.name === name | |||||
| ) | |||||
| } | } | ||||
| public getMaterialsOfType<TM extends IMaterial = IMaterial>(typeSlug: string | undefined): TM[] { | public getMaterialsOfType<TM extends IMaterial = IMaterial>(typeSlug: string | undefined): TM[] { | ||||
| applyMaterial(material: IMaterial, nameOrUuid: string): boolean { | applyMaterial(material: IMaterial, nameOrUuid: string): boolean { | ||||
| const mType = Object.getPrototypeOf(material).constructor.TYPE | const mType = Object.getPrototypeOf(material).constructor.TYPE | ||||
| let currentMats = this.findMaterialsByName(nameOrUuid) | |||||
| let currentMats = this.findMaterialsByName(nameOrUuid, true) | |||||
| if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameOrUuid) as any] | if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameOrUuid) as any] | ||||
| let applied = false | let applied = false | ||||
| for (const c of currentMats) { | for (const c of currentMats) { |
| return new Box3B().expandByObject(this, precise, ignoreInvisible) | return new Box3B().expandByObject(this, precise, ignoreInvisible) | ||||
| } | } | ||||
| private _v1 = new Vector3() | |||||
| private _v2 = new Vector3() | |||||
| /** | /** | ||||
| * Refreshes the scene active camera near far values, based on the scene bounding box. | * Refreshes the scene active camera near far values, based on the scene bounding box. | ||||
| * This is called automatically every time the camera is updated. | * This is called automatically every time the camera is updated. | ||||
| camera.far = camera.userData.maxFarPlane ?? 1000 | camera.far = camera.userData.maxFarPlane ?? 1000 | ||||
| return | return | ||||
| } | } | ||||
| // todo check if this takes too much time with large scenes(when moving the camera and not animating), but we also need to support animations | // todo check if this takes too much time with large scenes(when moving the camera and not animating), but we also need to support animations | ||||
| const bbox = this.getBounds(false) // todo: can we use this._sceneBounds or will it have some issue with animation? | const bbox = this.getBounds(false) // todo: can we use this._sceneBounds or will it have some issue with animation? | ||||
| const pos = camera.getWorldPosition(new Vector3()).sub(bbox.getCenter(new Vector3())) | |||||
| const radius = 1.5 * bbox.getSize(new Vector3()).length() / 2. | |||||
| const dist = pos.length() | |||||
| camera.getWorldPosition(this._v1).sub(bbox.getCenter(this._v2)) | |||||
| const radius = 1.5 * bbox.getSize(this._v2).length() / 2. | |||||
| const dist = this._v1.length() | |||||
| // new way | // new way | ||||
| // todo there is still some clipping when you are inside the model like a room. | |||||
| const dist1 = -pos.clone().normalize().dot(camera.getWorldDirection(new Vector3())) | |||||
| const dist1 = Math.max(0.1, -this._v1.normalize().dot(camera.getWorldDirection(new Vector3()))) | |||||
| const near = Math.max(camera.userData.minNearPlane ?? 0.2, dist1 * (dist - radius)) | const near = Math.max(camera.userData.minNearPlane ?? 0.2, dist1 * (dist - radius)) | ||||
| const far = Math.min(Math.max(near + 1, dist1 * (dist + radius)), camera.userData.maxFarPlane ?? 1000) | |||||
| const far = Math.min(Math.max(near + radius, dist1 * (dist + radius)), camera.userData.maxFarPlane ?? 1000) | |||||
| // old way, has issues when panning very far from the camera target | // old way, has issues when panning very far from the camera target | ||||
| // const near = Math.max(camera.userData.minNearPlane ?? 0.2, dist - radius) | // const near = Math.max(camera.userData.minNearPlane ?? 0.2, dist - radius) |
| 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' | |||||
| export interface AnimationResult{ | export interface AnimationResult{ | ||||
| id: string | id: string | ||||
| stop: () => void | stop: () => void | ||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | // eslint-disable-next-line @typescript-eslint/naming-convention | ||||
| _stop?: () => void | _stop?: () => void | ||||
| targetRef?: {target: any, key: string} | |||||
| } | } | ||||
| /** | /** | ||||
| * | * | ||||
| * Provides animation capabilities to the viewer using the popmotion library: https://popmotion.io/ | * Provides animation capabilities to the viewer using the popmotion library: https://popmotion.io/ | ||||
| * | * | ||||
| * Overrides the driver in popmotion to sync with the viewer and provide ways to store and stop animations. | |||||
| * Overrides the driver in popmotion to sync with the viewer and provide ways to keep track and stop animations. | |||||
| * | * | ||||
| * @category Plugin | * @category Plugin | ||||
| */ | */ | ||||
| readonly animations: Record<string, AnimationResult> = {} | readonly animations: Record<string, AnimationResult> = {} | ||||
| animate<V>(options: AnimationOptions<V>): AnimationResult { | |||||
| animateTarget<T>(target: T, key: keyof T, options: AnimationOptions<T[keyof T]>): AnimationResult { | |||||
| return this.animate({...options, target, key: key as string}) | |||||
| } | |||||
| animate<V>(options1: AnimationOptions<V> & {target?: any, key?: string}): AnimationResult { | |||||
| let targetRef = undefined | |||||
| const options = {...options1} as ((typeof options1) & {lastOnUpdate?: (a:V)=>void}) | |||||
| if (options.target !== undefined) { | |||||
| if (options.key === undefined) throw new Error('key must be defined') | |||||
| if (!(options.key in options.target)) { | |||||
| console.warn('key not present in target, creating', options.key, options.target) | |||||
| options.target[options.key] = options.from || 0 | |||||
| } | |||||
| const setter = makeSetterFor(options.target, options.key) | |||||
| const fromVal = options.target[options.key] | |||||
| options.lastOnUpdate = options.onUpdate | |||||
| options.onUpdate = (val: V)=>{ | |||||
| setter(val) | |||||
| options.lastOnUpdate && options.lastOnUpdate(val) | |||||
| } | |||||
| targetRef = {target: options.target, key: options.key} | |||||
| if (options.from === undefined) options.from = fromVal | |||||
| delete options.target | |||||
| delete options.key | |||||
| } | |||||
| const uuid = generateUUID() | const uuid = generateUUID() | ||||
| const a: any = { | |||||
| const a: AnimationResult = { | |||||
| id: uuid, | id: uuid, | ||||
| options, | options, | ||||
| stop: ()=>{ | stop: ()=>{ | ||||
| if (!this.animations[uuid]?._stop) console.warn('Animation not started') | if (!this.animations[uuid]?._stop) console.warn('Animation not started') | ||||
| else this.animations[uuid]?._stop?.() | else this.animations[uuid]?._stop?.() | ||||
| }, | }, | ||||
| promise: undefined as any, | |||||
| targetRef, | |||||
| } | } | ||||
| this.animations[uuid] = a | this.animations[uuid] = a | ||||
| a.promise = new Promise<void>((resolve, reject) => { | a.promise = new Promise<void>((resolve, reject) => { | ||||
| resolve() | resolve() | ||||
| }, | }, | ||||
| } | } | ||||
| // todo: support boolean using timeout. | |||||
| const anim = animate(opts) | const anim = animate(opts) | ||||
| this.animations[uuid]._stop = anim.stop | this.animations[uuid]._stop = anim.stop | ||||
| this.animations[uuid].options = opts | this.animations[uuid].options = opts | ||||
| return this.animations[uuid] | return this.animations[uuid] | ||||
| } | } | ||||
| async animateAsync<V>(options: AnimationOptions<V>): Promise<string> { | |||||
| async animateAsync<V>(options: AnimationOptions<V>& {target?: any, key?: string}): Promise<string> { | |||||
| return this.animate(options).promise | return this.animate(options).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 | |||||
| } | |||||
| // todo : animateObject/animateTarget | // todo : animateObject/animateTarget | ||||
| } | } |
| import { | |||||
| animate, | |||||
| AnimationOptions, | |||||
| anticipate, | |||||
| backIn, | |||||
| backInOut, | |||||
| backOut, | |||||
| bounceIn, | |||||
| bounceInOut, | |||||
| bounceOut, | |||||
| circIn, | |||||
| circInOut, | |||||
| circOut, | |||||
| easeIn, | |||||
| easeInOut, | |||||
| easeOut, | |||||
| Easing, | |||||
| KeyframeOptions, | |||||
| linear, | |||||
| } from 'popmotion' | |||||
| import {timeout} from 'ts-browser-helpers' | |||||
| export {animate} | |||||
| export type {AnimationOptions, KeyframeOptions, Easing} | |||||
| function easeInOutSine(x: number): number { | |||||
| return -(Math.cos(Math.PI * x) - 1) / 2 | |||||
| } | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| export const EasingFunctions = { | |||||
| linear: linear, | |||||
| easeIn: easeIn, | |||||
| easeOut: easeOut, | |||||
| easeInOut: easeInOut, | |||||
| circIn: circIn, | |||||
| circOut: circOut, | |||||
| circInOut: circInOut, | |||||
| backIn: backIn, | |||||
| backOut: backOut, | |||||
| backInOut: backInOut, | |||||
| anticipate: anticipate, | |||||
| bounceOut: bounceOut, | |||||
| bounceIn: bounceIn, | |||||
| bounceInOut: bounceInOut, | |||||
| easeInOutSine: easeInOutSine, | |||||
| } | |||||
| /** | |||||
| * EasingFunctionType: | |||||
| * anticipate, backIn, backInOut, backOut, bounceIn, bounceInOut, bounceOut, circIn, circInOut, circOut, easeIn, easeInOut, easeOut, easeInOutSine | |||||
| */ | |||||
| export type EasingFunctionType = keyof typeof EasingFunctions | |||||
| export type AnimateResult = ReturnType<typeof animate> | |||||
| export function makeSetterFor<V>(target: any, key: string, setDirty?: ()=>void) { | |||||
| const v = target[key] as any | |||||
| const dirty = ()=>{ | |||||
| if (typeof target?.setDirty === 'function') target.setDirty() | |||||
| setDirty?.() | |||||
| } | |||||
| if (v && typeof v.copy === 'function') | |||||
| return (a: any) => { | |||||
| v.copy(a) | |||||
| dirty() | |||||
| } | |||||
| else | |||||
| return (a: V)=>{ | |||||
| target[key] = a | |||||
| dirty() | |||||
| } | |||||
| } | |||||
| export async function animateTarget<V>(target: any, key: string, options: AnimationOptions<V>, animations?: AnimateResult[]) { | |||||
| if (!(key in target)) { | |||||
| console.error('invalid key', key, target) | |||||
| } | |||||
| const setter = makeSetterFor(target, key) | |||||
| const fromVal = target[key] | |||||
| const onUpdate = (val: V)=>{ | |||||
| setter(val) | |||||
| options.onUpdate && options.onUpdate(val) | |||||
| } | |||||
| if (typeof fromVal === 'boolean') { | |||||
| const {duration} = options as KeyframeOptions // todo: divide by 2? or support keyframes. | |||||
| return timeout(duration ?? 0).then(()=>onUpdate(options.to as V)) | |||||
| } else { | |||||
| if (typeof options.to === 'function') { | |||||
| options = {...options, to: options.to(fromVal, target)} // need to duplicate options | |||||
| } | |||||
| return animateAsync({ | |||||
| ...options, | |||||
| from: fromVal, | |||||
| onUpdate, | |||||
| } as AnimationOptions<V>, animations) | |||||
| } | |||||
| } | |||||
| export async function animateAsync<V=number>(options: AnimationOptions<V>, animations?: AnimateResult[]) { | |||||
| const complete = options.onComplete | |||||
| const stop = options.onStop | |||||
| options = {...options} | |||||
| return new Promise<void>((resolve, reject) => { | |||||
| options.onComplete = ()=>{ | |||||
| try { | |||||
| complete?.() | |||||
| } catch (e: any) { | |||||
| reject(e) | |||||
| return | |||||
| } | |||||
| resolve() | |||||
| } | |||||
| options.onStop = ()=>{ | |||||
| try { | |||||
| stop?.() | |||||
| } catch (e: any) { | |||||
| reject(e) | |||||
| return | |||||
| } | |||||
| resolve() | |||||
| } | |||||
| const an = animate(options) | |||||
| if (animations) animations.push(an) | |||||
| }) | |||||
| } | |||||
| 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 type {Easing, KeyframeOptions, AnimationOptions, EasingFunctionType, AnimateResult} from './animation' | |||||
| readonly assetManager: AssetManager | readonly assetManager: AssetManager | ||||
| @uiConfig() @serialize('renderManager') | @uiConfig() @serialize('renderManager') | ||||
| readonly renderManager: ViewerRenderManager | readonly renderManager: ViewerRenderManager | ||||
| get materialManager() { | |||||
| return this.assetManager.materials | |||||
| } | |||||
| public readonly plugins: Record<string, IViewerPlugin> = {} | public readonly plugins: Record<string, IViewerPlugin> = {} | ||||
| /** | /** | ||||
| * Scene with object hierarchy used for rendering | * Scene with object hierarchy used for rendering | ||||
| * @param setBackground - Set the background image of the scene from the same map. | * @param setBackground - Set the background image of the scene from the same map. | ||||
| * @param options - Options for importing the asset. See {@link ImportAssetOptions} | * @param options - Options for importing the asset. See {@link ImportAssetOptions} | ||||
| */ | */ | ||||
| async setEnvironmentMap(map: string | IAsset | null | ITexture, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||||
| async setEnvironmentMap(map: string | IAsset | null | ITexture | undefined, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||||
| this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | ||||
| if (setBackground) return this.setBackgroundMap(this._scene.environment) | if (setBackground) return this.setBackgroundMap(this._scene.environment) | ||||
| return this._scene.environment | return this._scene.environment | ||||
| * @param setEnvironment - Set the environment map of the scene from the same map. | * @param setEnvironment - Set the environment map of the scene from the same map. | ||||
| * @param options - Options for importing the asset. See {@link ImportAssetOptions} | * @param options - Options for importing the asset. See {@link ImportAssetOptions} | ||||
| */ | */ | ||||
| async setBackgroundMap(map: string | IAsset | null | ITexture, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||||
| async setBackgroundMap(map: string | IAsset | null | ITexture | undefined, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||||
| this._scene.background = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | this._scene.background = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | ||||
| if (setEnvironment) return this.setEnvironmentMap(this._scene.background) | if (setEnvironment) return this.setEnvironmentMap(this._scene.background) | ||||
| return this._scene.background | return this._scene.background |