| - [@threepipe/plugin-tweakpane](#threepipeplugin-tweakpane) Tweakpane UI Plugin | - [@threepipe/plugin-tweakpane](#threepipeplugin-tweakpane) Tweakpane UI Plugin | ||||
| - [@threepipe/plugin-blueprintjs](#threepipeplugin-blueprintjs) BlueprintJs UI Plugin | - [@threepipe/plugin-blueprintjs](#threepipeplugin-blueprintjs) BlueprintJs UI Plugin | ||||
| - [@threepipe/plugin-tweakpane-editor](#threepipeplugin-tweakpane-editor) - Tweakpane Editor Plugin | - [@threepipe/plugin-tweakpane-editor](#threepipeplugin-tweakpane-editor) - Tweakpane Editor Plugin | ||||
| - [@threepipe/plugin-extra-importers](#threepipeplugin-extra-importers) - Plugin for loading more file types supported by loaders in three.js | |||||
| - [@threepipe/plugins-extra-importers](#threepipeplugins-extra-importers) - Plugin for loading more file types supported by loaders in three.js | |||||
| - [@threepipe/plugin-blend-importer](#threepipeplugin-blend-importer) - Blender to add support for loading .blend file | - [@threepipe/plugin-blend-importer](#threepipeplugin-blend-importer) - Blender to add support for loading .blend file | ||||
| - [@threepipe/plugin-geometry-generator](#threepipeplugin-extra-importers) - Generate parametric geometry types that can be re-generated from UI/API. | |||||
| - [@threepipe/plugin-geometry-generator](#threepipeplugin-geometry-generator) - Generate parametric geometry types that can be re-generated from UI/API. | |||||
| ## Getting Started | ## Getting Started | ||||
| * ktx - Using [KTXLoadPlugin](#KTXLoadPlugin) | * ktx - Using [KTXLoadPlugin](#KTXLoadPlugin) | ||||
| * ktx2 - Using [KTX2LoadPlugin](#KTX2LoadPlugin) | * ktx2 - Using [KTX2LoadPlugin](#KTX2LoadPlugin) | ||||
| Plugins to support more model formats are available in the package [@threepipe/plugin-extra-importers](#threepipeplugin-extra-importers) including .3ds, | |||||
| Plugins to support more model formats are available in the package [@threepipe/plugins-extra-importers](#threepipeplugins-extra-importers) including .3ds, | |||||
| .3mf, .collada, .amf, .bvh, .vox, .gcode, .mdd, .pcd, .tilt, .wrl, .mpd, .vtk, .xyz | .3mf, .collada, .amf, .bvh, .vox, .gcode, .mdd, .pcd, .tilt, .wrl, .mpd, .vtk, .xyz | ||||
| ## Loading files | ## Loading files | ||||
| camera.controlsMode = 'customOrbit' // this will initialize the controls with the customOrbit constructor and set it on the camera | camera.controlsMode = 'customOrbit' // this will initialize the controls with the customOrbit constructor and set it on the camera | ||||
| // Disable interactions to the camera. (eg when animating) | // Disable interactions to the camera. (eg when animating) | ||||
| camera.interactionsEnabled = false | |||||
| camera.setInteractions(false, 'animation') | |||||
| // Enable interactions back | |||||
| camera.setInteractions(true, 'animation') // this will enable interactions when all the keys have been set to true(which were set to false earlier) | |||||
| // Force refresh aspect ratio (this is done automatically with a ResizeObserver on the canvas in the viewer) | // Force refresh aspect ratio (this is done automatically with a ResizeObserver on the canvas in the viewer) | ||||
| camera.refreshAspect() | camera.refreshAspect() | ||||
| [`camera.setControlsCtor`](https://threepipe.org/docs/classes/PerspectiveCamera2.html#setControlsCtor) - Register a custom camera controls constructor. The controls can be set by setting `controlsMode` to the key/name of the controls. | [`camera.setControlsCtor`](https://threepipe.org/docs/classes/PerspectiveCamera2.html#setControlsCtor) - Register a custom camera controls constructor. The controls can be set by setting `controlsMode` to the key/name of the controls. | ||||
| [`camera.interactionsEnabled`](https://threepipe.org/docs/classes/PerspectiveCamera2.html#interactionsEnabled) - If `true`, the camera can be interacted with. This is useful when animating the camera or using the window scroll or programmatically automating the viewer. | |||||
| [`camera.setInteractions`](https://threepipe.org/docs/classes/PerspectiveCamera2.html#setInteractions) - If `true`, the camera can be interacted with. This is useful when animating the camera or using the window scroll or programmatically automating the viewer. Using this multiple plugins can disable interactions and it will be enabled again when all of them enable it back. | |||||
| [`camera.refreshAspect`](https://threepipe.org/docs/classes/PerspectiveCamera2.html#refreshAspect) - Force refresh aspect ratio (this is done automatically with a ResizeObserver on the canvas in the viewer or when `viewer.resize()` is called) | [`camera.refreshAspect`](https://threepipe.org/docs/classes/PerspectiveCamera2.html#refreshAspect) - Force refresh aspect ratio (this is done automatically with a ResizeObserver on the canvas in the viewer or when `viewer.resize()` is called) | ||||
| }) | }) | ||||
| ``` | ``` | ||||
| ## @threepipe/plugin-extra-importers | |||||
| ## @threepipe/plugins-extra-importers | |||||
| Exports several plugins to add support for various file types. | Exports several plugins to add support for various file types. | ||||
| [Source Code](./plugins/extra-importers/src/index.ts) — | [Source Code](./plugins/extra-importers/src/index.ts) — | ||||
| [API Reference](https://threepipe.org/plugins/extra-importers/docs) | [API Reference](https://threepipe.org/plugins/extra-importers/docs) | ||||
| NPM: `npm install @threepipe/plugin-extra-importers` | |||||
| NPM: `npm install @threepipe/plugins-extra-importers` | |||||
| CDN: https://threepipe.org/plugins/extra-importers/dist/index.mjs | CDN: https://threepipe.org/plugins/extra-importers/dist/index.mjs | ||||
| To add all the plugins at once use `extraImporters`. This adds support for loading all the above file types. | To add all the plugins at once use `extraImporters`. This adds support for loading all the above file types. | ||||
| ```typescript | ```typescript | ||||
| import {ThreeViewer} from 'threepipe' | import {ThreeViewer} from 'threepipe' | ||||
| import {extraImporters} from '@threepipe/plugin-extra-importers' | |||||
| import {extraImporters} from '@threepipe/plugins-extra-importers' | |||||
| const viewer = new ThreeViewer({...}) | const viewer = new ThreeViewer({...}) | ||||
| viewer.addPluginsSync(extraImporters) | viewer.addPluginsSync(extraImporters) |
| // rollup.config.js | // rollup.config.js | ||||
| import commonjs from '@rollup/plugin-commonjs'; | |||||
| import json from '@rollup/plugin-json'; | import json from '@rollup/plugin-json'; | ||||
| import resolve from '@rollup/plugin-node-resolve'; | import resolve from '@rollup/plugin-node-resolve'; | ||||
| import typescript from '@rollup/plugin-typescript'; | import typescript from '@rollup/plugin-typescript'; | ||||
| import license from 'rollup-plugin-license' | |||||
| import packageJson from './package.json' assert {type: 'json'}; | import packageJson from './package.json' assert {type: 'json'}; | ||||
| import path from 'path' | import path from 'path' | ||||
| import {fileURLToPath} from 'url'; | import {fileURLToPath} from 'url'; | ||||
| import glsl from "rollup-plugin-glsl" | import glsl from "rollup-plugin-glsl" | ||||
| import replace from "@rollup/plugin-replace"; | import replace from "@rollup/plugin-replace"; | ||||
| import terser from "@rollup/plugin-terser"; | import terser from "@rollup/plugin-terser"; | ||||
| import commonjs from "@rollup/plugin-commonjs"; | |||||
| import license from "rollup-plugin-license"; | |||||
| const __filename = fileURLToPath(import.meta.url); | const __filename = fileURLToPath(import.meta.url); | ||||
| const __dirname = path.dirname(__filename); | const __dirname = path.dirname(__filename); | ||||
| const settings = { | const settings = { | ||||
| globals: {}, | globals: {}, | ||||
| sourcemap: true | |||||
| sourcemap: isProduction | |||||
| } | } | ||||
| export default { | export default { | ||||
| input: './src/index.ts', | input: './src/index.ts', | ||||
| output: [ | output: [ | ||||
| // { | |||||
| // { | |||||
| // file: main, | // file: main, | ||||
| // name: main, | // name: main, | ||||
| // ...settings, | // ...settings, | ||||
| // preserveModulesRoot: 'src', // optional but useful to create a more plain folder structure | // preserveModulesRoot: 'src', // optional but useful to create a more plain folder structure | ||||
| format: 'es' | format: 'es' | ||||
| }, | }, | ||||
| { | |||||
| isProduction ? { | |||||
| file: browser, | file: browser, | ||||
| ...settings, | ...settings, | ||||
| name: name, | name: name, | ||||
| plugins: [ | plugins: [ | ||||
| isProduction && terser() | isProduction && terser() | ||||
| ] | ] | ||||
| } | |||||
| } : null, | |||||
| ], | ], | ||||
| external: [], | external: [], | ||||
| plugins: [ | plugins: [ |
| import {Object3D} from 'three' | import {Object3D} from 'three' | ||||
| import {Class, serialize} from 'ts-browser-helpers' | |||||
| import {Class, onChange, serialize} from 'ts-browser-helpers' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | import {AViewerPluginSync, ThreeViewer} from '../../viewer' | ||||
| import {BoxSelectionWidget, ObjectPicker, SelectionWidget} from '../../three' | import {BoxSelectionWidget, ObjectPicker, SelectionWidget} from '../../three' | ||||
| import {IObject3D, IObject3DEvent, ISceneEvent} from '../../core' | import {IObject3D, IObject3DEvent, ISceneEvent} from '../../core' | ||||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | ||||
| import {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | |||||
| export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'hoverObjectChanged'|'hitObject'> { | export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'hoverObjectChanged'|'hitObject'> { | ||||
| @serialize() enabled = true | |||||
| private _enableWidget = true | |||||
| @serialize() | |||||
| @onChange(PickingPlugin.prototype._enableChange) | |||||
| enabled = true | |||||
| private _enableChange() { | |||||
| if (!this._viewer) return | |||||
| if (!this.enabled) this.setSelectedObject(undefined) // todo | |||||
| } | |||||
| get picker(): ObjectPicker|undefined { | get picker(): ObjectPicker|undefined { | ||||
| return this._picker | return this._picker | ||||
| // @serialize() // todo | // @serialize() // todo | ||||
| autoFocusHover = false | autoFocusHover = false | ||||
| /** | |||||
| * Note: this is for runtime use only, not serialized | |||||
| */ | |||||
| @onChange(PickingPlugin.prototype._widgetEnabledChange) | |||||
| widgetEnabled = true | |||||
| protected _widgetEnabledChange() { | |||||
| if (this.widgetEnabled && this._picker?.selectedObject) | |||||
| this._widget?.attach(this._picker.selectedObject) | |||||
| else | |||||
| this._widget?.detach() | |||||
| } | |||||
| public setDirty() { | public setDirty() { | ||||
| this._viewer?.setDirty() | this._viewer?.setDirty() | ||||
| } | } | ||||
| onAdded(viewer: ThreeViewer): void { | onAdded(viewer: ThreeViewer): void { | ||||
| super.onAdded(viewer) | super.onAdded(viewer) | ||||
| this._enableChange() | |||||
| this._picker = new ObjectPicker(viewer.scene.modelRoot, viewer.canvas, viewer.scene.mainCamera, (obj)=>{ | this._picker = new ObjectPicker(viewer.scene.modelRoot, viewer.canvas, viewer.scene.mainCamera, (obj)=>{ | ||||
| const hasMat = obj.material | const hasMat = obj.material | ||||
| if (!hasMat) return false | if (!hasMat) return false | ||||
| private _onObjectSelectEvent = (e: IObject3DEvent)=>{ | private _onObjectSelectEvent = (e: IObject3DEvent)=>{ | ||||
| if (e.source === PickingPlugin.PluginType) return | if (e.source === PickingPlugin.PluginType) return | ||||
| if (e.object === undefined && e.value === undefined) console.error('e.object or e.value must be set for picking, can be null to unselect') | if (e.object === undefined && e.value === undefined) console.error('e.object or e.value must be set for picking, can be null to unselect') | ||||
| else this.setSelectedObject(e.value, this.autoFocus || e.focusCamera) | |||||
| else this.setSelectedObject(e.object || e.value, this.autoFocus || e.focusCamera) | |||||
| } | } | ||||
| private _selectedObjectChanged = (e: any) => { | private _selectedObjectChanged = (e: any) => { | ||||
| if (!this._viewer) return | |||||
| this.dispatchEvent(e) | this.dispatchEvent(e) | ||||
| const selected = this._picker?.selectedObject || undefined | |||||
| const selected = this._picker?.selectedObject || undefined // or use e.object. doing this so that listeners can change the selected object in dispatch above | |||||
| const frameFade = this._viewer.getPlugin(FrameFadePlugin) | |||||
| if (frameFade) { | |||||
| if (selected) frameFade.disable(PickingPlugin.PluginType) | |||||
| else frameFade.enable(PickingPlugin.PluginType) | |||||
| } | |||||
| this._viewer.scene.autoNearFarEnabled = !selected // for widgets etc, this can be removed when they are rendered in a separate pass | |||||
| if (this._pickUi) { | if (this._pickUi) { | ||||
| const sUiConfig = (selected as IUiConfigContainer)?.uiConfig | const sUiConfig = (selected as IUiConfigContainer)?.uiConfig | ||||
| } | } | ||||
| const widget = this._widget | const widget = this._widget | ||||
| if (widget && this._enableWidget) { | |||||
| if (widget && this.widgetEnabled) { | |||||
| if (selected) widget.attach(selected) | if (selected) widget.attach(selected) | ||||
| else widget.detach() | else widget.detach() | ||||
| } | } | ||||
| // if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected}) | // if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected}) | ||||
| this._viewer?.setDirty() | |||||
| this._viewer.setDirty() | |||||
| if (this.autoFocus) { | if (this.autoFocus) { | ||||
| // this._viewer?.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)}) | |||||
| // this._viewer.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)}) | |||||
| this.focusObject(selected) | this.focusObject(selected) | ||||
| } | } | ||||
| const selected = this._picker?.hoverObject || undefined | const selected = this._picker?.hoverObject || undefined | ||||
| const widget = this._hoverWidget | const widget = this._hoverWidget | ||||
| if (widget && this._enableWidget) { | |||||
| if (widget && this.widgetEnabled) { | |||||
| if (selected) widget.attach(selected) | if (selected) widget.attach(selected) | ||||
| else widget.detach() | else widget.detach() | ||||
| } | } | ||||
| this._viewer?.fitToView(selected, 1.25, 1000, 'easeOut') | this._viewer?.fitToView(selected, 1.25, 1000, 'easeOut') | ||||
| } | } | ||||
| public enableWidget(enable: boolean): void { | |||||
| this._enableWidget = enable | |||||
| if (enable) { | |||||
| const selected = this._picker?.selectedObject || undefined | |||||
| if (selected) | |||||
| this._widget?.attach(selected) | |||||
| } else { | |||||
| this._widget?.detach() | |||||
| } | |||||
| } | |||||
| private _uiConfigChildren: UiObjectConfig[] = [ | private _uiConfigChildren: UiObjectConfig[] = [ | ||||
| { | { | ||||
| label: 'Enabled', | label: 'Enabled', | ||||
| hidden: ()=>!this.hoverEnabled, | hidden: ()=>!this.hoverEnabled, | ||||
| property: [this, 'autoFocusHover'], | property: [this, 'autoFocusHover'], | ||||
| }, | }, | ||||
| { | |||||
| label: 'Widget Enabled', | |||||
| type: 'checkbox', | |||||
| property: [this, 'widgetEnabled'], | |||||
| }, | |||||
| ] | ] | ||||
| uiConfig: UiObjectConfig = { | uiConfig: UiObjectConfig = { |