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.
295 lines • 12.7 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 { Vector3 } from 'three';
import { AViewerPluginSync } from '../../viewer';
import { serialize } from 'ts-browser-helpers';
import { snapObject } from '../../three';
/**
* Switch Node Plugin (Base)
*
* This plugin allows you to configure object variations in a file and apply them in the scene.
* Each SwitchNode is a parent object with multiple direct children. Only one child is visible at a time.
* This works by toggling the `visible` property of the children of a parent object.
* The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations.
* It also provides a function to create snapshot previews of individual variations. This creates a limited render of the object with the selected child visible.
* To get a proper render, it's better to render it offline and set the image as a preview.
*
* See `SwitchNodePlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer.
*
* @category Plugins
*/
export class SwitchNodeBasePlugin extends AViewerPluginSync {
constructor() {
super();
this.enabled = true;
this._uiNeedRefresh = false;
/**
* Whether refreshScene should be called when a node is selected. Refreshing scene will notify the plugins about the update, like shadows can be baked.
* Disable this when nothing significant geometry/node changes happen when switch nodes are changed.
*/
this.refreshScene = true;
/**
* Apply all variations(by selected index or first item) when a config is loaded
*/
this.applyOnLoad = true;
this.variations = [];
this._selectedSwitchNode = () => {
const obj = this._picking?.getSelectedObject(); // (?.material || undefined) as IMaterial | undefined
if (!obj?.isObject3D)
return undefined;
const nodes = this.variations.map(v => v.name);
let found = undefined;
obj.traverseAncestors(a => {
if (found)
return;
if (!a.name)
return;
if (nodes.includes(a.name))
found = a;
});
return found;
};
/**
* If true, the plugin will automatically take snapshots of the icons in _refreshUi and put them in the object.userdata.__icon
* Otherwise, call {@link snapIcons} manually
*/
this.autoSnapIcons = false;
this.uiConfig = {
label: 'Switch Node Plugin',
type: 'folder',
// expanded: true,
children: [
{
type: 'checkbox',
label: 'Enabled',
property: [this, 'enabled'],
},
() => [
{
type: 'folder',
label: 'All nodes',
expanded: true,
children: [
this.variations.map(v => ({
type: 'input',
label: v.title,
property: [v, 'name'],
onChange: () => this.refreshUi(),
})),
],
},
{
type: 'button',
label: 'Add Node',
value: () => {
this.addNode({
name: 'switch_node',
selected: '',
title: 'Switch Node',
camView: 'front',
camDistance: 1,
});
},
},
{
type: 'button',
label: 'Refresh UI',
value: () => this.refreshUi(),
},
{
type: 'input',
label: 'Selected node title',
hidden: () => !this._selectedSwitchNode(),
property: () => {
const node = this._selectedSwitchNode();
if (!node)
return [];
return [this.variations.find(v => v.name === node.name), 'title'];
},
onChange: () => this.refreshUi(),
},
{
type: 'slider',
bounds: [0.01, 2],
stepSize: 0.01,
label: 'Cam Distance',
hidden: () => !this._selectedSwitchNode(),
property: () => {
const node = this._selectedSwitchNode();
if (!node)
return [];
return [this.variations.find(v => v.name === node.name), 'camDistance'];
},
// onChange: ()=> this.refreshUi(),
},
{
type: 'dropdown',
label: 'Cam View',
hidden: () => !this._selectedSwitchNode(),
property: () => {
const node = this._selectedSwitchNode();
if (!node)
return [];
return [this.variations.find(v => v.name === node.name), 'camView'];
},
onChange: () => this.refreshUi(),
children: ['top', 'bottom', 'front', 'back', 'left', 'right'].map(k => ({
label: k,
value: k,
})),
},
],
],
};
this._postFrame = this._postFrame.bind(this);
this.refreshUiConfig = this.refreshUiConfig.bind(this);
this.addEventListener('deserialize', async () => {
// await timeout(200) // not needed actually
this.refreshUi();
});
}
onAdded(viewer) {
super.onAdded(viewer);
// todo subscribe to plugin add event if picking is not added yet.
this._picking = viewer.getPlugin('Picking');
this._picking?.addEventListener('selectedObjectChanged', this.refreshUiConfig); // don't call this.refreshUi here
viewer.addEventListener('postFrame', this._postFrame);
}
onRemove(viewer) {
this._picking = viewer.getPlugin('Picking');
this._picking?.removeEventListener('selectedObjectChanged', this.refreshUiConfig);
viewer.removeEventListener('postFrame', this._postFrame);
super.onRemove(viewer);
}
_postFrame() {
if (this._uiNeedRefresh)
this._refreshUi(); // only call this from here.
}
/**
* Select a switch node variation with name or uuid.
* @param node
* @param nameOrUuid
* @param setDirty - set dirty in the viewer after update.
*/
selectNode(node, nameOrUuid, setDirty = true) {
const obj = this._viewer?.scene.getObjectByName(node.name);
if (!obj || obj.children.length < 1)
return;
const child = typeof nameOrUuid === 'number' ?
obj.children[nameOrUuid] :
obj.children.find(c => c.name === nameOrUuid || c.uuid === nameOrUuid);
if (!child) {
this._viewer?.console.warn('SwitchNodePlugin: child not found', nameOrUuid);
return false;
}
node.selected = child.name || child.uuid;
let changed = false;
for (const child1 of obj.children) {
const visible = child1.visible;
child1.visible = (child1.name || child1.uuid) === node.selected;
changed = changed || visible !== child1.visible;
}
if (changed && setDirty)
this._viewer.scene.setDirty({ refreshScene: this.refreshScene, frameFade: true });
return changed;
}
/**
* Reapply all selected variations again.
* Useful when the scene is loaded or changed and the variations are not applied.
*/
reapplyAll() {
this.variations.forEach(v => this.selectNode(v, v.selected || 0, false));
this._viewer.scene.setDirty({ refreshScene: true, frameFade: true });
}
fromJSON(data, meta) {
this.variations = [];
if (!super.fromJSON(data, meta))
return null; // its not a promise
if (data.applyOnLoad === undefined) { // old files
this.applyOnLoad = true; // setting true because all the items will be visible otherwise.
}
if (this.applyOnLoad)
this.reapplyAll();
return this;
}
refreshUi() {
if (!this.enabled)
return;
this._uiNeedRefresh = true;
}
_refreshUi() {
if (!this.enabled)
return false;
if (!this._viewer)
return false;
this._uiNeedRefresh = false;
if (this.autoSnapIcons)
this.snapIcons();
this.refreshUiConfig();
return true;
}
refreshUiConfig() {
if (!this.enabled)
return;
this.uiConfig.uiRefresh?.();
}
/**
* Get the preview for a switch node variation
* Should be called from preFrame ideally. (or preRender but set viewerSetDirty = false)
* @param child - Child Object to get the preview for
* @param variation - Switch node variation that contains the child.
* @param viewerSetDirty - call viewer.setDirty() after setting the preview. So that the preview is cleared from the canvas.
*/
getPreview(variation, child, viewerSetDirty = true) {
if (!this._viewer || !variation)
return '';
// const m = typeof material === 'number' ? variation.materials[material] : material
const cv = variation.camView;
const camOffset = new Vector3((cv.includes('right') ? 1 : 0) - (cv.includes('left') ? 1 : 0), (cv.includes('top') ? 1 : 0) - (cv.includes('bottom') ? 1 : 0), (cv.includes('front') ? 1 : 0) - (cv.includes('back') ? 1 : 0));
if (!variation.camDistance)
variation.camDistance = 1;
const image = snapObject(this._viewer.renderManager.renderer, child, this._viewer?.scene, 7, camOffset.multiplyScalar(variation.camDistance * 2));
if (viewerSetDirty)
this._viewer.setDirty(); // because called from preFrame
return image;
}
addNode(node, refreshUi = true) {
this.variations.push(node);
if (refreshUi)
this.refreshUi();
}
/**
* Snapshots icons and puts in the userdata.__icon
*/
snapIcons() {
for (const variation of this.variations) {
const obj = this._viewer.scene.getObjectByName(variation.name);
if (!obj) {
console.warn('no object found for variation, skipping', variation);
continue;
}
if (obj.children.length < 1) {
console.warn('SwitchNode does not have enough children', variation);
}
for (const child of obj.children) {
if (child.userData.__icon)
return;
const image = this.getPreview(variation, child, false);
if (image)
child.userData.__icon = image;
}
}
}
}
SwitchNodeBasePlugin.PluginType = 'SwitchNodePlugin';
__decorate([
serialize()
], SwitchNodeBasePlugin.prototype, "refreshScene", void 0);
__decorate([
serialize()
], SwitchNodeBasePlugin.prototype, "variations", void 0);
//# sourceMappingURL=SwitchNodeBasePlugin.js.map