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.

291 lines (282 loc) 12.5 kB
import {UiObjectConfig} from 'uiconfig.js' import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry' import {autoGPUInstanceMeshes, toIndexedGeometry} from '../../three/utils' import {BufferGeometry, Vector3} from 'three' import {ThreeViewer} from '../../viewer' import {IObject3D} from '../IObject' export const iGeometryCommons = { setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void { this.dispatchEvent({bubbleToObject: true, ...options, type: 'geometryUpdate', geometry: this}) // this sets sceneUpdate in root scene this.refreshUi() }, refreshUi: function(this: IGeometry) { this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) }, /** @ignore */ dispose: (superDispose: BufferGeometry['dispose']): IGeometry['dispose'] => function(this: IGeometry, force = true): void { if (!force && this.userData.disposeOnIdle === false) return superDispose.call(this) }, /** @ignore */ clone: (superClone: BufferGeometry['clone']): IGeometry['clone'] => function(this: IGeometry): IGeometry { return iGeometryCommons.upgradeGeometry.call(superClone.call(this)) }, upgradeGeometry: upgradeGeometry, /** @ignore */ center: (superCenter: BufferGeometry['center']): IGeometry['center'] => function(this: IGeometry, offset?: Vector3, keepWorldPosition = false, setDirty = true): IGeometry { if (keepWorldPosition) { offset = offset ? offset.clone() : new Vector3() superCenter.call(this, offset) offset.negate() const meshes = this.appliedMeshes for (const m of meshes) { m.updateMatrix() m.position.copy(offset).applyMatrix4(m.matrix) if (setDirty && m.setDirty) m.setDirty() } } else { superCenter.call(this, offset) } if (setDirty) this.setDirty() return this }, center2: function(this: IGeometry, offset?: Vector3, keepWorldPosition = false, setDirty = true): ()=>void { const offset1 = offset ? offset : new Vector3() if (keepWorldPosition) { this.center(offset1, false, false) const meshes = this.appliedMeshes const positions = new WeakMap<IObject3D, Vector3>() for (const m of meshes) { m.updateMatrix() positions.set(m, m.position.clone()) m.position.set(-offset1.x, -offset1.y, -offset1.z).applyMatrix4(m.matrix) if (setDirty) m.setDirty && m.setDirty() } if (setDirty) this.setDirty && this.setDirty() return ()=>{ // undo for (const m of meshes) { const pos = positions.get(m) if (!pos) { console.warn('GeometryCommons: No position found for mesh', m) continue } m.position.copy(pos) if (setDirty && m.setDirty) m.setDirty() } if (setDirty) this.setDirty && this.setDirty() } } else { this.center(offset1, false, false) if (setDirty) this.setDirty && this.setDirty() return ()=>{ // undo this.translate(-offset1.x, -offset1.y, -offset1.z) if (setDirty) this.setDirty && this.setDirty() } } }, makeUiConfig: function(this: IGeometry): UiObjectConfig { if (this.uiConfig) return this.uiConfig return { label: 'Geometry', uuid: 'geom', type: 'folder', children: [ { type: 'input', property: [this, 'name'], }, { type: 'input', property: [this, 'uuid'], disabled: true, tags: ['advanced'], }, ()=>this.groups.length ? { type: 'folder', label: 'Groups', uuid: 'groups', tags: ['advanced'], children: this.groups.map((g, i) => ({ type: 'folder', label: `Group ${i}`, uuid: 'group-' + i, tags: ['advanced'], children: [ { type: 'input', label: 'Start', uuid: 'start', getValue: () => g.start, setValue: (v: number) => { g.start = v this.setDirty && this.setDirty() }, }, { type: 'input', label: 'Count', uuid: 'count', getValue: () => g.count, setValue: (v: number) => { g.count = v this.setDirty && this.setDirty() }, }, { type: 'input', label: 'Material Index', uuid: 'mi', getValue: () => g.materialIndex, setValue: (v: number) => { g.materialIndex = v this.setDirty && this.setDirty() }, }, ], })), } : null, { type: 'divider', }, { type: 'button', label: 'Center Geometry', tags: ['context-menu'], value: async() => { if (!await ThreeViewer.Dialog.confirm('This will move the objects based on the geometry center, do you want to continue?')) return return this.center2() }, }, { type: 'button', label: 'Center Geometry (keep position)', tags: ['context-menu'], value: async() => { if (!await ThreeViewer.Dialog.confirm('This will move the geometry center keeping the object position, do you want to continue?')) return return this.center2(undefined, true) }, }, { type: 'button', label: 'Compute vertex normals', tags: ['context-menu'], value: async() => { if (this.hasAttribute('normal') && !await ThreeViewer.Dialog.confirm('Normals already exist, replace with computed normals?\nThis action cannot be undone.')) return this.computeVertexNormals() this.setDirty && this.setDirty() }, }, { type: 'button', label: 'Compute vertex tangents', tags: ['context-menu'], value: async() => { if (this.hasAttribute('tangent') && !await ThreeViewer.Dialog.confirm('Tangents already exist, replace with computed tangents?\nThis action cannot be undone.')) return this.computeTangents() this.setDirty && this.setDirty() }, }, { type: 'button', label: 'Normalize normals', tags: ['context-menu'], value: () => { this.normalizeNormals() this.setDirty && this.setDirty() }, }, { type: 'button', label: 'Convert to indexed', hidden: () => !!this.index, tags: ['context-menu'], value: async() => { if (this.attributes.index) return const tolerance = parseFloat(await ThreeViewer.Dialog.prompt('Convert to Indexed: Tolerance?', '-1') ?? '-1') toIndexedGeometry(this, tolerance) this.setDirty && this.setDirty() }, }, { type: 'button', label: 'Convert to non-indexed', hidden: () => !this.index, tags: ['context-menu'], value: () => { if (!this.attributes.index) return this.toNonIndexed() this.setDirty && this.setDirty() }, }, { type: 'button', label: 'Create uv1 from uv', tags: ['context-menu'], value: async() => { if (this.hasAttribute('uv1')) { if (!await ThreeViewer.Dialog.confirm('uv1 already exists, replace with uv data?\nThis action cannot be undone.')) return } this.setAttribute('uv1', this.getAttribute('uv')) this.setDirty && this.setDirty() }, }, { type: 'button', label: 'Remove vertex color attribute', hidden: () => !this.hasAttribute('color'), tags: ['context-menu'], value: async() => { if (!this.hasAttribute('color')) { await ThreeViewer.Dialog.alert('No color attribute found') return } if (!await ThreeViewer.Dialog.confirm('Remove color attribute?')) return this.deleteAttribute('color') this.setDirty && this.setDirty() }, }, { type: 'button', label: 'Auto GPU Instances', hidden: ()=> !this.appliedMeshes || this.appliedMeshes.size < 2, tags: ['context-menu'], value: async()=>{ if (!await ThreeViewer.Dialog.confirm('This will automatically create Instanced Mesh from geometry instances. This action is irreversible, do you want to continue?')) return autoGPUInstanceMeshes(this) }, }, { type: 'input', label: 'Mesh count', getValue: () => this.appliedMeshes?.size ?? 0, disabled: true, }, ], } }, } function upgradeGeometry(this: IGeometry) { if (this.assetType === 'geometry') return this// already upgraded if (!this.isBufferGeometry) { console.error('Geometry is not a BufferGeometry', this) return this } this.assetType = 'geometry' this.dispose = iGeometryCommons.dispose(this.dispose) this.center = iGeometryCommons.center(this.center) this.clone = iGeometryCommons.clone(this.clone) if (!this.center2) this.center2 = iGeometryCommons.center2 if (!this.setDirty) this.setDirty = iGeometryCommons.setDirty if (!this.refreshUi) this.refreshUi = iGeometryCommons.refreshUi if (!this.appliedMeshes) this.appliedMeshes = new Set() if (!this.userData) this.userData = {} this.uiConfig = iGeometryCommons.makeUiConfig.call(this) // todo: dispose uiconfig on geometry dispose // todo: add serialization? return this }