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.
162 lines (142 loc) • 6.86 kB
text/typescript
import {BufferAttribute, BufferGeometry, Float32BufferAttribute} from 'three'
import {Class, getOrCall} from 'ts-browser-helpers'
import {generateUiConfig, UiObjectConfig} from 'uiconfig.js'
import {IGeometry} from '../../core/IGeometry'
import {IMaterial} from '../../core/IMaterial'
import {IObject3D} from '../../core/IObject'
import {BufferGeometry2} from '../../core/geometry/BufferGeometry2'
import {LineGeometry2} from '../../core/geometry/LineGeometry2'
import {Mesh2} from '../../core/object/Mesh2'
import {PhysicalMaterial} from '../../core/material/PhysicalMaterial'
export interface GeometryGenerator<T=any>{
generate(g?: IGeometry, parameters?: T): IGeometry
createUiConfig?(geometry: IGeometry): UiObjectConfig[]
}
function updateAttribute<T extends BufferAttribute=Float32BufferAttribute>(geometry: BufferGeometry, attribute: string, itemSize: number, array: T | number[], cls?: Class<T>) {
const attr = geometry.getAttribute(attribute) as T
const count = Array.isArray(array) ? array.length / itemSize : array.count
if (attr && attr.count === count) {
attr.set(Array.isArray(array) ? array : (array as T).array)
attr.needsUpdate = true
} else {
geometry.setAttribute(attribute, Array.isArray(array) ? new (cls ?? Float32BufferAttribute)(array, itemSize) : array as T)
}
return attr
}
function updateIndices(geometry: BufferGeometry, indices: number[] | BufferAttribute) {
const index = geometry.index
if (index && index.count === (Array.isArray(indices) ? indices.length : indices.count)) {
index.set(Array.isArray(indices) ? indices : (indices as BufferAttribute).array)
index.needsUpdate = true // todo: wireframe attribute is not updating
} else geometry.setIndex(indices)
}
export function updateUi(geometry: BufferGeometry, childrenUi: () => UiObjectConfig[]) {
const uiConfig = (geometry as any).uiConfig as UiObjectConfig
if (!uiConfig) return
let oldUi = uiConfig.children?.find((c) => typeof c === 'object' && c.tags?.includes('generatedGeometry')) as UiObjectConfig | undefined
if (!oldUi) {
oldUi = {
type: 'folder',
label: 'Generation Params',
expanded: true,
tags: ['generatedGeometry'],
children: [],
}
const dividerIndex = uiConfig.children?.findIndex((c) => typeof c === 'object' && (c.type === 'divider' || c.type === 'separator')) ?? -1
if (dividerIndex >= 0) {
uiConfig.children?.splice(dividerIndex, 0, oldUi)
} else uiConfig.children?.push(oldUi)
}
if (geometry.userData.__generationParamsUiType !== geometry.userData.generationParams.type) {
oldUi.children = childrenUi()
geometry.userData.__generationParamsUiType = geometry.userData.generationParams.type
oldUi.uiRefresh?.(true, 'postFrame')
}
}
export function removeUi(geometry: BufferGeometry) {
const uiConfig = (geometry as any).uiConfig as UiObjectConfig
if (!uiConfig) return
const index = uiConfig.children?.findIndex((c) => typeof c === 'object' && c.tags?.includes('generatedGeometry')) ?? -1
if (index >= 0) {
uiConfig.children?.splice(index, 1)
uiConfig.uiRefresh?.(true, 'postFrame')
}
}
/**
* Abstract base class for geometry generators.
*
* Provides the framework for generating parametric geometries with automatic
* UI configuration and buffer management. Subclasses implement {@link _generateData}
* to define the actual vertex/index data for a specific geometry type.
*
* @category Plugins
*/
export abstract class AGeometryGenerator<Tp extends object=any, Tt extends string = string> implements GeometryGenerator<Tp> {
constructor(public type: Tt) {
}
abstract defaultParams: Tp
defaultMeshClass: ()=>Class<IObject3D> = ()=>Mesh2
defaultMaterialClass?: ()=>Class<IMaterial> = ()=>PhysicalMaterial
defaultGeometryClass?: ()=>Class<IGeometry> = ()=>BufferGeometry2
createUiConfig(geometry: IGeometry): UiObjectConfig[] {
if (!geometry.userData.generationParams) return []
const ui = (generateUiConfig(geometry.userData.generationParams)
?.flatMap(v=>getOrCall(v))
.filter(v=>getOrCall(v.property)?.[1] !== 'type') || []) as UiObjectConfig[]
ui.forEach(u=> {
u.onChange = () => this.generate(geometry)
})
return ui
}
protected abstract _generateData(params: Tp): {
indices?: number[] | BufferAttribute
vertices: number[] | BufferAttribute
normals?: number[] | BufferAttribute
uvs?: number[] | BufferAttribute
groups?: {start: number, count: number, materialIndex?: number}[]
positions?: number[] // for lines
}
generate(g?: IGeometry, parameters: Partial<Tp> = {}): IGeometry|BufferGeometry2 {
const geometry: IGeometry = g ?? new BufferGeometry2()
if ((parameters as any).type && (parameters as any).type !== this.type) {
console.error('Cannot change type of generated geometry here, use the plugin instead')
return geometry
}
if (!geometry.userData.generationParams) geometry.userData.generationParams = {type: this.type}
geometry.userData.generationParams.type = this.type
const params = {
...this.defaultParams,
...geometry.userData.generationParams,
...parameters,
type: this.type,
} as Tp
const {indices, vertices, normals, uvs, groups, positions} = this._generateData(params)
if (positions !== undefined && (geometry as LineGeometry2).setPositions) {
(geometry as LineGeometry2).setPositions(positions)
} else {
// console.log(indices, vertices, normals, uvs, groups)
indices && updateIndices(geometry, indices)
vertices && updateAttribute(geometry, 'position', 3, vertices)
normals && updateAttribute(geometry, 'normal', 3, normals)
uvs && updateAttribute(geometry, 'uv', 2, uvs)
}
if (groups) {
geometry.clearGroups()
for (const group of groups) {
geometry.addGroup(group.start, group.count, group.materialIndex)
}
}
geometry.computeBoundingBox && geometry.computeBoundingBox()
geometry.computeBoundingSphere && geometry.computeBoundingSphere()
// todo clean params with only allowed properties
Object.assign(geometry.userData.generationParams, params)
const childrenUi = ()=>this.createUiConfig(geometry)
updateUi(geometry, childrenUi)
geometry.setDirty()
return geometry
}
setDefaultParams(params: Partial<Tp>) {
Object.assign(this.defaultParams, params)
return this
}
}