| @@ -48,6 +48,7 @@ | |||
| "files": [ | |||
| "dist", | |||
| "src", | |||
| "lib", | |||
| "examples", | |||
| "plugins/*/dist", | |||
| "plugins/*/src", | |||
| @@ -1,4 +1,4 @@ | |||
| import {IDisposable, PartialRecord} from 'ts-browser-helpers' | |||
| import {PartialRecord} from 'ts-browser-helpers' | |||
| import { | |||
| Blending, | |||
| Clock, | |||
| @@ -36,7 +36,7 @@ export type IRenderManagerEvent = Partial<IAnimationLoopEvent>&Partial<IRenderMa | |||
| } | |||
| export type IRenderManagerEventTypes = 'animationLoop'|'update'|'resize'|'contextLost'|'contextRestored' | |||
| export interface RendererBlitOptions {source?: Texture, viewport?: Vector4, material?: ShaderMaterial, clear?: boolean, respectColorSpace?: boolean, blending?: Blending, transparent?: boolean} | |||
| export interface IRenderManager<E extends IRenderManagerEvent = IRenderManagerEvent, ET extends string = IRenderManagerEventTypes> extends RenderTargetManager<E, ET>, IDisposable, IShaderPropertiesUpdater{ | |||
| export interface IRenderManager<E extends IRenderManagerEvent = IRenderManagerEvent, ET extends string = IRenderManagerEventTypes> extends RenderTargetManager<E, ET>, IShaderPropertiesUpdater{ | |||
| readonly renderer: IWebGLRenderer | |||
| readonly needsRender: boolean | |||
| rebuildPipeline(setDirty?: boolean): void | |||
| @@ -262,7 +262,7 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| this.setDirty({refreshScene: true}) | |||
| } | |||
| @uiButton() | |||
| @uiButton(undefined, {sendArgs: false}) | |||
| centerAllGeometries(keepPosition = true, obj?: IObject3D) { | |||
| const geoms = new Set<IGeometry>() | |||
| ;(obj ?? this.modelRoot).traverse((o) => o.geometry && geoms.add(o.geometry)) | |||
| @@ -276,10 +276,14 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| setDirty && this.setDirty({refreshScene: true}) | |||
| } | |||
| disposeSceneModels(setDirty = true) { | |||
| [...this.modelRoot.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent()) | |||
| this.modelRoot.clear() | |||
| if (setDirty) this.setDirty({refreshScene: true}) | |||
| disposeSceneModels(setDirty = true, clear = true) { | |||
| if (clear) { | |||
| [...this.modelRoot.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent()) | |||
| this.modelRoot.clear() | |||
| if (setDirty) this.setDirty({refreshScene: true}) | |||
| } else { | |||
| this.modelRoot.children.forEach(child => child.dispose && child.dispose()) | |||
| } | |||
| } | |||
| private _onEnvironmentChange() { | |||
| @@ -372,16 +376,22 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| * Dispose the scene and clear all resources. | |||
| * @warn Not fully implemented yet, just clears the scene. | |||
| */ | |||
| dispose(): void { | |||
| this.disposeSceneModels(); | |||
| [...this.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent()) | |||
| this.clear() | |||
| dispose(clear = true): void { | |||
| this.disposeSceneModels(false, clear) | |||
| if (clear) { | |||
| [...this.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent()) | |||
| this.clear() | |||
| } | |||
| // todo: dispose more stuff? | |||
| this.environment?.dispose() | |||
| if ((this.background as ITexture)?.isTexture) (this.background as ITexture)?.dispose?.() | |||
| this.environment = null | |||
| this.background = null | |||
| if (clear) { | |||
| this.environment = null | |||
| this.background = null | |||
| } | |||
| return | |||
| } | |||
| @@ -118,7 +118,7 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| return super.onRemove(viewer) | |||
| } | |||
| @uiButton('Reset To First View') | |||
| @uiButton('Reset To First View', {sendArgs: false}) | |||
| public async resetToFirstView(duration = 100) { | |||
| if (this.isDisabled()) return | |||
| this._currentView = undefined | |||
| @@ -138,7 +138,7 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| } | |||
| addView(view: CameraView) { | |||
| this._cameraViews.push(view) | |||
| if (!this._cameraViews.includes(view)) this._cameraViews.push(view) | |||
| view.addEventListener('setView', this._viewSetView as any) | |||
| view.addEventListener('updateView', this._viewUpdateView as any) | |||
| view.addEventListener('deleteView', this._viewDeleteView as any) | |||
| @@ -185,6 +185,10 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| const i = this._cameraViews.indexOf(view) | |||
| if (i >= 0) | |||
| this._cameraViews.splice(i, 1) | |||
| view.removeEventListener('setView', this._viewSetView as any) | |||
| view.removeEventListener('updateView', this._viewUpdateView as any) | |||
| view.removeEventListener('deleteView', this._viewDeleteView as any) | |||
| view.removeEventListener('animateView', this._viewAnimateView as any) | |||
| this.uiConfig.uiRefresh?.() | |||
| this.dispatchEvent({type: 'viewDelete', view}) | |||
| } | |||
| @@ -203,7 +207,7 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| private _currentView: CameraView | undefined | |||
| @uiButton('Focus Next') focusNext = (wrap = true)=>{ | |||
| @uiButton('Focus Next', {sendArgs: false}) focusNext = (wrap = true)=>{ | |||
| if (this._animating) return | |||
| if (this._cameraViews.length < 2) return | |||
| let index = this._cameraViews.findIndex(v=>v === this._currentView) | |||
| @@ -213,7 +217,7 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| else index = index % this._cameraViews.length | |||
| this.animateToView(index) | |||
| } | |||
| @uiButton('Focus Previous') focusPrevious = (wrap = true)=> { | |||
| @uiButton('Focus Previous', {sendArgs: false}) focusPrevious = (wrap = true)=> { | |||
| if (this._animating) return | |||
| if (this._cameraViews.length < 2 || !this._currentView) return | |||
| let index = this._cameraViews.findIndex(v=>v === this._currentView) | |||
| @@ -226,7 +230,7 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| private _popAnimations: AnimationResult[] = [] | |||
| async animateToView(_view: CameraView|number, duration?: number, easing?: Easing|EasingFunctionType, camera?: ICamera, throwOnStop = false) { | |||
| async animateToView(_view: CameraView|number|string, duration?: number, easing?: Easing|EasingFunctionType, camera?: ICamera, throwOnStop = false) { | |||
| camera = camera || this._viewer?.scene.mainCamera | |||
| if (!camera) return | |||
| // if (this._currentView === view) return // todo: also check if the camera is at the correct position and orientation, till then use resetToFirstView to reset current view | |||
| @@ -245,7 +249,13 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| return | |||
| } | |||
| } | |||
| const view = typeof _view === 'number' ? this._cameraViews[_view] : _view | |||
| const view = typeof _view === 'number' ? this._cameraViews[_view] : | |||
| typeof _view === 'string' ? this._cameraViews.find(v=>v.name === _view) : | |||
| _view | |||
| if (!view) { | |||
| this._viewer?.console.warn('Invalid view', _view) | |||
| return | |||
| } | |||
| this._currentView = view | |||
| this._animating = true | |||
| @@ -303,6 +313,7 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| fromJSON(data: any, meta?: any): this | null { | |||
| this._cameraViews.forEach(v=>this.deleteView(v)) // deserialize pushes to the existing array | |||
| if (super.fromJSON(data, meta)) { | |||
| this._cameraViews.forEach(v=>this.addView(v)) | |||
| this.uiConfig.uiRefresh?.() | |||
| return this | |||
| } | |||
| @@ -311,7 +322,7 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| public async animateToObject(selected?: Object3D, distanceMultiplier = 4, duration?: number, ease?: Easing|EasingFunctionType, distanceBounds = {min: 0.5, max: 5.0}) { | |||
| if (!this._viewer) return | |||
| const bbox = new Box3B().expandByObject(selected || this._viewer.scene.modelRoot.modelObject, false, true) | |||
| const bbox = new Box3B().expandByObject(selected || this._viewer.scene.modelRoot, false, true) | |||
| const center = bbox.getCenter(new Vector3()) | |||
| const size = bbox.getSize(new Vector3()) | |||
| const radius = size.length() / 2 | |||
| @@ -340,7 +340,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| this._viewer?.setDirty() | |||
| } | |||
| @uiButton('Stop') | |||
| @uiButton('Stop', {sendArgs: false}) | |||
| stopAnimation(reset = false) { | |||
| this._animationState = 'stopped' | |||
| // safeSetProperty(this._viewer?.getPlugin<PickingPlugin>('Picking'), 'enabled', true) | |||
| @@ -355,7 +355,7 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| } | |||
| @uiButton('Reset') | |||
| @uiButton('Reset', {sendArgs: false}) | |||
| resetAnimation() { | |||
| if (this._animationState !== 'stopped' && this._animationState !== 'none') { | |||
| this.stopAnimation(true) // reset and stop | |||
| @@ -89,7 +89,7 @@ export class CanvasSnapshotPlugin extends AViewerPluginSync<''> { | |||
| quality: 0.9, | |||
| } | |||
| @uiButton('Download .png') | |||
| // @uiButton('Download .png', {sendArgs: false}) | |||
| async downloadSnapshot(filename?: string, options: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {waitForProgressive: true}): Promise<void> { | |||
| if (!this._viewer) return | |||
| if (!options.mimeType && !filename) this.filename = this.filename.split('.').slice(0, -1).join('.') + '.png' | |||
| @@ -97,6 +97,11 @@ export class CanvasSnapshotPlugin extends AViewerPluginSync<''> { | |||
| if (file) await this._viewer.exportBlob(file, file.name) | |||
| } | |||
| @uiButton('Download .png') | |||
| protected async _downloadPng(): Promise<void> { | |||
| this.filename = this.filename.split('.').slice(0, -1).join('.') + '.png' | |||
| return this.downloadSnapshot(undefined, {mimeType: 'image/png'}) | |||
| } | |||
| @uiButton('Download .jpeg') | |||
| protected async _downloadJpeg(): Promise<void> { | |||
| this.filename = this.filename.split('.').slice(0, -1).join('.') + '.jpeg' | |||
| @@ -17,11 +17,11 @@ import {BaseGroundPlugin} from '../base/BaseGroundPlugin' | |||
| import {GBufferRenderPass} from '../../postprocessing' | |||
| import {ThreeViewer} from '../../viewer' | |||
| import {IRenderTarget} from '../../rendering' | |||
| import {uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js' | |||
| import {uiPanelContainer, uiSlider, uiToggle} from 'uiconfig.js' | |||
| import {HVBlurHelper} from '../../three/utils/HVBlurHelper' | |||
| import {shaderReplaceString} from '../../utils' | |||
| @uiFolderContainer('Contact Shadow Ground') | |||
| @uiPanelContainer('Contact Shadow Ground') | |||
| export class ContactShadowGroundPlugin extends BaseGroundPlugin { | |||
| static readonly PluginType = 'ContactShadowGroundPlugin' | |||
| @@ -27,7 +27,7 @@ export class Object3DGeneratorPlugin extends AViewerPluginSync<''> { | |||
| })) | |||
| protected _selectedType = '' | |||
| @uiButton('Generate') | |||
| @uiButton('Generate', {sendArgs: false}) | |||
| generate(type?: string, params?: any, addToScene = true) { | |||
| if (!this._viewer) throw new Error('No viewer') | |||
| const obj = this.generators[type ?? this._selectedType]?.(params) | |||
| @@ -149,7 +149,7 @@ export abstract class SimplifyModifierPlugin extends AViewerPluginSync<''> { | |||
| */ | |||
| protected abstract _simplify(geometry: IGeometry, count: number): IGeometry | |||
| @uiButton('Simplify All') | |||
| @uiButton('Simplify All', {sendArgs: false}) | |||
| async simplifyAll(root?: IObject3D, options?: SimplifyOptions) { | |||
| if (!root && this._viewer) root = this._viewer.scene.modelRoot | |||
| if (!root) { | |||
| @@ -51,7 +51,7 @@ export class FullScreenPlugin extends AViewerPluginSync<'enter'|'exit'> { | |||
| } | |||
| } | |||
| @uiButton('Enter FullScreen') | |||
| @uiButton('Enter FullScreen', {sendArgs: false}) | |||
| async enter(element?: HTMLElement): Promise<void> { | |||
| if (this.isFullScreen()) return | |||
| @@ -82,7 +82,7 @@ export class FullScreenPlugin extends AViewerPluginSync<'enter'|'exit'> { | |||
| return elem.msRequestFullscreen() | |||
| } | |||
| } | |||
| @uiButton('Exit FullScreen') | |||
| @uiButton('Exit FullScreen', {sendArgs: false}) | |||
| async exit(): Promise<void> { | |||
| if (document.exitFullscreen) { | |||
| return document.exitFullscreen() | |||
| @@ -94,7 +94,7 @@ export class FullScreenPlugin extends AViewerPluginSync<'enter'|'exit'> { | |||
| return (document as any).msExitFullscreen() | |||
| } | |||
| } | |||
| @uiButton('Toggle FullScreen') | |||
| @uiButton('Toggle FullScreen', {sendArgs: false}) | |||
| async toggle(element?: HTMLElement): Promise<void> { | |||
| if (this.isFullScreen()) { | |||
| return this.exit() | |||
| @@ -13,7 +13,7 @@ import ParallaxMappingPluginReliefShader from './shaders/ParallaxMappingPlugin.r | |||
| * This is a port of Relief Parallax Mapping from [Rabbid76/graphics-snippets](https://github.com/Rabbid76/graphics-snippets/blob/master/html/technique/parallax_005_parallax_relief_mapping_derivative_tbn.html) | |||
| * @category Plugins | |||
| */ | |||
| @uiFolderContainer('Parallax Mapping') | |||
| @uiFolderContainer('Parallax Bump Mapping (MatExt)') | |||
| export class ParallaxMappingPlugin extends AViewerPluginSync<''> { | |||
| public static PluginType = 'ReliefParallaxMapping' | |||
| @@ -25,8 +25,9 @@ export class GBufferRenderPass<TP extends IPassID=IPassID, T extends WebGLMultip | |||
| preprocessMaterial = (material: IMaterial, renderToGBuffer?: boolean) => { | |||
| renderToGBuffer = renderToGBuffer ?? material.userData.renderToGBuffer | |||
| if (material.userData.pluginsDisabled) renderToGBuffer = false | |||
| if ( | |||
| material.transparent && renderToGBuffer || // transparent and render to gbuffer | |||
| material.transparent && (renderToGBuffer || material.opacity > 0.99) || // transparent and render to gbuffer | |||
| !material.transparent && !material.transmission && renderToGBuffer === false // opaque and dont render to gbuffer | |||
| ) { | |||
| this._transparentMats.add(material) | |||
| @@ -96,7 +96,7 @@ export class RenderManager<TEvent extends BaseEvent = IRenderManagerEvent, TEven | |||
| @onChange2(RenderManager.prototype.rebuildPipeline) | |||
| public autoBuildPipeline = true | |||
| @uiButton('Rebuild Pipeline') | |||
| @uiButton('Rebuild Pipeline', {sendArgs: false}) | |||
| rebuildPipeline(setDirty = true): void { | |||
| this._passesNeedsUpdate = true | |||
| if (setDirty) this._updated({change: 'rebuild'}) | |||
| @@ -275,8 +275,8 @@ export class RenderManager<TEvent extends BaseEvent = IRenderManagerEvent, TEven | |||
| this._updated({change: 'passRefresh'}) | |||
| } | |||
| dispose(): void { | |||
| super.dispose() | |||
| dispose(clear = true): void { | |||
| super.dispose(clear) | |||
| this._renderer.dispose() | |||
| } | |||
| @@ -521,7 +521,7 @@ export class RenderManager<TEvent extends BaseEvent = IRenderManagerEvent, TEven | |||
| * @param quality | |||
| * @param textureIndex - index of the texture to use in the render target (only in case of multiple render target) | |||
| */ | |||
| renderTargetToDataUrl(target: WebGLMultipleRenderTargets|WebGLRenderTarget, mimeType = 'image/png', quality = 90, textureIndex = 0): string { | |||
| renderTargetToDataUrl(target: WebGLMultipleRenderTargets|WebGLRenderTarget|IRenderTarget, mimeType = 'image/png', quality = 90, textureIndex = 0): string { | |||
| const canvas = document.createElement('canvas') | |||
| canvas.width = target.width | |||
| canvas.height = target.height | |||
| @@ -530,11 +530,11 @@ export class RenderManager<TEvent extends BaseEvent = IRenderManagerEvent, TEven | |||
| const texture = Array.isArray(target.texture) ? target.texture[textureIndex] : target.texture | |||
| const imageData = ctx.createImageData(target.width, target.height, {colorSpace: ['display-p3', 'srgb'].includes(texture.colorSpace) ? <PredefinedColorSpace>texture.colorSpace : undefined}) | |||
| if (texture.type === HalfFloatType || texture.type === FloatType) { | |||
| const buffer = this.renderTargetToBuffer(target, textureIndex) | |||
| const buffer = this.renderTargetToBuffer(target as any, textureIndex) | |||
| textureDataToImageData({data: buffer, width: target.width, height: target.height}, texture.colorSpace, imageData) // this handles converting to srgb | |||
| } else { | |||
| // todo: handle rgbm to srgb conversion? | |||
| this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, imageData.data, undefined, textureIndex) | |||
| this._renderer.readRenderTargetPixels(target as any, 0, 0, target.width, target.height, imageData.data, undefined, textureIndex) | |||
| } | |||
| ctx.putImageData(imageData, 0, 0) | |||
| @@ -150,12 +150,14 @@ export abstract class RenderTargetManager<E extends BaseEvent = BaseEvent, ET ex | |||
| protected abstract _createTargetClass(clazz: Class<WebGLRenderTarget>, size: number[], options: WebGLRenderTargetOptions): IRenderTarget | |||
| dispose() { | |||
| dispose(clear = true) { | |||
| this._trackedTargets.forEach(t=>t.dispose()) | |||
| Object.values(this._trackedTempTargets).forEach(t=>t.dispose()) | |||
| this._trackedTargets = [] | |||
| this._releasedTempTargets = {} | |||
| this._trackedTempTargets = [] | |||
| if (clear) { | |||
| this._trackedTargets = [] | |||
| this._releasedTempTargets = {} | |||
| this._trackedTempTargets = [] | |||
| } | |||
| } | |||
| /** | |||
| @@ -570,27 +570,31 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| /** | |||
| * 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()` | |||
| * This function is not fully implemented yet. There might be some memory leaks. | |||
| * This function is not fully implemented yet. There might be some leaks. | |||
| * @todo - return promise? | |||
| */ | |||
| public dispose(): void { | |||
| public dispose(clear = true): void { | |||
| // todo: dispose stuff from constructor etc | |||
| for (const plugin of [...Object.values(this.plugins)]) { | |||
| this.removePlugin(plugin, true) | |||
| if (clear) { | |||
| for (const plugin of [...Object.values(this.plugins)]) { | |||
| this.removePlugin(plugin, true) | |||
| } | |||
| } | |||
| this._scene.dispose() | |||
| this.renderManager.dispose() | |||
| this._scene.dispose(clear) | |||
| this.renderManager.dispose(clear) | |||
| this._canvas.removeEventListener('webglcontextrestored', this._onContextRestore, false) | |||
| this._canvas.removeEventListener('webglcontextlost', this._onContextLost, false) | |||
| if (clear) { | |||
| this._canvas.removeEventListener('webglcontextrestored', this._onContextRestore, false) | |||
| this._canvas.removeEventListener('webglcontextlost', this._onContextLost, false) | |||
| ;(window as any).threeViewers?.splice((window as any).threeViewers.indexOf(this), 1) | |||
| ;(window as any).threeViewers?.splice((window as any).threeViewers.indexOf(this), 1) | |||
| if (this.resizeObserver) this.resizeObserver.unobserve(this._canvas) | |||
| window.removeEventListener('resize', this.resize) | |||
| if (this.resizeObserver) this.resizeObserver.unobserve(this._canvas) | |||
| window.removeEventListener('resize', this.resize) | |||
| } | |||
| this.dispatchEvent({type: 'dispose'}) | |||
| this.dispatchEvent({type: 'dispose', clear}) | |||
| } | |||
| /** | |||
| @@ -1 +1 @@ | |||
| export const VERSION = '0.0.31' | |||
| export const VERSION = '0.0.32' | |||