diff --git a/Test/1121212-offline.astral b/Test/1121212-offline.astral new file mode 100644 index 0000000..d7b53bb Binary files /dev/null and b/Test/1121212-offline.astral differ diff --git a/Test/1121212.astral b/Test/1121212.astral new file mode 100644 index 0000000..31eabab Binary files /dev/null and b/Test/1121212.astral differ diff --git a/Test/7ff6dd32-25b3-4eea-abdc-6f316ecd668e.astral b/Test/7ff6dd32-25b3-4eea-abdc-6f316ecd668e.astral new file mode 100644 index 0000000..bfb2f34 Binary files /dev/null and b/Test/7ff6dd32-25b3-4eea-abdc-6f316ecd668e.astral differ diff --git a/Test/index-nosplit.html b/Test/index-nosplit.html new file mode 100644 index 0000000..51c66bd --- /dev/null +++ b/Test/index-nosplit.html @@ -0,0 +1,364 @@ + + + + + + TkAstral3D 非分包离线测试 + + + +
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/Test/index-split.html b/Test/index-split.html new file mode 100644 index 0000000..3dcd65c --- /dev/null +++ b/Test/index-split.html @@ -0,0 +1,369 @@ + + + + + + TkAstral3D 分包离线测试 + + + +
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/Test/index.html b/Test/index.html new file mode 100644 index 0000000..71c03a0 --- /dev/null +++ b/Test/index.html @@ -0,0 +1,425 @@ + + + + + + TkAstral3D Offline Package Test + + + +
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/packages/sdk/lib/core/loader/Package.ts b/packages/sdk/lib/core/loader/Package.ts index bfcf0fe..37f0bc9 100644 --- a/packages/sdk/lib/core/loader/Package.ts +++ b/packages/sdk/lib/core/loader/Package.ts @@ -5,7 +5,7 @@ * @update 2025-02-14 * @version 5.0.0 */ -import { Mesh, Group, Bone } from "three"; +import { Mesh, Group, Bone, Object3D, Texture } from "three"; import JSZip from "jszip"; import { strToU8 } from 'three/examples/jsm/libs/fflate.module.js'; import { BASE64_TYPES, TYPED_ARRAYS } from "@/constant"; @@ -123,6 +123,7 @@ export class Package { private textureMap: Map; private callFunNum: { value: number; }; private skeletonClass: PackageSkeleton; + private dataComponentMap: Record | null = null; // 离线包相关属性 // @ts-ignore @@ -202,6 +203,15 @@ export class Package { * @returns {string} 返回贴图存储文件名称 */ handleImage(imageJson:ITHREEScene.ImageJSON, zipData:SourceData[]): string { + if (imageJson.url && typeof imageJson.url === "object" && !imageJson.url.type) { + const ktx2Data = (imageJson.url as any).ktx2OriginalData; + if (ktx2Data) { + const name = imageJson.uuid + `.ktx2`; + zipData.push({ name, texture: ktx2Data }); + return name; + } + } + if (typeof imageJson.url === "string") { const name = imageJson.uuid + `.${BASE64_TYPES[imageJson.url.split(",")[0]]}`; zipData.push({ name, texture: imageJson.url }); @@ -221,6 +231,22 @@ export class Package { return name; } + private findTextureByImageUuid(mesh: Mesh, imageUuid: string): Texture | null { + if (!mesh.material) return null; + + const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]; + for (const material of materials) { + for (const key in material) { + const value = (material as any)[key]; + if (value && value.isTexture && value.source?.uuid === imageUuid) { + return value as Texture; + } + } + } + + return null; + } + /** * 处理 mesh json * @param mesh @@ -252,6 +278,12 @@ export class Package { !json.images && (json.images = []); + const texture = this.findTextureByImageUuid(mesh, image.uuid); + const textureMeta = (texture as any)?.metadata; + if (textureMeta?.isKTX2 && textureMeta?.ktx2OriginalData && typeof image.url === "object") { + (image.url as any).ktx2OriginalData = textureMeta.ktx2OriginalData; + } + const name = this.handleImage(image, zipData) if(name){ json.images.push(name); @@ -314,6 +346,60 @@ export class Package { } } + private sanitizeDataComponentList(list: any) { + if (!Array.isArray(list) || list.length === 0) return null; + return list.map((entry) => { + if (!entry || typeof entry !== "object") return entry; + const config = entry.config && typeof entry.config === "object" ? { ...entry.config } : entry.config; + return { ...entry, config, data: null }; + }); + } + + private collectDataComponentMap(root: Object3D) { + const map: Record = {}; + const condition = (obj: any) => !obj.ignore; + const visit = (obj: any) => { + const sanitized = this.sanitizeDataComponentList(obj?.dataComponent); + if (sanitized) { + map[obj.uuid] = sanitized; + } + }; + + if (typeof (root as any).traverseByCondition === "function") { + (root as any).traverseByCondition(visit, condition); + } else { + root.traverse((obj: any) => { + if (!condition(obj)) return; + visit(obj); + }); + } + + return map; + } + + private applyDataComponentMap(root: Object3D) { + const map = this.dataComponentMap; + if (!map || Object.keys(map).length === 0) return; + + const condition = (obj: any) => !obj.ignore; + const apply = (obj: any) => { + if (!Object.prototype.hasOwnProperty.call(map, obj.uuid)) return; + const sanitized = this.sanitizeDataComponentList(map[obj.uuid]); + if (sanitized) { + obj.dataComponent = sanitized; + } + }; + + if (typeof (root as any).traverseByCondition === "function") { + (root as any).traverseByCondition(apply, condition); + } else { + root.traverse((obj: any) => { + if (!condition(obj)) return; + apply(obj); + }); + } + } + /** * 按 group 分组各打包为1个zip文件 * @param {IPackConfig} packConfig @@ -422,6 +508,14 @@ export class Package { console.log(sceneZipData,drawingInfo) } + const dataComponents = this.collectDataComponentMap(this.viewer.scene); + if (Object.keys(dataComponents).length > 0) { + sceneZipData.push({ + name: "data_components.json", + json: JSON.stringify(dataComponents) + }); + } + // 项目配置 sceneZipData.push({ name: "config.json", @@ -430,6 +524,8 @@ export class Package { xr: App.project.getKey("xr"), // 项目渲染器配置 renderer: App.project.getKey("renderer"), + // BVH 配置 + bvh: App.project.getKey("bvh"), // 项目级联阴影映射 csm: App.project.getKey("csm"), // 项目后处理配置 @@ -712,16 +808,170 @@ export class Package { } } + private decodeUint8Text(data: Uint8Array): string { + return new TextDecoder().decode(data); + } + + private parseOfflineManifest(unzipped: Record): any | null { + const manifestData = unzipped["offline.json"]; + if (!manifestData) return null; + try { + return JSON.parse(this.decodeUint8Text(manifestData)); + } catch { + return null; + } + } + + private buildOfflineFlatPackages(unzipped: Record, packagesPrefix: string): Map> { + const prefix = packagesPrefix.endsWith("/") ? packagesPrefix : `${packagesPrefix}/`; + const map = new Map>(); + + for (const [name, data] of Object.entries(unzipped)) { + if (name === "offline.json") continue; + if (!name.startsWith(prefix)) continue; + + const rest = name.slice(prefix.length); + const splitIndex = rest.indexOf("/"); + if (splitIndex <= 0) continue; + + const baseName = rest.slice(0, splitIndex); + const innerPath = rest.slice(splitIndex + 1); + if (!innerPath) continue; + + let pkg = map.get(baseName); + if (!pkg) { + pkg = {}; + map.set(baseName, pkg); + } + pkg[innerPath] = data; + } + + return map; + } + + private async unzipJsZipToMap(zipInput: Blob | ArrayBuffer | Uint8Array): Promise> { + const jszip = new JSZip(); + const zipRes = await jszip.loadAsync(zipInput as any); + const out: Record = {}; + for (const key in zipRes.files) { + if (zipRes.files[key].dir) continue; + const fileName = zipRes.files[key].name; + const content = await zipRes.file(fileName)?.async("uint8array"); + if (content) out[fileName] = content; + } + return out; + } + + private async buildOfflinePackageMapFromFiles(files: File[]): Promise> { + const map = new Map(); + for (const file of files) { + const buffer = new Uint8Array(await file.arrayBuffer()); + const normalized = this.normalizeOfflinePackageName(file.name); + map.set(normalized, buffer); + + const uuid = this.extractUuidFromName(file.name); + if (uuid) { + const canonical = this.normalizeOfflinePackageName(uuid.toLowerCase()); + if (!map.has(canonical)) { + map.set(canonical, buffer); + } + } + } + return map; + } + + private async initOfflineSource(unpackConfig: IUnpackConfig): Promise { + this.isOfflineImportContext = Boolean(unpackConfig.offlineBundle || (unpackConfig.offlineFiles && unpackConfig.offlineFiles.length > 0)); + this.offlineZipMap = null; + this.offlineEntryZip = null; + this.offlineFlatPackages = null; + this.offlineFlatEntryBase = null; + + const setupFlat = (unzipped: Record, manifest?: any, fallbackEntry?: string) => { + const packagesPrefix = manifest?.packagesPrefix || this.offlinePackagesPrefix; + this.offlineFlatPackages = this.buildOfflineFlatPackages(unzipped, packagesPrefix); + const entryName = manifest?.entry + ? this.normalizeOfflinePackageName(String(manifest.entry)) + : (fallbackEntry ? this.normalizeOfflinePackageName(fallbackEntry) : undefined); + const entryBase = manifest?.entryBase || (entryName ? this.getOfflinePackageBaseName(entryName) : undefined); + if (entryBase) this.offlineFlatEntryBase = entryBase; + if (entryName) this.offlineEntryZip = entryName; + }; + + if (unpackConfig.offlineBundle) { + let u8: Uint8Array; + if (unpackConfig.offlineBundle instanceof Uint8Array) { + u8 = unpackConfig.offlineBundle; + } else if (unpackConfig.offlineBundle instanceof ArrayBuffer) { + u8 = new Uint8Array(unpackConfig.offlineBundle); + } else { + u8 = new Uint8Array(await unpackConfig.offlineBundle.arrayBuffer()); + } + + const unzipped = await this.unzipJsZipToMap(u8); + const manifest = this.parseOfflineManifest(unzipped); + + if (manifest?.mode === "single") { + setupFlat(unzipped, manifest); + return; + } + + const map = new Map(); + for (const [name, data] of Object.entries(unzipped)) { + if (name === "offline.json") continue; + if (name.toLowerCase().endsWith(".astral")) { + map.set(this.normalizeOfflinePackageName(name), data); + } + } + this.offlineZipMap = map; + + if (manifest?.entry) { + this.offlineEntryZip = this.normalizeOfflinePackageName(String(manifest.entry)); + } else if (unpackConfig.offlineEntry) { + this.offlineEntryZip = this.normalizeOfflinePackageName(unpackConfig.offlineEntry); + } + } else if (unpackConfig.offlineFiles && unpackConfig.offlineFiles.length > 0) { + if (unpackConfig.offlineFiles.length === 1) { + const file = unpackConfig.offlineFiles[0]; + try { + const unzipped = await this.unzipJsZipToMap(await file.arrayBuffer()); + const manifest = this.parseOfflineManifest(unzipped); + if (manifest?.mode === "single") { + setupFlat(unzipped, manifest, file.name); + return; + } + } catch (err) { + // 兼容直接传入 .astral 分包文件的场景,忽略单包探测失败 + } + } + + this.offlineZipMap = await this.buildOfflinePackageMapFromFiles(unpackConfig.offlineFiles); + if (unpackConfig.offlineEntry) { + this.offlineEntryZip = this.normalizeOfflinePackageName(unpackConfig.offlineEntry); + } + } + + if (this.offlineZipMap && !this.offlineEntryZip) { + this.offlineEntryZip = this.normalizeOfflinePackageName(unpackConfig.url); + } + } + /** * 从首包开始解包 * @param {IUnpackConfig} unpackConfig */ - public unpack(unpackConfig: IUnpackConfig) { + public async unpack(unpackConfig: IUnpackConfig) { unpackConfig.onProgress && unpackConfig.onProgress(0); let totalZipNumber = 0, progress = 0; - const match = unpackConfig.url.match( /(.*[\\/])?([a-zA-Z0-9]+-V\d+)(?=[\\/]|$)/); - this.prefix_url = this.viewer.options.request?.baseUrl + (match ? match[0] : unpackConfig.url.substring(0, unpackConfig.url.lastIndexOf("/"))); + await this.initOfflineSource(unpackConfig); + + if (!this.offlineZipMap && !this.offlineFlatPackages) { + const match = unpackConfig.url.match( /(.*[\\/])?([a-zA-Z0-9]+-V\d+)(?=[\\/]|$)/); + this.prefix_url = this.viewer.options.request?.baseUrl + (match ? match[0] : unpackConfig.url.substring(0, unpackConfig.url.lastIndexOf("/"))); + } else { + this.prefix_url = ""; + } // indexDb存储 // const db = window.VIEWPORT.modules["db"]; @@ -743,6 +993,11 @@ export class Package { that.imagesMap.clear(); that.materialsMap.clear(); that.textureMap.clear(); + that.offlineZipMap = null; + that.offlineEntryZip = null; + that.offlineFlatPackages = null; + that.offlineFlatEntryBase = null; + that.dataComponentMap = null; // @ts-ignore 清除loader that.loader = undefined; } @@ -806,6 +1061,11 @@ export class Package { App.FPS = Number(configJson.renderer.fps); } + const bvhConfig = (configJson as any)?.bvh; + if (bvhConfig) { + App.project.setKey("bvh", bvhConfig); + } + if(configJson.csm){ const projectCSM = App.project.getKey("csm"); let _csmNotChange = true; @@ -859,6 +1119,9 @@ export class Package { unpackConfig.onSceneLoad && unpackConfig.onSceneLoad(sceneJson, configJson); + this.applyDataComponentMap(App.scene as unknown as Object3D); + this.dataComponentMap = null; + // 防止项目只有一个包的情况造成不触发proxy set if (this.callFunNum.value === 0) { this.callFunNum.value = 0; @@ -872,15 +1135,12 @@ export class Package { }) } - const networkGet = () => { - // 下载场景包 - fetch(this.viewer.options.request?.baseUrl + unpackConfig.url) - .then(zipRes => zipRes.blob()) - .then(async (file) => { + const parseSceneZip = async (file: Blob | Uint8Array | ArrayBuffer) => { unpackConfig.onProgress && unpackConfig.onProgress(1); // @ts-ignore let sceneJson: ISceneJson = undefined, configJson: IAppProject.Config = undefined; + let dataComponentMap: Record | null = null; // 开始解压首包 const zip = new JSZip(); @@ -898,7 +1158,7 @@ export class Package { } } - const res = await zip.loadAsync(file); + const res = await zip.loadAsync(file as any); /** * res.files里包含整个zip里的文件描述、目录描述列表 @@ -917,6 +1177,9 @@ export class Package { } else if (fileName === "config.json") { // 项目配置json const content = await res.file(fileName)?.async('string') as string; configJson = JSON.parse(content); + } else if (fileName === "data_components.json") { + const content = await res.file(fileName)?.async('string') as string; + dataComponentMap = JSON.parse(content); } else if (fileName.substring(0, 9) === "Textures/") { /** * 贴图 @@ -928,6 +1191,11 @@ export class Package { // 转换回贴图原始信息,存入map const content = await res.file(fileName)?.async('arraybuffer'); this.unGzipImage(fileName.replace("Textures/", ""), content); + } else if (/\.ktx2$/i.test(fileName)) { + const content = await res.file(fileName)?.async('arraybuffer') as ArrayBuffer; + const blob = new Blob([content], { type: "image/ktx2" }); + const blobUrl = URL.createObjectURL(blob) + `#${fileName.replace("Textures/", "")}`; + this.unGzipImage(fileName.replace("Textures/", ""), blobUrl); } else { const content = await res.file(fileName)?.async('string') this.unGzipImage(fileName.replace("Textures/", ""), content); @@ -964,6 +1232,7 @@ export class Package { } totalZipNumber = sceneJson.totalZipNumber || 0; + this.dataComponentMap = dataComponentMap; // 贴图还原至sceneJson sceneJson.scene.images = sceneJson.scene.images.map((item) => { @@ -1003,12 +1272,42 @@ export class Package { // 解压处理好的数据添加至 indexDB -> Msy3D -> scene // db.setItem(dbKey, sceneJson); - }) + } + + const networkGet = async () => { + if (this.offlineFlatPackages) { + const entryBase = this.offlineFlatEntryBase || this.getOfflinePackageBaseName(this.offlineEntryZip || unpackConfig.url); + const entry = this.offlineFlatPackages.get(entryBase); + if (!entry) { + console.error(`[Package] 离线包缺少首包内容: ${entryBase}`); + return; + } + + const zipData = await this.zipRawFiles(entry); + await parseSceneZip(zipData); + return; + } + + if (this.offlineZipMap) { + const entryName = this.offlineEntryZip || this.normalizeOfflinePackageName(unpackConfig.url); + const zipData = this.offlineZipMap.get(entryName); + if (!zipData) { + console.error(`[Package] 离线包缺少首包: ${entryName}`); + return; + } + + await parseSceneZip(zipData); + return; + } + + // 下载场景包 + const file = await fetch(this.viewer.options.request?.baseUrl + unpackConfig.url).then(zipRes => zipRes.blob()); + await parseSceneZip(file); } // db.getItem(dbKey).then((dbData) => { // if (dbData === undefined) { - networkGet(); + await networkGet(); // }else{ // this.recordUuid(dbData.scene); // @@ -1044,6 +1343,8 @@ export class Package { const check = (object, group) => { // 检查数据是否已完善 let isDone = true; + + const useMaterials = new Set(); object.children.forEach((child) => { // 检查几何数据是否都已拥有 if (child.geometry && group.geometries?.findIndex((geometry) => geometry.uuid === child.geometry) === -1) { @@ -1055,30 +1356,9 @@ export class Package { } // material->texture->image - if (child.material && group.materials?.findIndex((material) => material.uuid === child.material) === -1) { - if (!this.materialsMap.has(child.material)) { - isDone = false; - } else { - group.materials.push(this.materialsMap.get(child.material)); - - const material = this.materialsMap.get(child.material); - if (material.map && group.textures?.findIndex((texture) => texture.uuid === material.map) === -1) { - if (!this.textureMap.has(material.map)) { - isDone = false; - } else { - group.textures.push(this.textureMap.get(material.map)); - - const texture = this.textureMap.get(material.map); - if (texture.image && group.images?.findIndex((image) => image.uuid === texture.image) === -1) { - if (!this.imagesMap.has(texture.image)) { - isDone = false; - } else { - group.images.push(this.imagesMap.get(texture.image)); - } - } - } - } - } + if (child.material) { + const materialUUIDs = Array.isArray(child.material) ? child.material : [child.material]; + materialUUIDs.forEach((materialUuid) => useMaterials.add(materialUuid)); } if (child.children?.length > 0 && isDone) { @@ -1086,6 +1366,68 @@ export class Package { } }) + if (!isDone) return isDone; + + const useTextures = new Set(); + const textureFields = [ + "map", "alphaMap", "aoMap", "anisotropyMap", "bumpMap", "clearcoatMap", "clearcoatNormalMap", "clearcoatRoughnessMap", + "displacementMap", "envMap", "emissiveMap", "iridescenceMap", "iridescenceThicknessMap", "lightMap", "sheenColorMap", + "sheenRoughnessMap", "specularColorMap", "specularIntensityMap", "specularMap", "thicknessMap", "transmissionMap", + "metalnessMap", "roughnessMap", "normalMap", "gradientMap" + ]; + + useMaterials.forEach((materialUuid) => { + if (!isDone) return; + + const material = this.materialsMap.get(materialUuid); + if (!material) { + isDone = false; + return; + } + + if (group.materials?.findIndex((item) => item.uuid === materialUuid) === -1) { + group.materials.push(material); + } + + textureFields.forEach((field) => { + const textureUuid = material[field]; + if (textureUuid) useTextures.add(textureUuid); + }); + }); + + if (!isDone) return isDone; + + const useImages = new Set(); + useTextures.forEach((textureUuid) => { + if (!isDone) return; + + const texture = this.textureMap.get(textureUuid); + if (!texture) { + isDone = false; + return; + } + + if (group.textures?.findIndex((item) => item.uuid === textureUuid) === -1) { + group.textures.push(texture); + } + + if (texture.image) useImages.add(texture.image); + }); + + if (!isDone) return isDone; + + useImages.forEach((imageUuid) => { + if (!isDone) return; + if (!this.imagesMap.has(imageUuid)) { + isDone = false; + return; + } + + if (group.images?.findIndex((item) => item.uuid === imageUuid) === -1) { + group.images.push(this.imagesMap.get(imageUuid)); + } + }); + return isDone; } @@ -1132,112 +1474,135 @@ export class Package { } } + const parseGroupZip = async (file: Blob | Uint8Array | ArrayBuffer) => { + const zip = new JSZip(); + let json: GroupJson; + + // 几何数据数组 + let geometries: Array = []; + + const unzipDone = () => { + // 贴图还原至sceneJson + json.images = json.images.map((item) => { + const nameSplit = item.split("."); + if (nameSplit[1] === "env") { + const urlSplit = nameSplit[0].split("!"); + return this.imagesMap.get(urlSplit[3]) + } else { + return this.imagesMap.get(nameSplit[0]) + } + }); + + // 几何数据还原至sceneJson + json.geometries = geometries; + + this.recordUuid(json); + + // 遍历children,拉取group zip还原 + const children: any = []; + json.object.children.forEach((childUuid) => { + if (typeof childUuid === "string") { + // 保存uuid对应的function + funcMap.set(childUuid, this.unpackGroup); + + this.callFunNum.value++; + } else { + children.push(childUuid) + } + }) + json.object.children = children; + + parse(json); + } + + const res = await zip.loadAsync(file as any); + let num = new Proxy({ value: Object.keys(res.files).length }, { + set(target, p, value) { + target[p] = value; + if (value === 0) { + unzipDone(); + } + return true; + } + }) + + for (let key in res.files) { + //判断是否是目录 + if (!res.files[key].dir) { + const fileName = res.files[key].name; + + //找到我们压缩包所需要的json文件 + if (fileName === `${uuid}.json`) { // 场景json + res.file(fileName)?.async('string').then(content => { + json = JSON.parse(content); + num.value--; + }) + } else if (fileName.substring(0, 9) === "Textures/") { + if (/\.env$/.test(fileName)) { + // 转换回贴图原始信息,存入map + res.file(fileName)?.async('arraybuffer').then(content => { + this.unGzipImage(fileName.replace("Textures/", ""), content); + num.value--; + }) + } else if (/\.ktx2$/i.test(fileName)) { + res.file(fileName)?.async('arraybuffer').then(content => { + const blob = new Blob([content], { type: "image/ktx2" }); + const blobUrl = URL.createObjectURL(blob) + `#${fileName.replace("Textures/", "")}`; + this.unGzipImage(fileName.replace("Textures/", ""), blobUrl); + num.value--; + }) + } else { + res.file(fileName)?.async('string').then(content => { + this.unGzipImage(fileName.replace("Textures/", ""), content); + num.value--; + }) + } + } else if (/^Geometries\/geometries_\d*\.json$/.test(fileName)) { + res.file(fileName)?.async('string').then(content => { + geometries.push(...this.unGzipGeometryJson(JSON.parse(content))); + num.value--; + }) + } else { + num.value--; + } + } else { + num.value--; + } + } + } + const getByNetwork = () => { + if (this.offlineFlatPackages) { + const entry = this.offlineFlatPackages.get(uuid); + if (!entry) { + console.error(`[Package] 离线包缺少分包内容: ${uuid}`); + this.callFunNum.value--; + return; + } + + this.zipRawFiles(entry).then((zipData) => { + parseGroupZip(zipData); + }); + return; + } + + if (this.offlineZipMap) { + const fileName = this.normalizeOfflinePackageName(uuid); + const normalizedLowerName = this.normalizeOfflinePackageName(uuid.toLowerCase()); + const zipData = this.offlineZipMap.get(fileName) || this.offlineZipMap.get(normalizedLowerName); + if (!zipData) { + console.error(`[Package] 离线包缺少分包: ${fileName}`); + this.callFunNum.value--; + return; + } + + parseGroupZip(zipData); + return; + } + Package._fetch(`${this.prefix_url}/${uuid}.zip`, { onSuccess: (zipRes) => { - const file = zipRes.blob(); - - const zip = new JSZip(); - let json: GroupJson; - - // 几何数据数组 - let geometries: Array = []; - - const unzipDone = () => { - // 贴图还原至sceneJson - json.images = json.images.map((item) => { - const nameSplit = item.split("."); - if (nameSplit[1] === "env") { - const urlSplit = nameSplit[0].split("!"); - return this.imagesMap.get(urlSplit[3]) - } else { - return this.imagesMap.get(nameSplit[0]) - } - }); - - // 几何数据还原至sceneJson - json.geometries = geometries; - - this.recordUuid(json); - - // 遍历children,拉取group zip还原 - const children: any = []; - json.object.children.forEach((uuid) => { - if (typeof uuid === "string") { - // 保存uuid对应的function - funcMap.set(uuid, this.unpackGroup); - - this.callFunNum.value++; - } else { - children.push(uuid) - } - }) - json.object.children = children; - - //json.object.groupChildren = [...funcMap.keys()]; - - // 解压处理好的数据添加至 indexDB -> Msy3D -> ${dbTable} - // db.setItem(`${uuid}.zip`, json,dbTable); - - parse(json); - } - - zip.loadAsync(file as Blob).then(res => { - let num = new Proxy({ value: Object.keys(res.files).length }, { - set(target, p, value) { - target[p] = value; - if (value === 0) { - unzipDone(); - } - return true; - } - }) - for (let key in res.files) { - //判断是否是目录 - if (!res.files[key].dir) { - const fileName = res.files[key].name; - - //找到我们压缩包所需要的json文件 - if (fileName === `${uuid}.json`) { // 场景json - res.file(fileName)?.async('string').then(content => { - //得到scene.json文件的内容 - json = JSON.parse(content); - - num.value--; - }) - } else if (fileName.substring(0, 9) === "Textures/") { - /** - * 贴图 - * 分为两种情况: - * 1.贴图为env格式(type!width!height!uuid.env),转换为arraybuffer格式,存入map - * 2.贴图为普通图片格式,直接存入map - **/ - if (/\.env$/.test(fileName)) { - // 转换回贴图原始信息,存入map - res.file(fileName)?.async('arraybuffer').then(content => { - this.unGzipImage(fileName.replace("Textures/", ""), content); - - num.value--; - }) - } else { - res.file(fileName)?.async('string').then(content => { - this.unGzipImage(fileName.replace("Textures/", ""), content); - - num.value--; - }) - } - } else if (/^Geometries\/geometries_\d*\.json$/.test(fileName)) { - res.file(fileName)?.async('string').then(content => { - geometries.push(...this.unGzipGeometryJson(JSON.parse(content))); - - num.value--; - }) - } - } else { - num.value--; - } - } - }) + parseGroupZip(zipRes.blob()); } }); } @@ -1297,6 +1662,11 @@ export class Package { this.prefix_url = ""; this.callFunNum = { value: 0 }; // 重置为初始状态 this.totalSize = 0; + this.offlineZipMap = null; + this.offlineEntryZip = null; + this.offlineFlatPackages = null; + this.offlineFlatEntryBase = null; + this.dataComponentMap = null; // 5. 释放 viewer 引用(注意:不销毁 viewer,仅移除引用) this.viewer = null as any;