Bläddra i källkod

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 år sedan
förälder
incheckning
a80951b082
Inget konto är kopplat till bidragsgivarens mejladress

+ 10
- 2
README.md Visa fil

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')),
}) })



+ 4
- 9
examples/popmotion-plugin/script.ts Visa fil

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) => {

+ 2
- 2
plugins/extra-importers/package-lock.json Visa fil

{ {
"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/"

+ 6
- 2
src/assetmanager/MaterialManager.ts Visa fil

} }


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) {

+ 9
- 6
src/core/object/RootScene.ts Visa fil

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)

+ 39
- 4
src/plugins/animation/PopmotionPlugin.ts Visa fil

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
} }

+ 126
- 0
src/utils/animation.ts Visa fil

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 Visa fil

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'



+ 5
- 2
src/viewer/ThreeViewer.ts Visa fil

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

Laddar…
Avbryt
Spara