feat(all): 离线包加载测试
This commit is contained in:
@@ -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<string, any>;
|
||||
private callFunNum: { value: number; };
|
||||
private skeletonClass: PackageSkeleton;
|
||||
private dataComponentMap: Record<string, any> | 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<string, any> = {};
|
||||
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<string, Uint8Array>): 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<string, Uint8Array>, packagesPrefix: string): Map<string, Record<string, Uint8Array>> {
|
||||
const prefix = packagesPrefix.endsWith("/") ? packagesPrefix : `${packagesPrefix}/`;
|
||||
const map = new Map<string, Record<string, Uint8Array>>();
|
||||
|
||||
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<Record<string, Uint8Array>> {
|
||||
const jszip = new JSZip();
|
||||
const zipRes = await jszip.loadAsync(zipInput as any);
|
||||
const out: Record<string, Uint8Array> = {};
|
||||
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<Map<string, Uint8Array>> {
|
||||
const map = new Map<string, Uint8Array>();
|
||||
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<void> {
|
||||
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<string, Uint8Array>, 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<string, Uint8Array>();
|
||||
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<string, any> | 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<string>();
|
||||
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<string>();
|
||||
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<string>();
|
||||
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<any> = [];
|
||||
|
||||
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<any> = [];
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user