threepipe
Version:
A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.
313 lines • 12.8 kB
JavaScript
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, serialize } from 'ts-browser-helpers';
import { AViewerPluginSync } from '../../viewer';
import { BoxSelectionWidget, ObjectPicker } from '../../three';
import { FrameFadePlugin } from '../pipeline/FrameFadePlugin';
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.widgetEnabled && this._picker?.selectedObject)
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;
// @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._onSceneUpdate = (e) => {
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);
};
this._onObjectSelectEvent = (e) => {
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.object || e.value, this.autoFocus || e.focusCamera);
};
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 sUiConfig = selected?.uiConfig;
const ui = this.uiConfig;
ui.children = [...this._uiConfigChildren];
if (sUiConfig)
ui.children.push(sUiConfig);
ui.uiRefresh?.();
}
const widget = this._widget;
if (widget && this.widgetEnabled) {
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);
}
};
this._hoverObjectChanged = (e) => {
this.dispatchEvent(e);
const selected = this._picker?.hoverObject || undefined;
const widget = this._hoverWidget;
if (widget && this.widgetEnabled) {
if (selected)
widget.attach(selected);
else
widget.detach();
}
// if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected})
this._viewer?.setDirty();
if (this.autoFocusHover) {
// 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._uiConfigChildren = [
{
label: 'Enabled',
type: 'checkbox',
property: [this, 'enabled'],
},
{
label: 'Hover Enabled',
type: 'checkbox',
property: [this, 'hoverEnabled'],
onChange: () => this.uiConfig.uiRefresh?.(true), // for autoFocusHover
},
{
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,
],
};
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) {
const disabled = this.isDisabled();
if (disabled && !object)
return;
if (!this._picker)
return;
const t = this.autoFocus;
this.autoFocus = false;
this._picker.selectedObject = object || null;
this.autoFocus = t;
if (!disabled && 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;
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);
// 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.dispose();
this._picker.undoManager = undefined; // because setting above
this._picker = undefined;
}
super.onRemove(viewer);
}
dispose() {
super.dispose();
this._widget?.dispose();
this._hoverWidget?.dispose();
}
async focusObject(selected) {
this._viewer?.fitToView(selected, 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([
serialize()
], PickingPlugin.prototype, "autoFocus", void 0);
__decorate([
onChange(PickingPlugin.prototype._widgetEnabledChange)
], PickingPlugin.prototype, "widgetEnabled", void 0);
//# sourceMappingURL=PickingPlugin.js.map