Переглянути джерело

Fix regex match for string in findMaterialsByName, fix clipping issue in autoNearFar when inside the model, add target property animation in Popmotion, add EasingFunctions and animation utils, Add materialManager property in ThreeViewer, minor changes.

master
Palash Bansal 2 роки тому
джерело
коміт
a80951b082
Аккаунт користувача з таким Email не знайдено

+ 10
- 2
README.md Переглянути файл

@@ -2192,7 +2192,15 @@ const cube = viewer.scene.getObjectByName('cube');
const popmotion = viewer.addPluginSync(new PopmotionPlugin())

// 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,
to: cube.position.y + 1,
duration: 500, // ms
@@ -2200,7 +2208,7 @@ const anim = popmotion.animate({
cube.position.setY(v)
cube.setDirty()
},
onComplete: () => isMovedUp = !isMovedUp,
onComplete: () => isMovedUp = true,
onStop: () => throw(new Error('Animation stopped')),
})


+ 4
- 9
examples/popmotion-plugin/script.ts Переглянути файл

@@ -1,4 +1,4 @@
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'

async function init() {
@@ -20,16 +20,11 @@ async function init() {
createSimpleButtons({
['Move Up/Down']: async(btn) => {
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
onUpdate: (v) => {
cube.position.setY(v)
cube.setDirty()
},
onComplete: () => isMovedUp = !isMovedUp,
})
}) // setDirty is automatically called on the cube since it's the target
btn.disabled = false
},
['Rotate +90deg']: async(btn) => {

+ 2
- 2
plugins/extra-importers/package-lock.json Переглянути файл

@@ -1,12 +1,12 @@
{
"name": "@threepipe/plugins-extra-importers",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@threepipe/plugins-extra-importers",
"version": "0.1.0",
"version": "0.1.1",
"license": "Apache-2.0",
"dependencies": {
"threepipe": "file:./../../src/"

+ 6
- 2
src/assetmanager/MaterialManager.ts Переглянути файл

@@ -214,7 +214,11 @@ export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> {
}

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[] {
@@ -294,7 +298,7 @@ export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> {

applyMaterial(material: IMaterial, nameOrUuid: string): boolean {
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]
let applied = false
for (const c of currentMats) {

+ 9
- 6
src/core/object/RootScene.ts Переглянути файл

@@ -369,6 +369,9 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
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.
* This is called automatically every time the camera is updated.
@@ -381,17 +384,17 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
camera.far = camera.userData.maxFarPlane ?? 1000
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
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
// 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 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
// const near = Math.max(camera.userData.minNearPlane ?? 0.2, dist - radius)

+ 39
- 4
src/plugins/animation/PopmotionPlugin.ts Переглянути файл

@@ -5,6 +5,7 @@ import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin'
import type {ProgressivePlugin} from '../pipeline/ProgressivePlugin'
import {generateUUID} from '../../three'
import {makeSetterFor} from '../../utils'

export interface AnimationResult{
id: string
@@ -13,6 +14,8 @@ export interface AnimationResult{
stop: () => void
// eslint-disable-next-line @typescript-eslint/naming-convention
_stop?: () => void

targetRef?: {target: any, key: string}
}

/**
@@ -20,7 +23,7 @@ export interface AnimationResult{
*
* 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
*/
@@ -120,15 +123,42 @@ export class PopmotionPlugin extends AViewerPluginSync<''> {

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 a: any = {
const a: AnimationResult = {
id: uuid,
options,
stop: ()=>{
if (!this.animations[uuid]?._stop) console.warn('Animation not started')
else this.animations[uuid]?._stop?.()
},
promise: undefined as any,
targetRef,
}
this.animations[uuid] = a
a.promise = new Promise<void>((resolve, reject) => {
@@ -154,6 +184,7 @@ export class PopmotionPlugin extends AViewerPluginSync<''> {
resolve()
},
}
// todo: support boolean using timeout.
const anim = animate(opts)
this.animations[uuid]._stop = anim.stop
this.animations[uuid].options = opts
@@ -165,9 +196,13 @@ export class PopmotionPlugin extends AViewerPluginSync<''> {
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
}

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
}

+ 126
- 0
src/utils/animation.ts Переглянути файл

@@ -0,0 +1,126 @@
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)
})
}


+ 2
- 0
src/utils/index.ts Переглянути файл

@@ -6,4 +6,6 @@ export {Dropzone, type DropFile, type ListenerCallback, type DropEventType} from
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 {makeGLBFile} from './gltf'
export {animateAsync, animateTarget, EasingFunctions, makeSetterFor, animate} from './animation'
export type {Easing, KeyframeOptions, AnimationOptions, EasingFunctionType, AnimateResult} from './animation'


+ 5
- 2
src/viewer/ThreeViewer.ts Переглянути файл

@@ -189,6 +189,9 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
readonly assetManager: AssetManager
@uiConfig() @serialize('renderManager')
readonly renderManager: ViewerRenderManager
get materialManager() {
return this.assetManager.materials
}
public readonly plugins: Record<string, IViewerPlugin> = {}
/**
* Scene with object hierarchy used for rendering
@@ -412,7 +415,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
* @param setBackground - Set the background image of the scene from the same map.
* @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
if (setBackground) return this.setBackgroundMap(this._scene.environment)
return this._scene.environment
@@ -424,7 +427,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
* @param setEnvironment - Set the environment map of the scene from the same map.
* @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
if (setEnvironment) return this.setEnvironmentMap(this._scene.background)
return this._scene.background

Завантаження…
Відмінити
Зберегти