Ver código fonte

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.

master
Palash Bansal 1 ano atrás
pai
commit
ab9121168a
Nenhuma conta vinculada ao e-mail do autor do commit

+ 3
- 1
src/assetmanager/AssetManager.ts Ver arquivo

if (!this.importer || !this.viewer) return [] if (!this.importer || !this.viewer) return []
const imported = await this.importer.import<T>(assetOrPath, options) const imported = await this.importer.import<T>(assetOrPath, options)
if (!imported) { if (!imported) {
console.warn('Unable to import', assetOrPath, imported)
const path = typeof assetOrPath === 'string' ? assetOrPath : (assetOrPath as IAsset)?.path
if (path && !path.split('?')[0].endsWith('.vjson'))
console.warn('Threepipe AssetManager - Unable to import', assetOrPath, imported)
return [] return []
} }
return this.loadImported<(T | undefined)[]>(imported, options) return this.loadImported<(T | undefined)[]>(imported, options)

+ 26
- 13
src/assetmanager/MaterialManager.ts Ver arquivo

} }


applyMaterial(material: IMaterial, nameRegexOrUuid: string, regex = true): boolean { applyMaterial(material: IMaterial, nameRegexOrUuid: string, regex = true): boolean {
const mType = Object.getPrototypeOf(material).constructor.TYPE
let currentMats = this.findMaterialsByName(nameRegexOrUuid, regex) let currentMats = this.findMaterialsByName(nameRegexOrUuid, regex)
if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameRegexOrUuid) as any] if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameRegexOrUuid) as any]
let applied = false let applied = false
if (!c) continue if (!c) continue
if (c === material) continue if (c === material) continue
if (c.userData.__isVariation) continue if (c.userData.__isVariation) continue
const cType = Object.getPrototypeOf(c).constructor.TYPE
// console.log(cType, mType)
if (cType === mType) {
const n = c.name
c.setValues(material)
c.name = n
applied = true
} else {
// todo
// if ((c as any)['__' + mType]) continue
const newMat = (c as any)['__' + mType] || this.create(mType)
if (!newMat) continue
const applied2 = this.copyMaterialProps(c, material)
if (applied2) applied = true
}
return applied
}

/**
* copyProps from material to c
* @param c
* @param material
*/
copyMaterialProps(c: IMaterial, material: IMaterial) {
let applied = false
const mType = Object.getPrototypeOf(material).constructor.TYPE
const cType = Object.getPrototypeOf(c).constructor.TYPE
// console.log(cType, mType)
if (cType === mType) {
const n = c.name
c.setValues(material)
c.name = n
applied = true
} else {
// todo
// if ((c as any)['__' + mType]) continue
const newMat = (c as any)['__' + mType] || this.create(mType)
if (newMat) {
const n = c.name const n = c.name
newMat.setValues(material) newMat.setValues(material)
newMat.name = n newMat.name = n

+ 7
- 4
src/core/camera/PerspectiveCamera2.ts Ver arquivo

this.getWorldPosition(this._positionWorld) this.getWorldPosition(this._positionWorld)


iCameraCommons.setDirty.call(this, options) iCameraCommons.setDirty.call(this, options)

this._camUi.forEach(u=>u?.uiRefresh?.(false, 'postFrame', 1)) // because camera changes a lot. so we dont want to deep refresh ui on every change
if (options?.last !== false)
this._camUi.forEach(u=>u?.uiRefresh?.(false, 'postFrame', 1)) // because camera changes a lot. so we dont want to deep refresh ui on every change
} }


/** /**
if (this.autoAspect) { if (this.autoAspect) {
if (!this._canvas) console.error('cannot calculate aspect ratio without canvas/container') if (!this._canvas) console.error('cannot calculate aspect ratio without canvas/container')
else { else {
this.aspect = this._canvas.clientWidth / this._canvas.clientHeight
let aspect = this._canvas.clientWidth / this._canvas.clientHeight
if (!isFinite(aspect)) aspect = 1
this.aspect = aspect
this.updateProjectionMatrix?.() this.updateProjectionMatrix?.()
} }
} }
private _initCameraControls() { private _initCameraControls() {
const mode = this.controlsMode const mode = this.controlsMode
this._controls = this._controlsCtors.get(mode)?.(this, this._canvas) ?? undefined this._controls = this._controlsCtors.get(mode)?.(this, this._canvas) ?? undefined
if (!this._controls && mode !== '') console.error('Unable to create controls with mode ' + mode + '. Are you missing a plugin?')
if (!this._controls && mode !== '') console.error('PerspectiveCamera2 - Unable to create controls with mode ' + mode + '. Are you missing a plugin?')
this._controls?.addEventListener('change', this._controlsChanged) this._controls?.addEventListener('change', this._controlsChanged)
this._currentControlsMode = this._controls ? mode : '' this._currentControlsMode = this._controls ? mode : ''
// todo maybe set target like this: // todo maybe set target like this:

+ 18
- 15
src/core/geometry/iGeometryCommons.ts Ver arquivo

import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry' import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry'
import {autoGPUInstanceMeshes, isInScene, toIndexedGeometry} from '../../three/utils' import {autoGPUInstanceMeshes, isInScene, toIndexedGeometry} from '../../three/utils'
import {BufferGeometry, Vector3} from 'three' import {BufferGeometry, Vector3} from 'three'
import {ThreeViewer} from '../../viewer'


export const iGeometryCommons = { export const iGeometryCommons = {
setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void { setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void {
{ {
type: 'button', type: 'button',
label: 'Center Geometry', label: 'Center Geometry',
value: () => {
value: async() => {
if (!await ThreeViewer.Dialog.confirm('This will move the objects based on the geometry center, do you want to continue?\nThis action cannot be undone.')) return
this.center() this.center()
}, },
}, },
{ {
type: 'button', type: 'button',
label: 'Center Geometry (keep position)', label: 'Center Geometry (keep position)',
value: () => {
value: async() => {
if (!await ThreeViewer.Dialog.confirm('This will move the geometry center keeping the object position, do you want to continue?\nThis action cannot be undone.')) return
this.center(undefined, true) this.center(undefined, true)
}, },
}, },
{ {
type: 'button', type: 'button',
label: 'Compute vertex normals', label: 'Compute vertex normals',
value: () => {
if (this.hasAttribute('normal') && !confirm('Normals already exist, replace with computed normals?')) return
value: async() => {
if (this.hasAttribute('normal') && !await ThreeViewer.Dialog.confirm('Normals already exist, replace with computed normals?\nThis action cannot be undone.')) return
this.computeVertexNormals() this.computeVertexNormals()
this.setDirty() this.setDirty()
}, },
{ {
type: 'button', type: 'button',
label: 'Compute vertex tangents', label: 'Compute vertex tangents',
value: () => {
if (this.hasAttribute('tangent') && !confirm('Tangents already exist, replace with computed tangents?')) return
value: async() => {
if (this.hasAttribute('tangent') && !await ThreeViewer.Dialog.confirm('Tangents already exist, replace with computed tangents?\nThis action cannot be undone.')) return
this.computeTangents() this.computeTangents()
this.setDirty() this.setDirty()
}, },
type: 'button', type: 'button',
label: 'Convert to indexed', label: 'Convert to indexed',
hidden: () => !!this.index, hidden: () => !!this.index,
value: () => {
value: async() => {
if (this.attributes.index) return if (this.attributes.index) return
const tolerance = parseFloat(prompt('Tolerance', '-1') ?? '-1')
const tolerance = parseFloat(await ThreeViewer.Dialog.prompt('Convert to Indexed: Tolerance?', '-1') ?? '-1')
toIndexedGeometry(this, tolerance) toIndexedGeometry(this, tolerance)
this.setDirty() this.setDirty()
}, },
{ {
type: 'button', type: 'button',
label: 'Create uv1 from uv', label: 'Create uv1 from uv',
value: () => {
value: async() => {
if (this.hasAttribute('uv1')) { if (this.hasAttribute('uv1')) {
if (!confirm('uv1 already exists, replace with uv data?')) return
if (!await ThreeViewer.Dialog.confirm('uv1 already exists, replace with uv data?\nThis action cannot be undone.')) return
} }
this.setAttribute('uv1', this.getAttribute('uv')) this.setAttribute('uv1', this.getAttribute('uv'))
this.setDirty() this.setDirty()
type: 'button', type: 'button',
label: 'Remove vertex color attribute', label: 'Remove vertex color attribute',
hidden: () => !this.hasAttribute('color'), hidden: () => !this.hasAttribute('color'),
value: () => {
value: async() => {
if (!this.hasAttribute('color')) { if (!this.hasAttribute('color')) {
prompt('No color attribute found')
await ThreeViewer.Dialog.prompt('No color attribute found')
return return
} }
if (!confirm('Remove color attribute?')) return
if (!await ThreeViewer.Dialog.confirm('Remove color attribute?')) return
this.deleteAttribute('color') this.deleteAttribute('color')
this.setDirty() this.setDirty()
}, },
type: 'button', type: 'button',
label: 'Auto GPU Instances', label: 'Auto GPU Instances',
hidden: ()=> !this.appliedMeshes || this.appliedMeshes.size < 2, hidden: ()=> !this.appliedMeshes || this.appliedMeshes.size < 2,
value: ()=>{
if (!confirm('This action is irreversible, do you want to continue?')) return
value: async()=>{
if (!await ThreeViewer.Dialog.confirm('This will automatically create Instanced Mesh from geometry instances. This action is irreversible, do you want to continue?')) return
autoGPUInstanceMeshes(this) autoGPUInstanceMeshes(this)
}, },
}, },

+ 11
- 8
src/core/material/IMaterialUi.ts Ver arquivo

{ {
type: 'button', type: 'button',
label: `Select ${material.constructor.TypeSlug}`, label: `Select ${material.constructor.TypeSlug}`,
value: ()=>{
uploadFile(false, false, material.constructor.TypeSlug).then(async(files)=>files?.[0]?.text()).then((text)=>{
if (!text) return
const json = JSON.parse(text)
if (json.uuid) delete json.uuid // just copy the material properties
material.fromJSON(json, getEmptyMeta())
})
},
value: async()=>uploadFile(false, false, material.constructor.TypeSlug).then(async(files)=>files?.[0]?.text()).then((text)=>{
if (!text) return
const json = JSON.parse(text)
if (json.uuid) delete json.uuid // just copy the material properties
const currentJson = material.toJSON()
material.fromJSON(json, getEmptyMeta())
return {
undo: ()=>material.fromJSON(currentJson, getEmptyMeta()),
redo: ()=>material.fromJSON(json, getEmptyMeta()),
}
}),
}, },
], ],
roughMetal: (material: PhysicalMaterial): UiObjectConfig => ( roughMetal: (material: PhysicalMaterial): UiObjectConfig => (

+ 1
- 1
src/core/material/iMaterialCommons.ts Ver arquivo

setDirty: function(this: IMaterial, options?: IMaterialSetDirtyOptions): void { setDirty: function(this: IMaterial, options?: IMaterialSetDirtyOptions): void {
if (options?.needsUpdate !== false) this.needsUpdate = true if (options?.needsUpdate !== false) this.needsUpdate = true
this.dispatchEvent({bubbleToObject: true, bubbleToParent: true, ...options, type: 'materialUpdate'}) // this sets sceneUpdate in root scene this.dispatchEvent({bubbleToObject: true, bubbleToParent: true, ...options, type: 'materialUpdate'}) // this sets sceneUpdate in root scene
this.uiConfig?.uiRefresh?.(true, 'postFrame', 1)
if (options?.last !== false) this.uiConfig?.uiRefresh?.(true, 'postFrame', 1)
}, },
setValues: (superSetValues: Material['setValues']): IMaterial['setValues'] => setValues: (superSetValues: Material['setValues']): IMaterial['setValues'] =>
function(this: IMaterial, parameters: Material | (MaterialParameters & {type?: string})): IMaterial { function(this: IMaterial, parameters: Material | (MaterialParameters & {type?: string})): IMaterial {

+ 25
- 10
src/core/object/IObjectUi.ts Ver arquivo

import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js'
import {ICamera} from '../ICamera' import {ICamera} from '../ICamera'
import {Vector3} from 'three' import {Vector3} from 'three'
import {ThreeViewer} from '../../viewer'


export function makeICameraCommonUiConfig(this: ICamera, config: UiObjectConfig): UiObjectConfig[] { export function makeICameraCommonUiConfig(this: ICamera, config: UiObjectConfig): UiObjectConfig[] {
return [ return [
type: 'button', type: 'button',
label: 'Auto Scale', label: 'Auto Scale',
hidden: ()=>!this.autoScale, hidden: ()=>!this.autoScale,
prompt: ['Auto Scale Radius: Object will be scaled to the given radius', this.userData.autoScaleRadius || '2', true],
// prompt: ['Auto Scale Radius: Object will be scaled to the given radius', this.userData.autoScaleRadius || '2', true],
value: async()=>{ value: async()=>{
const def = (this.userData.autoScaleRadius || 2) + '' const def = (this.userData.autoScaleRadius || 2) + ''
const res = prompt('Auto Scale Radius: Object will be scaled to the given radius', def)
const res = await ThreeViewer.Dialog.prompt('Auto Scale Radius: Object will be scaled to the given radius', def)
if (res === null) return if (res === null) return
const rad = parseFloat(res || def) const rad = parseFloat(res || def)
if (Math.abs(rad) > 0) { if (Math.abs(rad) > 0) {
this.autoScale?.(rad)
return ()=>this.autoScale?.(rad, undefined, undefined, true)
return {
action: ()=>this.autoScale?.(rad),
undo: ()=>this.autoScale?.(rad, undefined, undefined, true),
}
} }
}, },
}, },
type: 'button', type: 'button',
label: 'Auto Center', label: 'Auto Center',
value: ()=>{ value: ()=>{
const res = confirm('Auto Center: Object will be centered, are you sure you want to proceed?')
if (!res) return
this.autoCenter?.(true)
return ()=>this.autoCenter?.(true, true)
// const res = await ThreeViewer.Dialog.confirm('Auto Center: Object will be centered, are you sure you want to proceed?')
// if (!res) return
return {
action: ()=>this.autoCenter?.(true),
undo: ()=>this.autoCenter?.(true, true),
}
}, },
}, },
{ {
type: 'button', type: 'button',
label: 'Rotate ' + l + '90', label: 'Rotate ' + l + '90',
value: ()=>{ value: ()=>{
this.rotateOnAxis(new Vector3(l.includes('X') ? 1 : 0, l.includes('Y') ? 1 : 0, l.includes('Z') ? 1 : 0), Math.PI / 2 * (l.includes('-') ? -1 : 1))
this.setDirty?.({refreshScene: true, refreshUi: false})
const axis = new Vector3(l.includes('X') ? 1 : 0, l.includes('Y') ? 1 : 0, l.includes('Z') ? 1 : 0)
const angle = Math.PI / 2 * (l.includes('-') ? -1 : 1)
return {
action: ()=>{
this.rotateOnAxis(axis, angle)
this.setDirty?.({refreshScene: true, refreshUi: false})
},
undo: ()=>{
this.rotateOnAxis(axis, -angle)
this.setDirty?.({refreshScene: true, refreshUi: false})
},
}
}, },
} }
}), }),

+ 3
- 3
src/core/object/iObjectCommons.ts Ver arquivo

import {IMaterial} from '../IMaterial' import {IMaterial} from '../IMaterial'
import {objectHasOwn} from 'ts-browser-helpers' import {objectHasOwn} from 'ts-browser-helpers'
import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject' import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject'
import {copyObject3DUserData} from '../../utils/serialization'
import {copyObject3DUserData} from '../../utils'
import {IGeometry, IGeometryEvent} from '../IGeometry' import {IGeometry, IGeometryEvent} from '../IGeometry'
import {Box3B} from '../../three/math/Box3B'
import {Box3B} from '../../three'
import {makeIObject3DUiConfig} from './IObjectUi' import {makeIObject3DUiConfig} from './IObjectUi'
import {iGeometryCommons} from '../geometry/iGeometryCommons' import {iGeometryCommons} from '../geometry/iGeometryCommons'
import {iMaterialCommons} from '../material/iMaterialCommons' import {iMaterialCommons} from '../material/iMaterialCommons'
export const iObjectCommons = { export const iObjectCommons = {
setDirty: function(this: IObject3D, options?: IObjectSetDirtyOptions): void { setDirty: function(this: IObject3D, options?: IObjectSetDirtyOptions): void {
this.dispatchEvent({bubbleToParent: true, ...options, type: 'objectUpdate', object: this}) // this sets sceneUpdate in root scene this.dispatchEvent({bubbleToParent: true, ...options, type: 'objectUpdate', object: this}) // this sets sceneUpdate in root scene
if (options?.refreshUi !== false) this.refreshUi?.()
if (options?.refreshUi !== false && options?.last !== false) this.refreshUi?.()
// console.log('object update') // console.log('object update')
}, },



+ 10
- 3
src/plugins/animation/CameraViewPlugin.ts Ver arquivo

import {EasingFunctions, EasingFunctionType} from '../../utils' import {EasingFunctions, EasingFunctionType} from '../../utils'
import {CameraView, ICamera, ICameraView, PerspectiveCamera2} from '../../core' import {CameraView, ICamera, ICameraView, PerspectiveCamera2} from '../../core'
import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin' import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin'
import {InteractionPromptPlugin} from '../interaction/InteractionPromptPlugin'


export interface CameraViewPluginOptions{duration?: number, ease?: EasingFunctionType, interpolateMode?: 'spherical'|'linear'} export interface CameraViewPluginOptions{duration?: number, ease?: EasingFunctionType, interpolateMode?: 'spherical'|'linear'}


return return
} }


const interactionPrompt = this._viewer?.getPlugin(InteractionPromptPlugin)
if (interactionPrompt && interactionPrompt.animationRunning) {
await interactionPrompt.stopAnimation({reset: true})
}

this._currentView = view this._currentView = view
this._animating = true this._animating = true




const cam = this._viewer.scene.mainCamera const cam = this._viewer.scene.mainCamera
let cameraZ = 1 let cameraZ = 1
if (cam.isPerspectiveCamera) {
if (cam.isPerspectiveCamera && size.length() > 0.0001) {
const aspect = isFinite(cam.aspect) ? cam.aspect : 1
// get the max side of the bounding box (fits to width OR height as needed ) // 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 fov = Math.max(1, (cam as PerspectiveCamera2).fov) * (Math.PI / 180)
const fovh = 2 * Math.atan(Math.tan(fov / 2) * aspect)
const dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)) 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)) const dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2))
cameraZ = Math.max(dx, dy) cameraZ = Math.max(dx, dy)

+ 4
- 4
src/plugins/animation/PopmotionPlugin.ts Ver arquivo

driver: this.defaultDriver, driver: this.defaultDriver,
...options, ...options,
onUpdate: !isBool ? options.onUpdate : undefined, onUpdate: !isBool ? options.onUpdate : undefined,
onComplete: ()=>{
onComplete: async()=>{
try { try {
if (isBool) options.onUpdate?.(options.to as any) if (isBool) options.onUpdate?.(options.to as any)
options.onComplete && options.onComplete()
options.onComplete && await options.onComplete()
} catch (e: any) { } catch (e: any) {
if (!end2()) return if (!end2()) return
reject(e) reject(e)
if (!end2()) return if (!end2()) return
resolve() resolve()
}, },
onStop: ()=>{
onStop: async()=>{
try { try {
options.onStop && options.onStop()
options.onStop && await options.onStop()
} catch (e: any) { } catch (e: any) {
if (!end2()) return if (!end2()) return
reject(e) reject(e)

+ 5
- 4
src/plugins/base/BaseGroundPlugin.ts Ver arquivo

return mesh return mesh
} }


setGeometry(g: BufferGeometry) {
if (this._geometry) this._geometry.dispose()
this._geometry = iGeometryCommons.upgradeGeometry.call(g)
setGeometry(g?: BufferGeometry) {
if (!g) g = this._geometry
else if (this._geometry) this._geometry.dispose()
if (!g) return
iGeometryCommons.upgradeGeometry.call(g)
if (!this._geometry.attributes.uv2) { if (!this._geometry.attributes.uv2) {
this._geometry.attributes.uv2 = (this._geometry.attributes.uv as any as BufferAttribute | InterleavedBufferAttribute).clone() this._geometry.attributes.uv2 = (this._geometry.attributes.uv as any as BufferAttribute | InterleavedBufferAttribute).clone()
this._geometry.attributes.uv2.needsUpdate = true this._geometry.attributes.uv2.needsUpdate = true
if (this._mesh) this._mesh.geometry = this._geometry if (this._mesh) this._mesh.geometry = this._geometry
} }



protected _createMaterial(material?: PhysicalMaterial): PhysicalMaterial { protected _createMaterial(material?: PhysicalMaterial): PhysicalMaterial {
if (!material) material = new PhysicalMaterial({ if (!material) material = new PhysicalMaterial({
name: 'BaseGroundMaterial', name: 'BaseGroundMaterial',

+ 14
- 10
src/plugins/configurator/MaterialConfiguratorBasePlugin.ts Ver arquivo

super.onAdded(viewer) super.onAdded(viewer)


// todo subscribe to plugin add event if picking is not added yet. // todo subscribe to plugin add event if picking is not added yet.
this._picking = viewer.getPlugin<PickingPlugin>('Picking')
viewer.forPlugin(PickingPlugin, (p)=>{
this._picking = p
this._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig)
}, ()=>{
this._picking?.removeEventListener('selectedObjectChanged', this._refreshUiConfig)
this._picking = undefined
})
this._previewGenerator = new MaterialPreviewGenerator() this._previewGenerator = new MaterialPreviewGenerator()

this._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig)
viewer.addEventListener('preFrame', this._refreshUi) viewer.addEventListener('preFrame', this._refreshUi)
} }


this.variations.splice(this.variations.indexOf(variation), 1) this.variations.splice(this.variations.indexOf(variation), 1)
this.refreshUi() this.refreshUi()
} }
addVariation(material?: IMaterial) {
const clone = material?.clone?.()
addVariation(material?: IMaterial, variationKey?: string, cloneMaterial = true) {
const clone = cloneMaterial && material?.clone ? material.clone() : material
if (material && clone) { if (material && clone) {
let variation = this.findVariation(material.uuid)
if (!variation && material.name.length > 0) variation = this.findVariation(material.name)
let variation = this.findVariation(variationKey ?? material.uuid)
if (!variation && !variationKey && material.name.length > 0) variation = this.findVariation(material.name)
if (!variation) { if (!variation) {
variation = this.createVariation(material)
variation = this.createVariation(material, variationKey)
} }
variation.materials.push(clone) variation.materials.push(clone)
this.refreshUi() this.refreshUi()
} }
} }


createVariation(material: IMaterial) {
createVariation(material: IMaterial, variationKey?: string) {
this.variations.push({ this.variations.push({
uuid: material.name.length > 0 ? material.name : material.uuid,
uuid: variationKey ?? material.name.length > 0 ? material.name : material.uuid,
title: material.name.length > 0 ? material.name : 'No Name', title: material.name.length > 0 ? material.name : 'No Name',
preview: 'generate:sphere', preview: 'generate:sphere',
materials: [], materials: [],

+ 30
- 1
src/plugins/configurator/SwitchNodeBasePlugin.ts Ver arquivo

* This works by toggling the `visible` property of the children of a parent object. * This works by toggling the `visible` property of the children of a parent object.
* The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations. * The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations.
* It also provides a function to create snapshot previews of individual variations. This creates a limited render of the object with the selected child visible. * It also provides a function to create snapshot previews of individual variations. This creates a limited render of the object with the selected child visible.
* To get a proper render, its better to render it offline and set the image as a preview.
* To get a proper render, it's better to render it offline and set the image as a preview.
* *
* See `SwitchNodePlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer. * See `SwitchNodePlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer.
* *
if (!this.enabled) return false if (!this.enabled) return false
if (!this._viewer) return false if (!this._viewer) return false
this._uiNeedRefresh = false this._uiNeedRefresh = false
if (this.autoSnapIcons) this.snapIcons()


this.refreshUiConfig() this.refreshUiConfig()
return true return true
if (refreshUi) this.refreshUi() if (refreshUi) this.refreshUi()
} }


/**
* If true, the plugin will automatically take snapshots of the icons in _refreshUi and put them in the object.userdata.__icon
* Otherwise, call {@link snapIcons} manually
*/
autoSnapIcons = false

/**
* Snapshots icons and puts in the userdata.__icon
*/
snapIcons() {
for (const variation of this.variations) {
const obj = this._viewer!.scene.getObjectByName(variation.name)
if (!obj) {
console.warn('no object found for variation, skipping', variation)
continue
}
if (obj.children.length < 1) {
console.warn('SwitchNode does not have enough children', variation)
}

for (const child of obj.children) {
if (child.userData.__icon) return
const image = this.getPreview(variation, child, false)
if (image) child.userData.__icon = image
}
}
}

uiConfig: UiObjectConfig = { uiConfig: UiObjectConfig = {
label: 'Switch Node Plugin', label: 'Switch Node Plugin',
type: 'folder', type: 'folder',

+ 7
- 2
src/plugins/interaction/InteractionPromptPlugin.ts Ver arquivo

// } // }
} }


@uiButton() stopAnimation = () => {
@uiButton() stopAnimation = async({reset = true}: {reset?: boolean} = {}) => {
if (!this._viewer || !this.cursorEl) return // dont check for enabled here. if (!this._viewer || !this.cursorEl) return // dont check for enabled here.
this.animationRunning = false this.animationRunning = false
this.cursorEl.style.opacity = '0' this.cursorEl.style.opacity = '0'
if (this.currentSphericalPosition && reset) {
this._viewer.scene.mainCamera.position.setFromSpherical(this.currentSphericalPosition).add(this._viewer.scene.mainCamera.target)
this._viewer.scene.mainCamera.setDirty()
}
this._viewer.scene.mainCamera.setInteractions(true, InteractionPromptPlugin.PluginType) this._viewer.scene.mainCamera.setInteractions(true, InteractionPromptPlugin.PluginType)
// if (this.interactionsDisabled) { // if (this.interactionsDisabled) {
// this._viewer.scene.mainCamera.interactionsEnabled = true // this._viewer.scene.mainCamera.interactionsEnabled = true
// this.interactionsDisabled = false // this.interactionsDisabled = false
// } // }
return this._viewer.doOnce('postFrame')
} }


private _pointerDown = () => { private _pointerDown = () => {
if (this.isDisabled()) return if (this.isDisabled()) return
if (this.autoStop) this.stopAnimation()
if (this.autoStop) this.stopAnimation({reset: false}) // todo dont reset only on pointer drag, not down
this.lastActionTime = now() this.lastActionTime = now()
} }
private _x = 0 private _x = 0

+ 14
- 2
src/plugins/interaction/LoadingScreenPlugin.ts Ver arquivo

@onChange(LoadingScreenPlugin.prototype.refresh) @onChange(LoadingScreenPlugin.prototype.refresh)
@serialize() textColor = '#222222' @serialize() textColor = '#222222'


static LS_DEFAULT_LOGO = 'https://threepipe.org/logo.svg'

@uiInput('Logo Image') @uiInput('Logo Image')
@onChange(LoadingScreenPlugin.prototype.refresh) @onChange(LoadingScreenPlugin.prototype.refresh)
@serialize() logoImage = 'https://threepipe.org/logo.svg'
@serialize() logoImage = LoadingScreenPlugin.LS_DEFAULT_LOGO


private _isPreviewing = false private _isPreviewing = false
private _previewState = new Map([['file.glb', {state: 'downloading', progress: 50}], ['environment.hdr', {state: 'adding'}]]) private _previewState = new Map([['file.glb', {state: 'downloading', progress: 50}], ['environment.hdr', {state: 'adding'}]])


private _isHidden = false private _isHidden = false


get visible() {
return !this._isHidden
}

async hide() { async hide() {
this._isHidden = true this._isHidden = true
this._mainDiv.style.opacity = '0' this._mainDiv.style.opacity = '0'
errors.length === processState.size && this.hideOnOnlyErrors)) { errors.length === processState.size && this.hideOnOnlyErrors)) {
this.hideDelay ? this.hideWithDelay() : this.hide() this.hideDelay ? this.hideWithDelay() : this.hide()
} else if (processState.size > 0 && this.showOnFilesLoading) { } else if (processState.size > 0 && this.showOnFilesLoading) {
const sceneObjects = this._viewer.scene.modelRoot.children
if (sceneObjects.length > 0 && this.minimizeOnSceneObjectLoad && this._viewer.scene.environment) this.minimize()
else this.maximize()
this.show() this.show()
} }
} }
} }


// disables showOnSceneEmpty
isEditor = false

private _sceneUpdate = (e: any) => { private _sceneUpdate = (e: any) => {
if (!this._viewer) return if (!this._viewer) return
if (!e.hierarchyChanged) return if (!e.hierarchyChanged) return
const sceneObjects = this._viewer.scene.modelRoot.children const sceneObjects = this._viewer.scene.modelRoot.children
if (sceneObjects.length === 0 && this.showOnSceneEmpty) {
if (sceneObjects.length === 0 && this.showOnSceneEmpty && !this.isEditor) {
this.show() this.show()
} }
// console.log(sceneObjects.length) // console.log(sceneObjects.length)

+ 7
- 4
src/plugins/pipeline/FrameFadePlugin.ts Ver arquivo



dependencies = [ProgressivePlugin] dependencies = [ProgressivePlugin]


// disables fadeOn... options but not serialized
isEditor = false

@serialize() @uiToggle() fadeOnActiveCameraChange = true @serialize() @uiToggle() fadeOnActiveCameraChange = true
@serialize() @uiToggle() fadeOnMaterialUpdate = true @serialize() @uiToggle() fadeOnMaterialUpdate = true
@serialize() @uiToggle() fadeOnSceneUpdate = true @serialize() @uiToggle() fadeOnSceneUpdate = true
} }


private _fadeCam = async(ev: any)=> private _fadeCam = async(ev: any)=>
ev.frameFade !== false && this.fadeOnActiveCameraChange && this.startTransition(ev.fadeDuration || 1000)
ev.frameFade !== false && !this.isEditor && this.fadeOnActiveCameraChange && this.startTransition(ev.fadeDuration || 1000)
private _fadeMat = async(ev: any)=> private _fadeMat = async(ev: any)=>
ev.frameFade !== false && this.fadeOnMaterialUpdate && this.startTransition(ev.fadeDuration || 200)
ev.frameFade !== false && !this.isEditor && this.fadeOnMaterialUpdate && this.startTransition(ev.fadeDuration || 200)
private _fadeScene = async(ev: any)=> private _fadeScene = async(ev: any)=>
ev.frameFade !== false && this.fadeOnSceneUpdate && this.startTransition(ev.fadeDuration || 500)
ev.frameFade !== false && !this.isEditor && this.fadeOnSceneUpdate && this.startTransition(ev.fadeDuration || 500)
private _fadeObjectUpdate = async(ev: any)=> private _fadeObjectUpdate = async(ev: any)=>
ev.frameFade && this.startTransition(ev.fadeDuration || 500)
ev.frameFade && !this.isEditor && this.startTransition(ev.fadeDuration || 500)


private _onPointerMove = (ev: PointerEvent)=> { private _onPointerMove = (ev: PointerEvent)=> {
const canvas = this._viewer?.canvas const canvas = this._viewer?.canvas

+ 6
- 12
src/plugins/pipeline/SSAOPlugin.ts Ver arquivo

import {Matrix4, Texture, TextureDataType, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget} from 'three' import {Matrix4, Texture, TextureDataType, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget} from 'three'
import {ExtendedShaderPass, IPassID, IPipelinePass} from '../../postprocessing' import {ExtendedShaderPass, IPassID, IPipelinePass} from '../../postprocessing'
import {type IViewerEvent, ThreeViewer} from '../../viewer'
import {ThreeViewer} from '../../viewer'
import {PipelinePassPlugin} from '../base/PipelinePassPlugin' import {PipelinePassPlugin} from '../base/PipelinePassPlugin'
import {uiConfig, uiFolderContainer, uiImage, uiSlider} from 'uiconfig.js' import {uiConfig, uiFolderContainer, uiImage, uiSlider} from 'uiconfig.js'
import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../../core' import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../../core'


onAdded(viewer: ThreeViewer) { onAdded(viewer: ThreeViewer) {
super.onAdded(viewer) super.onAdded(viewer)
const gbuffer = viewer.getPlugin(GBufferPlugin)
if (gbuffer) gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this))
else viewer.addEventListener('addPlugin', this._onPluginAdd)
viewer.forPlugin(GBufferPlugin, (gbuffer) => {
gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this))
}, (gbuffer)=>{
gbuffer.unregisterGBufferUpdater(this.constructor.PluginType)
})
this._gbufferUnpackExtensionChanged() this._gbufferUnpackExtensionChanged()
viewer.renderManager.addEventListener('gbufferUnpackExtensionChanged', this._gbufferUnpackExtensionChanged) viewer.renderManager.addEventListener('gbufferUnpackExtensionChanged', this._gbufferUnpackExtensionChanged)
} }


private _onPluginAdd = (e: IViewerEvent)=>{
if (e.plugin?.constructor?.PluginType !== GBufferPlugin.PluginType) return
const gbuffer = e.plugin as GBufferPlugin
gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this))
this._viewer?.removeEventListener('addPlugin', this._onPluginAdd)
}

onRemove(viewer: ThreeViewer): void { onRemove(viewer: ThreeViewer): void {
viewer.removeEventListener('addPlugin', this._onPluginAdd)
this._disposeTarget() this._disposeTarget()
return super.onRemove(viewer) return super.onRemove(viewer)
} }

+ 6
- 12
src/plugins/postprocessing/AScreenPassExtensionPlugin.ts Ver arquivo

import {type AViewerPlugin, AViewerPluginSync} from '../../viewer/AViewerPlugin' import {type AViewerPlugin, AViewerPluginSync} from '../../viewer/AViewerPlugin'
import type {IViewerEvent, ThreeViewer} from '../../viewer'
import type {ThreeViewer} from '../../viewer'
import {MaterialExtension} from '../../materials' import {MaterialExtension} from '../../materials'
import {Shader, Vector4, WebGLRenderer} from 'three' import {Shader, Vector4, WebGLRenderer} from 'three'
import {IMaterial} from '../../core' import {IMaterial} from '../../core'


onAdded(viewer: ThreeViewer) { onAdded(viewer: ThreeViewer) {
super.onAdded(viewer) super.onAdded(viewer)
const gbuffer = viewer.getPlugin(GBufferPlugin)
if (gbuffer) gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this))
else viewer.addEventListener('addPlugin', this._onPluginAdd)
viewer.forPlugin(GBufferPlugin, (gbuffer) => {
gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this))
}, (gbuffer)=>{
gbuffer.unregisterGBufferUpdater(this.constructor.PluginType)
})
viewer.renderManager.screenPass.material.registerMaterialExtensions([this]) viewer.renderManager.screenPass.material.registerMaterialExtensions([this])
} }


private _onPluginAdd = (e: IViewerEvent)=>{
if (e.plugin?.constructor?.PluginType !== GBufferPlugin.PluginType) return
const gbuffer = e.plugin as GBufferPlugin
gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this))
this._viewer?.removeEventListener('addPlugin', this._onPluginAdd)
}

onRemove(viewer: ThreeViewer) { onRemove(viewer: ThreeViewer) {
viewer.removeEventListener('addPlugin', this._onPluginAdd)
viewer.getPlugin(GBufferPlugin)?.unregisterGBufferUpdater(this.constructor.PluginType) viewer.getPlugin(GBufferPlugin)?.unregisterGBufferUpdater(this.constructor.PluginType)
viewer.renderManager.screenPass.material.unregisterMaterialExtensions([this]) viewer.renderManager.screenPass.material.unregisterMaterialExtensions([this])
super.onRemove(viewer) super.onRemove(viewer)

+ 1
- 2
src/three/utils/texture.ts Ver arquivo

FloatType, FloatType,
HalfFloatType, HalfFloatType,
LinearSRGBColorSpace, LinearSRGBColorSpace,
Texture,
Texture, TextureImageData,
TextureDataType, TextureDataType,
UnsignedByteType, UnsignedByteType,
WebGLRenderer, WebGLRenderer,
} from 'three' } from 'three'
import {TextureImageData} from 'three/src/textures/types'
import {canvasFlipY, LinearToSRGB} from 'ts-browser-helpers' import {canvasFlipY, LinearToSRGB} from 'ts-browser-helpers'


export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType { export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType {

+ 77
- 10
src/viewer/ThreeViewer.ts Ver arquivo

if (oldType) this.plugins[oldType] = p if (oldType) this.plugins[oldType] = p


await p.onAdded(this) await p.onAdded(this)
this.dispatchEvent({type: 'addPlugin', target: this, plugin: p})
this.setDirty(p)
this._onPluginAdd(p)
return p return p
} }


if (oldType && this.plugins[oldType]) this.console.error('Plugin type mismatch') if (oldType && this.plugins[oldType]) this.console.error('Plugin type mismatch')
if (oldType) this.plugins[oldType] = p if (oldType) this.plugins[oldType] = p
p.onAdded(this) p.onAdded(this)
this.dispatchEvent({type: 'addPlugin', target: this, plugin: p})
this.setDirty(p)
this._onPluginAdd(p)
return p return p
} }


const type = p.constructor.PluginType const type = p.constructor.PluginType
if (!this.plugins[type]) return if (!this.plugins[type]) return
await p.onRemove(this) await p.onRemove(this)
this.dispatchEvent({type: 'removePlugin', target: this, plugin: p})
delete this.plugins[type]
if (dispose) await p.dispose() // todo await?
this.setDirty(p)
this._onPluginRemove(p, dispose)
} }


/** /**
const type = p.constructor.PluginType const type = p.constructor.PluginType
if (!this.plugins[type]) return if (!this.plugins[type]) return
p.onRemove(this) p.onRemove(this)
this.dispatchEvent({type: 'removePlugin', target: this, plugin: p})
this._onPluginRemove(p, dispose)
delete this.plugins[type] delete this.plugins[type]
if (dispose) p.dispose() if (dispose) p.dispose()
this.setDirty(p) this.setDirty(p)
if (filter && filter.length === 0) return [] if (filter && filter.length === 0) return []
return Object.entries(this.plugins).map(p=> { return Object.entries(this.plugins).map(p=> {
if (filter && !filter.includes(p[1].constructor.PluginType)) return if (filter && !filter.includes(p[1].constructor.PluginType)) return
if (this.serializePluginsIgnored.includes((p[1].constructor as any).PluginType)) return
// if (!p[1].toJSON) this.console.log(`Plugin of type ${p[0]} is not serializable`) // if (!p[1].toJSON) this.console.log(`Plugin of type ${p[0]} is not serializable`)
return p[1].serializeWithViewer !== false ? p[1].toJSON?.(meta) : undefined return p[1].serializeWithViewer !== false ? p[1].toJSON?.(meta) : undefined
}).filter(p=> !!p) }).filter(p=> !!p)
this.console.warn('Invalid plugin to import ', p) this.console.warn('Invalid plugin to import ', p)
return return
} }
if (this.serializePluginsIgnored.includes(p.type)) return
const plugin = this.getPlugin(p.type) const plugin = this.getPlugin(p.type)
if (!plugin) { if (!plugin) {
// this.console.warn(`Plugin of type ${p.type} is not added, cannot deserialize`) // this.console.warn(`Plugin of type ${p.type} is not added, cannot deserialize`)
return return
} }
plugin.fromJSON?.(p, meta)
plugin.fromJSON && plugin.fromJSON(p, meta)
}) })
return this return this
} }
return this.plugins[type] as T | undefined return this.plugins[type] as T | undefined
} }



private _onPluginAdd(p: IViewerPlugin) {
const ev = {type: 'addPlugin', target: this, plugin: p} as const
this.dispatchEvent(ev)
this._pluginListeners.add.filter(l=> !l.p.length || l.p.includes(p.constructor.PluginType)).forEach(l=> l.l(ev))
this.setDirty(p)
}
private _onPluginRemove(p: IViewerPlugin, dispose = false) {
const ev = {type: 'removePlugin', target: this, plugin: p} as const
this.dispatchEvent(ev)
this._pluginListeners.remove.filter(l=> !l.p.length || l.p.includes(p.constructor.PluginType)).forEach(l=> l.l(ev))
delete this.plugins[p.constructor.PluginType]
if (dispose) p.dispose() // todo await?
this.setDirty(p)
}

private _pluginListeners: Record<'add' | 'remove', ({p: string[], l: (event: IViewerEvent) => void})[]> = {
add: [],
remove: [],
}

addPluginListener(type: 'add' | 'remove', listener: (event: IViewerEvent) => void, ...plugins: string[]): void {
this._pluginListeners[type].push({p: plugins, l: listener})
}
removePluginListener(type: 'add' | 'remove', listener: (event: IViewerEvent) => void): void {
this._pluginListeners[type] = this._pluginListeners[type].filter(l=> l.l !== listener)
}

/**
* Can be used to "subscribe" to plugins.
* @param plugin
* @param mount
* @param unmount
*/
forPlugin<T extends IViewerPlugin>(plugin: string|Class<T>, mount: (p: T) => void, unmount?: (p: T) => void): void {
const um = ()=>{
if (unmount) {
const lis = () => {
const p1 = this.getPlugin(plugin)
if (!p1) return
this.removePluginListener('remove', lis)
unmount(p1)
}
this.addPluginListener('remove', lis, typeof plugin === 'string' ? plugin : (plugin as any).PluginType)
}
}

const p = this.getPlugin(plugin)
if (p) {
mount(p)
um()
} else {
const lis = () => {
const p1 = this.getPlugin(plugin)
if (!p1) return
this.removePluginListener('add', lis)
mount(p1)
um()
}
this.addPluginListener('add', lis, typeof plugin === 'string' ? plugin : (plugin as any).PluginType)
}

}

/**
* plugins that are not serialized/deserialized with the viewer from config. useful when loading files exported from the editor, etc
* (runtime only, not serialized itself)
*/
serializePluginsIgnored: string[] = []

} }

+ 1
- 1
vite.config.js Ver arquivo



const isProd = process.env.NODE_ENV === 'production' const isProd = process.env.NODE_ENV === 'production'
const { name, version, author } = packageJson const { name, version, author } = packageJson
const {main, module, browser} = packageJson['clean-package'].replace
const {main, module, browser} = packageJson


export default defineConfig({ export default defineConfig({
optimizeDeps: { optimizeDeps: {

Carregando…
Cancelar
Salvar