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