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.

826 lines 38.9 kB
import { arrayBufferToBase64, base64ToArrayBuffer, getTypedArray, Serialization } from 'ts-browser-helpers'; import { CanvasTexture, Color, CubeTexture, DataTexture, MaterialLoader, Matrix3, Matrix4, ObjectLoader, Quaternion, Source, Texture, Vector2, Vector3, Vector4, } from 'three'; import { textureToCanvas } from '../three/utils/texture'; const copier = (c) => (v, o) => o?.copy?.(v) ?? new c().copy(v); export class ThreeSerialization { } (() => { 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']], ]; Serialization.RegisterSerializer(...primitives.map(p => ({ priority: 1, isType: (obj) => obj[p[1]], serialize: (obj) => { 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) => obj.isTexture || obj.metadata?.type === 'Texture', serialize: (obj, meta) => { 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 && typeof obj.userData.rootPath === 'string' && (obj.userData.rootPath.startsWith('http') || obj.userData.rootPath.startsWith('data:')); let res = {}; const ud = obj.userData; try { // need try catch here because of hasRootPath 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 } } } obj.userData = {}; // toJSON will call JSON.stringify, which will serialize userData const meta2 = { images: {} }; // in-case meta is undefined res = obj.toJSON(meta || meta2); if (!meta && res.image) res.image = hasRootPath && !obj.userData.embedUrlImagePreviews ? undefined : meta2.images[res.image]; res.userData = Serialization.Serialize(copyTextureUserData({}, ud), meta, false); } catch (e) { console.error('Threepipe Serialization: Unable to serialize texture'); console.error(e); } obj.userData = ud; // should be outside try catch 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 && !res.resource) { if (!meta.textures[res.uuid]) meta.textures[res.uuid] = res; res = { uuid: res.uuid, resource: 'textures' }; } return res; }, deserialize: (dat, obj, meta) => { 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; if (meta && !data.image.isSource) { if (!meta._context.imagePromises) meta._context.imagePromises = []; meta._context.imagePromises.push(new Promise((resolve) => { imageOnLoad = resolve; })); } const sources = data.image.isSource ? { [data.image.uuid]: data.image } : 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) => obj.isMaterial || obj.metadata?.type === 'Material', serialize: (obj, meta) => { 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 = {}; const tempTextures = {}; 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 = {}; try { res = obj.toJSON(meta || meta2, true); // copying userData is handled in toJSON, see MeshStandardMaterial2 serializeMaterialUserData(res, userData, meta); res.userData.uuid = userData.uuid; // todo: override generator to mention that this is a custom serializer? 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]; } catch (e) { console.error('Threepipe Serialization: Unable to serialize material'); console.error(e); } obj.userData = userData; // 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 (res) { 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.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, obj, meta) => { function finalCopy(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 }; if (data.userData) data.userData = Serialization.Deserialize(data.userData, undefined, meta, false); // const textures = {}; 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]?.[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) => obj.isWebGLRenderTarget || obj.metadata?.type === 'RenderTarget', serialize: (obj, meta) => { 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 = { 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, obj, meta) => { 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} */ ThreeSerialization.Serialize = Serialization.Serialize; /** * Deserialize an object * {@link Serialization.Deserialize} */ ThreeSerialization.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, source, ignoredKeysInRoot = [], isRoot = true) { 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, source, ignoredKeysInRoot = ['uuid'], isRoot = true) { 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, source, ignoredKeysInRoot = ['uuid'], isRoot = true) { 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, source, ignoredKeysInRoot = ['uuid'], isRoot = true) { 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, userData, meta) { data.userData = {}; copyMaterialUserData(data.userData, userData); // Serialize the userData const meta2 = meta || { textures: Object.fromEntries(data.textures?.map((t) => [t.uuid, t]) || []), images: Object.fromEntries(data.images?.map((t) => [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) { Object.values(meta).forEach((res) => { if (res) Object.values(res).forEach((item) => { 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) { Object.values(meta).forEach((res) => { if (res) Object.values(res).forEach((item) => { 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) { return { 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 class MetaImporter { /** * @param json * @param objLoader * @param extraResources - preloaded resources in the format of viewer config resources. */ static async ImportMeta(json, extraResources) { // console.log(json) if (json.__isLoadedResources) return json; const resources = 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), 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) : {}; for (const key1 of Object.keys(resources.textures)) { let tex = resources.textures[key1]; if (!tex) continue; // __texCtor is set in MetaImporter.LoadRootPathTextures if (tex.source.__texCtor) { const newTex = new tex.source.__texCtor(tex.source.data); if (!newTex || typeof newTex.copy !== 'function') continue; newTex.copy(tex); delete tex.source.__texCtor; resources.textures[key1] = newTex; tex = newTex; } if (tex.source.data instanceof HTMLCanvasElement && !tex.isCanvasTexture) { const newTex = new CanvasTexture(tex.source.data).copy(tex); resources.textures[key1] = newTex; tex = newTex; } } // 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) => { 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 = 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); objLoader.bindSkeletons(resources.object, resources.skeletons); } } if (json.extras) { resources.extras = json.extras; for (const e of Object.values(json.extras)) { 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 }, importer, usePreviewImages = true) { const pms = []; for (const inpTexture of Array.isArray(textures) ? textures : Object.values(textures ?? {})) { const path = inpTexture?.userData?.rootPath; const hasImage = usePreviewImages && 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(path, { processRaw: false }).then((texture) => { const source = texture?.source; // 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; // only these are supported by ObjectLoader.parseTextures, see parseTextures2 if (texture.constructor !== Texture && texture.constructor !== DataTexture && texture.constructor !== CubeTexture) { source2.__texCtor = texture.constructor; } 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) { if (!meta) return {}; const res = { ...meta }; if (res._context) delete res._context; return res; } export function metaFromResources(resources, viewer) { 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) { const b = new Blob([JSON.stringify(json)], { type: 'application/json' }); b.ext = 'json'; return b; } /** * Used in {@link LUTCubeTextureWrapper} and {@link KTX2LoadPlugin} and imported in {@link ThreeViewer.loadConfigResources} * @param texture * @param meta * @param name * @param mime */ export function serializeTextureInExtras(texture, meta, name, mime) { if (meta?.extras[texture.uuid]) return { uuid: texture.uuid, resource: 'extras' }; let url = ''; if (texture.source?._sourceImgBuffer || texture.__sourceBuffer) { // serialize blob to data in image. // Note: do not change to Uint16Array because it's encoded to rgbe in `processViewer` const data = new Uint8Array(texture.source?._sourceImgBuffer || texture.__sourceBuffer); const mimeType = mime || texture.userData.mimeType || ''; url = { data: Array.from(data), // texture need to be a normal array, not a typed array. type: data.constructor.name, path: texture.userData.__sourceBlob?.name || texture.userData.rootPath || 'file.' + mimeType.split('/')[1], }; if (mimeType) url.mimeType = mimeType; } else if (texture.userData.rootPath) { url = texture.userData.rootPath; } else { console.error('Unable to serialize LUT texture, not loaded through asset manager.'); } const tex = { uuid: texture.uuid, url, userData: copyTextureUserData({}, texture.userData), type: texture.type, name: name || texture.name, }; if (meta?.extras) { meta.extras[texture.uuid] = tex; return { uuid: texture.uuid, resource: 'extras' }; } return tex; } //# sourceMappingURL=serialization.js.map