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.

295 lines 12.7 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 { 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