Compare commits
2 Commits
309bb0a5e6
...
151bc7c8a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
151bc7c8a2 | ||
|
|
404e804143 |
@ -25,8 +25,10 @@ import {onMounted} from "vue";
|
||||
import { DocumentImport, DocumentExport } from "@vicons/carbon";
|
||||
import { t } from "@/language";
|
||||
import { App, Export, Loader } from "@astral3d/engine";
|
||||
import { useGlobalConfigStore } from "@/store/modules/globalConfig";
|
||||
|
||||
const exportClass = new Export();
|
||||
const globalConfigStore = useGlobalConfigStore();
|
||||
|
||||
const exportOptions = [
|
||||
{
|
||||
@ -35,41 +37,41 @@ const exportOptions = [
|
||||
children: [
|
||||
{
|
||||
label: "JSON",
|
||||
key: "exportObjectToJSON"
|
||||
key: "exportObjectToJSON",
|
||||
},
|
||||
{
|
||||
label: "GLB",
|
||||
key: "exportObjectToGlb"
|
||||
key: "exportObjectToGlb",
|
||||
},
|
||||
{
|
||||
label: "GLTF",
|
||||
key: "exportObjectToGltf"
|
||||
key: "exportObjectToGltf",
|
||||
},
|
||||
{
|
||||
label: "OBJ",
|
||||
key: "exportObjectToObj"
|
||||
key: "exportObjectToObj",
|
||||
},
|
||||
{
|
||||
label: "PLY",
|
||||
key: "exportObjectToPly"
|
||||
key: "exportObjectToPly",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['PLY (Binary)']"),
|
||||
key: "exportObjectToPlyBinary"
|
||||
key: "exportObjectToPlyBinary",
|
||||
},
|
||||
{
|
||||
label: "STL",
|
||||
key: "exportObjectToStl"
|
||||
key: "exportObjectToStl",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['STL (Binary)']"),
|
||||
key: "exportObjectToStlBinary"
|
||||
key: "exportObjectToStlBinary",
|
||||
},
|
||||
{
|
||||
label: "USDZ",
|
||||
key: "exportObjectToUSDZ"
|
||||
}
|
||||
]
|
||||
key: "exportObjectToUSDZ",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("layout.header['Export Scene']"),
|
||||
@ -77,55 +79,82 @@ const exportOptions = [
|
||||
children: [
|
||||
{
|
||||
label: "JSON",
|
||||
key: "exportSceneToJSON"
|
||||
key: "exportSceneToJSON",
|
||||
},
|
||||
{
|
||||
label: "GLB",
|
||||
key: "exportSceneToGlb"
|
||||
key: "exportSceneToGlb",
|
||||
},
|
||||
{
|
||||
label: "GLTF",
|
||||
key: "exportSceneToGltf"
|
||||
key: "exportSceneToGltf",
|
||||
},
|
||||
{
|
||||
label: "OBJ",
|
||||
key: "exportSceneToObj"
|
||||
key: "exportSceneToObj",
|
||||
},
|
||||
{
|
||||
label: "PLY",
|
||||
key: "exportSceneToPly"
|
||||
key: "exportSceneToPly",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['PLY (Binary)']"),
|
||||
key: "exportSceneToPlyBinary"
|
||||
key: "exportSceneToPlyBinary",
|
||||
},
|
||||
{
|
||||
label: "STL",
|
||||
key: "exportSceneToStl"
|
||||
key: "exportSceneToStl",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['STL (Binary)']"),
|
||||
key: "exportSceneToStlBinary"
|
||||
key: "exportSceneToStlBinary",
|
||||
},
|
||||
{
|
||||
label: "USDZ",
|
||||
key: "exportSceneToUSDZ"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
key: "exportSceneToUSDZ",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("layout.header['Offline package']"),
|
||||
key: "exportOffline",
|
||||
children: [
|
||||
{
|
||||
label: t("layout.header['Single packet']"),
|
||||
key: "exportOfflineSingle",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['Multiple packet']"),
|
||||
key: "exportOfflineMultiple",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function handleImport() {
|
||||
const form = document.createElement('form');
|
||||
form.style.display = 'none';
|
||||
const form = document.createElement("form");
|
||||
form.style.display = "none";
|
||||
document.body.appendChild(form);
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.multiple = true;
|
||||
fileInput.type = 'file';
|
||||
fileInput.addEventListener('change', function () {
|
||||
Loader.loadFiles(fileInput.files, undefined)
|
||||
.catch((err) => {
|
||||
fileInput.type = "file";
|
||||
fileInput.addEventListener("change", function () {
|
||||
const files = fileInput.files;
|
||||
if (!files || files.length === 0) {
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const astralFiles = Array.from(files).filter(file => file.name.toLowerCase().endsWith(".astral"));
|
||||
if (astralFiles.length > 0) {
|
||||
handleOfflineFiles(files);
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
Loader.loadFiles(files, undefined)
|
||||
.catch(err => {
|
||||
window.$message?.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
@ -138,6 +167,15 @@ function handleImport() {
|
||||
}
|
||||
|
||||
function handleExportSelect(key: string) {
|
||||
if (key === "exportOfflineSingle") {
|
||||
exportOffline("single");
|
||||
return;
|
||||
}
|
||||
if (key === "exportOfflineMultiple") {
|
||||
exportOffline("multiple");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.startsWith("exportObject")) {
|
||||
if (App.selected === null) {
|
||||
window.$message?.error(window.$t("prompt['No object selected.']"));
|
||||
@ -145,11 +183,121 @@ function handleExportSelect(key: string) {
|
||||
}
|
||||
}
|
||||
|
||||
exportClass[key]();
|
||||
exportClass[key] && exportClass[key]();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
function downloadFile(file: File) {
|
||||
const url = URL.createObjectURL(file);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function downloadFilesInBatches(files: File[], batchSize = 9, delayMs = 1100) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
downloadFile(files[i]);
|
||||
if ((i + 1) % batchSize === 0 && i < files.length - 1) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exportOffline(mode: "single" | "multiple") {
|
||||
const packer = (window.viewer as any)?.package;
|
||||
if (!packer || typeof packer.packOffline !== "function") {
|
||||
window.$message?.error("当前SDK暂不支持离线包导出");
|
||||
return;
|
||||
}
|
||||
|
||||
const sceneInfo = App.project.getKey("sceneInfo");
|
||||
const sceneName = sceneInfo?.sceneName || "scene";
|
||||
|
||||
globalConfigStore.loading = true;
|
||||
globalConfigStore.loadingText = "正在导出离线包...";
|
||||
|
||||
packer
|
||||
.packOffline({
|
||||
name: sceneName,
|
||||
layer: 2,
|
||||
mode,
|
||||
onProgress: (progress: number) => {
|
||||
globalConfigStore.loadingText = `离线包导出 ${progress}%`;
|
||||
},
|
||||
})
|
||||
.then(async (result: { files: File[] }) => {
|
||||
await downloadFilesInBatches(result.files);
|
||||
window.$message?.success("离线包导出完成");
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
if (err?.code === "SDK_FEATURE_DISABLED") {
|
||||
window.$message?.error("当前授权未开通离线包导出能力");
|
||||
return;
|
||||
}
|
||||
if (typeof err?.message === "string" && err.message.trim()) {
|
||||
window.$message?.error(err.message);
|
||||
return;
|
||||
}
|
||||
window.$message?.error("离线包导出失败");
|
||||
})
|
||||
.finally(() => {
|
||||
globalConfigStore.loading = false;
|
||||
globalConfigStore.loadingText = "";
|
||||
});
|
||||
}
|
||||
|
||||
function handleOfflineFiles(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const astralFiles = Array.from(files).filter(file => file.name.toLowerCase().endsWith(".astral"));
|
||||
if (astralFiles.length === 0) {
|
||||
window.$message?.error("请选择离线包文件(.astral)");
|
||||
return;
|
||||
}
|
||||
|
||||
const packer = (window.viewer as any)?.package;
|
||||
if (!packer || typeof packer.unpack !== "function") {
|
||||
window.$message?.error("当前SDK暂不支持离线包导入");
|
||||
return;
|
||||
}
|
||||
|
||||
const entryFile = guessEntryFile(astralFiles);
|
||||
|
||||
globalConfigStore.loading = true;
|
||||
globalConfigStore.loadingText = "离线包加载中...";
|
||||
|
||||
packer.unpack({
|
||||
url: entryFile.name,
|
||||
offlineFiles: astralFiles,
|
||||
offlineEntry: entryFile.name,
|
||||
onProgress: (progress: number) => {
|
||||
globalConfigStore.loadingText = `离线包加载 ${progress.toFixed(2)}%`;
|
||||
},
|
||||
onComplete: () => {
|
||||
globalConfigStore.loading = false;
|
||||
globalConfigStore.loadingText = "";
|
||||
window.$message?.success("离线包加载完成");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function guessEntryFile(files: File[]): File {
|
||||
const uuidReg = /^[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}$/;
|
||||
const findBaseName = (name: string) => name.replace(/\.[^.]+$/, "");
|
||||
|
||||
const candidate = files.find(file => !uuidReg.test(findBaseName(file.name)));
|
||||
return candidate || files[0];
|
||||
}
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@ -2,7 +2,7 @@
|
||||
import {nextTick} from "vue";
|
||||
import {Save} from "@vicons/carbon";
|
||||
import {t} from "@/language";
|
||||
import {App,Package} from "@astral3d/engine";
|
||||
import {App} from "@astral3d/engine";
|
||||
import { useGlobalConfigStore } from '@/store/modules/globalConfig';
|
||||
import {fetchUpload} from "@/http/api/sys";
|
||||
import {filterSize} from "@/utils/common/file";
|
||||
@ -59,7 +59,7 @@ function save(){
|
||||
|
||||
globalConfigStore.loadingText = window.$t("scene['Scene is being compressed...']");
|
||||
|
||||
const p = new Package(window.viewer);
|
||||
const p = window.viewer.package;
|
||||
p.pack({
|
||||
// 首包名称
|
||||
name:`${sceneInfo.sceneName}`,
|
||||
@ -96,8 +96,6 @@ function save(){
|
||||
|
||||
setTimeout(() => {
|
||||
globalConfigStore.loading = false;
|
||||
|
||||
p.dispose();
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
@ -77,6 +77,9 @@ export default {
|
||||
Export: '导出',
|
||||
'Export Object': '导出物体',
|
||||
'Export Scene': '导出场景',
|
||||
'Offline package': '离线包',
|
||||
'Single packet': '单包',
|
||||
'Multiple packet': '多包',
|
||||
'PLY (Binary)': 'PLY(二进制)',
|
||||
'STL (Binary)': 'STL(二进制)',
|
||||
/* File 下的选项 End */
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import {ref, onMounted, nextTick,provide} from 'vue';
|
||||
import {Hooks,App,Package,defaultProjectInfo} from "@astral3d/engine";
|
||||
import {Hooks,App,defaultProjectInfo} from "@astral3d/engine";
|
||||
import * as Layout from './layouts';
|
||||
import {connectWebSocket} from "@/hooks/useWebSocket";
|
||||
import {useRoute} from "vue-router";
|
||||
@ -82,8 +82,7 @@ function getScene(sceneInfo) {
|
||||
closable: false,
|
||||
})
|
||||
|
||||
const p = new Package(window.viewer);
|
||||
p.unpack({
|
||||
window.viewer.package.unpack({
|
||||
url: sceneInfo.zip,
|
||||
onSceneLoad: () => {
|
||||
drawingInfo.value = App.project.getKey("drawing");
|
||||
@ -94,8 +93,6 @@ function getScene(sceneInfo) {
|
||||
Hooks.useDispatchSignal("sceneLoadComplete");
|
||||
|
||||
notice.destroy();
|
||||
|
||||
p.dispose();
|
||||
}
|
||||
})
|
||||
return;
|
||||
|
||||
@ -3,7 +3,7 @@ import {nextTick, onMounted, provide, ref} from "vue";
|
||||
import {useRoute} from 'vue-router';
|
||||
import {t} from "@/language";
|
||||
import {fetchGetOneScene} from "@/http/api/scenes";
|
||||
import {App,Viewer,Hooks,Package,defaultProjectInfo} from "@astral3d/engine";
|
||||
import {App,Viewer,Hooks,defaultProjectInfo} from "@astral3d/engine";
|
||||
import {usePreviewOperationStore} from "@/store/modules/previewOperation";
|
||||
import EsCubeLoading from "@/components/es/EsCubeLoading.vue";
|
||||
import PreviewSceneTree from "@/views/preview/components/PreviewSceneTree.vue";
|
||||
@ -97,8 +97,7 @@ function getScene(sceneInfo) {
|
||||
closable: false,
|
||||
})
|
||||
|
||||
const p = new Package(window.viewer);
|
||||
p.unpack({
|
||||
window.viewer.package.unpack({
|
||||
url: sceneInfo.zip,
|
||||
onSceneLoad: (sceneJson: ISceneJson) => {
|
||||
if (sceneJson.controls?.state) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -391,7 +457,7 @@ export class Package {
|
||||
});
|
||||
|
||||
// 首包上传
|
||||
const firstUploadResult = await this.zip(sceneZipData, packConfig.name, packConfig.zipUploadFun);
|
||||
const firstUploadResult = await this.zip(sceneZipData, packConfig.name, packConfig.zipUploadFun, packConfig.rawUploadFun);
|
||||
|
||||
// 进度
|
||||
let progress = 0;
|
||||
@ -454,7 +520,7 @@ export class Package {
|
||||
const content = JSON.stringify(json);
|
||||
zipData.push({ name, json: content });
|
||||
|
||||
await this.zip(zipData, group.uuid, packConfig.zipUploadFun);
|
||||
await this.zip(zipData, group.uuid, packConfig.zipUploadFun, packConfig.rawUploadFun);
|
||||
|
||||
progress++;
|
||||
packConfig.onProgress && packConfig.onProgress(parseFloat((progress / groupArr.length * 100).toFixed(2)));
|
||||
@ -478,7 +544,43 @@ export class Package {
|
||||
* @param {string | number} zipName 打包文件名
|
||||
* @return {Promise<any>} 返回包上传接口结果
|
||||
*/
|
||||
private async zip(sourceData: SourceData[], zipName: string | number, zipUploadFun: (zip: File) => Promise<any>): Promise<any> {
|
||||
private toUint8(data: string | ArrayBuffer | Uint8Array): Uint8Array {
|
||||
if (data instanceof Uint8Array) return data;
|
||||
if (typeof data === "string") return strToU8(data);
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
|
||||
private buildFilesMapFromSourceData(sourceData: SourceData[]): Record<string, Uint8Array> {
|
||||
const files: Record<string, Uint8Array> = {};
|
||||
|
||||
sourceData.forEach((item) => {
|
||||
if (item.texture) {
|
||||
files[`Textures/${item.name}`] = this.toUint8(item.texture);
|
||||
} else if (item.geometry) {
|
||||
files[`Geometries/${item.name}`] = this.toUint8(item.geometry);
|
||||
} else if (item.json) {
|
||||
files[item.name] = this.toUint8(item.json);
|
||||
} else if (item.drawing) {
|
||||
files[`Drawing/${item.name}`] = this.toUint8(item.drawing);
|
||||
}
|
||||
})
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private async zip(
|
||||
sourceData: SourceData[],
|
||||
zipName: string | number,
|
||||
zipUploadFun: (zip: File) => Promise<any>,
|
||||
rawUploadFun?: (raw: { name: string; files: Record<string, Uint8Array> }) => Promise<any>
|
||||
): Promise<any> {
|
||||
if (rawUploadFun) {
|
||||
const files = this.buildFilesMapFromSourceData(sourceData);
|
||||
const size = Object.values(files).reduce((sum, data) => sum + data.byteLength, 0);
|
||||
this.totalSize += size;
|
||||
return await rawUploadFun({ name: String(zipName), files });
|
||||
}
|
||||
|
||||
const jszip = new JSZip();
|
||||
const imgFolder = jszip.folder("Textures") as JSZip; // 贴图文件夹
|
||||
const geometriesFolder = jszip.folder("Geometries") as JSZip; // 几何数据文件夹
|
||||
@ -1199,4 +1301,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;
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,7 @@ import ParticleEmitter from "@/core/objects/ParticleEmitter.ts";
|
||||
import {ViewerPathTracer} from "@/core/viewer/ViewerPathTracer.ts";
|
||||
import {Helper as ScriptHelper} from "../script";
|
||||
import Tiles from "../objects/Tile.ts";
|
||||
import {Package} from "@/core/loader/Package";
|
||||
|
||||
export interface ViewerEventMap {
|
||||
// 场景加载完成时执行,仅执行一次
|
||||
@ -198,6 +199,7 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
|
||||
public timer = new Timer();
|
||||
//整个主场景的box3
|
||||
public sceneBox3 = new THREE.Box3();
|
||||
public package: Package;
|
||||
|
||||
constructor(options: IViewerSetting) {
|
||||
super();
|
||||
@ -216,6 +218,7 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
|
||||
this.renderer = this.createEngine();
|
||||
|
||||
this.modules = this.initModules();
|
||||
this.package = new Package(this);
|
||||
|
||||
/** helpers **/
|
||||
if (this.options.grid.enabled) {
|
||||
@ -1148,6 +1151,8 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
|
||||
this.dispatchEvent({type: "afterDestroy"});
|
||||
|
||||
this.unInstallScripts();
|
||||
this.package?.dispose();
|
||||
this.package = null as any;
|
||||
}
|
||||
|
||||
/* -----------------暂时放在Viewer下的工具方法-------------------- */
|
||||
|
||||
@ -28,7 +28,8 @@ export default defineConfig(async ({mode, command}) => {
|
||||
root,
|
||||
compress: {
|
||||
compress: VITE_BUILD_COMPRESS,
|
||||
deleteOriginFile: VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE,
|
||||
// SDK 作为 workspace 依赖需要保留原始产物,package.json 的 main/module/exports 依赖这些文件
|
||||
deleteOriginFile: false,
|
||||
},
|
||||
enableAnalyze: VITE_ENABLE_ANALYZE,
|
||||
enableConfig:VITE_ENABLE_CONFIG_GENERATE
|
||||
|
||||
Loading…
Reference in New Issue
Block a user