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.

370 lines 15.5 kB
import { Vector3 } from 'three'; import { ThreeViewer } from '../../viewer'; import { UnlitMaterial } from '../material/UnlitMaterial'; import { PhysicalMaterial } from '../material/PhysicalMaterial'; import { LineMaterial2 } from '../material/LineMaterial2'; import { UnlitLineMaterial } from '../material/UnlitLineMaterial'; import { generateUUID } from '../../three'; // todo move somewhere? const defaultMaterial = new UnlitMaterial(); defaultMaterial.name = 'Default Unlit Material'; defaultMaterial.uiConfig = undefined; const defaultUnlitLineMaterial = new UnlitLineMaterial(); defaultUnlitLineMaterial.name = 'Default Unlit Line Material'; defaultUnlitLineMaterial.uiConfig = undefined; const defaultLineMaterial = new LineMaterial2(); defaultLineMaterial.name = 'Default Line Material'; defaultLineMaterial.uiConfig = undefined; export function objectExtensionsUiConfig() { return () => this.objectExtensions?.flatMap(v => { v.uuid = v.uuid || generateUUID(); // caching the uiconfig here. todo: reset the uiconfig when cache key changes? or we could just return a dynamic/function uiconfig from getUiConfig this.__objExtUiConfigs = this.__objExtUiConfigs || {}; if (!this.__objExtUiConfigs[v.uuid]) this.__objExtUiConfigs[v.uuid] = v.getUiConfig?.(this, this.uiConfig?.uiRefresh); return this.__objExtUiConfigs[v.uuid]; }).filter(v => v); } export function makeICameraCommonUiConfig(config) { return [ { type: 'button', label: 'Set View', value: () => { this.setViewToMain({ ui: true }); config.uiRefresh?.(true, 'postFrame'); // config is parent config }, }, { type: 'button', label: 'Activate main', hidden: () => this?.isMainCamera, value: () => { this.activateMain({ ui: true }); config.uiRefresh?.(true, 'postFrame'); }, }, { type: 'button', label: 'Deactivate main', hidden: () => !this?.isMainCamera, value: () => { this.deactivateMain({ ui: true }); config.uiRefresh?.(true, 'postFrame'); }, }, { type: 'checkbox', label: 'Auto LookAt Target', getValue: () => this.userData.autoLookAtTarget ?? false, setValue: (v) => { this.userData.autoLookAtTarget = v; config.uiRefresh?.(true, 'postFrame'); }, }, ]; } export function makeIObject3DUiConfig(isMesh) { if (!this) return {}; if (this.uiConfig) return this.uiConfig; const config = { type: 'folder', label: () => this.name || 'unnamed', onChange: (ev) => { if (!ev.config || ev.config.onChange) return; let key = Array.isArray(ev.config.property) ? ev.config.property[1] : ev.config.property; key = typeof key === 'string' ? key : undefined; this.setDirty({ uiChangeEvent: ev, refreshScene: false, refreshUi: !!ev.last, change: key }); }, children: [ { type: 'checkbox', label: 'Visible', property: [this, 'visible'], onChange: (e) => { this.setDirty?.({ uiChangeEvent: e, refreshScene: true, refreshUi: true, change: 'visible' }); }, }, // moved to PickingPlugin // { // type: 'button', // label: 'Pick/Focus', // todo: move to the plugin that does the picking // value: ()=>{ // this.dispatchEvent({type: 'select', ui: true, object: this, bubbleToParent: true, focusCamera: true}) // }, // }, // { // type: 'button', // label: 'Pick Parent', // todo: move to the plugin that does the picking // hidden: ()=>!this.parent, // value: ()=>{ // const parent = this.parent // if (parent) { // parent.dispatchEvent({type: 'select', ui: true, bubbleToParent: true, object: parent}) // } // }, // }, { type: 'input', label: 'Name', property: [this, 'name'], onChange: (e) => { if (e.last) this.setDirty?.({ uiChangeEvent: e, refreshScene: true, frameFade: false, refreshUi: true, change: 'name' }); }, }, { type: 'checkbox', label: 'Casts Shadow', hidden: () => !this.isMesh, property: [this, 'castShadow'], onChange: (e) => { this.setDirty?.({ uiChangeEvent: e, refreshScene: true, refreshUi: true, change: 'castShadow' }); }, }, { type: 'checkbox', label: 'Receive Shadow', hidden: () => !this.isMesh, property: [this, 'receiveShadow'], onChange: (e) => { this.setDirty?.({ uiChangeEvent: e, refreshScene: true, refreshUi: true, change: 'receiveShadow' }); }, }, { type: 'checkbox', label: 'Frustum culled', property: [this, 'frustumCulled'], }, { type: 'vec3', label: 'Position', property: [this, 'position'], }, { type: 'vec3', label: 'Rotation', property: [this, 'rotation'], }, { type: 'vec3', label: 'Scale', property: [this, 'scale'], }, { type: 'input', label: 'Render Order', property: [this, 'renderOrder'], }, { type: 'button', label: 'Auto Scale', hidden: () => !this.autoScale, // prompt: ['Auto Scale Radius: Object will be scaled to the given radius', this.userData.autoScaleRadius || '2', true], value: async () => { const def = (this.userData.autoScaleRadius || 2) + ''; const res = await ThreeViewer.Dialog.prompt('Auto Scale Radius: Object will be scaled to the given radius', def); if (res === null) return; const rad = parseFloat(res || def); if (Math.abs(rad) > 0) { return { action: () => this.autoScale?.(rad), undo: () => this.autoScale?.(rad, undefined, undefined, true), }; } }, }, { type: 'button', label: 'Auto Center', value: () => { // const res = await ThreeViewer.Dialog.confirm('Auto Center: Object will be centered, are you sure you want to proceed?') // if (!res) return return { action: () => this.autoCenter?.(true), undo: () => this.autoCenter?.(true, true), }; }, }, { type: 'button', label: 'Pivot to Node Center', tags: ['context-menu', 'interaction'], value: async () => { const res = await ThreeViewer.Dialog.confirm('Pivot to Center: Adjust the pivot to bounding box center. The object will rotate around the new pivot, are you sure you want to proceed?'); if (!res) return; return this.pivotToBoundsCenter?.(true); // return value is the undo function }, }, { type: 'button', label: 'Duplicate Object', tags: ['context-menu'], value: async () => { const parent = this.parent; const clone = this.clone(true); clone.name = this.name + ' (copy)'; return { action: () => { if (parent && !clone.parent) parent.add(clone); // todo same index? }, undo: () => { if (clone.parent === parent) clone.removeFromParent(); }, }; }, }, { type: 'button', label: 'Delete Object', tags: ['context-menu'], value: async () => { const res = await ThreeViewer.Dialog.confirm('Delete Object: Are you sure you want to delete this object?'); if (!res) return; const parent = this.parent; this.dispose(true); return () => { if (parent) parent.add(this); }; }, }, { type: 'folder', label: 'Rotate model', children: [ 'X +', 'X -', 'Y +', 'Y -', 'Z +', 'Z -', ].map((l) => { return { type: 'button', label: 'Rotate ' + l + '90', value: () => { const axis = new Vector3(l.includes('X') ? 1 : 0, l.includes('Y') ? 1 : 0, l.includes('Z') ? 1 : 0); const angle = Math.PI / 2 * (l.includes('-') ? -1 : 1); return { action: () => { this.rotateOnAxis(axis, angle); this.setDirty?.({ refreshScene: true, refreshUi: false }); }, undo: () => { this.rotateOnAxis(axis, -angle); this.setDirty?.({ refreshScene: true, refreshUi: false }); }, }; }, }; }), }, this.userData.license !== undefined ? { type: 'input', label: 'License/Credits', property: [this.userData, 'license'], } : {}, ], }; if ((this.isLine || this.isMesh) && isMesh !== false) { // todo: move to make mesh ui function? const ui = [ // morph targets () => { const dict = Object.entries(this.morphTargetDictionary || {}); return dict.length ? { label: 'Morph Targets', type: 'folder', children: dict.map(([name, i]) => ({ type: 'slider', label: name, bounds: [0, 1], stepSize: 0.0001, property: [this.morphTargetInfluences, i], onChange: (e) => { this.setDirty?.({ refreshScene: e.last, frameFade: false, refreshUi: false }); }, })), } : undefined; }, { type: 'divider', }, // geometry () => this.geometry?.uiConfig, // material(s) () => Array.isArray(this.material) ? this.material.length < 1 ? undefined : { label: 'Materials', type: 'folder', children: this.material.map((a) => a?.uiConfig).filter(a => a), } : this.material?.uiConfig, { label: 'Remove Material(s)', type: 'button', hidden: () => !this.materials?.length || this.materials.length === 1 && [defaultMaterial, defaultLineMaterial, defaultUnlitLineMaterial].includes(this.materials[0]), value: () => { const mat = this.materials; this.material = this.isLineSegments2 ? [defaultLineMaterial] : this.isLineSegments ? [defaultUnlitLineMaterial] : [defaultMaterial]; return () => this.material = mat; }, }, { label: 'New Line Material', type: 'button', hidden: () => !this.isLineSegments2 || !(!this.materials?.length || this.materials.length === 1 && this.materials[0] === defaultLineMaterial), value: () => { const mat = this.materials; this.material = [new LineMaterial2()]; return () => this.material = mat; }, }, { label: 'New Unlit Line Material', type: 'button', hidden: () => !this.isLineSegments || !(!this.materials?.length || this.materials.length === 1 && this.materials[0] === defaultUnlitLineMaterial), value: () => { const mat = this.materials; this.material = [new UnlitLineMaterial()]; return () => this.material = mat; }, }, { label: 'New Physical Material', type: 'button', hidden: () => !(!this.materials?.length || this.materials.length === 1 && this.materials[0] === defaultMaterial) || !!this.isLineSegments2 || !!this.isLineSegments, value: () => { const mat = this.materials; this.material = [new PhysicalMaterial()]; return () => this.material = mat; }, }, { label: 'New Unlit Material', type: 'button', hidden: () => !(!this.materials?.length || this.materials.length === 1 && this.materials[0] === defaultMaterial) || !!this.isLineSegments2 || !!this.isLineSegments, value: () => { const mat = this.materials; this.material = [new UnlitMaterial()]; return () => this.material = mat; }, }, ]; config.children.push(...ui); } // todo: if we are replacing all the cameras in the scene, is this even required? if (this.isCamera) { const ui = makeICameraCommonUiConfig.call(this, config); config.children.push(...ui); } config.children.push(objectExtensionsUiConfig.call(this)); // todo: lights? this.uiConfig = config; return config; } //# sourceMappingURL=IObjectUi.js.map