Pārlūkot izejas kodu

Some rearrangement, minor fixes and docs.

master
Palash Bansal pirms 2 gadiem
vecāks
revīzija
4068b58986
Revīzijas autora e-pasta adrese nav piesaistīta nevienam kontam

+ 1
- 2
.eslintrc.cjs Parādīt failu

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

+ 4
- 4
package.json Parādīt failu

@@ -1,6 +1,6 @@
{
"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.",
"main": "src/index.ts",
"module": "dist/index.mjs",
@@ -102,7 +102,7 @@
"popmotion": "^11.0.5"
},
"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/wicg-file-system-access": "^2020.9.5",
"ts-browser-helpers": "^0.8.0"
@@ -113,8 +113,8 @@
"ts-browser-helpers": "^0.8.0",
"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",
"@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"
},
"local_dependencies": {

+ 4
- 3
scripts/utils.mjs Parādīt failu

@@ -11,13 +11,14 @@ export function loopPluginDirs(callback){
const pluginDir = path.join(pluginsDir, pluginFolder)
const packageJsonPath = path.join(pluginDir, 'package.json')
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}`)
execSync(command, {cwd: pluginDir, stdio: 'inherit'})
})

+ 4
- 1
src/assetmanager/AssetManager.ts Parādīt failu

@@ -74,7 +74,6 @@ export type AddRawOptions = ProcessRawOptions & AddAssetOptions
* @category Asset Manager
*/
export class AssetManager extends EventDispatcher<BaseEvent&{data: ImportResult}, 'loadAsset'> {
static readonly PluginType = 'AssetManager'
readonly viewer: ThreeViewer
readonly importer: AssetImporter
readonly exporter: AssetExporter
@@ -497,5 +496,9 @@ export class AssetManager extends EventDispatcher<BaseEvent&{data: ImportResult}
return this.viewer?.loadConfigResources(json, extraResources)
}

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

+ 1
- 0
src/core/IRenderer.ts Parādīt failu

@@ -81,6 +81,7 @@ export interface IRenderManagerOptions {
rgbm?: boolean,
msaa?: boolean,
depthBuffer?: boolean,
renderScale?: number,
}

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

+ 1
- 1
src/core/IScene.ts Parādīt failu

@@ -107,7 +107,7 @@ export interface IScene<E extends ISceneEvent = ISceneEvent, ET extends ISceneEv
// region deprecated

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

+ 106
- 91
src/core/object/RootScene.ts Parādīt failu

@@ -36,12 +36,12 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
readonly modelRoot: IObject3D

@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

@onChange2(RootScene.prototype._onBackgroundChange)
@onChange2(RootScene.prototype.onBackgroundChange)
@serialize() @uiImage('Background Image')
background: null | Color | ITexture | 'environment' = null
/**
@@ -266,7 +266,7 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
this.refreshUi?.()
}

private _onBackgroundChange() {
onBackgroundChange() {
this.dispatchEvent({type: 'backgroundChanged', background: this.background, backgroundColor: this.backgroundColor})
this.setDirty({refreshScene: true, geometryChanged: false})
this.refreshUi?.()
@@ -358,20 +358,6 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
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.
* @param precise
@@ -414,36 +400,6 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
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
* @param meta
@@ -480,49 +436,6 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
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
// re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936
@@ -581,6 +494,108 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
// }


/**
* 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.
* @deprecated renamed to {@link addObject}

+ 16
- 2
src/rendering/RenderManager.ts Parādīt failu

@@ -107,11 +107,12 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen
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()
this._animationLoop = this._animationLoop.bind(this)
// this._xrPreAnimationLoop = this._xrPreAnimationLoop.bind(this)
this._renderSize = new Vector2(canvas.clientWidth, canvas.clientHeight)
this._renderScale = renderScale
this._renderer = this._initWebGLRenderer(canvas, alpha)
this._context = this._renderer.getContext()
this._isWebGL2 = this._renderer.capabilities.isWebGL2
@@ -346,6 +347,9 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen
return this._context
}

/**
* Same as {@link renderer}
*/
get webglRenderer(): WebGLRenderer {
return this._renderer
}
@@ -369,7 +373,7 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen
// region Utils

/**
*
* blit - blits a texture to the screen or another render target.
* @param destination - destination target, or screen if undefined or null
* @param source - source Texture
* @param viewport - viewport and scissor
@@ -518,6 +522,10 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen
return string
}

/**
* Rend pixels from a render target into a new Uint8Array|Uint16Array|Float32Array buffer
* @param target
*/
renderTargetToBuffer(target: WebGLRenderTarget): Uint8Array|Uint16Array|Float32Array {
const buffer =
target.texture.type === HalfFloatType ?
@@ -529,6 +537,12 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen
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 {
const hdrFormats = ['image/x-exr']
let hdr = target.texture.type === HalfFloatType || target.texture.type === FloatType

+ 34
- 1
src/utils/CustomContextMenu.ts Parādīt failu

@@ -1,9 +1,23 @@
import styles from './CustomContextMenu.css'

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

/**
* Indicates whether the context menu has been initialized.
*/
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 {
this._inited = true
document.addEventListener('pointerdown', (e) => {
@@ -13,7 +27,23 @@ export class CustomContextMenu {
})
}

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.Element) this.Remove()
@@ -40,6 +70,9 @@ export class CustomContextMenu {
return container
}

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

+ 1
- 0
src/utils/serialization.ts Parādīt failu

@@ -759,6 +759,7 @@ export function metaToResources(meta?: SerializationMetaType): Partial<Serializa
if (res._context) delete res._context
return res
}

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

+ 9
- 0
src/utils/shader-helpers.ts Parādīt failu

@@ -1,5 +1,14 @@
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, {
replaceAll = false,
prepend = false,

+ 237
- 209
src/viewer/ThreeViewer.ts Parādīt failu

@@ -104,7 +104,9 @@ export interface ThreeViewerOptions {
*/
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
/**
@@ -112,6 +114,14 @@ export interface ThreeViewerOptions {
*/
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

/**
@@ -165,62 +175,58 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
}
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.
* @type {boolean}
*/
enabled = true


/**
* Enable or disable all rendering, Animation loop including any frame/render events won't be fired when this is false.
*/
@onChange(ThreeViewer.prototype._renderEnabledChanged)
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.
* Note: should be max (screen refresh rate / animation frame rate) like 60Hz / 30fps
* @type {number}
*/
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.
@@ -228,13 +234,34 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
*/
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 _lastCameraQuat: Quaternion = new Quaternion()
private _lastCameraTarget: Vector3 = new Vector3()
private _tempVec: Vector3 = new Vector3()
private _tempQuat: Quaternion = new Quaternion()

readonly debug: boolean
/**
* Create a viewer instance for using the webgi viewer SDK.
* @param options - {@link ThreeViewerOptions}
@@ -324,6 +351,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
zPrepass: options.zPrepass ?? options.useGBufferDepth ?? false,
depthBuffer: !(options.zPrepass ?? options.useGBufferDepth ?? false),
screenShader: options.screenShader,
renderScale: options.renderScale,
})
this.renderManager.addEventListener('animationLoop', this._animationLoop as any)
this.renderManager.addEventListener('resize', ()=> this._scene.mainCamera.refreshAspect())
@@ -356,61 +384,6 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes

}

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.
* Same as {@link AssetManager.addAssetSingle}
@@ -433,19 +406,6 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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.
* @param map
@@ -470,6 +430,47 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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.
* @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()`
@@ -496,7 +497,26 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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._isRenderingFrame) {
this.console.warn('animation loop: frame skip') // not possible actually, since this is not async
@@ -581,23 +601,6 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes

}


/**
* 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.
* Use string type if the plugin is not a dependency and you don't want to bundle the plugin.
@@ -609,21 +612,21 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
}

/**
* 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
* @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> {
const plugin = this.getPlugin(type)
if (plugin) return plugin
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 {
const plugin = this.getPlugin(type)
if (plugin) return plugin
@@ -686,10 +689,18 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
return p
}

/**
* Add multiple plugins to the viewer.
* @param plugins - List of plugin instances or classes
*/
async addPlugins(plugins: (IViewerPlugin | Class<IViewerPlugin>)[]): Promise<void> {
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> {
for (const p of plugins) this.addPluginSync(p)
}
@@ -709,6 +720,11 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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 {
const type = p.constructor.PluginType
if (!this.plugins[type]) return
@@ -731,32 +747,6 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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.
* @param callback
@@ -765,7 +755,23 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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}.
@@ -904,7 +910,6 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
return data
}


/**
* 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}.
@@ -995,15 +1000,14 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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 {
@@ -1039,45 +1043,6 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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 {
this.dispatchEvent({type: this.renderEnabled ? 'renderEnabled' : 'renderDisabled'})
}
@@ -1093,6 +1058,42 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
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

// /**
@@ -1145,4 +1146,31 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes

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

}

Notiek ielāde…
Atcelt
Saglabāt