Просмотр исходного кода

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 год назад
Родитель
Сommit
ab9121168a
Аккаунт пользователя с таким Email не найден

+ 3
- 1
src/assetmanager/AssetManager.ts Просмотреть файл

@@ -121,7 +121,9 @@ export class AssetManager extends EventDispatcher<BaseEvent&{data?: ImportResult
if (!this.importer || !this.viewer) return []
const imported = await this.importer.import<T>(assetOrPath, options)
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 this.loadImported<(T | undefined)[]>(imported, options)

+ 26
- 13
src/assetmanager/MaterialManager.ts Просмотреть файл

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

applyMaterial(material: IMaterial, nameRegexOrUuid: string, regex = true): boolean {
const mType = Object.getPrototypeOf(material).constructor.TYPE
let currentMats = this.findMaterialsByName(nameRegexOrUuid, regex)
if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameRegexOrUuid) as any]
let applied = false
@@ -328,18 +327,32 @@ export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> {
if (!c) continue
if (c === material) 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
newMat.setValues(material)
newMat.name = n

+ 7
- 4
src/core/camera/PerspectiveCamera2.ts Просмотреть файл

@@ -204,8 +204,9 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
this.getWorldPosition(this._positionWorld)

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
}

/**
@@ -216,7 +217,9 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
if (this.autoAspect) {
if (!this._canvas) console.error('cannot calculate aspect ratio without canvas/container')
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?.()
}
}
@@ -266,7 +269,7 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
private _initCameraControls() {
const mode = this.controlsMode
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._currentControlsMode = this._controls ? mode : ''
// todo maybe set target like this:

+ 18
- 15
src/core/geometry/iGeometryCommons.ts Просмотреть файл

@@ -2,6 +2,7 @@ import {UiObjectConfig} from 'uiconfig.js'
import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry'
import {autoGPUInstanceMeshes, isInScene, toIndexedGeometry} from '../../three/utils'
import {BufferGeometry, Vector3} from 'three'
import {ThreeViewer} from '../../viewer'

export const iGeometryCommons = {
setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void {
@@ -57,22 +58,24 @@ export const iGeometryCommons = {
{
type: 'button',
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()
},
},
{
type: 'button',
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)
},
},
{
type: 'button',
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.setDirty()
},
@@ -80,8 +83,8 @@ export const iGeometryCommons = {
{
type: 'button',
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.setDirty()
},
@@ -98,9 +101,9 @@ export const iGeometryCommons = {
type: 'button',
label: 'Convert to indexed',
hidden: () => !!this.index,
value: () => {
value: async() => {
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)
this.setDirty()
},
@@ -118,9 +121,9 @@ export const iGeometryCommons = {
{
type: 'button',
label: 'Create uv1 from uv',
value: () => {
value: async() => {
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.setDirty()
@@ -130,12 +133,12 @@ export const iGeometryCommons = {
type: 'button',
label: 'Remove vertex color attribute',
hidden: () => !this.hasAttribute('color'),
value: () => {
value: async() => {
if (!this.hasAttribute('color')) {
prompt('No color attribute found')
await ThreeViewer.Dialog.prompt('No color attribute found')
return
}
if (!confirm('Remove color attribute?')) return
if (!await ThreeViewer.Dialog.confirm('Remove color attribute?')) return
this.deleteAttribute('color')
this.setDirty()
},
@@ -144,8 +147,8 @@ export const iGeometryCommons = {
type: 'button',
label: 'Auto GPU Instances',
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)
},
},

+ 11
- 8
src/core/material/IMaterialUi.ts Просмотреть файл

@@ -259,14 +259,17 @@ export const iMaterialUI = {
{
type: 'button',
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 => (

+ 1
- 1
src/core/material/iMaterialCommons.ts Просмотреть файл

@@ -77,7 +77,7 @@ export const iMaterialCommons = {
setDirty: function(this: IMaterial, options?: IMaterialSetDirtyOptions): void {
if (options?.needsUpdate !== false) this.needsUpdate = true
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'] =>
function(this: IMaterial, parameters: Material | (MaterialParameters & {type?: string})): IMaterial {

+ 25
- 10
src/core/object/IObjectUi.ts Просмотреть файл

@@ -2,6 +2,7 @@ import {IObject3D} from '../IObject'
import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js'
import {ICamera} from '../ICamera'
import {Vector3} from 'three'
import {ThreeViewer} from '../../viewer'

export function makeICameraCommonUiConfig(this: ICamera, config: UiObjectConfig): UiObjectConfig[] {
return [
@@ -127,15 +128,17 @@ export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjec
type: 'button',
label: 'Auto Scale',
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()=>{
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
const rad = parseFloat(res || def)
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),
}
}
},
},
@@ -143,10 +146,12 @@ export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjec
type: 'button',
label: 'Auto Center',
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),
}
},
},
{
@@ -159,8 +164,18 @@ export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjec
type: 'button',
label: 'Rotate ' + l + '90',
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 Просмотреть файл

@@ -2,9 +2,9 @@ import {Event, Mesh, Vector3} from 'three'
import {IMaterial} from '../IMaterial'
import {objectHasOwn} from 'ts-browser-helpers'
import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject'
import {copyObject3DUserData} from '../../utils/serialization'
import {copyObject3DUserData} from '../../utils'
import {IGeometry, IGeometryEvent} from '../IGeometry'
import {Box3B} from '../../three/math/Box3B'
import {Box3B} from '../../three'
import {makeIObject3DUiConfig} from './IObjectUi'
import {iGeometryCommons} from '../geometry/iGeometryCommons'
import {iMaterialCommons} from '../material/iMaterialCommons'
@@ -13,7 +13,7 @@ import {ILight} from '../light/ILight'
export const iObjectCommons = {
setDirty: function(this: IObject3D, options?: IObjectSetDirtyOptions): void {
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')
},


+ 10
- 3
src/plugins/animation/CameraViewPlugin.ts Просмотреть файл

@@ -7,6 +7,7 @@ import {generateUiConfig, uiButton, uiDropdown, uiInput, UiObjectConfig, uiSlide
import {EasingFunctions, EasingFunctionType} from '../../utils'
import {CameraView, ICamera, ICameraView, PerspectiveCamera2} from '../../core'
import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin'
import {InteractionPromptPlugin} from '../interaction/InteractionPromptPlugin'

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

@@ -257,6 +258,11 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC
return
}

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

this._currentView = view
this._animating = true

@@ -337,10 +343,11 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC

const cam = this._viewer.scene.mainCamera
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 )
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 dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2))
cameraZ = Math.max(dx, dy)

+ 4
- 4
src/plugins/animation/PopmotionPlugin.ts Просмотреть файл

@@ -188,10 +188,10 @@ export class PopmotionPlugin extends AViewerPluginSync<''> {
driver: this.defaultDriver,
...options,
onUpdate: !isBool ? options.onUpdate : undefined,
onComplete: ()=>{
onComplete: async()=>{
try {
if (isBool) options.onUpdate?.(options.to as any)
options.onComplete && options.onComplete()
options.onComplete && await options.onComplete()
} catch (e: any) {
if (!end2()) return
reject(e)
@@ -200,9 +200,9 @@ export class PopmotionPlugin extends AViewerPluginSync<''> {
if (!end2()) return
resolve()
},
onStop: ()=>{
onStop: async()=>{
try {
options.onStop && options.onStop()
options.onStop && await options.onStop()
} catch (e: any) {
if (!end2()) return
reject(e)

+ 5
- 4
src/plugins/base/BaseGroundPlugin.ts Просмотреть файл

@@ -236,9 +236,11 @@ export class BaseGroundPlugin<TEvent extends string = ''> extends AViewerPluginS
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) {
this._geometry.attributes.uv2 = (this._geometry.attributes.uv as any as BufferAttribute | InterleavedBufferAttribute).clone()
this._geometry.attributes.uv2.needsUpdate = true
@@ -246,7 +248,6 @@ export class BaseGroundPlugin<TEvent extends string = ''> extends AViewerPluginS
if (this._mesh) this._mesh.geometry = this._geometry
}


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

+ 14
- 10
src/plugins/configurator/MaterialConfiguratorBasePlugin.ts Просмотреть файл

@@ -36,10 +36,14 @@ export class MaterialConfiguratorBasePlugin extends AViewerPluginSync<''> {
super.onAdded(viewer)

// 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._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig)
viewer.addEventListener('preFrame', this._refreshUi)
}

@@ -256,22 +260,22 @@ export class MaterialConfiguratorBasePlugin extends AViewerPluginSync<''> {
this.variations.splice(this.variations.indexOf(variation), 1)
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) {
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) {
variation = this.createVariation(material)
variation = this.createVariation(material, variationKey)
}
variation.materials.push(clone)
this.refreshUi()
}
}

createVariation(material: IMaterial) {
createVariation(material: IMaterial, variationKey?: string) {
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',
preview: 'generate:sphere',
materials: [],

+ 30
- 1
src/plugins/configurator/SwitchNodeBasePlugin.ts Просмотреть файл

@@ -13,7 +13,7 @@ import {snapObject} from '../../three'
* 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.
* 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.
*
@@ -117,6 +117,7 @@ export class SwitchNodeBasePlugin extends AViewerPluginSync<''> {
if (!this.enabled) return false
if (!this._viewer) return false
this._uiNeedRefresh = false
if (this.autoSnapIcons) this.snapIcons()

this.refreshUiConfig()
return true
@@ -169,6 +170,34 @@ export class SwitchNodeBasePlugin extends AViewerPluginSync<''> {
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 = {
label: 'Switch Node Plugin',
type: 'folder',

+ 7
- 2
src/plugins/interaction/InteractionPromptPlugin.ts Просмотреть файл

@@ -226,20 +226,25 @@ export class InteractionPromptPlugin extends AViewerPluginSync<''> {
// }
}

@uiButton() stopAnimation = () => {
@uiButton() stopAnimation = async({reset = true}: {reset?: boolean} = {}) => {
if (!this._viewer || !this.cursorEl) return // dont check for enabled here.
this.animationRunning = false
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)
// if (this.interactionsDisabled) {
// this._viewer.scene.mainCamera.interactionsEnabled = true
// this.interactionsDisabled = false
// }
return this._viewer.doOnce('postFrame')
}

private _pointerDown = () => {
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()
}
private _x = 0

+ 14
- 2
src/plugins/interaction/LoadingScreenPlugin.ts Просмотреть файл

@@ -86,9 +86,11 @@ export class LoadingScreenPlugin extends AAssetManagerProcessStatePlugin {
@onChange(LoadingScreenPlugin.prototype.refresh)
@serialize() textColor = '#222222'

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

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

private _isPreviewing = false
private _previewState = new Map([['file.glb', {state: 'downloading', progress: 50}], ['environment.hdr', {state: 'adding'}]])
@@ -127,6 +129,10 @@ export class LoadingScreenPlugin extends AAssetManagerProcessStatePlugin {

private _isHidden = false

get visible() {
return !this._isHidden
}

async hide() {
this._isHidden = true
this._mainDiv.style.opacity = '0'
@@ -207,16 +213,22 @@ export class LoadingScreenPlugin extends AAssetManagerProcessStatePlugin {
errors.length === processState.size && this.hideOnOnlyErrors)) {
this.hideDelay ? this.hideWithDelay() : this.hide()
} 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()
}
}
}

// disables showOnSceneEmpty
isEditor = false

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

+ 7
- 4
src/plugins/pipeline/FrameFadePlugin.ts Просмотреть файл

@@ -27,6 +27,9 @@ export class FrameFadePlugin

dependencies = [ProgressivePlugin]

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

@serialize() @uiToggle() fadeOnActiveCameraChange = true
@serialize() @uiToggle() fadeOnMaterialUpdate = true
@serialize() @uiToggle() fadeOnSceneUpdate = true
@@ -102,13 +105,13 @@ export class FrameFadePlugin
}

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)=>
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)=>
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)=>
ev.frameFade && this.startTransition(ev.fadeDuration || 500)
ev.frameFade && !this.isEditor && this.startTransition(ev.fadeDuration || 500)

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

+ 6
- 12
src/plugins/pipeline/SSAOPlugin.ts Просмотреть файл

@@ -1,6 +1,6 @@
import {Matrix4, Texture, TextureDataType, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget} from 'three'
import {ExtendedShaderPass, IPassID, IPipelinePass} from '../../postprocessing'
import {type IViewerEvent, ThreeViewer} from '../../viewer'
import {ThreeViewer} from '../../viewer'
import {PipelinePassPlugin} from '../base/PipelinePassPlugin'
import {uiConfig, uiFolderContainer, uiImage, uiSlider} from 'uiconfig.js'
import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../../core'
@@ -108,22 +108,16 @@ export class SSAOPlugin

onAdded(viewer: ThreeViewer) {
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()
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 {
viewer.removeEventListener('addPlugin', this._onPluginAdd)
this._disposeTarget()
return super.onRemove(viewer)
}

+ 6
- 12
src/plugins/postprocessing/AScreenPassExtensionPlugin.ts Просмотреть файл

@@ -1,5 +1,5 @@
import {type AViewerPlugin, AViewerPluginSync} from '../../viewer/AViewerPlugin'
import type {IViewerEvent, ThreeViewer} from '../../viewer'
import type {ThreeViewer} from '../../viewer'
import {MaterialExtension} from '../../materials'
import {Shader, Vector4, WebGLRenderer} from 'three'
import {IMaterial} from '../../core'
@@ -81,21 +81,15 @@ export abstract class AScreenPassExtensionPlugin<T extends string> extends AView

onAdded(viewer: ThreeViewer) {
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])
}

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) {
viewer.removeEventListener('addPlugin', this._onPluginAdd)
viewer.getPlugin(GBufferPlugin)?.unregisterGBufferUpdater(this.constructor.PluginType)
viewer.renderManager.screenPass.material.unregisterMaterialExtensions([this])
super.onRemove(viewer)

+ 1
- 2
src/three/utils/texture.ts Просмотреть файл

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

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

+ 77
- 10
src/viewer/ThreeViewer.ts Просмотреть файл

@@ -762,8 +762,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
if (oldType) this.plugins[oldType] = p

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

@@ -792,8 +791,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
if (oldType && this.plugins[oldType]) this.console.error('Plugin type mismatch')
if (oldType) this.plugins[oldType] = p
p.onAdded(this)
this.dispatchEvent({type: 'addPlugin', target: this, plugin: p})
this.setDirty(p)
this._onPluginAdd(p)
return p
}

@@ -823,10 +821,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
const type = p.constructor.PluginType
if (!this.plugins[type]) return
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)
}

/**
@@ -838,7 +833,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
const type = p.constructor.PluginType
if (!this.plugins[type]) return
p.onRemove(this)
this.dispatchEvent({type: 'removePlugin', target: this, plugin: p})
this._onPluginRemove(p, dispose)
delete this.plugins[type]
if (dispose) p.dispose()
this.setDirty(p)
@@ -988,6 +983,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
if (filter && filter.length === 0) return []
return Object.entries(this.plugins).map(p=> {
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`)
return p[1].serializeWithViewer !== false ? p[1].toJSON?.(meta) : undefined
}).filter(p=> !!p)
@@ -1005,12 +1001,13 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
this.console.warn('Invalid plugin to import ', p)
return
}
if (this.serializePluginsIgnored.includes(p.type)) return
const plugin = this.getPlugin(p.type)
if (!plugin) {
// this.console.warn(`Plugin of type ${p.type} is not added, cannot deserialize`)
return
}
plugin.fromJSON?.(p, meta)
plugin.fromJSON && plugin.fromJSON(p, meta)
})
return this
}
@@ -1405,4 +1402,74 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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 Просмотреть файл

@@ -9,7 +9,7 @@ import path from 'node:path';

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

export default defineConfig({
optimizeDeps: {

Загрузка…
Отмена
Сохранить