|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779 |
- import {arrayBufferToBase64, base64ToArrayBuffer, getTypedArray, Serialization} from 'ts-browser-helpers'
- import {
- Color,
- Material,
- MaterialLoader,
- Matrix3,
- Matrix4,
- ObjectLoader,
- Quaternion,
- Source,
- Texture,
- Vector2,
- Vector3,
- Vector4,
- } from 'three'
- import type {AssetImporter, AssetManager, MaterialManager} from '../assetmanager'
- import {BlobExt, IAssetImporter} from '../assetmanager'
- import {ThreeViewer} from '../viewer'
- import {ITexture} from '../core'
- import {IRenderTarget, RenderManager} from '../rendering'
- import {textureToCanvas} from '../three/utils/texture'
-
- const copier = (c: any) => (v: any, o: any) => o?.copy?.(v) ?? new c().copy(v)
- export class ThreeSerialization {
- static {
- const primitives = [
- [Vector2, 'isVector2', ['x', 'y']],
- [Vector3, 'isVector3', ['x', 'y', 'z']],
- [Vector4, 'isVector4', ['x', 'y', 'z', 'w']],
- [Quaternion, 'isQuaternion', ['x', 'y', 'z', 'w']],
- [Color, 'isColor', ['r', 'g', 'b']],
- [Matrix3, 'isMatrix3', ['elements']],
- [Matrix4, 'isMatrix4', ['elements']],
- ] as const
- Serialization.RegisterSerializer(...primitives.map(p=>({
- priority: 1,
- isType: (obj: any) => obj[p[1]],
- serialize: (obj: any) => {
- const ret = {[p[1]]: true}
- for (const k of p[2]) ret[k] = obj[k]
- return ret
- },
- deserialize: copier(p[0]),
- })))
-
- // texture
- Serialization.RegisterSerializer({
- priority: 2,
- isType: (obj: any) => obj.isTexture || obj.metadata?.type === 'Texture',
- serialize: (obj: any, meta?: SerializationMetaType) => {
- if (!obj?.isTexture) throw new Error('Expected a texture')
- if (obj.isRenderTargetTexture) return undefined // todo: support render targets
- // if (obj.isRenderTargetTexture && !obj.userData?.serializableRenderTarget) return undefined
- if (meta?.textures[obj.uuid]) return {uuid: obj.uuid, resource: 'textures'}
- const imgData = obj.source.data
- const hasRootPath = !obj.isRenderTargetTexture && obj.userData.rootPath
- if (hasRootPath) {
- if (obj.source.data) {
- if (!obj.userData.embedUrlImagePreviews) // todo make sure its only Texture, check for svg etc
- obj.source.data = null // handled in GLTFWriter2.processImage
- else {
- obj.source.data = textureToCanvas(obj, 16, obj.flipY) // todo: check flipY
- }
- }
- }
- const ud = obj.userData
- obj.userData = {} // toJSON will call JSON.stringify, which will serialize userData
- const meta2 = {images: {} as any} // in-case meta is undefined
- let res = obj.toJSON(meta || meta2)
- if (!meta && res.image) res.image = hasRootPath && !obj.userData.embedUrlImagePreviews ? undefined : meta2.images[res.image]
- obj.userData = ud
- res.userData = Serialization.Serialize(copyTextureUserData({}, ud), meta, false)
- if (hasRootPath) {
- if (meta && !obj.userData.embedUrlImagePreviews) delete meta.images[obj.source.uuid] // because its empty. uuid still stored in the texture.image
- obj.source.data = imgData
- }
-
- if (meta?.textures && !res.resource) {
- if (!meta.textures[res.uuid])
- meta.textures[res.uuid] = res
- res = {uuid: res.uuid, resource: 'textures'}
- }
- return res
- },
- deserialize: (dat: any, obj: any, meta?: SerializationMetaType) => {
- if (dat.isTexture) return dat
- if (dat.resource === 'textures' && meta?.textures?.[dat.uuid]) return meta.textures[dat.uuid]
-
- console.warn('Cannot deserialize texture into object like primitive, since textures need to be loaded asynchronously. Trying with ObjectLoader. Load events might not work properly.', dat, obj)
- const loader = meta?._context.objectLoader ?? new ObjectLoader(meta?._context.assetImporter?.loadingManager)
- const data = {...dat}
- if (typeof data.image === 'string') {
- if (!meta?.images) {
- console.error('Cannot deserialize texture with image url without meta.images', data)
- } else {
- data.image = meta.images[data.image]
- }
- }
- if (!data.image || typeof data.image === 'string' || !data.image.isSource && !data.image.url) {
- console.error('Cannot deserialize texture', data)
- return obj
- }
- let imageOnLoad: undefined | (()=>void)
- if (meta && !data.image.isSource) {
- if (!meta._context.imagePromises) meta._context.imagePromises = []
- meta._context.imagePromises.push(new Promise<void>((resolve) => {
- imageOnLoad = resolve
- }))
- }
- const sources = data.image.isSource ? {[data.image.uuid]: data.image as Source} : loader.parseImages([data.image], imageOnLoad)
- data.image = Object.keys(sources)[0]
- if (meta?.images) meta.images[data.image] = sources[data.image]
- if (data.userData) data.userData = ThreeSerialization.Deserialize(data.userData, {}, meta)
- const textures = loader.parseTextures([data], sources)
- const uuid = Object.keys(textures)[0]
- if (!uuid || !textures[uuid]) {
- console.error('Cannot deserialize texture', data)
- return obj
- }
- if (meta?.textures) meta.textures[uuid] = textures[uuid]
- return textures[uuid]
- },
- })
-
- // material
- Serialization.RegisterSerializer({
- priority: 2,
- isType: (obj: any) => obj.isMaterial || obj.metadata?.type === 'Material',
- serialize: (obj: any, meta?: SerializationMetaType) => {
- if (!obj?.isMaterial) throw new Error('Expected a material')
- if (meta?.materials[obj.uuid]) return {uuid: obj.uuid, resource: 'materials'}
- if (obj.userData.rootPath) {
- // todo
- // it works for textures because image(Source) are immutable
- console.error('TODO: handle material with root path with material inheritance/hierarchy')
-
- }
-
- // serialize textures separately
- const meta2 = meta ?? {textures: {}, images: {}}
- const objTextures: any = {}
- const tempTextures: any = {}
- const propList = Object.keys(obj.constructor.MaterialProperties || obj)
- for (const k of propList) {
- if (k.startsWith('__')) continue // skip private/internal textures/properties
- const v = obj[k]
- if (v?.isTexture) {
- const ser = Serialization.Serialize(v, meta2)
- objTextures[k] = ser
- tempTextures[k] = v
- obj[k] = ser ? {isTexture: true, toJSON: ()=> ser} : null // because of how threejs Material.toJSON serializes textures
- }
- }
-
- // Serialize without userData because three.js tries to convert it to string. We are serializing it separately
- const userData = obj.userData
- obj.userData = {}
- let res = obj.toJSON(meta, true) // copying userData is handled in toJSON, see MeshStandardMaterial2
- obj.userData = userData
- serializeMaterialUserData(res, userData, meta)
-
- // todo: override generator to mention that this is a custom serializer?
-
- res.userData.uuid = obj.userData.uuid
- if (obj.constructor.TYPE) res.type = obj.constructor.TYPE // override type if specified as static property in the class
-
- // Remove undefined values. Note that null values are kept.
- for (const key of Object.keys(res)) if (res[key] === undefined) delete res[key]
-
- // Restore textures
- for (const [k, v] of Object.entries(tempTextures)) {
- obj[k] = v
- delete tempTextures[k]
- }
- // Add material, textures, images to meta
- // serialize textures are already added to meta by the texture serializer
- if (meta) {
- for (const [k, v] of Object.entries(objTextures)) {
- if (v) res[k] = v // can be undefined because of RenderTargetTexture...
- }
- if (meta.materials) {
- if (!meta.materials[res.uuid])
- meta.materials[res.uuid] = res
- res = {uuid: res.uuid, resource: 'materials'}
- }
- } else {
- for (const [k, v] of Object.entries(objTextures)) {
- if (v) res[k] = (v as any).uuid // to remain compatible with how three.js saves
- }
- res.textures = Object.values(meta2.textures)
- res.images = Object.values(meta2.images)
- }
- return res
- },
- deserialize: (dat: any, obj: any, meta?: SerializationMetaType) => {
- function finalCopy(material: Material) {
- if (material.isMaterial) {
- if (obj?.isMaterial && obj.uuid === material.uuid) {
- if (obj !== material && typeof obj.setValues === 'function') {
- console.warn('material uuid already exists, copying values to old material')
- obj.setValues(material)
- }
- return obj
- } else {
- return material
- }
- }
- return undefined
- }
-
- let ret = finalCopy(dat)
- if (ret !== undefined) return ret
- if (dat.resource === 'materials' && meta?.materials?.[dat.uuid]) {
- ret = finalCopy(meta.materials[dat.uuid])
- if (ret !== undefined) return ret
- console.error('cannot find material in meta', dat, ret)
- }
-
- const type = dat.type
- if (!type) {
- console.error('Cannot deserialize material without type', dat)
- return obj
- }
- const data = {...dat} as Record<string, any>
- if (data.userData) data.userData = Serialization.Deserialize(data.userData, undefined, meta, false)
- //
- const textures: Record<string, Texture> = {}
- for (const [k, v] of Object.entries(data)) { // for textures
- if (typeof v === 'string' && meta?.textures?.[v]) {
- data[k] = meta.textures[v]
- textures[k] = meta.textures[v]
- }
- if (!v || !v.resource || typeof v.resource !== 'string') continue
- const resource = meta?.[v.resource as 'textures'|'extras']?.[v.uuid]
- data[k] = resource || null
- if (v.resource === 'textures' && resource?.isTexture) {
- textures[k] = resource
- }
- }
-
- // we have 2 options, either obj is null or it is a material.
- // if the material is not the same type, we can't use it, should we throw an error or create a new material and assign it. maybe a warning and create a new material?
- // to create a material, we need to know the type, type->material initialization can be done in either material manager or MaterialLoader
-
- // data has deserialized textures and userData, assuming the rest can be deserialized by material.fromJSON
-
- if (!obj || !obj.isMaterial || obj.type !== type && obj.constructor?.TYPE !== type) {
- if (obj && Object.keys(obj).length) console.warn('Material type mismatch during deserialize, creating a new material', obj, data, type, obj.constructor?.type)
- obj = null
- }
- // if obj is not null
- if (obj && (!data.uuid || obj.uuid === data.uuid)) {
- if (obj.fromJSON) obj.fromJSON(data, meta, true)
- else if (obj.setValues) obj.setValues(data)
- else console.error('Cannot deserialize material, no fromJSON or setValues method', obj, data)
- return obj
- }
-
- // obj is null or type mismatch, so ignore obj and create a new material
-
- // generate from material manager generator and call fromJSON with internal true which will call setValues
- const materialManager = meta?._context.materialManager
- if (materialManager) {
- const material = materialManager.create(type)
- if (material) {
- if (material.fromJSON) material.fromJSON(data, meta, true)
- else if (material.setValues) material.setValues(data)
- else console.error('Cannot deserialize material, no fromJSON or setValues method', material, data)
- return material
- }
- }
-
- console.warn('Legacy three.js material deserialization')
-
- // normal three.js material
- const loader = new MaterialLoader() // todo: get loader from meta.loaders
- for (const [k, v] of Object.entries(textures)) {
- data[k] = v.uuid
- }
- const texs = {...loader.textures}
- loader.setTextures(textures)
- const mat = loader.parse(data)
- loader.setTextures(texs)
-
- ret = finalCopy(mat)
- if (ret !== undefined) return ret
- console.error('cannot deserialize material', dat, ret, mat)
-
- },
- })
-
- // render target
- Serialization.RegisterSerializer({
- priority: 2,
- isType: (obj: any) => obj.isWebGLRenderTarget || obj.metadata?.type === 'RenderTarget',
- serialize: (obj: IRenderTarget, meta?: SerializationMetaType) => {
- if (!obj?.isWebGLRenderTarget || !obj.uuid) throw new Error('Expected a IRenderTarget')
- if (meta?.extras[obj.uuid]) return {uuid: obj.uuid, resource: 'extras'}
-
- // This is for the class implementing IRenderTarget, check {@link RenderTargetManager} for class implementation
- const tex = Array.isArray(obj.texture) ? obj.texture[0] : obj.texture
- let res: any = {
- metadata: {type: 'RenderTarget'},
- uuid: obj.uuid,
- width: obj.width,
- height: obj.height,
- depth: obj.depth,
- sizeMultiplier: obj.sizeMultiplier,
- count: Array.isArray(obj.texture) ? obj.texture.length : undefined,
- isCubeRenderTarget: obj.isWebGLCubeRenderTarget || undefined,
- isTemporary: obj.isTemporary,
- textureName: Array.isArray(obj.texture) ? obj.texture.map(t => t.name) : obj.texture?.name,
- options: {
- wrapS: tex?.wrapS,
- wrapT: tex?.wrapT,
- magFilter: tex?.magFilter,
- minFilter: tex?.minFilter,
- format: tex?.format,
- type: tex?.type,
- anisotropy: tex?.anisotropy,
- depthBuffer: !!obj.depthBuffer,
- stencilBuffer: !!obj.stencilBuffer,
- generateMipmaps: tex?.generateMipmaps,
- depthTexture: !!obj.depthTexture,
- colorSpace: tex?.colorSpace,
- samples: obj.samples,
- },
- }
-
- if (meta?.extras) {
- if (!meta.extras[res.uuid])
- meta.extras[res.uuid] = res
- res = {uuid: res.uuid, resource: 'extras'}
- }
- return res
- },
- deserialize: (dat: any, obj: any, meta?: SerializationMetaType) => {
- if (obj?.uuid === dat.uuid) return obj
- if (dat.isWebGLRenderTarget) return dat
-
- const renderManager = meta?._context.renderManager
- if (!renderManager) {
- console.error('Cannot deserialize render target without render manager', dat)
- return obj
- }
- if (dat.isWebGLCubeRenderTarget || dat.isTemporary) {
- // todo support cube, temporary render target here
- console.warn('Cannot deserialize WebGLCubeRenderTarget or temporary render target yet', dat)
- return obj
- }
-
- const res = renderManager.createTarget({
- sizeMultiplier: dat.sizeMultiplier || undefined,
- size: dat.sizeMultiplier ? undefined : {width: dat.width, height: dat.height},
- textureCount: dat.count,
- ...dat.options,
- })
- if (dat.textureName) {
- if (Array.isArray(dat.textureName) && Array.isArray(res.texture)) {
- for (let i = 0; i < dat.textureName.length; i++) {
- res.texture[i].name = dat.textureName[i]
- }
- } else if (!Array.isArray(res.texture)) {
- res.texture.name = Array.isArray(dat.textureName) ? dat.textureName[0] : dat.textureName
- }
- }
- if (!res) return res
- res.uuid = dat.uuid
- if (meta?.extras) meta.extras[dat.uuid] = res
- return res
- },
- })
-
- }
-
- /**
- * Serialize an object
- * {@link Serialization.Serialize}
- */
- static Serialize = Serialization.Serialize
-
- /**
- * Deserialize an object
- * {@link Serialization.Deserialize}
- */
- static Deserialize = Serialization.Deserialize
-
- }
-
-
- /**
- * Deep copy/clone from source to dest, assuming both are userData objects for three.js objects/materials/textures etc.
- * This will clone any property that can be cloned (apart from Object3D, Texture, Material) and deep copy the objects and arrays.
- * @note Keep synced with copyMaterialUserData in three.js -> Material.js todo: merge these functions? by putting this inside three.js?
- * @param dest
- * @param source
- * @param ignoredKeysInRoot - keys to ignore in the root object
- * @param isRoot - always true, used for recursion
- */
- export function copyUserData(dest: any, source: any, ignoredKeysInRoot: (string|symbol)[] = [], isRoot = true): any {
- if (!source) return dest
- for (const key of Object.keys(source)) {
- if (isRoot && ignoredKeysInRoot.includes(key)) continue
- if (key.startsWith('__')) continue // double underscore
- const src = source[key]
- if (typeof dest[key] === 'function' || typeof src === 'function') continue
- // todo only clone vectors, colors etc
- const skipClone = !src || src.isTexture || src.isObject3D || src.isMaterial
- if (!skipClone && typeof src.clone === 'function')
- dest[key] = src.clone()
- // else if (!skipClone && (typeof src === 'object' || Array.isArray(src)))
- else if (!skipClone && (src.constructor === Object || Array.isArray(src)))
- dest[key] = copyUserData(Array.isArray(src) ? [] : {}, src, ignoredKeysInRoot, false)
- else
- dest[key] = src
- }
- return dest
- }
-
- /**
- * Deep copy/clone from source to dest, assuming both are userData objects in Textures.
- * Same as {@link copyUserData} but ignores uuid in the root object.
- * @param dest
- * @param source
- * @param isRoot
- * @param ignoredKeysInRoot
- */
- export function copyTextureUserData(dest: any, source: any, ignoredKeysInRoot = ['uuid'], isRoot = true): any {
- return copyUserData(dest, source, ignoredKeysInRoot, isRoot)
- }
-
-
- /**
- * Deep copy/clone from source to dest, assuming both are userData objects in Materials.
- * Same as {@link copyUserData} but ignores uuid in the root object.
- * @note Keep synced with copyMaterialUserData in three.js -> Material.js
- * @param dest
- * @param source
- * @param isRoot
- * @param ignoredKeysInRoot
- */
- export function copyMaterialUserData(dest: any, source: any, ignoredKeysInRoot = ['uuid'], isRoot = true): any {
- return copyUserData(dest, source, ignoredKeysInRoot, isRoot)
- }
-
-
- /**
- * Deep copy/clone from source to dest, assuming both are userData objects in Object3D.
- * Same as {@link copyUserData} but ignores uuid in the root object.
- * @param dest
- * @param source
- * @param isRoot
- * @param ignoredKeysInRoot
- */
- export function copyObject3DUserData(dest: any, source: any, ignoredKeysInRoot = ['uuid'], isRoot = true): any {
- return copyUserData(dest, source, ignoredKeysInRoot, isRoot)
- }
-
- /**
- * Serialize userData and sets to data.userData. This is required because three.js Material.toJSON does not serialize userData.
- * @param data
- * @param userData
- * @param meta
- */
- function serializeMaterialUserData(data: any, userData: any, meta?: SerializationMetaType) {
- data.userData = {}
-
- copyMaterialUserData(data.userData, userData)
-
- // Serialize the userData
- const meta2 = meta || { // Make meta object for the Serializer from the data. This requires changing from Array to Object for textures and images
- textures: Object.fromEntries(data.textures?.map((t: any) => [t.uuid, t]) || []),
- images: Object.fromEntries(data.images?.map((t: any) => [t.uuid, t]) || []),
- }
- data.userData = Serialization.Serialize(data.userData, meta2) // here meta is required for textures otherwise images will be lost. Material.toJSON sets the result as meta if not provided.
- if (!meta) {
- // Add textures and images to the result if meta is not provided. This is to remain compatible with how three.js saves materials. See (MaterialLoader and ThreeMaterialLoader)
- if (Object.keys(meta2.textures).length > 0) data.textures = Object.values(meta2.textures)
- if (Object.keys(meta2.images).length > 0) data.images = Object.values(meta2.images)
- }
- }
-
- /**
- * Converts array buffers to base64 strings in meta.
- * This is useful when storing .json files, as storing as number arrays takes a lot of space.
- * Used in viewer.toJSON()
- * @param meta
- */
- export function convertArrayBufferToStringsInMeta(meta: SerializationMetaType) {
- Object.values(meta).forEach((res: any) => { // similar to processViewer in gltf export.
- if (res) Object.values(res).forEach((item: any) => {
- if (!item.url) return
- // console.log(item.url)
- if (!(item.url.data instanceof ArrayBuffer) && !Array.isArray(item.url.data)) return
- if (item.url.type === 'Uint16Array') {
- if (!(item.url.data instanceof Uint16Array)) { // because it can be a typed array
- item.url.data = new Uint16Array(item.url.data)
- }
- item.url.data = 'data:application/octet-stream;base64,' + arrayBufferToBase64(item.url.data.buffer)
- } else if (item.url.type === 'Uint8Array') {
- if (!(item.url.data instanceof Uint8Array)) { // because it can be a typed array
- item.url.data = new Uint8Array(item.url.data)
- }
- // todo: just use jpeg or PNG encoding for this ?
- item.url.data = 'data:application/octet-stream;base64,' + arrayBufferToBase64(item.url.data.buffer)
- } else if (item.url.data instanceof ArrayBuffer) {
- item.url.data = 'data:application/octet-stream;base64,' + arrayBufferToBase64(item.url.data.buffer)
- } else {
- console.warn('Unsupported buffer type', item.url.type)
- }
- })
- })
- }
-
- /**
- * Converts strings(base64 or utf-8) to array buffers in meta. This is the reverse of {@link convertArrayBufferToStringsInMeta}
- * Used in viewer.fromJSON()
- */
- export function convertStringsToArrayBuffersInMeta(meta: SerializationMetaType) {
- Object.values(meta).forEach((res: any) => { // similar to processViewer in gltf export.
- if (res) Object.values(res).forEach((item: any) => {
- if (!item || !item.url) return
- if (typeof item.url.data !== 'string') return
-
- // base64 data uri or any mime type
- // console.log(item.url.data?.match?.(/^data:.*;base64,(.*)$/))
- const dataUriMatch = item.url.data.match(/^data:.*;base64,(.*)$/)
- if (dataUriMatch?.[1]) {
- item.url.data = base64ToArrayBuffer(dataUriMatch?.[1])
- } else { // utf-8 string, not used at the moment
- if (item.url.type !== 'Uint8Array') {
- console.error('Unsupported buffer type string for ', item.url.type, 'use base64')
- }
- item.url.data = new TextEncoder().encode(item.url.data).buffer // todo: this doesnt work in ie/edge maybe, but this feature is not used.
- }
-
- })
- })
- }
-
- export function getEmptyMeta(res?: Partial<SerializationResourcesType>): SerializationMetaType {
- return { // see Object3D.js toJSON for more details
- geometries: {...res?.geometries},
- materials: {...res?.materials},
- textures: {...res?.textures},
- images: {...res?.images},
- shapes: {...res?.shapes},
- skeletons: {...res?.skeletons},
- animations: {...res?.animations},
- extras: {...res?.extras},
- _context: {},
- }
- }
-
- export interface SerializationResourcesType {
- geometries: Record<string, any>,
- materials: Record<string, any>,
- textures: Record<string, any>,
- images: Record<string, any>,
- shapes: Record<string, any>,
- skeletons: Record<string, any>,
- animations: Record<string, any>,
- extras: Record<string, any>,
- object?: any,
-
- [key: string]: any,
-
- }
- export interface SerializationMetaType extends SerializationResourcesType {
- _context: {
- assetImporter?: AssetImporter,
- objectLoader?: ObjectLoader,
- materialManager?: MaterialManager,
- assetManager?: AssetManager,
- renderManager?: RenderManager,
-
- imagePromises?: Promise<any>[],
-
- [key: string]: any,
- }
-
- __isLoadedResources?: boolean
-
- }
- export class MetaImporter {
-
- /**
- * @param json
- * @param objLoader
- * @param extraResources - preloaded resources in the format of viewer config resources.
- */
- static async ImportMeta(json: SerializationMetaType, extraResources?: Partial<SerializationResourcesType>) {
- // console.log(json)
- if (json.__isLoadedResources) return json
-
- const resources: SerializationMetaType = metaFromResources()
- resources._context = json._context
-
- convertStringsToArrayBuffersInMeta(json)
-
- // console.log(viewerConfig)
- const assetImporter = json._context.assetImporter
- if (!assetImporter) throw new Error('assetImporter not found in meta context, which is required for import meta.')
-
- const objLoader = json._context.objectLoader || new ObjectLoader(assetImporter.loadingManager)
-
- // see ObjectLoader.parseAsync
- resources.animations = json.animations ? objLoader.parseAnimations(Object.values(json.animations)) : {}
- if (extraResources && extraResources.animations) resources.animations = {...resources.animations, ...extraResources.animations}
-
- resources.shapes = json.shapes ? objLoader.parseShapes(Object.values(json.shapes)) : {}
- if (extraResources && extraResources.shapes) resources.shapes = {...resources.shapes, ...extraResources.shapes}
-
- resources.geometries = json.geometries ? objLoader.parseGeometries(Object.values(json.geometries), Object.values(resources.shapes)) : {}
- if (extraResources && extraResources.geometries) resources.geometries = {...resources.geometries, ...extraResources.geometries}
-
- resources.images = json.images ? await objLoader.parseImagesAsync(Object.values(json.images)) : {} // local images only like data url and data textures
- if (extraResources && extraResources.images) resources.images = {...resources.images, ...extraResources.images}
-
- // const onLoad = () => { // todo: do it after all the images not after one
- // Object.values(resources.textures).forEach((t: any) => {
- // if (t.isTexture && t.image?.complete) t.needsUpdate = true
- // })
- // }
-
- if (Array.isArray(json.textures)) {
- console.error('TODO: check file format')
- json.textures = json.textures.reduce((acc, cur) => {
- if (!cur) return acc
- acc[cur.uuid] = cur
- return acc
- })
- }
-
- await MetaImporter.LoadRootPathTextures({textures: json.textures, images: resources.images}, assetImporter)
-
- // console.log(json.textures)
- const textures = []
- for (const texture of Object.values(json.textures)) {
- const tex = {...texture}
- if (tex.userData) tex.userData = ThreeSerialization.Deserialize(tex.userData, {}, resources)
- textures.push(tex)
- }
- resources.textures = json.textures ? objLoader.parseTextures(textures, resources.images) : {}
-
- // replace the source of the textures(which has preview) with the loaded images, see {@link LoadRootPathTextures} for `rootPathPromise`
- // todo: should this be moved after processRaw?
- const textures2 = {...resources.textures}
- for (const inpTexture of Object.values(json.textures)) {
- inpTexture.rootPathPromise?.then((v: Source|null) => {
- if (!v) return
- const texture = textures2[inpTexture.uuid]
- texture.dispose()
- texture.source = v
- texture.source.needsUpdate = true
- texture.needsUpdate = true
- })
- }
-
- for (const entry of Object.entries(resources.textures)) {
- entry[1] = await assetImporter.processRawSingle(entry[1], {})
- if (entry[1]) resources.textures[entry[0]] = entry[1]
- else delete resources.textures[entry[0]]
- }
- if (extraResources && extraResources.textures) resources.textures = {...resources.textures, ...extraResources.textures}
-
-
- const jsonMats: any[] = json.materials ? Object.values(json.materials) : []
- resources.materials = {}
- for (const material of jsonMats) {
- if (!material?.uuid) continue
- // Object.entries(material).forEach(([k, data]: [string, any]) => {
- // if (data && data.resource && data.uuid && data.resource === 'textures') { // for textures put in by serialize.ts
- // material[k] = data.uuid
- // }
- // })
- resources.materials[material.uuid] = ThreeSerialization.Deserialize(material, undefined, resources)
- }
- if (extraResources && extraResources.materials) resources.materials = {...resources.materials, ...extraResources.materials}
-
- if (json.object) {
- resources.object = objLoader.parseObject(json.object, resources.geometries, resources.materials, resources.textures, resources.animations)
- if (json.skeletons) {
- resources.skeletons = objLoader.parseSkeletons(Object.values(json.skeletons), resources.object as any)
- objLoader.bindSkeletons(resources.object as any, resources.skeletons)
- }
- }
-
- if (json.extras) {
- resources.extras = json.extras
- for (const e of (Object.values(json.extras) as any as any[])) {
- if (!e.uuid) continue
- if (!e.url) {
- resources.extras[e.uuid] = ThreeSerialization.Deserialize(e, undefined, resources)
- continue
- }
- // see LUTCubeTextureWrapper, KTX2LoadPlugin for sample use
- if (typeof e.url === 'string') {
- const r = await assetImporter.importPath(e.url)
- if (r?.length > 0) resources.extras[e.uuid] = r[0]
- } else if (e.url.data) {
- const file = new File([getTypedArray(e.url.type, e.url.data)], e.url.path)
- // console.log(file, e)
- const r = await assetImporter.importAsset({path: file.name, file})
- // console.log(r)
- // todo: userdata? name? other properties?
- if (r?.length > 0) resources.extras[e.uuid] = r[0]
- } else {
- console.warn('invalid URL type while loading extra resource')
- }
- }
- // console.log(resources.extras)
- }
- if (extraResources && extraResources.extras) resources.extras = {...resources.extras, ...extraResources.extras}
-
- // console.log(resources, json)
- resources.__isLoadedResources = true
- return resources
- }
-
-
- static async LoadRootPathTextures({textures, images}: Pick<SerializationMetaType, 'textures'|'images'>, importer: IAssetImporter) {
- const pms = []
-
- for (const inpTexture of Array.isArray(textures) ? textures : Object.values(textures ?? {} as any) as any as any[]) {
- const path = inpTexture?.userData?.rootPath
- const hasImage = inpTexture.image && images[inpTexture.image] // its possible to have both image and rootPath, then the image will be preview image.
- if (!path) continue
- // console.warn(path, inpTexture, images)
- const promise = importer.importSingle<ITexture>(path, {processRaw: false}).then((texture) => {
- const source = texture?.source as any
- // const image = texture?.image as any
- if (!texture || !source) return null
- // console.log(typeof image)
- const source2 = new Source(source.data)
- if (inpTexture.image) source2.uuid = inpTexture.image
- inpTexture.image = source2.uuid
- if (!hasImage)
- images[source2.uuid] = source2
- texture.dispose() // todo: what happens when we reimport a cached disposed texture asset, is three.js able to recreate the webgl texture on render?
- return source2
- }).catch((e) => {
- console.error(e)
- delete inpTexture.userData.rootPath
- return null
- })
- if (hasImage) inpTexture.rootPathPromise = promise
- else pms.push(promise)
- }
-
- await Promise.allSettled(pms)
- }
-
- }
-
- export function metaToResources(meta?: SerializationMetaType): Partial<SerializationResourcesType> {
- if (!meta) return {}
- const res: Partial<SerializationResourcesType> = {...meta}
- if (res._context) delete res._context
- return res
- }
- export function metaFromResources(resources?: Partial<SerializationResourcesType>, viewer?: ThreeViewer): SerializationMetaType {
- return {
- ...resources,
- ...getEmptyMeta(resources),
- _context: {
- assetManager: viewer?.assetManager,
- assetImporter: viewer?.assetManager.importer,
- materialManager: viewer?.assetManager.materials,
- renderManager: viewer?.renderManager,
- }, // clear context even if its present in resources
- }
- }
-
- export function jsonToBlob(json: any): BlobExt {
- const b = new Blob([JSON.stringify(json)], {type: 'application/json'}) as BlobExt
- b.ext = 'json'
- return b
- }
|