| return | return | ||||
| } | } | ||||
| const initing = this.init() | const initing = this.init() | ||||
| const selected = this.exportSelected ? this._viewer.getPlugin(PickingPlugin)?.getSelectedObject() : undefined | |||||
| object = object || selected || this._viewer.scene.modelRoot | |||||
| const selected = this.exportSelected ? this._viewer.getPlugin(PickingPlugin)?.getSelectedObject<IObject3D>() : undefined | |||||
| object = object || (selected?.isObject3D ? selected : this._viewer.scene.modelRoot) | |||||
| // export to glb | // export to glb | ||||
| const blob = await this._viewer.export(object, options) | const blob = await this._viewer.export(object, options) |
| import {PickingPlugin} from '../interaction/PickingPlugin' | import {PickingPlugin} from '../interaction/PickingPlugin' | ||||
| import {imageBitmapToBase64, makeColorSvgCircle, serialize} from 'ts-browser-helpers' | import {imageBitmapToBase64, makeColorSvgCircle, serialize} from 'ts-browser-helpers' | ||||
| import {UiObjectConfig} from 'uiconfig.js' | import {UiObjectConfig} from 'uiconfig.js' | ||||
| import {IMaterial, PhysicalMaterial} from '../../core' | |||||
| import {IMaterial, IObject3D, PhysicalMaterial} from '../../core' | |||||
| import {MaterialPreviewGenerator} from '../../three' | import {MaterialPreviewGenerator} from '../../three' | ||||
| import {Color} from 'three' | import {Color} from 'three' | ||||
| @serialize() | @serialize() | ||||
| variations: MaterialVariations[] = [] | variations: MaterialVariations[] = [] | ||||
| private _selectedMaterial = () => (this._picking?.getSelectedObject()?.material || undefined) as IMaterial | undefined | |||||
| private _selectedMaterial = () => { | |||||
| const selected = this._picking?.getSelectedObject() | |||||
| if (!selected) return undefined | |||||
| if ((selected as IMaterial).isMaterial) return selected as IMaterial | |||||
| else { | |||||
| const mat = ((selected as IObject3D)?.material || undefined) as IMaterial | undefined | |||||
| if (Array.isArray(mat)) return mat[0] | |||||
| return mat | |||||
| } | |||||
| } | |||||
| uiConfig: UiObjectConfig = { | uiConfig: UiObjectConfig = { | ||||
| label: 'Material Configurator', | label: 'Material Configurator', | ||||
| type: 'folder', | type: 'folder', |
| import {UiObjectConfig} from 'uiconfig.js' | import {UiObjectConfig} from 'uiconfig.js' | ||||
| import {serialize} from 'ts-browser-helpers' | import {serialize} from 'ts-browser-helpers' | ||||
| import {snapObject} from '../../three' | import {snapObject} from '../../three' | ||||
| import {IObject3D} from '../../core' | |||||
| /** | /** | ||||
| * Switch Node Plugin (Base) | * Switch Node Plugin (Base) | ||||
| @serialize() variations: ObjectSwitchNode[] = [] | @serialize() variations: ObjectSwitchNode[] = [] | ||||
| protected _selectedSwitchNode = (): Object3D | undefined => { | protected _selectedSwitchNode = (): Object3D | undefined => { | ||||
| const obj = this._picking?.getSelectedObject() // (?.material || undefined) as IMaterial | undefined | |||||
| if (!obj) return undefined | |||||
| const obj = this._picking?.getSelectedObject<IObject3D>() // (?.material || undefined) as IMaterial | undefined | |||||
| if (!obj?.isObject3D) return undefined | |||||
| const nodes = this.variations.map(v => v.name) | const nodes = this.variations.map(v => v.name) | ||||
| let found: Object3D | undefined = undefined | let found: Object3D | undefined = undefined | ||||
| obj.traverseAncestors(a => { | obj.traverseAncestors(a => { |
| async exportSelected(options?: ExportAssetOptions, download = true) { | async exportSelected(options?: ExportAssetOptions, download = true) { | ||||
| const selected = this._viewer?.getPlugin<PickingPlugin>('PickingPlugin')?.getSelectedObject() as any | const selected = this._viewer?.getPlugin<PickingPlugin>('PickingPlugin')?.getSelectedObject() as any | ||||
| if (!selected) { | if (!selected) { | ||||
| alert('Nothing selected') | |||||
| this._viewer?.dialog.alert('Export Selected: Nothing selected') | |||||
| return | return | ||||
| } | } | ||||
| const name = selected.name || 'selected' | |||||
| const blob = await this._viewer!.assetManager.exporter.exportObject(selected, options ?? this.exportOptions) | const blob = await this._viewer!.assetManager.exporter.exportObject(selected, options ?? this.exportOptions) | ||||
| if (blob && download) await this._viewer?.exportBlob(blob, 'object.' + blob.ext) | |||||
| if (blob && download) await this._viewer?.exportBlob(blob, name + '.' + blob.ext) | |||||
| return blob | return blob | ||||
| } | } | ||||
| /** | /** | ||||
| * Only for {@link downloadSnapshot} and functions using that | * Only for {@link downloadSnapshot} and functions using that | ||||
| */ | */ | ||||
| @uiConfig() | |||||
| @uiConfig(undefined, {label: 'Options'}) | |||||
| @serialize() | @serialize() | ||||
| defaultOptions: CanvasSnapshotPluginOptions = { | defaultOptions: CanvasSnapshotPluginOptions = { | ||||
| waitForProgressive: true, | waitForProgressive: true, |
| simplifyGeometries(geometry?: ValOrArr<IGeometry>, options?: SimplifyOptions) { | simplifyGeometries(geometry?: ValOrArr<IGeometry>, options?: SimplifyOptions) { | ||||
| if (!geometry) { | if (!geometry) { | ||||
| const selected = this._pickingPlugin?.getSelectedObject() | |||||
| const selected = this._pickingPlugin?.getSelectedObject<IObject3D>() | |||||
| if (!selected?.isObject3D) return | |||||
| const geom: IGeometry[] = [] | const geom: IGeometry[] = [] | ||||
| selected?.traverse((o) => { | selected?.traverse((o) => { | ||||
| if (o.geometry && !geom.includes(o.geometry)) geom.push(o.geometry) | if (o.geometry && !geom.includes(o.geometry)) geom.push(o.geometry) | ||||
| disposeOnReplace = false, | disposeOnReplace = false, | ||||
| }: SimplifyOptions = {}): IGeometry|undefined { | }: SimplifyOptions = {}): IGeometry|undefined { | ||||
| if (!geometry) { | if (!geometry) { | ||||
| const selected = this._pickingPlugin?.getSelectedObject() | |||||
| const selected = this._pickingPlugin?.getSelectedObject<IObject3D>() | |||||
| geometry = selected?.geometry | geometry = selected?.geometry | ||||
| if (!geometry) return undefined | if (!geometry) return undefined | ||||
| } | } | ||||
| return | return | ||||
| } | } | ||||
| } | } | ||||
| const selected = this._pickingPlugin?.getSelectedObject() | |||||
| if (!selected) { | |||||
| await this._viewer.dialog.alert('Simplify: Nothing Selected') | |||||
| const selected = this._pickingPlugin?.getSelectedObject<IObject3D>() | |||||
| if (!selected?.isObject3D) { | |||||
| await this._viewer.dialog.alert('Simplify: No Object Selected') | |||||
| return | return | ||||
| } | } | ||||
| let doAll = false | let doAll = false |
| import {EventListener2, Object3D} from 'three' | import {EventListener2, Object3D} from 'three' | ||||
| import {Class, onChange, serialize} from 'ts-browser-helpers' | import {Class, onChange, serialize} from 'ts-browser-helpers' | ||||
| import {AViewerPluginEventMap, AViewerPluginSync, ThreeViewer} from '../../viewer' | import {AViewerPluginEventMap, AViewerPluginSync, ThreeViewer} from '../../viewer' | ||||
| import {BoxSelectionWidget, ObjectPicker, SelectionWidget} from '../../three' | |||||
| import {bindToValue, BoxSelectionWidget, ObjectPicker, SelectionWidget} from '../../three' | |||||
| import {IMaterial, IObject3D, IScene, ISceneEventMap} from '../../core' | import {IMaterial, IObject3D, IScene, ISceneEventMap} from '../../core' | ||||
| import {UiObjectConfig} from 'uiconfig.js' | import {UiObjectConfig} from 'uiconfig.js' | ||||
| import {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | import {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | ||||
| import {ObjectPickerEventMap} from '../../three/utils/ObjectPicker' | import {ObjectPickerEventMap} from '../../three/utils/ObjectPicker' | ||||
| import {CameraViewPlugin} from '../animation/CameraViewPlugin' | import {CameraViewPlugin} from '../animation/CameraViewPlugin' | ||||
| export interface PickingPluginEventMap extends AViewerPluginEventMap{ | |||||
| selectedObjectChanged: {object: IObject3D|null} | |||||
| hoverObjectChanged: {object: IObject3D|null} | |||||
| hitObject: {intersects: {selectedObject: IObject3D|null}} | |||||
| export interface PickingPluginEventMap extends AViewerPluginEventMap, ObjectPickerEventMap{ | |||||
| } | } | ||||
| export class PickingPlugin extends AViewerPluginSync<PickingPluginEventMap> { | export class PickingPlugin extends AViewerPluginSync<PickingPluginEventMap> { | ||||
| this.uiConfig && this.uiConfig.uiRefresh?.() | this.uiConfig && this.uiConfig.uiRefresh?.() | ||||
| } | } | ||||
| @bindToValue({obj: '_picker', key: 'selectionMode'}) | |||||
| selectionMode: 'object' | 'material' = 'object' | |||||
| @serialize() | @serialize() | ||||
| autoFocus | autoFocus | ||||
| widgetEnabled = true | widgetEnabled = true | ||||
| protected _widgetEnabledChange() { | protected _widgetEnabledChange() { | ||||
| if (this.widgetEnabled && this._picker?.selectedObject) | |||||
| this._widget?.attach(this._picker.selectedObject) | |||||
| if (!this._widget) return | |||||
| if (this.widgetEnabled && (this._picker?.selectedObject as IObject3D)?.isObject3D) | |||||
| this._widget.attach(this._picker!.selectedObject as IObject3D) | |||||
| else | else | ||||
| this._widget?.detach() | |||||
| this._widget.detach() | |||||
| this.uiConfig?.uiRefresh?.(true) | this.uiConfig?.uiRefresh?.(true) | ||||
| } | } | ||||
| this.dispatchEvent = this.dispatchEvent.bind(this) | this.dispatchEvent = this.dispatchEvent.bind(this) | ||||
| } | } | ||||
| getSelectedObject<T extends IObject3D = IObject3D>(): T|undefined { | |||||
| getSelectedObject<T extends IObject3D|IMaterial = IObject3D|IMaterial>(): T|undefined { | |||||
| if (this.isDisabled()) return | if (this.isDisabled()) return | ||||
| return this._picker?.selectedObject as T || undefined | return this._picker?.selectedObject as T || undefined | ||||
| } | } | ||||
| setSelectedObject(object: IObject3D|undefined, focusCamera = false, trackUndo = true) { // todo: listen to object disposed | |||||
| setSelectedObject(object: IObject3D|IMaterial|undefined, focusCamera = false, trackUndo = true) { // todo: listen to object disposed | |||||
| const disabled = this.isDisabled() | const disabled = this.isDisabled() | ||||
| if (disabled && !object) return | if (disabled && !object) return | ||||
| if (!this._picker) return | if (!this._picker) return | ||||
| this.autoFocus = false | this.autoFocus = false | ||||
| this._picker.setSelected(object || null, trackUndo) | this._picker.setSelected(object || null, trackUndo) | ||||
| this.autoFocus = t | this.autoFocus = t | ||||
| if (!disabled && object && (t || focusCamera)) this.focusObject(object) | |||||
| if (!disabled && object && this.selectionMode === 'object' && (t || focusCamera)) this.focusObject(object as IObject3D) | |||||
| } | } | ||||
| onAdded(viewer: ThreeViewer): void { | onAdded(viewer: ThreeViewer): void { | ||||
| this._picker.addEventListener('selectedObjectChanged', this._selectedObjectChanged) | this._picker.addEventListener('selectedObjectChanged', this._selectedObjectChanged) | ||||
| this._picker.addEventListener('hoverObjectChanged', this._hoverObjectChanged) | this._picker.addEventListener('hoverObjectChanged', this._hoverObjectChanged) | ||||
| this._picker.addEventListener('hitObject', this._onObjectHit) | this._picker.addEventListener('hitObject', this._onObjectHit) | ||||
| this._picker.addEventListener('selectionModeChanged', this._selectionModeChanged) | |||||
| // on material drop on selected object | // on material drop on selected object | ||||
| // viewer.scene.addEventListener('addSceneObject', async(e) => { | // viewer.scene.addEventListener('addSceneObject', async(e) => { | ||||
| this._picker.removeEventListener('selectedObjectChanged', this._selectedObjectChanged) | this._picker.removeEventListener('selectedObjectChanged', this._selectedObjectChanged) | ||||
| this._picker.removeEventListener('hoverObjectChanged', this._hoverObjectChanged) | this._picker.removeEventListener('hoverObjectChanged', this._hoverObjectChanged) | ||||
| this._picker.removeEventListener('hitObject', this._onObjectHit) | this._picker.removeEventListener('hitObject', this._onObjectHit) | ||||
| this._picker.removeEventListener('selectionModeChanged', this._selectionModeChanged) | |||||
| this._picker.dispose() | this._picker.dispose() | ||||
| this._picker.undoManager = undefined // because setting above | this._picker.undoManager = undefined // because setting above | ||||
| this._picker = undefined | this._picker = undefined | ||||
| } | } | ||||
| private _checkSelectedInScene() { | private _checkSelectedInScene() { | ||||
| if (this.isDisabled()) return | |||||
| if (this.isDisabled() || !this._viewer) return | |||||
| const s = this.getSelectedObject() | const s = this.getSelectedObject() | ||||
| if (!s || !(s as IObject3D).isObject3D) return // ignoring checking for materials in scene | |||||
| let inScene = false | let inScene = false | ||||
| s?.traverseAncestors((o) => { | |||||
| if (o === this._viewer?.scene) inScene = true | |||||
| ;(s as IObject3D).traverseAncestors((o) => { | |||||
| if (inScene || o !== this._viewer!.scene) return | |||||
| inScene = true | |||||
| }) | }) | ||||
| if (!inScene) this.setSelectedObject(undefined, false, false) | if (!inScene) this.setSelectedObject(undefined, false, false) | ||||
| } | } | ||||
| private _onObjectSelectEvent: EventListener2<'select', ISceneEventMap, IScene> = (e)=>{ | private _onObjectSelectEvent: EventListener2<'select', ISceneEventMap, IScene> = (e)=>{ | ||||
| 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('PickingPlugin - Error handling object/material `select` event `e.object` or `e.value` must be set for picking, `value` can be null to unselect') | |||||
| else this.setSelectedObject(e.object || e.value, this.autoFocus || e.focusCamera, true) | else this.setSelectedObject(e.object || e.value, this.autoFocus || e.focusCamera, true) | ||||
| } | } | ||||
| const ui = this.uiConfig | const ui = this.uiConfig | ||||
| ui.children = [...this._uiConfigChildren] | ui.children = [...this._uiConfigChildren] | ||||
| if (selected) { | if (selected) { | ||||
| ui.children.push( | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Focus', | |||||
| value: ()=>{ | |||||
| // const selected = this.getSelectedObject() | |||||
| if (selected.assetType && selected.parentRoot) // todo also check if acceptChildEvents is set on some parent? | |||||
| selected.dispatchEvent({type: 'select', ui: true, object: selected, bubbleToParent: true, focusCamera: true}) | |||||
| else | |||||
| this.setSelectedObject(selected, true) | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Select Parent', | |||||
| hidden: ()=>!selected.parent, | |||||
| value: ()=>{ | |||||
| const parent = selected.parent | |||||
| if (parent) { | |||||
| if (parent.assetType && parent.parentRoot) // todo also check if acceptChildEvents is set on some parent? | |||||
| parent.dispatchEvent({type: 'select', ui: true, bubbleToParent: true, object: parent}) | |||||
| if ((selected as IObject3D).isObject3D) { | |||||
| const obj = (selected as IObject3D) | |||||
| ui.children.push( | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Focus', | |||||
| value: () => { | |||||
| if (!obj.isObject3D) return | |||||
| // const selected = this.getSelectedObject() | |||||
| if (selected.assetType && obj.parentRoot) // todo also check if acceptChildEvents is set on some parent? | |||||
| obj.dispatchEvent({ | |||||
| type: 'select', | |||||
| ui: true, | |||||
| object: obj, | |||||
| bubbleToParent: true, | |||||
| focusCamera: true, | |||||
| }) | |||||
| else | else | ||||
| this.setSelectedObject(parent, false) | |||||
| } | |||||
| this.setSelectedObject(obj, true) | |||||
| }, | |||||
| }, | }, | ||||
| }, | |||||
| ) | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Select Parent', | |||||
| hidden: () => !obj.parent, | |||||
| value: () => { | |||||
| if (!obj.isObject3D) return | |||||
| const parent = obj.parent | |||||
| if (parent) { | |||||
| if (parent.assetType && parent.parentRoot) // todo also check if acceptChildEvents is set on some parent? | |||||
| parent.dispatchEvent({ | |||||
| type: 'select', | |||||
| ui: true, | |||||
| bubbleToParent: true, | |||||
| object: parent, | |||||
| }) | |||||
| else | |||||
| this.setSelectedObject(parent, false) | |||||
| } | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| } | |||||
| let c = selected.uiConfig | let c = selected.uiConfig | ||||
| if (c) ui.children.push(c) | if (c) ui.children.push(c) | ||||
| else { | else { | ||||
| // check materials | // check materials | ||||
| const mats = selected.materials ?? [selected.material as IMaterial] | |||||
| const mats = (selected as IObject3D).materials ?? [(selected as IObject3D).material as IMaterial] | |||||
| for (const m of mats) { | for (const m of mats) { | ||||
| c = m?.uiConfig | c = m?.uiConfig | ||||
| if (c) ui.children.push(c) | if (c) ui.children.push(c) | ||||
| const widget = this._widget | const widget = this._widget | ||||
| if (widget && this.widgetEnabled) { | if (widget && this.widgetEnabled) { | ||||
| if (selected) widget.attach(selected) | |||||
| if ((selected as IObject3D)?.isObject3D) widget.attach((selected as IObject3D)) | |||||
| else widget.detach() | else widget.detach() | ||||
| } | } | ||||
| this._viewer.setDirty() | this._viewer.setDirty() | ||||
| if (this.autoFocus) { | |||||
| if (this.autoFocus && this.selectionMode === 'object') { | |||||
| // 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 as IObject3D | undefined) | |||||
| } | } | ||||
| } | } | ||||
| private _hoverObjectChanged = (e: any) => { | private _hoverObjectChanged = (e: any) => { | ||||
| if (!this._viewer) return | |||||
| this.dispatchEvent(e) | this.dispatchEvent(e) | ||||
| const selected = this._picker?.hoverObject || undefined | const selected = this._picker?.hoverObject || undefined | ||||
| const widget = this._hoverWidget | const widget = this._hoverWidget | ||||
| if (widget && this.widgetEnabled) { | if (widget && this.widgetEnabled) { | ||||
| if (selected) widget.attach(selected) | |||||
| if ((selected as IObject3D)?.isObject3D) widget.attach((selected as IObject3D)) | |||||
| else widget.detach() | else widget.detach() | ||||
| } | } | ||||
| this._viewer?.setDirty() | this._viewer?.setDirty() | ||||
| if (this.autoFocusHover) { | |||||
| if (this.autoFocusHover && this.selectionMode === 'object') { | |||||
| // 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 as IObject3D | undefined) | |||||
| } | } | ||||
| } | } | ||||
| this.dispatchEvent(e) | this.dispatchEvent(e) | ||||
| } | } | ||||
| private _selectionModeChanged = (e: any)=>{ | |||||
| if (!this._viewer) return | |||||
| this.dispatchEvent(e) | |||||
| if (this.isDisabled()) return | |||||
| this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) | |||||
| } | |||||
| public async focusObject(selected?: Object3D) { | |||||
| this._viewer?.fitToView(selected, 1.25, 1000, 'easeOut') | |||||
| public async focusObject(selected?: Object3D|null) { | |||||
| this._viewer?.fitToView(selected ?? undefined, 1.25, 1000, 'easeOut') | |||||
| } | } | ||||
| private _uiConfigChildren: UiObjectConfig[] = [ | private _uiConfigChildren: UiObjectConfig[] = [ | ||||
| property: [this, 'hoverEnabled'], | property: [this, 'hoverEnabled'], | ||||
| onChange: ()=>this.uiConfig.uiRefresh?.(true), // for autoFocusHover | onChange: ()=>this.uiConfig.uiRefresh?.(true), // for autoFocusHover | ||||
| }, | }, | ||||
| // { | |||||
| // label: 'Selection Mode', | |||||
| // type: 'dropdown', | |||||
| // children: ['object', 'material'].map(v=>({label: v, value: v})), | |||||
| // onChange: ()=>this.uiConfig.uiRefresh?.(true), | |||||
| // }, | |||||
| { | { | ||||
| label: 'Auto Focus', | label: 'Auto Focus', | ||||
| type: 'checkbox', | type: 'checkbox', |
| import {PickingPlugin} from './PickingPlugin' | import {PickingPlugin} from './PickingPlugin' | ||||
| import {JSUndoManager, onChange} from 'ts-browser-helpers' | import {JSUndoManager, onChange} from 'ts-browser-helpers' | ||||
| import {TransformControls} from '../../three/controls/TransformControls' | import {TransformControls} from '../../three/controls/TransformControls' | ||||
| import {UnlitLineMaterial, UnlitMaterial} from '../../core' | |||||
| import {IObject3D, UnlitLineMaterial, UnlitMaterial} from '../../core' | |||||
| import {Euler, Object3D, Vector3} from 'three' | import {Euler, Object3D, Vector3} from 'three' | ||||
| import type {UndoManagerPlugin} from './UndoManagerPlugin' | import type {UndoManagerPlugin} from './UndoManagerPlugin' | ||||
| this._pickingWidgetDisabled = false | this._pickingWidgetDisabled = false | ||||
| } | } | ||||
| if (this.transformControls) { | if (this.transformControls) { | ||||
| if (enabled && picking.getSelectedObject()) this.transformControls.attach(picking.getSelectedObject()!) | |||||
| if (enabled && picking.getSelectedObject<IObject3D>()?.isObject3D) this.transformControls.attach(picking.getSelectedObject<IObject3D>()!) | |||||
| else this.transformControls.detach() | else this.transformControls.detach() | ||||
| } | } | ||||
| this._viewer.setDirty() | this._viewer.setDirty() |
| import {EventDispatcher, Intersection, Raycaster, Vector2} from 'three' | import {EventDispatcher, Intersection, Raycaster, Vector2} from 'three' | ||||
| import {JSUndoManager, now} from 'ts-browser-helpers' | |||||
| import {ICamera, IObject3D} from '../../core' | |||||
| import {JSUndoManager, now, onChangeDispatchEvent} from 'ts-browser-helpers' | |||||
| import {ICamera, IMaterial, IObject3D} from '../../core' | |||||
| export interface ObjectPickerEventMap{ | export interface ObjectPickerEventMap{ | ||||
| hoverObjectChanged: {object: IObject3D | null} | |||||
| selectedObjectChanged: {object: IObject3D | null} | |||||
| hoverObjectChanged: {object: IObject3D | null, material: IMaterial | null, value: IObject3D | IMaterial | null}, | |||||
| selectedObjectChanged: {object: IObject3D | null, material: IMaterial | null, value: IObject3D | IMaterial | null}, | |||||
| hitObject: {time: number, intersects: {selectedObject: IObject3D | null, intersect: Intersection<IObject3D> | null, intersects: Intersection<IObject3D>[]}} | hitObject: {time: number, intersects: {selectedObject: IObject3D | null, intersect: Intersection<IObject3D> | null, intersects: Intersection<IObject3D>[]}} | ||||
| selectionModeChanged: {detail: {key: 'selectionMode', value: 'object' | 'material', oldValue: 'object' | 'material'}} | |||||
| } | } | ||||
| export class ObjectPicker extends EventDispatcher<ObjectPickerEventMap> { | export class ObjectPicker extends EventDispatcher<ObjectPickerEventMap> { | ||||
| private _firstHit: IObject3D | undefined | private _firstHit: IObject3D | undefined | ||||
| hoverEnabled = false | hoverEnabled = false | ||||
| @onChangeDispatchEvent('selectionModeChanged') | |||||
| selectionMode: 'object' | 'material' = 'object' | |||||
| /** | /** | ||||
| * Time threshold for a pointer click event | * Time threshold for a pointer click event | ||||
| */ | */ | ||||
| public selectionCondition: (o: IObject3D) => boolean | public selectionCondition: (o: IObject3D) => boolean | ||||
| public raycaster: Raycaster | public raycaster: Raycaster | ||||
| public mouse: Vector2 | public mouse: Vector2 | ||||
| private _selected: IObject3D[] | |||||
| private _hovering: IObject3D[] | |||||
| private _selected: IObject3D[] | IMaterial[] | |||||
| private _hovering: IObject3D[] | IMaterial[] | |||||
| public cursorStyles: {default: string; down: string} | public cursorStyles: {default: string; down: string} | ||||
| public domElement: HTMLElement | public domElement: HTMLElement | ||||
| constructor(root: IObject3D, domElement: HTMLElement, camera: ICamera, selectionCondition?: (o:IObject3D)=>boolean) { | constructor(root: IObject3D, domElement: HTMLElement, camera: ICamera, selectionCondition?: (o:IObject3D)=>boolean) { | ||||
| } | } | ||||
| dispose() { | dispose() { | ||||
| this.selectedObject = null | |||||
| this.setSelected(null) | |||||
| this.hoverObject = null | this.hoverObject = null | ||||
| this.domElement.removeEventListener('pointermove', this._onPointerMove) | this.domElement.removeEventListener('pointermove', this._onPointerMove) | ||||
| this._camera = value | this._camera = value | ||||
| } | } | ||||
| get selectedObject(): IObject3D | null { | |||||
| get selectedObject(): IObject3D | IMaterial | null { | |||||
| return this._selected.length > 0 ? this._selected[0] : null | return this._selected.length > 0 ? this._selected[0] : null | ||||
| } | } | ||||
| set selectedObject(object) { | |||||
| this.setSelected(object) | |||||
| } | |||||
| // set selectedObject(object) { | |||||
| // this.setSelected(object) | |||||
| // } | |||||
| setSelected(object: IObject3D|null, record = true) { | |||||
| setSelected(object: IObject3D | IMaterial | null, record = true) { | |||||
| if ((object as IObject3D)?.isObject3D && this.selectionMode === 'material' || | |||||
| (object as IMaterial)?.isMaterial && this.selectionMode === 'object') { | |||||
| this.selectionMode = (object as IMaterial)?.isMaterial ? 'material' : 'object' | |||||
| } | |||||
| if (!this._selected.length && !object || this._selected.length === 1 && this._selected[0] === object) return | if (!this._selected.length && !object || this._selected.length === 1 && this._selected[0] === object) return | ||||
| const current = [...this._selected] | const current = [...this._selected] | ||||
| this._selected = object ? Array.isArray(object) ? [...object] : [object] : [] | this._selected = object ? Array.isArray(object) ? [...object] : [object] : [] | ||||
| this.dispatchEvent({type: 'selectedObjectChanged', object: this.selectedObject}) | |||||
| const obj = this.selectedObject | |||||
| this.dispatchEvent({ | |||||
| type: 'selectedObjectChanged', | |||||
| object: (obj as IObject3D)?.isObject3D ? (obj as IObject3D) : null, | |||||
| material: (obj as IMaterial)?.isMaterial ? (obj as IMaterial) : null, | |||||
| value: obj, | |||||
| }) | |||||
| record && this.undoManager?.record({ | record && this.undoManager?.record({ | ||||
| undo: () => this.setSelected(current.length ? current[0] : null, false), | undo: () => this.setSelected(current.length ? current[0] : null, false), | ||||
| redo: () => this.setSelected(object, false), | redo: () => this.setSelected(object, false), | ||||
| }) | }) | ||||
| } | } | ||||
| get hoverObject(): IObject3D | null { | |||||
| get hoverObject(): IObject3D | IMaterial | null { | |||||
| return this._hovering.length > 0 ? this._hovering[0] : null | return this._hovering.length > 0 ? this._hovering[0] : null | ||||
| } | } | ||||
| set hoverObject(object: IObject3D | IObject3D[] | null) { | |||||
| set hoverObject(object: IObject3D | IObject3D[] | IMaterial | IMaterial[] | null) { | |||||
| if (!this._hovering.length && !object || this._hovering.length === 1 && this._hovering[0] === object) return | 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}) | |||||
| this._hovering = (object ? Array.isArray(object) ? [...object] : [object] : []) as (IObject3D[] | IMaterial[]) | |||||
| const obj = this.hoverObject | |||||
| this.dispatchEvent({ | |||||
| type: 'hoverObjectChanged', | |||||
| object: (obj as IObject3D)?.isObject3D ? (obj as IObject3D) : null, | |||||
| material: (obj as IMaterial)?.isMaterial ? (obj as IMaterial) : null, | |||||
| value: obj, | |||||
| }) | |||||
| } | } | ||||
| get time() { | get time() { | ||||
| const intersects = this.checkIntersection() | const intersects = this.checkIntersection() | ||||
| if (intersects) this.dispatchEvent({type: 'hitObject', time: this._mouseUpTime, intersects}) | if (intersects) this.dispatchEvent({type: 'hitObject', time: this._mouseUpTime, intersects}) | ||||
| else this.dispatchEvent({type: 'hitObject', time: this._mouseUpTime, intersects: {selectedObject: null, intersect: null, intersects: []}}) | else this.dispatchEvent({type: 'hitObject', time: this._mouseUpTime, intersects: {selectedObject: null, intersect: null, intersects: []}}) | ||||
| this.selectedObject = intersects?.selectedObject || null | |||||
| let obj: IObject3D|IMaterial|null = intersects?.selectedObject || null | |||||
| if (this.selectionMode === 'material' && obj && obj.material) { | |||||
| obj = Array.isArray(obj.material) ? obj.material[0] : obj.material | |||||
| } | |||||
| this.setSelected(obj) | |||||
| } | } | ||||
| checkIntersection() { | checkIntersection() { |