| - [DepthBufferPlugin](#depthbufferplugin) - Pre-rendering of depth buffer | - [DepthBufferPlugin](#depthbufferplugin) - Pre-rendering of depth buffer | ||||
| - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer | - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer | ||||
| - [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer | - [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer | ||||
| - [PickingPlugin](#pickingplugin) - Adds support for selecting objects in the viewer with user interactions and selection widgets | |||||
| - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | ||||
| - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | ||||
| - [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas | - [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas | ||||
| todo | todo | ||||
| ## PickingPlugin | |||||
| [//]: # (todo: image) | |||||
| Example: https://threepipe.org/examples/#picking-plugin/ | |||||
| Source Code: [src/plugins/pipeline/PickingPlugin.ts](./src/plugins/interaction/PickingPlugin.ts) | |||||
| API Reference: [PickingPlugin](https://threepipe.org/docs/classes/PickingPlugin.html) | |||||
| Picking Plugin adds support for selecting and hovering over objects in the viewer with user interactions and selection widgets. | |||||
| When the plugin is added to the viewer, it starts listening to the mouse move and click events over the canvas. | |||||
| When an object is clicked, it is selected, | |||||
| and if a UI plugin is added, the uiconfig for the selected object is populated in the interface. | |||||
| The events `selectedObjectChanged`, `hoverObjectChanged`, and `hitObject` can be listened to on the plugin. | |||||
| Picking plugin internally uses [ObjectPicker](https://threepipe.org/docs/classes/ObjectPicker.html), | |||||
| check out the documentation or source code for more information. | |||||
| ```typescript | |||||
| import {ThreeViewer, PickingPlugin} from 'threepipe' | |||||
| const viewer = new ThreeViewer({...}) | |||||
| const pickingPlugin = viewer.addPluginSync(new PickingPlugin()) | |||||
| // Hovering events are also supported, but since its computationally expensive for large scenes it is disabled by default. | |||||
| pickingPlugin.hoverEnabled = true | |||||
| pickingPlugin.addEventListener('hitObject', (e)=>{ | |||||
| // This is fired when the user clicks on the canvas. | |||||
| // The selected object hasn't been changed yet, and we have the option to change it or disable selection at this point. | |||||
| // e.intersects.selectedObject contains the object that the user clicked on. | |||||
| console.log('Hit: ', e.intersects.selectedObject) | |||||
| // It can be changed here | |||||
| // e.intersects.selectedObject = e.intersects.selectedObject.parent // select the parent | |||||
| // e.intersects.selectedObject = null // unselect | |||||
| // Check other properties on the event like intersects, mouse position, normal etc. | |||||
| console.log(e) | |||||
| }) | |||||
| pickingPlugin.addEventListener('selectedObjectChanged', (e)=>{ | |||||
| // This is fired when the selected object is changed. | |||||
| // e.object contains the new selected object. It can be null if nothing is selected. | |||||
| console.log('Selected: ', e.object) | |||||
| }) | |||||
| // Objects can be programmatically selected and unselected | |||||
| // to select | |||||
| pickingPlugin.setSelectedObject(object) | |||||
| // get the selected object | |||||
| console.log(pickingPlugin.getSelectedObject()) | |||||
| // to unselect | |||||
| pickingPlugin.setSelectedObject(null) | |||||
| // Select object with camera animation to the object | |||||
| pickingPlugin.setSelectedObject(object, true) | |||||
| pickingPlugin.addEventListener('hoverObjectChanged', (e)=>{ | |||||
| // This is fired when the hovered object is changed. | |||||
| // e.object contains the new hovered object. | |||||
| console.log('Hovering: ', e.object) | |||||
| }) | |||||
| ``` | |||||
| ## GLTFAnimationPlugin | ## GLTFAnimationPlugin | ||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Picking Plugin</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, IObject3D, PickingPlugin, ThreeViewer} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| }) | |||||
| const picking = viewer.addPluginSync(PickingPlugin) | |||||
| picking.hoverEnabled = true | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf') | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| ui.setupPluginUi(PickingPlugin) | |||||
| picking.addEventListener('hitObject', (e)=>{ | |||||
| console.log('Hit object', e) | |||||
| }) | |||||
| picking.addEventListener('selectedObjectChanged', (e)=>{ | |||||
| console.log('Selected Object Changed', e) | |||||
| }) | |||||
| picking.addEventListener('hoverObjectChanged', (e)=>{ | |||||
| console.log('Hover Object Changed', e) | |||||
| }) | |||||
| } | |||||
| init().then(_testFinish) |
| oldMaterial?: IMaterial|undefined|IMaterial[] // from materialChanged | oldMaterial?: IMaterial|undefined|IMaterial[] // from materialChanged | ||||
| geometry?: IGeometry|undefined // from geometryUpdate, geometryChanged | geometry?: IGeometry|undefined // from geometryUpdate, geometryChanged | ||||
| oldGeometry?: IGeometry|undefined // from geometryChanged | oldGeometry?: IGeometry|undefined // from geometryChanged | ||||
| source?: any | |||||
| } | } | ||||
| export interface ISetDirtyCommonOptions { | export interface ISetDirtyCommonOptions { | ||||
| license?: string | license?: string | ||||
| /** | |||||
| * When false, this object will not be selectable when clicking on it. | |||||
| */ | |||||
| userSelectable?: boolean | |||||
| // region root scene model root | // region root scene model root | ||||
| /** | /** | ||||
| } | } | ||||
| export interface IObject3D<E extends Event = IObject3DEvent, ET = IObject3DEventTypes> extends Object3D<E, ET>, IUiConfigContainer, IDisposable { | export interface IObject3D<E extends Event = IObject3DEvent, ET = IObject3DEventTypes> extends Object3D<E, ET>, IUiConfigContainer, IDisposable { | ||||
| assetType: 'model' | 'light' | 'camera' | |||||
| assetType: 'model' | 'light' | 'camera' | 'widget' | |||||
| isLight?: boolean | isLight?: boolean | ||||
| isCamera?: boolean | isCamera?: boolean | ||||
| isMesh?: boolean | isMesh?: boolean | ||||
| // isGroup?: boolean | // isGroup?: boolean | ||||
| isScene?: boolean | isScene?: boolean | ||||
| // isHelper?: boolean | // isHelper?: boolean | ||||
| isWidget?: boolean | |||||
| readonly isObject3D: true | readonly isObject3D: true | ||||
| material?: IMaterial | IMaterial[] | material?: IMaterial | IMaterial[] |
| export interface ISceneEvent<T extends string = ISceneEventTypes> extends IObject3DEvent<T> { | export interface ISceneEvent<T extends string = ISceneEventTypes> extends IObject3DEvent<T> { | ||||
| scene?: IScene | null | scene?: IScene | null | ||||
| hierarchyChanged?: boolean // for 'sceneUpdate' event | |||||
| // change?: string | // change?: string | ||||
| } | } | ||||
| export type ISceneSetDirtyOptions = IObjectSetDirtyOptions | export type ISceneSetDirtyOptions = IObjectSetDirtyOptions | ||||
| export type ISceneUserData = IObject3DUserData | export type ISceneUserData = IObject3DUserData | ||||
| export type IWidget = IObject3D // todo | |||||
| // todo improve | |||||
| export interface IWidget { | |||||
| attach(object: any): this; | |||||
| detach(): this; | |||||
| isWidget: true; | |||||
| } | |||||
| export interface IScene<E extends ISceneEvent = ISceneEvent, ET extends ISceneEventTypes = ISceneEventTypes> | export interface IScene<E extends ISceneEvent = ISceneEvent, ET extends ISceneEventTypes = ISceneEventTypes> | ||||
| extends Scene<E, ET>, IObject3D<E, ET>, IShaderPropertiesUpdater { | extends Scene<E, ET>, IObject3D<E, ET>, IShaderPropertiesUpdater { |
| label: 'Pick/Focus', | label: 'Pick/Focus', | ||||
| value: ()=>{ | value: ()=>{ | ||||
| // todo instead of dispatching, make a IObject3D.select function | // todo instead of dispatching, make a IObject3D.select function | ||||
| this.dispatchEvent({type: 'select', ui: true, value: this, bubbleToParent: true, focusCamera: true}) | |||||
| this.dispatchEvent({type: 'select', ui: true, object: this, bubbleToParent: true, focusCamera: true}) | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| value: ()=>{ | value: ()=>{ | ||||
| const parent = this.parent | const parent = this.parent | ||||
| if (parent) { | if (parent) { | ||||
| parent.dispatchEvent({type: 'select', ui: true, bubbleToParent: true, value: parent}) | |||||
| parent.dispatchEvent({type: 'select', ui: true, bubbleToParent: true, object: parent}) | |||||
| } | } | ||||
| }, | }, | ||||
| }, | }, |
| if (this.isLight) this.assetType = 'light' | if (this.isLight) this.assetType = 'light' | ||||
| else if (this.isCamera) this.assetType = 'camera' | else if (this.isCamera) this.assetType = 'camera' | ||||
| else if (this.isWidget) this.assetType = 'widget' | |||||
| else this.assetType = 'model' | else this.assetType = 'model' | ||||
| if (parent) this.parentRoot = parent | if (parent) this.parentRoot = parent |
| // interaction | // interaction | ||||
| export {DropzonePlugin, type DropzonePluginOptions} from './interaction/DropzonePlugin' | export {DropzonePlugin, type DropzonePluginOptions} from './interaction/DropzonePlugin' | ||||
| export {FullScreenPlugin} from './interaction/FullScreenPlugin' | export {FullScreenPlugin} from './interaction/FullScreenPlugin' | ||||
| export {PickingPlugin} from './interaction/PickingPlugin' | |||||
| // import | // import | ||||
| export {Rhino3dmLoadPlugin} from './import/Rhino3dmLoadPlugin' | export {Rhino3dmLoadPlugin} from './import/Rhino3dmLoadPlugin' |
| import {Object3D} from 'three' | |||||
| import {Class, serialize} from 'ts-browser-helpers' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {ObjectPicker} from '../../three/utils/ObjectPicker' | |||||
| import {IObject3D, IObject3DEvent, ISceneEvent} from '../../core' | |||||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||||
| import {BoxSelectionWidget, SelectionWidget} from '../../three/utils/SelectionWidget' | |||||
| export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'hoverObjectChanged'|'hitObject'> { | |||||
| @serialize() enabled = true | |||||
| private _enableWidget = true | |||||
| get picker(): ObjectPicker|undefined { | |||||
| return this._picker | |||||
| } | |||||
| static readonly PluginType = 'Picking' | |||||
| private _picker?: ObjectPicker | |||||
| private _widget?: SelectionWidget | |||||
| private _pickUi: boolean | |||||
| get hoverEnabled() { | |||||
| return this._picker?.hoverEnabled ?? false | |||||
| } | |||||
| set hoverEnabled(v: boolean) { | |||||
| if (!this._picker) return | |||||
| this._picker.hoverEnabled = v | |||||
| this.uiConfig && this.uiConfig.uiRefresh?.() | |||||
| } | |||||
| @serialize() | |||||
| autoFocus = false | |||||
| public setDirty() { | |||||
| this._viewer?.setDirty() | |||||
| } | |||||
| constructor(selection: Class<SelectionWidget>|undefined = BoxSelectionWidget, pickUi = true, autoFocus = false) { | |||||
| super() | |||||
| if (selection) { | |||||
| this._widget = new selection() | |||||
| } | |||||
| this._pickUi = pickUi | |||||
| this.autoFocus = autoFocus | |||||
| this.dispatchEvent = this.dispatchEvent.bind(this) | |||||
| } | |||||
| getSelectedObject<T extends IObject3D = IObject3D>(): T|undefined { | |||||
| if (!this.enabled) return | |||||
| return this._picker?.selectedObject as T || undefined | |||||
| } | |||||
| setSelectedObject(object: IObject3D|undefined, focusCamera = false) { // todo: listen to object dispose | |||||
| if (!this.enabled) return | |||||
| if (!this._picker) return | |||||
| const t = this.autoFocus | |||||
| this.autoFocus = false | |||||
| this._picker.selectedObject = object || null | |||||
| this.autoFocus = t | |||||
| if (t || focusCamera) this.focusObject(object) | |||||
| } | |||||
| onAdded(viewer: ThreeViewer): void { | |||||
| super.onAdded(viewer) | |||||
| this._picker = new ObjectPicker(viewer.scene.modelRoot, viewer.canvas, viewer.scene.mainCamera, (obj)=>{ | |||||
| const hasMat = obj.material | |||||
| if (!hasMat) return false | |||||
| let o: IObject3D|null = obj | |||||
| let ret = false | |||||
| while (o) { | |||||
| if (!o.visible) return false | |||||
| if (o.assetType === 'model' || o.assetType === 'light') ret = true | |||||
| if (o.assetType === 'widget') return false | |||||
| if (o.userData.userSelectable === false) return false | |||||
| if (o.userData.bboxVisible === false) return false | |||||
| o = o.parent | |||||
| } | |||||
| return ret | |||||
| }) | |||||
| if (this._widget) viewer.scene.addObject(this._widget, {addToRoot: true}) | |||||
| this._picker.addEventListener('selectedObjectChanged', this._selectedObjectChanged) | |||||
| this._picker.addEventListener('hoverObjectChanged', this.dispatchEvent) | |||||
| this._picker.addEventListener('hitObject', this._onObjectHit) | |||||
| // on material drop on selected object | |||||
| // viewer.scene.addEventListener('addSceneObject', async(e) => { | |||||
| // const obj = e.object | |||||
| // const selected: IModel<Mesh> = this.getSelectedObject()! as any | |||||
| // if (selected | |||||
| // && obj?.assetType === 'material' | |||||
| // && typeof selected?.setMaterial === 'function' | |||||
| // && selected?.modelObject?.isMesh | |||||
| // && await viewer.confirm('Applying material: Apply material to the selected object?') | |||||
| // ) { | |||||
| // const oldMat = selected.material | |||||
| // if (Array.isArray(oldMat)) { | |||||
| // console.warn('Dropping on material array not yet fully supported.') | |||||
| // selected.setMaterial(obj) | |||||
| // } else { | |||||
| // let meshes: IModel<Mesh>[] = Array.from(oldMat?.userData.__appliedMeshes ?? []) | |||||
| // const c = meshes.length > 1 ? !await viewer.confirm('Applying material: Apply to all objects using this material?') : meshes.length < 1 | |||||
| // if (c) meshes = [selected] | |||||
| // for (const mesh of meshes) { | |||||
| // if (mesh) mesh.setMaterial?.(obj) | |||||
| // } | |||||
| // } | |||||
| // } | |||||
| // }) | |||||
| viewer.scene.addEventListener('select', this._onObjectSelectEvent) | |||||
| viewer.scene.addEventListener('sceneUpdate', this._onSceneUpdate) | |||||
| viewer.scene.addEventListener('mainCameraChange', this._mainCameraChange) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer) { | |||||
| viewer.scene.removeEventListener('select', this._onObjectSelectEvent) | |||||
| viewer.scene.removeEventListener('sceneUpdate', this._onSceneUpdate) | |||||
| viewer.scene.removeEventListener('mainCameraChange', this._mainCameraChange) | |||||
| this._widget?.removeFromParent() | |||||
| if (this._picker) { | |||||
| this._picker.removeEventListener('selectedObjectChanged', this._selectedObjectChanged) | |||||
| this._picker.removeEventListener('hoverObjectChanged', this.dispatchEvent) | |||||
| this._picker.removeEventListener('hitObject', this._onObjectHit) | |||||
| this._picker.dispose() | |||||
| this._picker = undefined | |||||
| } | |||||
| super.onRemove(viewer) | |||||
| } | |||||
| dispose() { | |||||
| super.dispose() | |||||
| this._widget?.dispose() | |||||
| } | |||||
| private _mainCameraChange = ()=>{ | |||||
| if (!this._picker || !this._viewer) return | |||||
| this._picker.camera = this._viewer.scene.mainCamera | |||||
| } | |||||
| private _onSceneUpdate = (e: ISceneEvent)=>{ | |||||
| if (!e.hierarchyChanged) return | |||||
| const s = this.getSelectedObject() | |||||
| let inScene = false | |||||
| s?.traverseAncestors((o)=>{ | |||||
| if (o === this._viewer?.scene) inScene = true | |||||
| }) | |||||
| if (!inScene) this.setSelectedObject(undefined) | |||||
| } | |||||
| private _onObjectSelectEvent = (e: IObject3DEvent)=>{ | |||||
| 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') | |||||
| else this.setSelectedObject(e.value, this.autoFocus || e.focusCamera) | |||||
| } | |||||
| private _selectedObjectChanged = (e: any) => { | |||||
| this.dispatchEvent(e) | |||||
| const selected = this._picker?.selectedObject || undefined | |||||
| if (this._pickUi) { | |||||
| const sUiConfig = (selected as IUiConfigContainer)?.uiConfig | |||||
| const ui = this.uiConfig | |||||
| ui.children = [...this._uiConfigChildren] | |||||
| if (sUiConfig) ui.children.push(sUiConfig) | |||||
| ui.uiRefresh?.() | |||||
| } | |||||
| const widget = this._widget | |||||
| if (widget && this._enableWidget) { | |||||
| if (selected) widget.attach(selected) | |||||
| else widget.detach() | |||||
| } | |||||
| // if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected}) | |||||
| this._viewer?.setDirty() | |||||
| if (this.autoFocus) { | |||||
| // this._viewer?.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)}) | |||||
| this.focusObject(selected) | |||||
| } | |||||
| } | |||||
| private _onObjectHit = (e: any)=>{ | |||||
| if (!this._viewer) return | |||||
| if (!this.enabled) { | |||||
| e.intersects.selectedObject = null | |||||
| return | |||||
| } | |||||
| this.dispatchEvent(e) | |||||
| } | |||||
| // @ts-expect-error temporary | |||||
| public async focusObject(selected?: Object3D) { | |||||
| // const camViews = this._viewer?.getPluginByType<CameraViewPlugin>('CameraViews') | |||||
| // await camViews?.animateToFitObject(selected, 1.25, 1000, 'easeOut', {min: (this._viewer?.scene.activeCamera.getControls<OrbitControls3>()?.minDistance ?? 0.5) + 0.5, max: 50.0}) | |||||
| } | |||||
| 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[] = [ | |||||
| { | |||||
| label: 'Enabled', | |||||
| type: 'checkbox', | |||||
| property: [this, 'enabled'], | |||||
| }, | |||||
| { | |||||
| label: 'Hover Enabled', | |||||
| type: 'checkbox', | |||||
| property: [this, 'hoverEnabled'], | |||||
| }, | |||||
| { | |||||
| label: 'AutoFocus', | |||||
| type: 'checkbox', | |||||
| property: [this, 'autoFocus'], | |||||
| onChange: ()=>{ | |||||
| const o = this.getSelectedObject() | |||||
| if (this.autoFocus && o) this.setSelectedObject(o, true) | |||||
| }, | |||||
| }, | |||||
| ] | |||||
| uiConfig: UiObjectConfig = { | |||||
| type: 'panel', | |||||
| label: 'Picker', | |||||
| expanded: true, | |||||
| children: [ | |||||
| ...this._uiConfigChildren, | |||||
| ], | |||||
| } | |||||
| get widget(): SelectionWidget | undefined { | |||||
| return this._widget | |||||
| } | |||||
| } | |||||
| import {Event, EventDispatcher, Intersection, Raycaster, Vector2} from 'three' | |||||
| import {now} from 'ts-browser-helpers' | |||||
| import {ICamera, IObject3D} from '../../core' | |||||
| export class ObjectPicker extends EventDispatcher<Event, 'hoverObjectChanged'|'selectedObjectChanged'|'hitObject'> { | |||||
| private _firstHit: IObject3D | undefined | |||||
| hoverEnabled = false | |||||
| private _root: IObject3D | |||||
| private _camera: ICamera | |||||
| private _mouseDownTime: number | |||||
| private _mouseUpTime: number | |||||
| private _time: number | |||||
| public selectionCondition: (o: IObject3D) => boolean | |||||
| public raycaster: Raycaster | |||||
| public mouse: Vector2 | |||||
| private _selected: IObject3D[] | |||||
| private _hovering: IObject3D[] | |||||
| public cursorStyles: {default: string; down: string} | |||||
| public domElement: HTMLElement | |||||
| constructor(root: IObject3D, domElement: HTMLElement, camera: ICamera, selectionCondition?: (o:IObject3D)=>boolean) { | |||||
| super() | |||||
| this._root = root | |||||
| this._camera = camera | |||||
| this.domElement = domElement | |||||
| this._time = this.time | |||||
| this._mouseDownTime = 0 | |||||
| this._mouseUpTime = 1 | |||||
| this.selectionCondition = selectionCondition ?? ( | |||||
| (selectedObject: any) => { | |||||
| return selectedObject.userData.userSelectable !== false && selectedObject.userData.bboxVisible !== false && selectedObject.material != null && selectedObject.material.type !== 'ShadowMaterial' // sample to select only mesh with material and not shadowmaterial. | |||||
| }) | |||||
| this.raycaster = new Raycaster() | |||||
| this.mouse = new Vector2() | |||||
| this._selected = [] | |||||
| this._hovering = [] | |||||
| this.cursorStyles = { | |||||
| default: 'grab', | |||||
| down: 'grabbing', | |||||
| } | |||||
| this.domElement.style.touchAction = 'none' | |||||
| // this.domElement.style.cursor = this.cursorStyles.default | |||||
| this.domElement.addEventListener('pointermove', this._onPointerMove) | |||||
| this.domElement.addEventListener('pointerleave', this._onPointerLeave) | |||||
| this.domElement.addEventListener('pointerout', this._onPointerLeave) | |||||
| this.domElement.addEventListener('pointercancel', this._onPointerCancel) | |||||
| this.domElement.addEventListener('pointerenter', this._onPointerEnter) | |||||
| this.domElement.addEventListener('pointerdown', this._onPointerDown) | |||||
| this.domElement.addEventListener('pointerup', this._onPointerUp) | |||||
| } | |||||
| dispose() { | |||||
| this.selectedObject = null | |||||
| this.hoverObject = null | |||||
| this.domElement.removeEventListener('pointermove', this._onPointerMove) | |||||
| this.domElement.removeEventListener('pointerleave', this._onPointerLeave) | |||||
| this.domElement.removeEventListener('pointerout', this._onPointerLeave) | |||||
| this.domElement.removeEventListener('pointercancel', this._onPointerCancel) | |||||
| this.domElement.removeEventListener('pointerenter', this._onPointerEnter) | |||||
| this.domElement.removeEventListener('pointerdown', this._onPointerDown) | |||||
| this.domElement.removeEventListener('pointerup', this._onPointerUp) | |||||
| } | |||||
| get camera() { | |||||
| return this._camera | |||||
| } | |||||
| set camera(value) { | |||||
| this._camera = value | |||||
| } | |||||
| get selectedObject(): IObject3D | null { | |||||
| return this._selected.length > 0 ? this._selected[0] : null | |||||
| } | |||||
| set selectedObject(object) { | |||||
| if (!this._selected.length && !object || this._selected.length === 1 && this._selected[0] === object) return | |||||
| this._selected = object ? Array.isArray(object) ? [...object] : [object] : [] | |||||
| this.dispatchEvent({type: 'selectedObjectChanged', object: this.selectedObject}) | |||||
| } | |||||
| get hoverObject() { | |||||
| return this._hovering.length > 0 ? this._hovering[0] : null | |||||
| } | |||||
| set hoverObject(object: IObject3D | IObject3D[] | null) { | |||||
| if (!this._hovering.length && !object || this._hovering.length === 1 && this._hovering[0] === object) return | |||||
| this._hovering = object ? Array.isArray(object) ? [...object] : [object] : [] | |||||
| this.dispatchEvent({type: 'hoverObjectChanged', object: this.hoverObject}) | |||||
| } | |||||
| get time() { | |||||
| this._time = now() | |||||
| return this._time | |||||
| } | |||||
| get isMouseDown() { | |||||
| return this.mouseDownDeltaTime < 0 | |||||
| } | |||||
| get mouseDownDeltaTime() { | |||||
| return this._mouseUpTime - this._mouseDownTime | |||||
| } | |||||
| private _onPointerMove = (event: PointerEvent) => { | |||||
| if (event.isPrimary === false) return | |||||
| this.updateMouseFromEvent(event) | |||||
| if (this.hoverEnabled) | |||||
| this.hoverObject = this.checkIntersection()?.intersects[0].object ?? null | |||||
| } | |||||
| private _onPointerLeave = (event: PointerEvent) => { | |||||
| if (event.isPrimary === false) return | |||||
| this.domElement.style.cursor = this.cursorStyles.default | |||||
| // this.updateMouseFromEvent(event); | |||||
| if (this.hoverEnabled || this.hoverObject) | |||||
| this.hoverObject = null | |||||
| } | |||||
| private _onPointerEnter = (_: PointerEvent) => { | |||||
| // todo dispatch event? | |||||
| } | |||||
| private _onPointerCancel = (_: PointerEvent) => { | |||||
| // todo dispatch event? | |||||
| } | |||||
| updateMouseFromEvent(event: PointerEvent) { | |||||
| const rect = this.domElement.getBoundingClientRect() | |||||
| this.mouse.x = (event.clientX - rect.x) / rect.width * 2 - 1 | |||||
| this.mouse.y = -((event.clientY - rect.y) / rect.height) * 2 + 1 | |||||
| } | |||||
| private _onPointerDown = (event: PointerEvent) => { | |||||
| if (event.isPrimary === false) return | |||||
| this.domElement.style.cursor = this.cursorStyles.down | |||||
| this._mouseDownTime = this.time | |||||
| return undefined | |||||
| } | |||||
| private _onPointerUp = (event: PointerEvent) => { | |||||
| if (event.isPrimary === false) return | |||||
| this.domElement.style.cursor = this.cursorStyles.default | |||||
| this._mouseUpTime = this.time | |||||
| const delta = this.mouseDownDeltaTime | |||||
| if (delta < 200) { | |||||
| // click | |||||
| this._onPointerClick(event) | |||||
| } | |||||
| return undefined | |||||
| } | |||||
| private _onPointerClick = (event: PointerEvent) => { | |||||
| if (event.isPrimary === false) return | |||||
| this.updateMouseFromEvent(event) | |||||
| const intersects = this.checkIntersection() | |||||
| if (intersects) | |||||
| this.dispatchEvent({type: 'hitObject', time: this._mouseUpTime, intersects}) | |||||
| this.selectedObject = intersects?.selectedObject || null | |||||
| } | |||||
| checkIntersection() { | |||||
| const camera = this._camera | |||||
| if (!camera) return null | |||||
| this.raycaster.setFromCamera(this.mouse, camera) | |||||
| let intersects = this.raycaster.intersectObject<IObject3D>(this._root, true) | |||||
| const uniqueIds: number[] = [] | |||||
| const uniqueIntersects = intersects.filter(element => { | |||||
| const isDuplicate = uniqueIds.includes(element.object.id) | |||||
| if (!isDuplicate) { | |||||
| uniqueIds.push(element.object.id) | |||||
| return true | |||||
| } | |||||
| return false | |||||
| }) | |||||
| intersects = uniqueIntersects | |||||
| let selectedObject:IObject3D | null = null | |||||
| let intersect: Intersection<IObject3D> | undefined | |||||
| const intersects2 = [] | |||||
| for (const intersect1 of intersects) { | |||||
| selectedObject = intersect1.object | |||||
| intersect = intersect1 | |||||
| while (selectedObject != null && (!selectedObject.visible || !this.selectionCondition(selectedObject))) { | |||||
| selectedObject = selectedObject.parent | |||||
| } | |||||
| if (selectedObject != null) intersects2.push(intersect1) | |||||
| } | |||||
| intersects = intersects2 | |||||
| if (intersects.length > 0) { | |||||
| selectedObject = intersects[0].object | |||||
| intersect = intersects[0] | |||||
| if (this._firstHit && selectedObject.id !== this._firstHit.id) { | |||||
| selectedObject = intersect.object | |||||
| } else { | |||||
| for (let i = 0; i < intersects.length; i++) { | |||||
| if (this.selectedObject && this.selectedObject.id === intersects[i].object.id) { | |||||
| const n = i + 1 // Use ( i + 1 ) % intersects.length for looping through objects | |||||
| if (n < intersects.length) { | |||||
| intersect = intersects[n] | |||||
| selectedObject = intersect.object | |||||
| } else { | |||||
| return null | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| this._firstHit = intersects[0].object | |||||
| } | |||||
| if (selectedObject && intersect) { | |||||
| if (selectedObject) // sorted by distance | |||||
| return {selectedObject, intersect, intersects, mouse: this.mouse.toArray()} | |||||
| return null | |||||
| } else { | |||||
| return null | |||||
| } | |||||
| } | |||||
| isHovering() { | |||||
| return this.hoverObject != null // if something is highlighted. | |||||
| } | |||||
| isSelected() { | |||||
| return this.selectedObject != null // if something is selected. | |||||
| } | |||||
| } |
| import {Group, Sphere, Vector2} from 'three' | |||||
| import {LineMaterial} from 'three/examples/jsm/lines/LineMaterial.js' | |||||
| import {AnyOptions} from 'ts-browser-helpers' | |||||
| import {Box3B} from '../math/Box3B' | |||||
| import {IObject3D, IWidget} from '../../core' | |||||
| import {LineSegments2} from 'three/examples/jsm/lines/LineSegments2.js' | |||||
| import {LineSegmentsGeometry} from 'three/examples/jsm/lines/LineSegmentsGeometry.js' | |||||
| export class SelectionWidget extends Group implements IWidget { | |||||
| isWidget = true as const | |||||
| private _object: IObject3D | null = null | |||||
| boundingScaleMultiplier = 1. | |||||
| setDirty?: (options?: AnyOptions) => void | |||||
| protected _updater() { | |||||
| const selected: IObject3D | null | undefined = this._object | |||||
| if (selected) { | |||||
| const bbox = new Box3B().expandByObject(selected, false) | |||||
| bbox.getCenter(this.position) | |||||
| const scale = bbox.getBoundingSphere(new Sphere()).radius | |||||
| this.scale.setScalar(scale * this.boundingScaleMultiplier) | |||||
| this.setVisible(true) | |||||
| } else { | |||||
| this.setVisible(false) | |||||
| } | |||||
| } | |||||
| constructor() { | |||||
| super() | |||||
| this.position.set(0, 0, 0) | |||||
| this.visible = false | |||||
| this.renderOrder = 100 // Don't draw too early, thus obscuring other transparent objects | |||||
| this.userData.bboxVisible = false | |||||
| this._updater = this._updater.bind(this) | |||||
| } | |||||
| setVisible(v: boolean) { | |||||
| if (v !== this.visible) { | |||||
| this.visible = v | |||||
| this.setDirty?.({sceneUpdate: false}) | |||||
| } | |||||
| } | |||||
| attach(object: IObject3D): this { | |||||
| this.detach() | |||||
| if (!object) return this | |||||
| this._object = object | |||||
| this._object.addEventListener('objectUpdate', this._updater) | |||||
| this._updater() | |||||
| return this | |||||
| } | |||||
| detach(): this { | |||||
| if (!this._object) return this | |||||
| this._object?.removeEventListener('objectUpdate', this._updater) | |||||
| this._object = null | |||||
| this._updater() | |||||
| return this | |||||
| } | |||||
| get object(): IObject3D | null { | |||||
| return this._object | |||||
| } | |||||
| dispose() { | |||||
| this.detach() | |||||
| } | |||||
| } | |||||
| export class BoxSelectionWidget extends SelectionWidget { | |||||
| constructor() { | |||||
| super() | |||||
| const matLine = new LineMaterial({ | |||||
| color: '#ff2222' as any, transparent: true, opacity: 0.9, | |||||
| linewidth: 5, // in pixels | |||||
| resolution: new Vector2(1024, 1024), // to be set by renderer, eventually | |||||
| dashed: false, | |||||
| toneMapped: false, | |||||
| }) | |||||
| const ls = new LineSegmentsGeometry() | |||||
| ls.setPositions([1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1].map(v=>v - 0.5)) | |||||
| const wireframe = new LineSegments2(ls, matLine) | |||||
| wireframe.computeLineDistances() | |||||
| wireframe.scale.set(1, 1, 1) | |||||
| wireframe.visible = true | |||||
| this.add(wireframe) | |||||
| } | |||||
| protected _updater() { | |||||
| super._updater() | |||||
| const selected = this.object | |||||
| if (selected) { | |||||
| const bbox = new Box3B().expandByObject(selected, false) | |||||
| // const scale = bbox.getBoundingSphere(new Sphere()).radius | |||||
| bbox.getSize(this.scale).multiplyScalar(this.boundingScaleMultiplier).clampScalar(0.1, 100) | |||||
| this.setVisible(true) | |||||
| } | |||||
| } | |||||
| } |
| export {generateUUID, toIndexedGeometry, isInScene} from './misc' | export {generateUUID, toIndexedGeometry, isInScene} from './misc' | ||||
| export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDataUrl, texImageToCanvas} from './texture' | export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDataUrl, texImageToCanvas} from './texture' | ||||
| export {threeConstMappings} from './const-mappings' | export {threeConstMappings} from './const-mappings' | ||||
| export {ObjectPicker} from './ObjectPicker' | |||||
| export {SelectionWidget, BoxSelectionWidget} from './SelectionWidget' | |||||
| // export {} from './constants' | // export {} from './constants' |
| export const VERSION = '0.0.14' | |||||
| export const VERSION = '0.0.13' |