瀏覽代碼

Some rearrangement, minor fixes and docs.

master
Palash Bansal 2 年之前
父節點
當前提交
4068b58986
沒有連結到貢獻者的電子郵件帳戶。

+ 1
- 2
.eslintrc.cjs 查看文件

'ecmaVersion': 2021, // Allows for the parsing of modern ECMAScript features 'ecmaVersion': 2021, // Allows for the parsing of modern ECMAScript features
'sourceType': 'module', // Allows for the use of imports 'sourceType': 'module', // Allows for the use of imports
'project': ['./tsconfig.json', './examples/tsconfig.json', 'project': ['./tsconfig.json', './examples/tsconfig.json',
'./plugins/tweakpane-editor/tsconfig.json',
'./plugins/tweakpane/tsconfig.json',
'./plugins/**/tsconfig.json',
], ],
'tsconfigRootDir': './', 'tsconfigRootDir': './',
}, },

+ 4
- 4
package.json 查看文件

{ {
"name": "threepipe", "name": "threepipe",
"version": "0.0.12",
"version": "0.0.13-dev.1",
"description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.", "description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.",
"main": "src/index.ts", "main": "src/index.ts",
"module": "dist/index.mjs", "module": "dist/index.mjs",
"popmotion": "^11.0.5" "popmotion": "^11.0.5"
}, },
"dependencies": { "dependencies": {
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1012/package.tgz",
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1013/package.tgz",
"@types/webxr": "^0.5.1", "@types/webxr": "^0.5.1",
"@types/wicg-file-system-access": "^2020.9.5", "@types/wicg-file-system-access": "^2020.9.5",
"ts-browser-helpers": "^0.8.0" "ts-browser-helpers": "^0.8.0"
"ts-browser-helpers": "^0.8.0", "ts-browser-helpers": "^0.8.0",
"three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2012/package.tgz", "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2012/package.tgz",
"three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.152.2012.tar.gz", "three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.152.2012.tar.gz",
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1012/package.tgz",
"@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.152.1012.tar.gz",
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1013/package.tgz",
"@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.152.1013.tar.gz",
"@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three" "@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three"
}, },
"local_dependencies": { "local_dependencies": {

+ 4
- 3
scripts/utils.mjs 查看文件

const pluginDir = path.join(pluginsDir, pluginFolder) const pluginDir = path.join(pluginsDir, pluginFolder)
const packageJsonPath = path.join(pluginDir, 'package.json') const packageJsonPath = path.join(pluginDir, 'package.json')
if (!fs.existsSync(packageJsonPath)) continue; if (!fs.existsSync(packageJsonPath)) continue;
callback(pluginDir)
callback(pluginDir, pluginFolder)
} }


} }


export function execEachPlugin(command){
loopPluginDirs((pluginDir) => {
export function execEachPlugin(command, templates = false){
loopPluginDirs((pluginDir, pluginFolder) => {
if(!templates && pluginFolder.startsWith('plugin-template-')) return;
console.log(`Executing ${command} in ${pluginDir}`) console.log(`Executing ${command} in ${pluginDir}`)
execSync(command, {cwd: pluginDir, stdio: 'inherit'}) execSync(command, {cwd: pluginDir, stdio: 'inherit'})
}) })

+ 4
- 1
src/assetmanager/AssetManager.ts 查看文件

* @category Asset Manager * @category Asset Manager
*/ */
export class AssetManager extends EventDispatcher<BaseEvent&{data: ImportResult}, 'loadAsset'> { export class AssetManager extends EventDispatcher<BaseEvent&{data: ImportResult}, 'loadAsset'> {
static readonly PluginType = 'AssetManager'
readonly viewer: ThreeViewer readonly viewer: ThreeViewer
readonly importer: AssetImporter readonly importer: AssetImporter
readonly exporter: AssetExporter readonly exporter: AssetExporter
return this.viewer?.loadConfigResources(json, extraResources) return this.viewer?.loadConfigResources(json, extraResources)
} }


/**
* @deprecated not a plugin anymore
*/
static readonly PluginType = 'AssetManager'
// endregion // endregion
} }

+ 1
- 0
src/core/IRenderer.ts 查看文件

rgbm?: boolean, rgbm?: boolean,
msaa?: boolean, msaa?: boolean,
depthBuffer?: boolean, depthBuffer?: boolean,
renderScale?: number,
} }


export interface IWebGLRenderer<TManager extends IRenderManager=IRenderManager> extends WebGLRenderer { export interface IWebGLRenderer<TManager extends IRenderManager=IRenderManager> extends WebGLRenderer {

+ 1
- 1
src/core/IScene.ts 查看文件

// region deprecated // region deprecated


/** /**
* @deprecated use {@link IObject3D.getObjectByName} instead
@deprecated use {@link getObjectByName} instead
* @param name * @param name
* @param parent * @param parent
*/ */

+ 106
- 91
src/core/object/RootScene.ts 查看文件

readonly modelRoot: IObject3D readonly modelRoot: IObject3D


@uiColor<RootScene>('Background Color', (s)=>({ @uiColor<RootScene>('Background Color', (s)=>({
onChange: ()=>s?._onBackgroundChange(),
onChange: ()=>s?.onBackgroundChange(),
})) }))
@serialize() @onChange2(RootScene.prototype._onBackgroundChange)
@serialize() @onChange2(RootScene.prototype.onBackgroundChange)
backgroundColor: Color | null = null // read in three.js WebGLBackground backgroundColor: Color | null = null // read in three.js WebGLBackground


@onChange2(RootScene.prototype._onBackgroundChange)
@onChange2(RootScene.prototype.onBackgroundChange)
@serialize() @uiImage('Background Image') @serialize() @uiImage('Background Image')
background: null | Color | ITexture | 'environment' = null background: null | Color | ITexture | 'environment' = null
/** /**
this.refreshUi?.() this.refreshUi?.()
} }


private _onBackgroundChange() {
onBackgroundChange() {
this.dispatchEvent({type: 'backgroundChanged', background: this.background, backgroundColor: this.backgroundColor}) this.dispatchEvent({type: 'backgroundChanged', background: this.background, backgroundColor: this.backgroundColor})
this.setDirty({refreshScene: true, geometryChanged: false}) this.setDirty({refreshScene: true, geometryChanged: false})
this.refreshUi?.() this.refreshUi?.()
return return
} }


/**
* Find objects by name exact match in the complete hierarchy.
* @param name - name
* @param parent - optional root node to start search from
* @returns Array of found objects
*/
public findObjectsByName(name: string, parent?: IObject3D): IObject3D[] {
const o: IObject3D[] = [];
(parent ?? this).traverse(object => {
if (object.name === name) o.push(object)
})
return o
}

/** /**
* Returns the bounding box of the scene model root. * Returns the bounding box of the scene model root.
* @param precise * @param precise
return this return this
} }


/**
* @deprecated
* Sets the camera pointing towards the object at a specific distance.
* @param rootObject - The object to point at.
* @param centerOffset - The distance offset from the object to point at.
* @param targetOffset - The distance offset for the target from the center of object to point at.
* @param options - Not used yet.
*/
resetCamera(rootObject:Object3D|undefined = undefined, centerOffset = new Vector3(1, 1, 1), targetOffset = new Vector3(0, 0, 0)): void {
if (this._mainCamera) {
this.matrixWorldNeedsUpdate = true
this.updateMatrixWorld(true)
const bounds = rootObject ? new Box3B().expandByObject(rootObject, true, true) : this.getBounds(true)
const center = bounds.getCenter(new Vector3())
const radius = bounds.getSize(new Vector3()).length() * 0.5

center.add(targetOffset.clone().multiplyScalar(radius))

this._mainCamera.position = new Vector3( // todo: for nested cameras?
center.x + centerOffset.x * radius,
center.y + centerOffset.y * radius,
center.z + centerOffset.z * radius,
)
this._mainCamera.target = center
// this.scene.mainCamera.controls?.targetOffset.set(0, 0, 0)
this.setDirty()
}

}

/** /**
* Serialize the scene properties * Serialize the scene properties
* @param meta * @param meta
super.addEventListener(type, listener) super.addEventListener(type, listener)
} }


/**
* Minimum Camera near plane
* @deprecated - use camera.userData.minNearPlane instead
*/
get minNearDistance(): number {
console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead')
return this.mainCamera.userData.minNearPlane ?? 0.02
}
/**
* @deprecated - use camera.userData.minNearPlane instead
*/
set minNearDistance(value: number) {
console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead')
if (this.mainCamera)
this.mainCamera.userData.minNearPlane = value
}


/**
* @deprecated
*/
get activeCamera(): ICamera {
console.error('activeCamera is deprecated. Use mainCamera instead.')
return this.mainCamera
}

/**
* @deprecated
*/
set activeCamera(camera: ICamera | undefined) {
console.error('activeCamera is deprecated. Use mainCamera instead.')
this.mainCamera = camera
}

/**
* Get the threejs scene object
* @deprecated
*/
get modelObject(): this {
return this as any
}




// region inherited type fixes // region inherited type fixes
// re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936 // re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936
// } // }




/**
* Find objects by name exact match in the complete hierarchy.
* @deprecated Use {@link getObjectByName} instead.
* @param name - name
* @param parent - optional root node to start search from
* @returns Array of found objects
*/
public findObjectsByName(name: string, parent?: IObject3D): IObject3D[] {
const o: IObject3D[] = [];
(parent ?? this).traverse(object => {
if (object.name === name) o.push(object)
})
return o
}

/**
* @deprecated
* Sets the camera pointing towards the object at a specific distance.
* @param rootObject - The object to point at.
* @param centerOffset - The distance offset from the object to point at.
* @param targetOffset - The distance offset for the target from the center of object to point at.
* @param options - Not used yet.
*/
resetCamera(rootObject:Object3D|undefined = undefined, centerOffset = new Vector3(1, 1, 1), targetOffset = new Vector3(0, 0, 0)): void {
if (this._mainCamera) {
this.matrixWorldNeedsUpdate = true
this.updateMatrixWorld(true)
const bounds = rootObject ? new Box3B().expandByObject(rootObject, true, true) : this.getBounds(true)
const center = bounds.getCenter(new Vector3())
const radius = bounds.getSize(new Vector3()).length() * 0.5

center.add(targetOffset.clone().multiplyScalar(radius))

this._mainCamera.position = new Vector3( // todo: for nested cameras?
center.x + centerOffset.x * radius,
center.y + centerOffset.y * radius,
center.z + centerOffset.z * radius,
)
this._mainCamera.target = center
// this.scene.mainCamera.controls?.targetOffset.set(0, 0, 0)
this.setDirty()
}

}


/**
* Minimum Camera near plane
* @deprecated - use camera.userData.minNearPlane instead
*/
get minNearDistance(): number {
console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead')
return this.mainCamera.userData.minNearPlane ?? 0.02
}
/**
* @deprecated - use camera.userData.minNearPlane instead
*/
set minNearDistance(value: number) {
console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead')
if (this.mainCamera)
this.mainCamera.userData.minNearPlane = value
}


/**
* @deprecated
*/
get activeCamera(): ICamera {
console.error('activeCamera is deprecated. Use mainCamera instead.')
return this.mainCamera
}

/**
* @deprecated
*/
set activeCamera(camera: ICamera | undefined) {
console.error('activeCamera is deprecated. Use mainCamera instead.')
this.mainCamera = camera
}

/**
* Get the threejs scene object
* @deprecated
*/
get modelObject(): this {
return this as any
}

/**
* @deprecated use {@link envMapIntensity} instead
*/
get environmentIntensity(): number {
return this.envMapIntensity
}

/**
* @deprecated use {@link envMapIntensity} instead
*/
set environmentIntensity(value: number) {
this.envMapIntensity = value
}

/** /**
* Add any processed scene object to the scene. * Add any processed scene object to the scene.
* @deprecated renamed to {@link addObject} * @deprecated renamed to {@link addObject}

+ 16
- 2
src/rendering/RenderManager.ts 查看文件

this.dispatchEvent({type: 'animationLoop', deltaTime, time, renderer: this._renderer, xrFrame: frame}) this.dispatchEvent({type: 'animationLoop', deltaTime, time, renderer: this._renderer, xrFrame: frame})
} }


constructor({canvas, alpha = true, targetOptions}:IRenderManagerOptions) {
constructor({canvas, alpha = true, renderScale = 1, targetOptions}:IRenderManagerOptions) {
super() super()
this._animationLoop = this._animationLoop.bind(this) this._animationLoop = this._animationLoop.bind(this)
// this._xrPreAnimationLoop = this._xrPreAnimationLoop.bind(this) // this._xrPreAnimationLoop = this._xrPreAnimationLoop.bind(this)
this._renderSize = new Vector2(canvas.clientWidth, canvas.clientHeight) this._renderSize = new Vector2(canvas.clientWidth, canvas.clientHeight)
this._renderScale = renderScale
this._renderer = this._initWebGLRenderer(canvas, alpha) this._renderer = this._initWebGLRenderer(canvas, alpha)
this._context = this._renderer.getContext() this._context = this._renderer.getContext()
this._isWebGL2 = this._renderer.capabilities.isWebGL2 this._isWebGL2 = this._renderer.capabilities.isWebGL2
return this._context return this._context
} }


/**
* Same as {@link renderer}
*/
get webglRenderer(): WebGLRenderer { get webglRenderer(): WebGLRenderer {
return this._renderer return this._renderer
} }
// region Utils // region Utils


/** /**
*
* blit - blits a texture to the screen or another render target.
* @param destination - destination target, or screen if undefined or null * @param destination - destination target, or screen if undefined or null
* @param source - source Texture * @param source - source Texture
* @param viewport - viewport and scissor * @param viewport - viewport and scissor
return string return string
} }


/**
* Rend pixels from a render target into a new Uint8Array|Uint16Array|Float32Array buffer
* @param target
*/
renderTargetToBuffer(target: WebGLRenderTarget): Uint8Array|Uint16Array|Float32Array { renderTargetToBuffer(target: WebGLRenderTarget): Uint8Array|Uint16Array|Float32Array {
const buffer = const buffer =
target.texture.type === HalfFloatType ? target.texture.type === HalfFloatType ?
return buffer return buffer
} }


/**
* Exports a render target to a blob. The type is automatically picked from exr to png based on the render target.
* @param target - render target to export
* @param mimeType - mime type to use.
* If auto (default), then it will be picked based on the render target type.
*/
exportRenderTarget(target: WebGLRenderTarget, mimeType = 'auto'): BlobExt { exportRenderTarget(target: WebGLRenderTarget, mimeType = 'auto'): BlobExt {
const hdrFormats = ['image/x-exr'] const hdrFormats = ['image/x-exr']
let hdr = target.texture.type === HalfFloatType || target.texture.type === FloatType let hdr = target.texture.type === HalfFloatType || target.texture.type === FloatType

+ 34
- 1
src/utils/CustomContextMenu.ts 查看文件

import styles from './CustomContextMenu.css' import styles from './CustomContextMenu.css'


/**
* Represents a custom context menu that can be created and managed dynamically.
*/
export class CustomContextMenu { export class CustomContextMenu {
/**
* The HTML element representing the context menu.
*/
public static Element: HTMLDivElement | undefined = undefined public static Element: HTMLDivElement | undefined = undefined

/**
* Indicates whether the context menu has been initialized.
*/
private static _inited = false private static _inited = false


/**
* Initializes the context menu by adding event listeners.
* This method should be called before creating a context menu.
*/
private static _initialize(): void { private static _initialize(): void {
this._inited = true this._inited = true
document.addEventListener('pointerdown', (e) => { document.addEventListener('pointerdown', (e) => {
}) })
} }


public static Create(items: Record<string, () => void>, x: number, y: number, show = true, removeOnSelect = true): HTMLDivElement {
/**
* Creates a custom context menu with specified items and options.
*
* @param items - An object containing menu item labels and corresponding callback functions.
* @param x - The horizontal position of the context menu.
* @param y - The vertical position of the context menu.
* @param show - Indicates whether the context menu should be displayed immediately.
* @param removeOnSelect - Indicates whether the context menu should be removed after an item is selected.
* @returns The HTML element representing the created context menu.
*/
public static Create(
items: Record<string, () => void>,
x: number,
y: number,
show = true,
removeOnSelect = true
): HTMLDivElement {
if (!this._inited) this._initialize() if (!this._inited) this._initialize()


if (this.Element) this.Remove() if (this.Element) this.Remove()
return container return container
} }


/**
* Removes the context menu from the DOM.
*/
public static Remove(): void { public static Remove(): void {
this.Element?.remove() this.Element?.remove()
this.Element = undefined this.Element = undefined

+ 1
- 0
src/utils/serialization.ts 查看文件

if (res._context) delete res._context if (res._context) delete res._context
return res return res
} }

export function metaFromResources(resources?: Partial<SerializationResourcesType>, viewer?: ThreeViewer): SerializationMetaType { export function metaFromResources(resources?: Partial<SerializationResourcesType>, viewer?: ThreeViewer): SerializationMetaType {
return { return {
...resources, ...resources,

+ 9
- 0
src/utils/shader-helpers.ts 查看文件

const warnEnabled = true const warnEnabled = true


/**
* Replace a string in a shader function with added options to prepend, append, show warning when not found, and replace all occurrences.
* @param shader - shader code
* @param str - string to replace
* @param newStr - new string to replace with
* @param replaceAll - replace all occurrences
* @param prepend - prepend new string to old string
* @param append - append new string to old string
*/
export function shaderReplaceString(shader: string, str: string, newStr: string, { export function shaderReplaceString(shader: string, str: string, newStr: string, {
replaceAll = false, replaceAll = false,
prepend = false, prepend = false,

+ 237
- 209
src/viewer/ThreeViewer.ts 查看文件

*/ */
msaa?: boolean, msaa?: boolean,
/** /**
* Use RGBM HDR Pipeline
* Use Uint8 RGBM HDR Render Pipeline.
* Provides better performance with post-processing.
* RenderManager Uses Half-float if set to false.
*/ */
rgbm?: boolean rgbm?: boolean
/** /**
*/ */
zPrepass?: boolean zPrepass?: boolean


/*
* Render scale, 1 = full resolution, 0.5 = half resolution, 2 = double resolution.
* Same as pixelRatio in three.js
* Can be set to `window.devicePixelRatio` to render at device resolution in browsers.
* An optimal value is `Math.min(2, window.devicePixelRatio)` to prevent issues on mobile.
*/
renderScale?: number

debug?: boolean debug?: boolean


/** /**
} }
static Dialog: IDialogWrapper = windowDialogWrapper static Dialog: IDialogWrapper = windowDialogWrapper


renderStats: GLStatsJS

readonly assetManager: AssetManager

get console(): IConsoleWrapper {
return ThreeViewer.Console
}
get dialog(): IDialogWrapper {
return ThreeViewer.Dialog
}

@serialize() readonly type = 'ThreeViewer'

private readonly _canvas: HTMLCanvasElement

// this can be used by other plugins to add ui elements alongside the canvas
private readonly _container: HTMLElement // todo: add a way to move the canvas to a new container... and dispatch event...

@uiConfig() @serialize('renderManager')
readonly renderManager: ViewerRenderManager

/**
* The Scene attached to the viewer, this cannot be changed.
* @type {RootScene}
*/
@uiConfig() @serialize('scene')
private readonly _scene: RootScene

public readonly plugins: Record<string, IViewerPlugin> = {}
private _needsResize = false

/** /**
* If the viewer is enabled. Set this `false` to disable RAF loop. * If the viewer is enabled. Set this `false` to disable RAF loop.
* @type {boolean} * @type {boolean}
*/ */
enabled = true enabled = true


/** /**
* Enable or disable all rendering, Animation loop including any frame/render events won't be fired when this is false. * Enable or disable all rendering, Animation loop including any frame/render events won't be fired when this is false.
*/ */
@onChange(ThreeViewer.prototype._renderEnabledChanged) @onChange(ThreeViewer.prototype._renderEnabledChanged)
renderEnabled = true renderEnabled = true

private _isRenderingFrame = false

renderStats: GLStatsJS
readonly assetManager: AssetManager
@uiConfig() @serialize('renderManager')
readonly renderManager: ViewerRenderManager
public readonly plugins: Record<string, IViewerPlugin> = {}
/**
* Scene with object hierarchy used for rendering
*/
get scene(): RootScene {
return this._scene
}
/** /**
* Specifies how many frames to render in a single request animation frame. Keep to 1 for realtime rendering. * Specifies how many frames to render in a single request animation frame. Keep to 1 for realtime rendering.
* Note: should be max (screen refresh rate / animation frame rate) like 60Hz / 30fps * Note: should be max (screen refresh rate / animation frame rate) like 60Hz / 30fps
* @type {number} * @type {number}
*/ */
public maxFramePerLoop = 1 public maxFramePerLoop = 1
readonly debug: boolean


get scene(): RootScene {
return this._scene
/**
* Get the HTML Element containing the canvas
* @returns {HTMLElement}
*/
get container(): HTMLElement {
return this._container
}

/**
* Get the HTML Canvas Element where the viewer is rendering
* @returns {HTMLCanvasElement}
*/
get canvas(): HTMLCanvasElement {
return this._canvas
}

get console(): IConsoleWrapper {
return ThreeViewer.Console
} }
get dialog(): IDialogWrapper {
return ThreeViewer.Dialog
}
@serialize() readonly type = 'ThreeViewer'


/** /**
* The ResizeObserver observing the canvas element. Add more elements to this observer to resize viewer on their size change. * The ResizeObserver observing the canvas element. Add more elements to this observer to resize viewer on their size change.
*/ */
readonly resizeObserver = window?.ResizeObserver ? new window.ResizeObserver(_ => this.resize()) : undefined readonly resizeObserver = window?.ResizeObserver ? new window.ResizeObserver(_ => this.resize()) : undefined


private readonly _canvas: HTMLCanvasElement
// this can be used by other plugins to add ui elements alongside the canvas
private readonly _container: HTMLElement // todo: add a way to move the canvas to a new container... and dispatch event...
/**
* The Scene attached to the viewer, this cannot be changed.
* @type {RootScene}
*/
@uiConfig() @serialize('scene')
private readonly _scene: RootScene
private _needsResize = false
private _isRenderingFrame = false
private _objectProcessor: IObjectProcessor = {
processObject: (object: IObject3D)=>{
if (object.material) {
if (Array.isArray(object.material)) this.assetManager.materials.registerMaterials(object.material)
else this.assetManager.materials.registerMaterial(object.material)
}
},
}
private _needsReset = true // renderer needs reset

// Helpers for tracking main camera change and setting dirty automatically
private _lastCameraPosition: Vector3 = new Vector3() private _lastCameraPosition: Vector3 = new Vector3()
private _lastCameraQuat: Quaternion = new Quaternion() private _lastCameraQuat: Quaternion = new Quaternion()
private _lastCameraTarget: Vector3 = new Vector3() private _lastCameraTarget: Vector3 = new Vector3()
private _tempVec: Vector3 = new Vector3() private _tempVec: Vector3 = new Vector3()
private _tempQuat: Quaternion = new Quaternion() private _tempQuat: Quaternion = new Quaternion()


readonly debug: boolean
/** /**
* Create a viewer instance for using the webgi viewer SDK. * Create a viewer instance for using the webgi viewer SDK.
* @param options - {@link ThreeViewerOptions} * @param options - {@link ThreeViewerOptions}
zPrepass: options.zPrepass ?? options.useGBufferDepth ?? false, zPrepass: options.zPrepass ?? options.useGBufferDepth ?? false,
depthBuffer: !(options.zPrepass ?? options.useGBufferDepth ?? false), depthBuffer: !(options.zPrepass ?? options.useGBufferDepth ?? false),
screenShader: options.screenShader, screenShader: options.screenShader,
renderScale: options.renderScale,
}) })
this.renderManager.addEventListener('animationLoop', this._animationLoop as any) this.renderManager.addEventListener('animationLoop', this._animationLoop as any)
this.renderManager.addEventListener('resize', ()=> this._scene.mainCamera.refreshAspect()) this.renderManager.addEventListener('resize', ()=> this._scene.mainCamera.refreshAspect())


} }


private _objectProcessor: IObjectProcessor = {
processObject: (object: IObject3D)=>{
if (object.material) {
if (Array.isArray(object.material)) this.assetManager.materials.registerMaterials(object.material)
else this.assetManager.materials.registerMaterial(object.material)
}
},
}

// todo: find a better fix for context loss and restore?
private _lastSize = new Vector2()
private _onContextRestore = (_: Event) => {
this.enabled = true
this._canvas.width = this._lastSize.width
this._canvas.height = this._lastSize.height
this.resize()
this._scene.setDirty({refreshScene: true, frameFade: false})
}
private _onContextLost = (_: Event) => {
this._lastSize.set(this._canvas.width, this._canvas.height)
this._canvas.width = 2
this._canvas.height = 2
this.resize()
this.enabled = false
}

/**
* Mark that the canvas is resized. If the size is changed, the renderer and all render targets are resized. This happens before the render of the next frame.
*/
resize = () => {
this._needsResize = true
this.setDirty()
}

private _needsReset = true // renderer reset
/**
* Set the viewer to dirty and trigger render of the next frame.
* @param source - The source of the dirty event. like plugin or 3d object
* @param event - The event that triggered the dirty event.
*/
setDirty(source?: any, event?: Event): void {
this._needsReset = true
source = source ?? this
this.dispatchEvent({...event ?? {}, type: 'update', source})
}

/**
* The renderer for the viewer that's attached to the canvas. This is wrapper around WebGLRenderer and EffectComposer and manages post-processing passes and rendering logic
* @deprecated - use {@link renderManager} instead
*/
get renderer(): ViewerRenderManager {
this.console.error('renderer is deprecated, use renderManager instead')
return this.renderManager
}

/** /**
* Add an object/model/material/viewer-config/plugin-preset/... to the viewer scene from url or an {@link IAsset} object. * Add an object/model/material/viewer-config/plugin-preset/... to the viewer scene from url or an {@link IAsset} object.
* Same as {@link AssetManager.addAssetSingle} * Same as {@link AssetManager.addAssetSingle}
return await this.assetManager.importer.importSingle<T>(obj, options) return await this.assetManager.importer.importSingle<T>(obj, options)
} }


/**
* Exports an object/mesh/material/texture/render-target/plugin-preset/viewer to a blob.
* If no object is given, a glb is exported with the current viewer state.
* @param obj
* @param options
*/
async export(obj?: IObject3D|IMaterial|ITexture|IRenderTarget|IViewerPlugin|(typeof this), options?: ExportFileOptions) {
if (!obj) obj = this._scene // this will export the glb with the scene and viewer config
if ((<typeof this>obj).type === this.type) return jsonToBlob((<typeof this>obj).exportConfig())
if ((<IViewerPlugin>obj).constructor?.PluginType) return jsonToBlob(this.exportPluginConfig(<IViewerPlugin>obj))
return await this.assetManager.exporter.exportObject(<IObject3D|IMaterial|ITexture|IRenderTarget>obj, options)
}

/** /**
* Set the environment map of the scene from url or an {@link IAsset} object. * Set the environment map of the scene from url or an {@link IAsset} object.
* @param map * @param map
return this._scene.background return this._scene.background
} }


/**
* Exports an object/mesh/material/texture/render-target/plugin-preset/viewer to a blob.
* If no object is given, a glb is exported with the current viewer state.
* @param obj
* @param options
*/
async export(obj?: IObject3D|IMaterial|ITexture|IRenderTarget|IViewerPlugin|(typeof this), options?: ExportFileOptions) {
if (!obj) obj = this._scene // this will export the glb with the scene and viewer config
if ((<typeof this>obj).type === this.type) return jsonToBlob((<typeof this>obj).exportConfig())
if ((<IViewerPlugin>obj).constructor?.PluginType) return jsonToBlob(this.exportPluginConfig(<IViewerPlugin>obj))
return await this.assetManager.exporter.exportObject(<IObject3D|IMaterial|ITexture|IRenderTarget>obj, options)
}

/**
* Export the scene to a file (default: glb with viewer config) and return a blob
* @param options
*/
async exportScene(options?: ExportFileOptions): Promise<BlobExt | undefined> {
return this.assetManager.exporter.exportObject(this._scene.modelRoot, options)
}

async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null> {
const blobPromise = async()=> new Promise<Blob|null>((resolve) => {
this._canvas.toBlob((blob) => {
resolve(blob)
}, mimeType, quality)
})
if (!this.renderEnabled) return blobPromise()
return await this.doOnce('postFrame', async() => {
this.renderEnabled = false
const blob = await blobPromise()
this.renderEnabled = true
return blob
})
}

async getScreenshotDataUrl({mimeType = 'image/jpeg', quality = 90} = {}): Promise<string | null> {
if (!this.renderEnabled) return this._canvas.toDataURL(mimeType, quality)
return await this.doOnce('postFrame', () => this._canvas.toDataURL(mimeType, quality))
}

/** /**
* Disposes the viewer and frees up all resource and events. Do not use the viewer after calling dispose. * Disposes the viewer and frees up all resource and events. Do not use the viewer after calling dispose.
* @note - If you want to reuse the viewer, set viewer.enabled to false instead, then set it to true again when required. To dispose all the objects, materials in the scene use `viewer.scene.disposeSceneModels()` * @note - If you want to reuse the viewer, set viewer.enabled to false instead, then set it to true again when required. To dispose all the objects, materials in the scene use `viewer.scene.disposeSceneModels()`
this.dispatchEvent({type: 'dispose'}) this.dispatchEvent({type: 'dispose'})
} }


private _animationLoop(event: IAnimationLoopEvent): void {
/**
* Mark that the canvas is resized. If the size is changed, the renderer and all render targets are resized. This happens before the render of the next frame.
*/
resize = () => {
this._needsResize = true
this.setDirty()
}

/**
* Set the viewer to dirty and trigger render of the next frame.
* @param source - The source of the dirty event. like plugin or 3d object
* @param event - The event that triggered the dirty event.
*/
setDirty(source?: any, event?: Event): void {
this._needsReset = true
source = source ?? this
this.dispatchEvent({...event ?? {}, type: 'update', source})
}

protected _animationLoop(event: IAnimationLoopEvent): void {
if (!this.enabled || !this.renderEnabled) return if (!this.enabled || !this.renderEnabled) return
if (this._isRenderingFrame) { if (this._isRenderingFrame) {
this.console.warn('animation loop: frame skip') // not possible actually, since this is not async this.console.warn('animation loop: frame skip') // not possible actually, since this is not async


} }



/**
* Get the HTML Element containing the canvas
* @returns {HTMLElement}
*/
get container(): HTMLElement {
return this._container
}

/**
* Get the HTML Canvas Element where the viewer is rendering
* @returns {HTMLCanvasElement}
*/
get canvas(): HTMLCanvasElement {
return this._canvas
}

/** /**
* Get the Plugin by a constructor type or by the string type. * Get the Plugin by a constructor type or by the string type.
* Use string type if the plugin is not a dependency and you don't want to bundle the plugin. * Use string type if the plugin is not a dependency and you don't want to bundle the plugin.
} }


/** /**
* Get the Plugin by the string type.
* @deprecated - Use {@link getPlugin} instead.
* Get the Plugin by a constructor type or add a new plugin of the specified type if it doesn't exist.
* @param type * @param type
* @returns {T | undefined}
* @param args - arguments for the constructor of the plugin, used when a new plugin is created.
*/ */
getPluginByType<T extends IViewerPlugin>(type: string): T | undefined {
return this.plugins[type] as T | undefined
}

async getOrAddPlugin<T extends IViewerPlugin>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): Promise<T> { async getOrAddPlugin<T extends IViewerPlugin>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): Promise<T> {
const plugin = this.getPlugin(type) const plugin = this.getPlugin(type)
if (plugin) return plugin if (plugin) return plugin
return this.addPlugin(type, ...args) return this.addPlugin(type, ...args)
} }


/**
* Get the Plugin by a constructor type or add a new plugin to the viewer of the specified type if it doesn't exist(sync).
* @param type
* @param args - arguments for the constructor of the plugin, used when a new plugin is created.
*/
getOrAddPluginSync<T extends IViewerPluginSync>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): T { getOrAddPluginSync<T extends IViewerPluginSync>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): T {
const plugin = this.getPlugin(type) const plugin = this.getPlugin(type)
if (plugin) return plugin if (plugin) return plugin
return p return p
} }


/**
* Add multiple plugins to the viewer.
* @param plugins - List of plugin instances or classes
*/
async addPlugins(plugins: (IViewerPlugin | Class<IViewerPlugin>)[]): Promise<void> { async addPlugins(plugins: (IViewerPlugin | Class<IViewerPlugin>)[]): Promise<void> {
for (const p of plugins) await this.addPlugin(p) for (const p of plugins) await this.addPlugin(p)
} }


/**
* Add multiple plugins to the viewer(sync).
* @param plugins - List of plugin instances or classes
*/
async addPluginsSync(plugins: (IViewerPluginSync | Class<IViewerPluginSync>)[]): Promise<void> { async addPluginsSync(plugins: (IViewerPluginSync | Class<IViewerPluginSync>)[]): Promise<void> {
for (const p of plugins) this.addPluginSync(p) for (const p of plugins) this.addPluginSync(p)
} }
this.setDirty(p) this.setDirty(p)
} }


/**
* Remove a plugin instance or a plugin class(sync). Works similar to {@link ThreeViewer.addPluginSync}
* @param p
* @param dispose
*/
removePluginSync(p: IViewerPluginSync, dispose = true): void { removePluginSync(p: IViewerPluginSync, dispose = true): void {
const type = p.constructor.PluginType const type = p.constructor.PluginType
if (!this.plugins[type]) return if (!this.plugins[type]) return
this.resize() this.resize()
} }


// private _addSceneObject = (e: IEvent<any>) => {
// if (!e || !e.object) return
// const config = e.object.__importedViewerConfig // this is set in gltf.ts when gltf file is imported. This is done here so that scene settings are applied whenever the imported object is added to scene.
// if (!config) return
// this.fromJSON(config, config.resources)
// }


/**
* @deprecated use {@link assetManager} instead.
* Gets the Asset manager, contains useful functions for managing, loading and inserting assets.
*/
getManager(): AssetManager|undefined {
return this.assetManager
}

// todo
// public async fitToView(selected?: Object3D, distanceMultiplier = 1.5, duration?: number, ease?: Easing|EasingFunctionType) {
// const camViews = this.getPluginByType<CameraViewPlugin>('CameraViews')
// if (!camViews) {
// this.console.error('CameraViews plugin is required for fitToView to work')
// return
// }
// await camViews?.animateToFitObject(selected, distanceMultiplier, duration, ease, {min: (this.scene.activeCamera.getControls<OrbitControls3>()?.minDistance ?? 0.5) + 0.5, max: 1000.0})
// }

/** /**
* Traverse all objects in scene model root. * Traverse all objects in scene model root.
* @param callback * @param callback
this._scene.modelRoot.traverse(callback) this._scene.modelRoot.traverse(callback)
} }


// todo: create/load texture utils
/**
* Add an object to the scene model root.
* If an imported scene model root is passed, it will be loaded with viewer configuration, unless importConfig is false
* @param imported
* @param options
*/
async addSceneObject<T extends IObject3D|Object3D|RootSceneImportResult = RootSceneImportResult>(imported: T, options?: AddObjectOptions): Promise<T> {
if (imported.userData?.rootSceneModelRoot) {
const obj = <RootSceneImportResult>imported
if (obj.importedViewerConfig && options?.importConfig !== false) await this.importConfig(obj.importedViewerConfig)
this._scene.loadModelRoot(obj, options)
return this._scene.modelRoot as T
}
this._scene.addObject(imported, options)
return imported
}



/** /**
* Serialize all the plugins and their settings to save or create presets. Used in {@link toJSON}. * Serialize all the plugins and their settings to save or create presets. Used in {@link toJSON}.
return data return data
} }



/** /**
* Deserialize all the viewer and plugin settings. * Deserialize all the viewer and plugin settings.
* @note use async {@link ThreeViewer.importConfig} to import a json/config exported with {@link ThreeViewer.exportConfig} or {@link ThreeViewer.toJSON}. * @note use async {@link ThreeViewer.importConfig} to import a json/config exported with {@link ThreeViewer.exportConfig} or {@link ThreeViewer.toJSON}.
return await MetaImporter.ImportMeta(meta, extraResources) return await MetaImporter.ImportMeta(meta, extraResources)
} }


async addSceneObject<T extends IObject3D|Object3D|RootSceneImportResult = RootSceneImportResult>(imported: T, options?: AddObjectOptions): Promise<T> {
if (imported.userData?.rootSceneModelRoot) {
const obj = <RootSceneImportResult>imported
if (obj.importedViewerConfig && options?.importConfig !== false) await this.importConfig(obj.importedViewerConfig)
this._scene.loadModelRoot(obj, options)
return this._scene.modelRoot as T
}
this._scene.addObject(imported, options)
return imported
async doOnce<TRet>(event: IViewerEventTypes, func: (...args: any[]) => TRet): Promise<TRet> {
return new Promise((resolve) => {
const listener = async(...args: any[]) => {
this.removeEventListener(event, listener)
resolve(await func(...args))
}
this.addEventListener(event, listener)
})
} }


private _setActiveCameraView(event: any = {}): void { private _setActiveCameraView(event: any = {}): void {
return p return p
} }


async doOnce<TRet>(event: IViewerEventTypes, func: (...args: any[]) => TRet): Promise<TRet> {
return new Promise((resolve) => {
const listener = async(...args: any[]) => {
this.removeEventListener(event, listener)
resolve(await func(...args))
}
this.addEventListener(event, listener)
})
}

/**
* Export the scene to a file (default: glb with viewer config) and return a blob
* @param options
*/
async exportScene(options?: ExportFileOptions): Promise<BlobExt | undefined> {
return this.assetManager.exporter.exportObject(this._scene.modelRoot, options)
}

async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null> {
const blobPromise = async()=> new Promise<Blob|null>((resolve) => {
this._canvas.toBlob((blob) => {
resolve(blob)
}, mimeType, quality)
})
if (!this.renderEnabled) return blobPromise()
return await this.doOnce('postFrame', async() => {
this.renderEnabled = false
const blob = await blobPromise()
this.renderEnabled = true
return blob
})
}

async getScreenshotDataUrl({mimeType = 'image/jpeg', quality = 90} = {}): Promise<string | null> {
if (!this.renderEnabled) return this._canvas.toDataURL(mimeType, quality)
return await this.doOnce('postFrame', () => this._canvas.toDataURL(mimeType, quality))
}


private _renderEnabledChanged(): void { private _renderEnabledChanged(): void {
this.dispatchEvent({type: this.renderEnabled ? 'renderEnabled' : 'renderDisabled'}) this.dispatchEvent({type: this.renderEnabled ? 'renderEnabled' : 'renderDisabled'})
} }
plugins: [], plugins: [],
} }


// todo: find a better fix for context loss and restore?
private _lastSize = new Vector2()
private _onContextRestore = (_: Event) => {
this.enabled = true
this._canvas.width = this._lastSize.width
this._canvas.height = this._lastSize.height
this.resize()
this._scene.setDirty({refreshScene: true, frameFade: false})
}
private _onContextLost = (_: Event) => {
this._lastSize.set(this._canvas.width, this._canvas.height)
this._canvas.width = 2
this._canvas.height = 2
this.resize()
this.enabled = false
}

// private _addSceneObject = (e: IEvent<any>) => {
// if (!e || !e.object) return
// const config = e.object.__importedViewerConfig // this is set in gltf.ts when gltf file is imported. This is done here so that scene settings are applied whenever the imported object is added to scene.
// if (!config) return
// this.fromJSON(config, config.resources)
// }

// todo
// public async fitToView(selected?: Object3D, distanceMultiplier = 1.5, duration?: number, ease?: Easing|EasingFunctionType) {
// const camViews = this.getPluginByType<CameraViewPlugin>('CameraViews')
// if (!camViews) {
// this.console.error('CameraViews plugin is required for fitToView to work')
// return
// }
// await camViews?.animateToFitObject(selected, distanceMultiplier, duration, ease, {min: (this.scene.activeCamera.getControls<OrbitControls3>()?.minDistance ?? 0.5) + 0.5, max: 1000.0})
// }

// todo: create/load texture utils

// region legacy creation functions // region legacy creation functions


// /** // /**


// endregion // endregion


/**
* The renderer for the viewer that's attached to the canvas. This is wrapper around WebGLRenderer and EffectComposer and manages post-processing passes and rendering logic
* @deprecated - use {@link renderManager} instead
*/
get renderer(): ViewerRenderManager {
this.console.error('renderer is deprecated, use renderManager instead')
return this.renderManager
}

/**
* @deprecated use {@link assetManager} instead.
* Gets the Asset manager, contains useful functions for managing, loading and inserting assets.
*/
getManager(): AssetManager|undefined {
return this.assetManager
}

/**
* Get the Plugin by the string type.
* @deprecated - Use {@link getPlugin} instead.
* @param type
* @returns {T | undefined}
*/
getPluginByType<T extends IViewerPlugin>(type: string): T | undefined {
return this.plugins[type] as T | undefined
}

} }

Loading…
取消
儲存