| @@ -999,10 +999,10 @@ Notes: | |||
| * All plugins that are present in the dependencies array when the plugin is added to the viewer, are created and attached to the viewer in `super.onAdded` | |||
| * Custom events can be dispatched with `this.dispatchEvent`, and subscribed to with `plugin.addEventListener`. The event type must be described in the class signature for typescript autocomplete to work. | |||
| * Event listeners and other hooks can be added and removed in `onAdded` and `onRemove` functions for the viewer and other plugins. | |||
| * To the viewer render the next frame, viewer.setDirty() can be called, or set this.dirty = true in preFrame and reset in postFrame to stop the rendering. (Note that rendering may continue if some other plugin sets the viewer dirty) | |||
| * All Plugins which inherit from AViewerPlugin support serialisation. Create property `serializeWithViewer = false` to disable serialization with the viewer in config and glb or `toJSON: any = undefined` to disable serialisation entirely | |||
| * plugin.toJSON() and plugin.fromJSON() or ThreeSerialization can be used to serialize and deserialize plugins. viewer.exportPluginConfig and viewer.importPluginConfig also exist for this. | |||
| * @serialize('label') decorator can be used to mark any public/private variable as serialisable. label (optional) corresponds to the key in JSON. | |||
| * To the viewer render the next frame, `viewer.setDirty()` can be called, or set `this.dirty = true` in preFrame and reset in postFrame to stop the rendering. (Note that rendering may continue if some other plugin sets the viewer dirty like `ProgressivePlugin` or any of the animation plugins). Check `isConverged` in `ProgressivePlugin` to check if its the final frame. | |||
| * All Plugins which inherit from AViewerPlugin support serialisation. Create property `serializeWithViewer = false` to disable serialisation with the viewer in config and glb or `toJSON: any = undefined` to disable serialisation entirely | |||
| * `plugin.toJSON()` and `plugin.fromJSON()` or `ThreeSerialization` can be used to serialize and deserialize plugins. `viewer.exportPluginConfig` and `viewer.importPluginConfig` also exist for this. | |||
| * @serialize('label') decorator can be used to mark any public/private variable as serializable. label (optional) corresponds to the key in JSON. | |||
| * @serialize supports instances of ITexture, IMaterial, all primitive types, simple JS objects, three.js math classes(Vector2, Vector3, Matrix3...), and some more. | |||
| * uiDecorators can be used to mark properties and functions that will be shown in the Ui. The Ui shows up automatically when TweakpaneUiPlugin is added to the viewer. Plugins have special features in the UI for download preset and saving state. | |||
| @@ -2448,9 +2448,16 @@ API Reference: [Rhino3dmLoadPlugin](https://threepipe.org/docs/classes/Rhino3dmL | |||
| Adds support for loading .3dm files generated by [Rhino 3D](https://www.rhino3d.com/). This plugin includes some changes with how 3dm files are loaded in three.js. The changes are around loading layer and primitive properties when set as reference in the 3dm files. | |||
| It also adds some helpful options to process the model after load. | |||
| ```typescript | |||
| import {Rhino3dmLoadPlugin} from 'threepipe' | |||
| viewer.addPluginSync(new Rhino3dmLoadPlugin()) | |||
| const rhino3dmPlugin = viewer.addPluginSync(new Rhino3dmLoadPlugin()) | |||
| rhino3dmPlugin.importMaterials = true // import materials source from 3dm file | |||
| rhino3dmPlugin.forceLayerMaterials = true // force material source to be layer in 3dm file. | |||
| rhino3dmPlugin.hideLineMesh = true // hide all lines and points in the model. | |||
| rhino3dmPlugin.replaceWithInstancedMesh = true // replace meshes with the same parent, geometry and material with a single instance mesh. | |||
| const mesh = await viewer.load('file.3dm') | |||
| ``` | |||
| @@ -96,6 +96,12 @@ export interface IObject3DUserData extends IImportResultUserData { | |||
| */ | |||
| userSelectable?: boolean | |||
| /** | |||
| * see {@link GLTFAnimationPlugin} | |||
| */ | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| gltfAnim_SyncMaxDuration?: boolean | |||
| // region root scene model root | |||
| /** | |||
| @@ -15,7 +15,7 @@ import {AnyOptions, onChange2, onChange3, serialize} from 'ts-browser-helpers' | |||
| import {PerspectiveCamera2} from '../camera/PerspectiveCamera2' | |||
| import {ThreeSerialization} from '../../utils' | |||
| import {ITexture} from '../ITexture' | |||
| import {AddObjectOptions, IScene, ISceneEvent, ISceneEventTypes, ISceneSetDirtyOptions} from '../IScene' | |||
| import {AddObjectOptions, IScene, ISceneEvent, ISceneEventTypes, ISceneSetDirtyOptions, IWidget} from '../IScene' | |||
| import {iObjectCommons} from './iObjectCommons' | |||
| import {RootSceneImportResult} from '../../assetmanager' | |||
| import {uiColor, uiConfig, uiFolderContainer, uiImage, UiObjectConfig, uiSlider, uiToggle} from 'uiconfig.js' | |||
| @@ -362,11 +362,16 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| * Returns the bounding box of the scene model root. | |||
| * @param precise | |||
| * @param ignoreInvisible | |||
| * @param ignoreWidgets | |||
| * @param ignoreObject | |||
| * @returns {Box3B} | |||
| */ | |||
| getBounds(precise = false, ignoreInvisible = true): Box3B { | |||
| getBounds(precise = false, ignoreInvisible = true, ignoreWidgets = true, ignoreObject?: (obj: Object3D)=>boolean): Box3B { | |||
| // See bboxVisible in userdata in Box3B | |||
| return new Box3B().expandByObject(this, precise, ignoreInvisible) | |||
| return new Box3B().expandByObject(this, precise, ignoreInvisible, (o: any)=>{ | |||
| if (ignoreWidgets && ((o as IWidget).isWidget || o.assetType === 'widget')) return true | |||
| return ignoreObject?.(o) ?? false | |||
| }) | |||
| } | |||
| private _v1 = new Vector3() | |||
| @@ -113,6 +113,11 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| */ | |||
| @uiToggle() @serialize() autoplayOnLoad = false | |||
| /** | |||
| * Sync the duration of all clips based on the max duration, helpful for things like timeline markers | |||
| */ | |||
| @uiToggle('syncMaxDuration(dev)') @serialize() syncMaxDuration = false | |||
| /** | |||
| * Get the current state of the animation. (read only) | |||
| * use {@link playAnimation}, {@link pauseAnimation}, {@link stopAnimation} to change the state. | |||
| @@ -476,7 +481,10 @@ export class GLTFAnimationPlugin extends AViewerPluginSync<'checkpointEnd'|'chec | |||
| if (clips.length < 1) return | |||
| const duration = Math.max(...clips.map(an=>an.duration)) | |||
| // clips.forEach(cp=>cp.duration = duration) // todo: check why do we need to do this? wont this create problems with looping or is it for that so that looping works in sync. | |||
| if (object.userData.gltfAnim_SyncMaxDuration ?? this.syncMaxDuration) { | |||
| clips.forEach(cp=>cp.duration = duration) | |||
| object.userData.gltfAnim_SyncMaxDuration = true | |||
| } // todo: check why do we need to do this? wont this create problems with looping or is it for that so that looping works in sync. | |||
| const mixer = new AnimationMixer(this._viewer.scene.modelRoot) // add to modelRoot so it works with GLTF export... | |||
| const actions = clips.map(an=>mixer.clipAction(an).setLoop(this.loopAnimations ? LoopRepeat : LoopOnce, this.loopRepetitions)) | |||
| @@ -164,7 +164,7 @@ export class ExtendedRenderPass extends RenderPass implements IPipelinePass<'ren | |||
| } | |||
| this.renderToScreen = false // for super RenderPass.render | |||
| if (!renderer.info.autoReset) throw 'renderer.info.autoReset must be true' | |||
| if (renderer.info && !renderer.info.autoReset) throw 'renderer.info.autoReset must be true' | |||
| // Opaque | |||
| { | |||
| @@ -210,7 +210,7 @@ export class ExtendedRenderPass extends RenderPass implements IPipelinePass<'ren | |||
| renderer.autoClearDepth = curClearDepth | |||
| } | |||
| if (renderer.info.render.calls > 0) { | |||
| if (!renderer.info || renderer.info.render.calls > 0) { | |||
| this._blendPass.uniforms.tDiffuse2.value = this.transparentTarget.texture | |||
| this._blendPass.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) | |||
| @@ -246,7 +246,7 @@ export class ExtendedRenderPass extends RenderPass implements IPipelinePass<'ren | |||
| } | |||
| // console.log(renderer.info.render.calls) | |||
| if (renderer.info.render.calls > 0) { | |||
| if (!renderer.info || renderer.info.render.calls > 0) { | |||
| // console.log('missive blit', renderer.info.render.frame) | |||
| this._blendPass.uniforms.tDiffuse2.value = this.transparentTarget.texture | |||
| @@ -5,9 +5,10 @@ export class Box3B extends Box3 { | |||
| private static _box = new Box3B() | |||
| private _vector = new Vector3() | |||
| expandByObject(object: Object3D|IObject3D, precise = false, ignoreInvisible = false): this { | |||
| expandByObject(object: Object3D|IObject3D, precise = false, ignoreInvisible = false, ignoreObject?: (obj: Object3D)=>boolean): this { | |||
| if (object.userData?.bboxVisible === false) return this | |||
| if (!object.visible && ignoreInvisible) return this | |||
| if (ignoreObject && ignoreObject(object)) return this | |||
| // copied the whole function from three.js to pass in ignoreInvisible | |||
| @@ -21,7 +21,7 @@ export class CustomContextMenu { | |||
| private static _initialize(): void { | |||
| this._inited = true | |||
| document.addEventListener('pointerdown', (e) => { | |||
| if (this.Element && !this.Element.contains(e.target as any)) { | |||
| if (this.Element && !this.Element.contains(e.target as any) && e.button === 0) { | |||
| this.Remove() | |||
| } | |||
| }) | |||
| @@ -490,7 +490,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| return this.assetManager.exporter.exportObject(this._scene.modelRoot, options) | |||
| } | |||
| async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null> { | |||
| async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null | undefined> { | |||
| const blobPromise = async()=> new Promise<Blob|null>((resolve) => { | |||
| this._canvas.toBlob((blob) => { | |||
| resolve(blob) | |||
| @@ -505,7 +505,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| }) | |||
| } | |||
| async getScreenshotDataUrl({mimeType = 'image/jpeg', quality = 90} = {}): Promise<string | null> { | |||
| async getScreenshotDataUrl({mimeType = 'image/jpeg', quality = 90} = {}): Promise<string | null | undefined> { | |||
| if (!this.renderEnabled) return this._canvas.toDataURL(mimeType, quality) | |||
| return await this.doOnce('postFrame', () => this._canvas.toDataURL(mimeType, quality)) | |||
| } | |||
| @@ -1039,11 +1039,11 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| return await MetaImporter.ImportMeta(meta, extraResources) | |||
| } | |||
| async doOnce<TRet>(event: IViewerEventTypes, func: (...args: any[]) => TRet): Promise<TRet> { | |||
| async doOnce<TRet>(event: IViewerEventTypes, func?: (...args: any[]) => TRet): Promise<TRet|undefined> { | |||
| return new Promise((resolve) => { | |||
| const listener = async(...args: any[]) => { | |||
| this.removeEventListener(event, listener) | |||
| resolve(await func(...args)) | |||
| resolve(await func?.(...args)) | |||
| } | |||
| this.addEventListener(event, listener) | |||
| }) | |||
| @@ -1063,8 +1063,6 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| private _resolvePluginOrClass<T extends IViewerPlugin>(plugin: T | Class<T>, ...args: ConstructorParameters<Class<T>>): T { | |||
| let p: T | |||
| if ((plugin as Class<IViewerPlugin>).prototype) p = new (plugin as Class<T>)(...args) | |||
| else p = plugin as T | |||
| if ((plugin as Class<IViewerPlugin>).prototype) { | |||
| const p1 = this.getPlugin(plugin as Class<T>) | |||
| if (p1) { | |||