UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

433 lines 18.6 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { onChange, safeSetProperty, serialize } from 'ts-browser-helpers'; import { AViewerPluginSync } from '../../viewer'; import { bindToValue, BoxSelectionWidget, ObjectPicker } from '../../three'; import { FrameFadePlugin } from '../pipeline/FrameFadePlugin'; import { CameraViewPlugin } from '../animation/CameraViewPlugin'; export class PickingPlugin extends AViewerPluginSync { get picker() { return this._picker; } get hoverEnabled() { return this._picker?.hoverEnabled ?? false; } set hoverEnabled(v) { if (!this._picker) return; this._picker.hoverEnabled = v; this.uiConfig && this.uiConfig.uiRefresh?.(); } _widgetEnabledChange() { if (!this._widget) return; if (this.widgetEnabled && this._picker?.selectedObject?.isObject3D) this._widget.attach(this._picker.selectedObject); else this._widget.detach(); this.uiConfig?.uiRefresh?.(true); } setDirty() { if (!this._viewer) return; if (this.isDisabled()) this.setSelectedObject(undefined); this._viewer.setDirty(); } constructor(selection = BoxSelectionWidget, pickUi = true, autoFocus = false) { super(); this.enabled = true; this.dependencies = [CameraViewPlugin]; this.selectionMode = 'object'; // @serialize() // todo this.autoFocusHover = false; /** * Note: this is for runtime use only, not serialized */ this.widgetEnabled = true; this._mainCameraChange = () => { if (!this._picker || !this._viewer) return; this._picker.camera = this._viewer.scene.mainCamera; }; this._sceneUpdated = false; this._onSceneUpdate = (e) => { if (!e.hierarchyChanged) return; this._sceneUpdated = true; }; this._viewerListeners = { preFrame: () => { if (!this._viewer || !this._picker) return; if (this._sceneUpdated) { this._checkSelectedInScene(); this._sceneUpdated = false; } }, }; this._onObjectSelectEvent = (e) => { if (e.source === PickingPlugin.PluginType) return; 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); }; this._selectedObjectChanged = (e) => { if (!this._viewer) return; this.dispatchEvent(e); 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(this); else frameFade.enable(this); } this._viewer.scene.autoNearFarEnabled = !selected; // for widgets etc, this can be removed when they are rendered in a separate pass if (this._pickUi) { const ui = this.uiConfig; ui.children = [...this._uiConfigChildren]; if (selected) { if (selected.isObject3D) { const obj = selected; 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 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; if (c) { if (c.type === 'folder') safeSetProperty(c, 'expanded', true, true); ui.children.push(c); // todo children need to be added back to config on selection change or error // const objChildren = c.children // find all children after type divider // const dividerIndex = objChildren?.findIndex((c1) => typeof c1 === 'object' && (c1.type === 'divider' || c1.type === 'separator')) ?? -1 // if (dividerIndex >= 0) { // ui.children.push(...objChildren!.slice(dividerIndex + 1)) // c.children = objChildren!.slice(0, dividerIndex) // } } else { // check materials const mats = selected.materials ?? [selected.material]; for (const m of mats) { c = m?.uiConfig; if (c) ui.children.push(c); } } } else { ui.children.push(this._pickPromptUi); } ui.uiRefresh?.(); } const widget = this._widget; if (widget && this.widgetEnabled) { if (selected?.isObject3D) widget.attach(selected); else widget.detach(); } // if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected}) this._viewer.setDirty(); if (this.autoFocus && this.selectionMode === 'object') { // this._viewer.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)}) this.focusObject(selected); } }; this._hoverObjectChanged = (e) => { if (!this._viewer) return; this.dispatchEvent(e); const selected = this._picker?.hoverObject || undefined; const widget = this._hoverWidget; if (widget && this.widgetEnabled) { if (selected?.isObject3D) widget.attach(selected); else widget.detach(); } // if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected}) this._viewer?.setDirty(); if (this.autoFocusHover && this.selectionMode === 'object') { // this._viewer?.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)}) this.focusObject(selected); } }; this._onObjectHit = (e) => { if (!this._viewer) return; if (this.isDisabled()) { e.intersects.selectedObject = null; return; } this.dispatchEvent(e); }; this._selectionModeChanged = (e) => { if (!this._viewer) return; this.dispatchEvent(e); if (this.isDisabled()) return; this.uiConfig?.uiRefresh?.(true, 'postFrame', 1); }; this._pickPromptUi = { type: 'button', label: 'Select an object to see its properties', readOnly: true, hidden: () => this.getSelectedObject() !== undefined, }; this._uiConfigChildren = [ { label: 'Enabled', type: 'checkbox', property: [this, 'enabled'], }, { label: 'Hover Enabled', type: 'checkbox', property: [this, 'hoverEnabled'], 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', type: 'checkbox', property: [this, 'autoFocus'], onChange: () => { const o = this.getSelectedObject(); if (this.autoFocus && o) this.setSelectedObject(o, true); }, }, { label: 'Auto Focus on Hover', type: 'checkbox', hidden: () => !this.hoverEnabled, property: [this, 'autoFocusHover'], }, { label: 'Widget Enabled', type: 'checkbox', property: [this, 'widgetEnabled'], }, ]; this.uiConfig = { type: 'panel', label: 'Picker', expanded: true, children: [ ...this._uiConfigChildren, this._pickPromptUi, ], }; if (selection) { this._widget = new selection(); this._hoverWidget = new selection(); if (this._hoverWidget.lineMaterial) { this._hoverWidget.lineMaterial.linewidth /= 2; this._hoverWidget.lineMaterial.color.set('#aa2222'); } } this._pickUi = pickUi; this.autoFocus = autoFocus; this.dispatchEvent = this.dispatchEvent.bind(this); } getSelectedObject() { if (this.isDisabled()) return; return this._picker?.selectedObject || undefined; } setSelectedObject(object, focusCamera = false, trackUndo = true) { const disabled = this.isDisabled(); if (disabled && !object) return; if (!this._picker) return; const t = this.autoFocus; this.autoFocus = false; this._picker.setSelected(object || null, trackUndo); this.autoFocus = t; if (!disabled && object && this.selectionMode === 'object' && (t || focusCamera)) this.focusObject(object); } onAdded(viewer) { super.onAdded(viewer); this.setDirty(); this._picker = new ObjectPicker(viewer.scene.modelRoot, viewer.canvas, viewer.scene.mainCamera, (obj) => { const hasMat = obj.material; if (!hasMat) return false; let o = 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; // todo colorwrite? o = o.parent; } return ret; }); if (this._widget) viewer.scene.addObject(this._widget, { addToRoot: true }); if (this._hoverWidget) viewer.scene.addObject(this._hoverWidget, { addToRoot: true }); this._picker.addEventListener('selectedObjectChanged', this._selectedObjectChanged); this._picker.addEventListener('hoverObjectChanged', this._hoverObjectChanged); this._picker.addEventListener('hitObject', this._onObjectHit); this._picker.addEventListener('selectionModeChanged', this._selectionModeChanged); // 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); viewer.forPlugin('UndoManagerPlugin', (um) => { if (!this._picker) return; this._picker.undoManager = um.undoManager; }, () => { if (!this._picker) return; this._picker.undoManager = undefined; }); } onRemove(viewer) { viewer.scene.removeEventListener('select', this._onObjectSelectEvent); viewer.scene.removeEventListener('sceneUpdate', this._onSceneUpdate); viewer.scene.removeEventListener('mainCameraChange', this._mainCameraChange); this._widget?.removeFromParent(); this._hoverWidget?.removeFromParent(); if (this._picker) { this._picker.removeEventListener('selectedObjectChanged', this._selectedObjectChanged); this._picker.removeEventListener('hoverObjectChanged', this._hoverObjectChanged); this._picker.removeEventListener('hitObject', this._onObjectHit); this._picker.removeEventListener('selectionModeChanged', this._selectionModeChanged); this._picker.dispose(); this._picker.undoManager = undefined; // because setting above this._picker = undefined; } super.onRemove(viewer); } dispose() { super.dispose(); this._widget?.dispose(); this._hoverWidget?.dispose(); } _checkSelectedInScene() { if (this.isDisabled() || !this._viewer) return; const s = this.getSelectedObject(); if (!s || !s.isObject3D) return; // ignoring checking for materials in scene let inScene = false; s.traverseAncestors((o) => { if (inScene || o !== this._viewer.scene) return; inScene = true; }); if (!inScene) this.setSelectedObject(undefined, false, false); } async focusObject(selected) { this._viewer?.fitToView(selected ?? undefined, 1.25, 1000, 'easeOut'); } get widget() { return this._widget; } } PickingPlugin.PluginType = 'Picking'; PickingPlugin.OldPluginType = 'PickingPlugin'; // todo: swap __decorate([ serialize(), onChange(PickingPlugin.prototype.setDirty) ], PickingPlugin.prototype, "enabled", void 0); __decorate([ bindToValue({ obj: '_picker', key: 'selectionMode' }) ], PickingPlugin.prototype, "selectionMode", void 0); __decorate([ serialize() ], PickingPlugin.prototype, "autoFocus", void 0); __decorate([ onChange(PickingPlugin.prototype._widgetEnabledChange) ], PickingPlugin.prototype, "widgetEnabled", void 0); //# sourceMappingURL=PickingPlugin.js.map