feat(all): 离线包迁移
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import { Mesh, Group, Bone } from "three";
|
||||
import JSZip from "jszip";
|
||||
import { strToU8 } from 'three/examples/jsm/libs/fflate.module.js';
|
||||
import { BASE64_TYPES, TYPED_ARRAYS } from "@/constant";
|
||||
import { unzip, zip, fetchController } from "@/utils";
|
||||
import { PackageSkeleton } from "@/core/loader/Package.Skeleton";
|
||||
@@ -16,18 +17,70 @@ import App from "@/core/app/App";
|
||||
import type Viewer from "@/core/viewer/Viewer";
|
||||
|
||||
interface IPackConfig {
|
||||
name: string; // 首包名称
|
||||
layer?: number; // 拆分的最深层级 0:拆分至最深层
|
||||
zipUploadFun: (zip: File) => Promise<any>; // 压缩包上传接口函数,多压缩包
|
||||
onProgress?: (progress: number) => void; // 打包进度回调
|
||||
onComplete?: (_: { firstUploadResult: any, totalSize: number, totalZipNumber: number }) => void; // 打包完成回调
|
||||
// 首包名称
|
||||
name: string;
|
||||
// 拆分的最深层级 0:拆分至最深层
|
||||
layer?: number;
|
||||
// 压缩包上传接口函数,多压缩包
|
||||
zipUploadFun: (zip: File) => Promise<any>;
|
||||
// 原始包上传接口函数(不压缩)
|
||||
rawUploadFun?: (raw: { name: string; files: Record<string, Uint8Array> }) => Promise<any>;
|
||||
// 压缩进度回调(0-100)
|
||||
onZipProgress?: (info: { name: string; progress: number }) => void;
|
||||
// 图片处理并发
|
||||
imageProcessLimit?: number;
|
||||
// 打包进度回调
|
||||
onProgress?: (progress: number) => void;
|
||||
// 打包完成回调
|
||||
onComplete?: (_: { firstUploadResult: any, totalSize: number, totalZipNumber: number }) => void;
|
||||
}
|
||||
|
||||
interface IOfflinePackConfig {
|
||||
// 离线包名称(作为首包 zip 名称)
|
||||
name: string;
|
||||
// 拆分的最深层级;0 表示拆分到最深层
|
||||
layer?: number;
|
||||
// 导出模式:single 为单 zip,multiple 为多 zip
|
||||
mode?: "single" | "multiple";
|
||||
// 单包导出时的包名(不含后缀)
|
||||
bundleName?: string;
|
||||
// 压缩进度回调(0-100)
|
||||
onZipProgress?: (info: { name: string; progress: number }) => void;
|
||||
// 图片处理并发
|
||||
imageProcessLimit?: number;
|
||||
// 进度回调
|
||||
onProgress?: (progress: number) => void;
|
||||
// 完成回调
|
||||
onComplete?: (result: OfflinePackResult) => void;
|
||||
}
|
||||
|
||||
interface OfflinePackResult {
|
||||
mode: "single" | "multiple";
|
||||
// 首包名称(含 .astral)
|
||||
entry: string;
|
||||
// 导出的包文件列表
|
||||
files: File[];
|
||||
// 总大小
|
||||
totalSize: number;
|
||||
// 包数量(首包 + 组包)
|
||||
totalZipNumber: number;
|
||||
}
|
||||
|
||||
interface IUnpackConfig {
|
||||
url: string, // 首包url
|
||||
onSceneLoad?: (sceneJson: ISceneJson, configJson: IAppProject.Config) => void, // 场景首包加载完成回调
|
||||
onProgress?: (progress: number) => void; // 场景加载进度回调
|
||||
onComplete?: () => void // 场景加载完成回调.
|
||||
// 首包url
|
||||
url: string;
|
||||
// 场景首包加载完成回调
|
||||
onSceneLoad?: (sceneJson: ISceneJson, configJson: IAppProject.Config) => void;
|
||||
// 场景加载进度回调
|
||||
onProgress?: (progress: number) => void;
|
||||
// 场景加载完成回调
|
||||
onComplete?: () => void;
|
||||
// 离线多包文件列表(可选)
|
||||
offlineFiles?: File[];
|
||||
// 离线单包(内部包含多个包)
|
||||
offlineBundle?: Blob | ArrayBuffer | Uint8Array;
|
||||
// 离线首包名称(可选,未提供则使用 url 的文件名)
|
||||
offlineEntry?: string;
|
||||
}
|
||||
|
||||
interface SourceData {
|
||||
@@ -71,6 +124,19 @@ export class Package {
|
||||
private callFunNum: { value: number; };
|
||||
private skeletonClass: PackageSkeleton;
|
||||
|
||||
// 离线包相关属性
|
||||
// @ts-ignore
|
||||
private offlineZipMap: Map<string, Uint8Array> | null = null;
|
||||
// @ts-ignore
|
||||
private offlineEntryZip: string | null = null;
|
||||
// @ts-ignore
|
||||
private offlineFlatPackages: Map<string, Record<string, Uint8Array>> | null = null;
|
||||
// @ts-ignore
|
||||
private offlineFlatEntryBase: string | null = null;
|
||||
private readonly offlinePackagesPrefix = "Packages/";
|
||||
// @ts-ignore
|
||||
private isOfflineImportContext = false;
|
||||
|
||||
constructor(viewer:Viewer) {
|
||||
this.viewer = viewer;
|
||||
|
||||
@@ -1199,4 +1265,140 @@ export class Package {
|
||||
// 5. 释放 viewer 引用(注意:不销毁 viewer,仅移除引用)
|
||||
this.viewer = null as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出离线包(单包或多包)
|
||||
*/
|
||||
async packOffline(offlineConfig: IOfflinePackConfig) {
|
||||
const mode = offlineConfig.mode || "multiple";
|
||||
const packageItems: Array<{ baseName: string; data: Uint8Array }> = [];
|
||||
const rawPackages: Array<{ baseName: string; files: Record<string, Uint8Array> }> = [];
|
||||
|
||||
const packResult = await this.pack({
|
||||
name: offlineConfig.name,
|
||||
layer: mode === "single" ? 1 : offlineConfig.layer,
|
||||
imageProcessLimit: offlineConfig.imageProcessLimit,
|
||||
zipUploadFun: async (zip: File) => {
|
||||
if (mode === "single") {
|
||||
return zip;
|
||||
}
|
||||
const data = new Uint8Array(await zip.arrayBuffer());
|
||||
const baseName = this.getOfflinePackageBaseName(zip.name);
|
||||
packageItems.push({ baseName, data });
|
||||
return zip;
|
||||
},
|
||||
rawUploadFun: mode === "single"
|
||||
? async (raw) => {
|
||||
const baseName = this.getOfflinePackageBaseName(raw.name);
|
||||
rawPackages.push({ baseName, files: raw.files });
|
||||
return raw;
|
||||
}
|
||||
: undefined,
|
||||
onZipProgress: offlineConfig.onZipProgress,
|
||||
onProgress: offlineConfig.onProgress,
|
||||
});
|
||||
|
||||
const entry = this.normalizeOfflinePackageName(offlineConfig.name);
|
||||
const entryBase = this.getOfflinePackageBaseName(entry);
|
||||
|
||||
if (mode === "single") {
|
||||
const filesMap: Record<string, Uint8Array> = {};
|
||||
|
||||
for (const item of rawPackages) {
|
||||
for (const [fileName, fileData] of Object.entries(item.files)) {
|
||||
const fullName = `${this.offlinePackagesPrefix}${item.baseName}/${fileName}`;
|
||||
if (!filesMap[fullName]) {
|
||||
filesMap[fullName] = fileData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
mode: "single",
|
||||
entry,
|
||||
entryBase,
|
||||
packagesPrefix: this.offlinePackagesPrefix,
|
||||
totalZipNumber: packResult.totalZipNumber,
|
||||
};
|
||||
filesMap["offline.json"] = strToU8(JSON.stringify(manifest));
|
||||
|
||||
const bundleName = this.normalizeOfflinePackageName(offlineConfig.bundleName || `${offlineConfig.name}-offline`);
|
||||
const bundleZip = await this.zipRawFiles(filesMap);
|
||||
const bundleFile = new File([bundleZip as any], bundleName, { type: "application/octet-stream" });
|
||||
|
||||
const result: OfflinePackResult = {
|
||||
mode: "single",
|
||||
entry,
|
||||
files: [bundleFile],
|
||||
totalSize: bundleFile.size,
|
||||
totalZipNumber: packResult.totalZipNumber,
|
||||
};
|
||||
|
||||
offlineConfig.onComplete && offlineConfig.onComplete(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const files = packageItems.map(item => new File([item.data as any], this.normalizeOfflinePackageName(item.baseName), { type: "application/octet-stream" }));
|
||||
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
||||
const result: OfflinePackResult = {
|
||||
mode: "multiple",
|
||||
entry,
|
||||
files,
|
||||
totalSize,
|
||||
totalZipNumber: packResult.totalZipNumber,
|
||||
};
|
||||
|
||||
offlineConfig.onComplete && offlineConfig.onComplete(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将原始文件映射压缩为 zip 数据
|
||||
*/
|
||||
private async zipRawFiles(files: Record<string, Uint8Array>): Promise<Uint8Array> {
|
||||
const jszip = new JSZip();
|
||||
for (const [name, data] of Object.entries(files)) {
|
||||
jszip.file(name, data);
|
||||
}
|
||||
return await jszip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一离线包名称(.astral)
|
||||
*/
|
||||
private normalizeOfflinePackageName(name: string): string {
|
||||
const baseName = name.split(/[\\/]/).pop() || name;
|
||||
const cleanName = baseName.split(/[?#]/)[0];
|
||||
const lower = cleanName.toLowerCase();
|
||||
if (lower.endsWith(".zip")) {
|
||||
return cleanName.slice(0, -4) + ".astral";
|
||||
}
|
||||
return cleanName + ".astral";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取离线包基础名称(不含后缀)
|
||||
*/
|
||||
private getOfflinePackageBaseName(name: string): string {
|
||||
const baseName = name.split(/[\\/]/).pop() || name;
|
||||
const cleanName = baseName.split(/[?#]/)[0];
|
||||
if (cleanName.toLowerCase().endsWith(".astral")) {
|
||||
return cleanName.slice(0, -7);
|
||||
}
|
||||
if (cleanName.toLowerCase().endsWith(".zip")) {
|
||||
return cleanName.slice(0, -4);
|
||||
}
|
||||
return cleanName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试从文件名中提取UUID(处理"xxx (1).astral"等情况)
|
||||
*/
|
||||
// @ts-ignore
|
||||
private extractUuidFromName(name: string): string | null {
|
||||
const match = name.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user