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.
369 lines (329 loc) • 14.2 kB
text/typescript
import {AssetExporterEventMap} from '../AssetExporter'
import {
IGeometry,
IMaterial,
iMaterialCommons,
IObject3D,
iObjectCommons,
ITexture,
MeshLine,
MeshLineSegments,
} from '../../core'
import {BufferAttribute, BufferGeometry} from 'three'
import {AssetImporter} from '../AssetImporter'
import {GLTFExporter2Options} from './GLTFExporter2'
import {getPartialProps, setPartialProps} from '../../utils'
export interface AssetExportHooks {
objectGeometry?: (obj: IObject3D, geometry: IGeometry, root: IObject3D)=>IGeometry|null|undefined
objectGeometryReplace?: (obj: IObject3D, geometry: IGeometry)=>void
object?: (obj: IObject3D, root: IObject3D)=>void
objectMaterials?: (obj: IObject3D, materials: IMaterial|IMaterial[]|undefined)=>void
objectMaterial?: (obj: IObject3D, material: IMaterial, root: IObject3D, materialIndex?: number)=>undefined | (()=>IMaterial)
objectMaterialsReplace?: (obj: IObject3D, materials: IMaterial|IMaterial[])=>void
replaceTexture?: (obj: IObject3D|IMaterial, texture: ITexture, mapName: string, root: IObject3D|IMaterial)=>ITexture|null
revertTextures?: (obj: IObject3D|IMaterial)=>void
revertObject?: (obj: IObject3D)=>void
}
export const assetExportHook = (e: AssetExporterEventMap['exportFile'], hooks: AssetExportHooks) => {
const options = e.exportOptions || {}
const mat = (e.obj as IMaterial).isMaterial ? e.obj as IMaterial : null
const obj = (e.obj as IObject3D).isObject3D ? e.obj as IObject3D : null
if (e.state === 'processing') {
if (mat) {
const maps: Map<string, ITexture> = hooks.replaceTexture ? iMaterialCommons.getMapsForMaterial.call(mat) : null
const savedMaps = maps && hooks.replaceTexture ? replaceExternalTextures(maps, mat, mat, hooks.replaceTexture) : null
mat.__exportState = {savedMaps}
}
if (obj) {
const {
objectList,
meshLines,
fomMap,
geomMap,
matCloneMap,
savedTextures,
} = obj.__exportState = {
objectList: new Set<IObject3D>(),
meshLines: new Map(),
fomMap: new Map(),
geomMap: new Map(),
matCloneMap: new Map(),
savedTextures: new Map(),
}
obj.traverse((obj1: IObject3D) => {
if (!obj1?.isObject3D) return
objectList.add(obj1)
if (obj1.children && obj1._sChildren) {
// @ts-expect-error temp
obj1._tChildren = obj1.children
obj1.children = obj1._sChildren as IObject3D[]
}
})
const textures: Map<IObject3D|IMaterial, Map<string, ITexture>> = new Map()
objectList.forEach(obj1=>{
if (options.preserveUUIDs !== false && obj1.uuid) obj1.userData.gltfUUID = obj1.uuid
hooks.object && hooks.object(obj1, obj)
// todo handle sProperties for objects as well
// if (obj1.userData.sProperties !== undefined) {
// const props = getSProps(obj1, obj1.userData.sProperties)
//
// }
processGLTFAnimations(obj1, options)
const geomOverride = replaceExternalGeometry(obj1, obj, hooks.objectGeometry)
if (geomOverride) {
hooks.objectGeometryReplace && hooks.objectGeometryReplace(obj1, geomOverride)
geomMap.set(obj1, obj1.forcedOverrideGeometry)
obj1.forcedOverrideGeometry = geomOverride
}
const current = !meshLines.has(obj1) ? processMeshLines(obj1) : null
if (current) meshLines.set(obj1, current)
hooks.objectMaterials && hooks.objectMaterials(obj1, obj.material)
const mats = processObjectMaterials(obj1, matCloneMap, textures, obj, hooks.objectMaterial)
if (mats !== undefined) {
hooks.objectMaterialsReplace && hooks.objectMaterialsReplace(obj1, mats)
fomMap.set(obj1, obj1.forcedOverrideMaterial)
obj1.forcedOverrideMaterial = mats
}
const textures1: Map<string, ITexture> = iObjectCommons.getMapsForObject3D.call(obj1)
textures.set(obj1, textures1)
})
if (hooks.replaceTexture)
textures.forEach((textures1, obj1)=>{
const savedMaps = replaceExternalTextures(textures1, obj1, obj, hooks.replaceTexture)
savedTextures.set(obj1, savedMaps)
})
textures.clear()
}
}
if (e.state === 'done' || e.state === 'error') {
if (mat && mat.__exportState) {
hooks.revertTextures && hooks.revertTextures(mat)
revertExternalTextures(mat.__exportState.savedMaps, mat)
delete mat.__exportState
}
if (obj && obj.__exportState) {
const {
objectList,
meshLines,
fomMap,
geomMap,
matCloneMap,
savedTextures,
} = obj.__exportState
delete obj.__exportState
objectList.forEach(obj1=>{
if (options.preserveUUIDs !== false && obj1.userData.gltfUUID) delete obj1.userData.gltfUUID
revertMeshLines(obj1, meshLines.get(obj1))
hooks.revertObject && hooks.revertObject(obj1)
// @ts-expect-error temp
if (obj1._tChildren) {
// @ts-expect-error temp
obj1.children = obj1._tChildren
// @ts-expect-error temp
delete obj1._tChildren
}
})
meshLines.clear()
objectList.clear()
new Set([...matCloneMap.values()]).forEach(m=>{
m.dispose && m.dispose()
})
matCloneMap.clear()
fomMap.forEach((fom, obj1)=>{
if (fom !== undefined) obj1.forcedOverrideMaterial = fom
else delete obj1.forcedOverrideMaterial
})
fomMap.clear()
geomMap.forEach((fom, obj1)=>{
if (fom !== undefined) obj1.forcedOverrideGeometry = fom
else delete obj1.forcedOverrideGeometry
})
geomMap.clear()
// revert external map references
if (hooks.revertTextures)
savedTextures.forEach((savedMaps, obj1)=>{
hooks.revertTextures && hooks.revertTextures(obj1)
revertExternalTextures(savedMaps, obj1)
})
savedTextures.clear()
}
}
}
/**
* Find all the textures that do not belong to that rootPath (asset) and replace them with null in the material,
* and save their ids in userData.tpAssetRefIds
* @param maps
* @param obj
*/
function replaceExternalTextures(maps: Map<string, ITexture>, obj: IMaterial|IObject3D, root: IObject3D|IMaterial, cb: AssetExportHooks['replaceTexture']) {
const extMaps: Record<string, ITexture|null> = {}
const savedMaps: Record<string, ITexture> = {}
maps.forEach((texture, k) => {
if (!cb) return
const r = cb(obj, texture, k, root)
if (r !== texture) {
extMaps[k] = r
savedMaps[k] = texture
}
})
setPartialProps(extMaps, obj)
return savedMaps
}
function revertExternalTextures(savedMaps: Record<string, ITexture>|null, obj: IObject3D|IMaterial) {
savedMaps && setPartialProps(savedMaps, obj)
}
function replaceExternalGeometry(obj1: IObject3D, root: IObject3D, cb?: AssetExportHooks['objectGeometry']) {
const geometry = obj1.geometry
if (!geometry) return undefined
let g: IGeometry|null = null
if (geometry.userData.isPlaceholder) {
g = AssetImporter.DummyGeometry
} else if (cb) {
const g1 = cb(obj1, geometry, root)
if (g1) g = g1
}
if (g && g !== geometry) {
return g
}
}
/**
* save the root where gltf animations are set, this is required since objects can have the same name in diff hierarchies
* @param obj1
* @param options
*/
function processGLTFAnimations(obj1: IObject3D, options: GLTFExporter2Options) {
if (obj1.animations) {
if (!options.animations) options.animations = []
for (const animation of obj1.animations) {
if (animation.__gltfExport === false) continue
const rootRefs: string[] = animation.userData.rootRefs || []
if (options.preserveUUIDs !== false && obj1.uuid) {
if (!rootRefs.includes(obj1.uuid)) {
rootRefs.push(obj1.uuid)
}
} else if (obj1.name) {
if (!rootRefs.includes(obj1.name)) {
rootRefs.push(obj1.name)
}
}
animation.userData.rootRefs = rootRefs
if (!options.animations.includes(animation))
options.animations.push(animation)
}
}
}
function processMeshLines(obj1: IObject3D) {
const line1 = (obj1 as any as MeshLine | MeshLineSegments)
const geometry = line1.geometry
// for mesh lines, create a temp line (BufferGeometry) so GLTFExporter correctly saves it as mode = line.
if (typeof geometry?.getPositions === 'function'
// && !obj1.geometry?.attributes.position
&& obj1.isLine === undefined && obj1.isLineSegments === undefined
&& (obj1.isLine2 || obj1.isLineSegments2)
) {
const positions = geometry.getPositions()
if (positions) {
const colors = geometry.getColors && line1.geometry.getColors()
const g1 = new BufferGeometry()
g1.attributes.position = new BufferAttribute(positions, 3)
if (colors) g1.attributes.color = new BufferAttribute(colors, 3)
g1.name = geometry.name
g1.userData = geometry.userData
g1.uuid = geometry.uuid
// todo groups? anything else
const current = obj1.geometry
if (obj1.assetType)
obj1._currentGeometry = g1 as any
else
obj1.geometry = g1 as any
if ((line1 as MeshLine).isLine2) obj1.isLine = true
else if ((line1 as MeshLine).isLineSegments2) {
obj1.isLine = true
obj1.isLineSegments = true
}
return current
}
}
}
function processObjectMaterials(obj1: IObject3D, matCloneMap: Map<IMaterial, IMaterial>, textures: Map<IObject3D | IMaterial, Map<string, ITexture>>, root: IObject3D, cb: AssetExportHooks['objectMaterial']) {
if (!obj1.material) return
const materials = obj1.material
const isArr = Array.isArray(materials)
const materialsArr = isArr ? materials : [materials]
let mats = isArr ? [...materials] : materials
const setMaterialRef = (i: number, material: IMaterial, ctor: ()=>IMaterial) => {
let mat2 = matCloneMap.get(material)
if (!mat2) {
mat2 = ctor()
matCloneMap.set(material, mat2)
}
if (isArr) {
// @ts-expect-error ts.
mats[i] = mat2
} else {
mats = mat2
}
}
materialsArr.forEach((material, i) => {
if (material.userData.isPlaceholder) {
// material is a dummy placeholder
setMaterialRef(i, material, ()=>AssetImporter.DummyMaterial)
} else {
const r = cb ? cb(obj1, material, root, i) : null
if (r) setMaterialRef(i, material, r)
else {
const textures1: Map<string, ITexture> = iMaterialCommons.getMapsForMaterial.call(material)
textures.set(material, textures1)
// todo do the same sProperties thing for objects as well.
if (material.userData.sProperties !== undefined) { // clone the material and save only the specified properties
setMaterialRef(i, material, ()=>{
const mat3: IMaterial = new material.constructor()
mat3.name = material.name
const props = getPartialProps(material, material.userData.sProperties)
setPartialProps(props, mat3)
mat3.userData.uuid = material.uuid
mat3.userData.sProperties = material.userData.sProperties
mat3.userData.rootPath = material.userData.rootPath
mat3.userData.rootPathOptions = material.userData.rootPathOptions
return mat3
})
}
}
}
})
if (!isArr ? mats !== materials : (mats as IMaterial[]).some((m, i) => m !== (materials as IMaterial[])[i])) {
return mats
}
return
}
function revertMeshLines(obj1: IObject3D, g1?: IGeometry) {
if (g1 && obj1.geometry) {
const g = obj1.geometry
if (obj1.assetType)
obj1._currentGeometry = g1
else
obj1.geometry = g1
g.dispose(true)
if (obj1.isLine) delete obj1.isLine
if (obj1.isLineSegments) delete obj1.isLineSegments
}
}
declare module '../../core/IObject' {
interface IObject3D{
__exportState?: {
objectList: Set<IObject3D>,
meshLines: Map<IObject3D, IGeometry>,
fomMap: Map<IObject3D, IObject3D['forcedOverrideMaterial']>,
geomMap: Map<IObject3D, IObject3D['forcedOverrideGeometry']>,
matCloneMap: Map<IMaterial, IMaterial>,
savedTextures: Map<IObject3D|IMaterial, Record<string, ITexture>>,
}
}
}
declare module '../../core/IMaterial' {
interface IMaterial{
__exportState?: {
savedMaps: Record<string, ITexture>|null
}
}
}