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
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, 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