Compare commits

..

No commits in common. "151bc7c8a2cb0902d400624364ba25a2a7bd9743" and "309bb0a5e64fc261e92df05d3930ae95a45bcba4" have entirely different histories.

8 changed files with 139 additions and 528 deletions

View File

@ -3,7 +3,7 @@
<n-button class="mr-2">
<template #icon>
<n-icon size="22">
<DocumentExport />
<DocumentExport/>
</n-icon>
</template>
{{ t("layout.header.Export") }}
@ -13,7 +13,7 @@
<n-button type="primary" @click="handleImport">
<template #icon>
<n-icon>
<DocumentImport />
<DocumentImport/>
</n-icon>
</template>
{{ t("layout.header.Import") }}
@ -21,57 +21,55 @@
</template>
<script setup lang="ts">
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";
import {onMounted} from "vue";
import {DocumentImport, DocumentExport} from "@vicons/carbon";
import {t} from "@/language";
import {App, Export, Loader} from "@astral3d/engine";
const exportClass = new Export();
const globalConfigStore = useGlobalConfigStore();
const exportClass = new Export();
const exportOptions = [
const exportOptions = [
{
label: t("layout.header['Export Object']"),
key: "exportObject",
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']"),
@ -79,82 +77,55 @@
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",
},
],
},
{
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",
},
],
},
];
key: "exportSceneToUSDZ"
}
]
}
]
function handleImport() {
const form = document.createElement("form");
form.style.display = "none";
function handleImport() {
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 () {
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 => {
fileInput.type = 'file';
fileInput.addEventListener('change', function () {
Loader.loadFiles(fileInput.files, undefined)
.catch((err) => {
window.$message?.error(err);
})
.finally(() => {
@ -164,18 +135,9 @@
form.appendChild(fileInput);
fileInput.click();
}
function handleExportSelect(key: string) {
if (key === "exportOfflineSingle") {
exportOffline("single");
return;
}
if (key === "exportOfflineMultiple") {
exportOffline("multiple");
return;
}
}
function handleExportSelect(key: string) {
if (key.startsWith("exportObject")) {
if (App.selected === null) {
window.$message?.error(window.$t("prompt['No object selected.']"));
@ -183,121 +145,11 @@
}
}
exportClass[key] && exportClass[key]();
}
exportClass[key]();
}
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(() => {});
onMounted(() => {
})
</script>
<style scoped lang="less"></style>

View File

@ -2,7 +2,7 @@
import {nextTick} from "vue";
import {Save} from "@vicons/carbon";
import {t} from "@/language";
import {App} from "@astral3d/engine";
import {App,Package} 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 = window.viewer.package;
const p = new Package(window.viewer);
p.pack({
//
name:`${sceneInfo.sceneName}`,
@ -96,6 +96,8 @@ function save(){
setTimeout(() => {
globalConfigStore.loading = false;
p.dispose();
}, 500)
})
}

View File

@ -77,9 +77,6 @@ export default {
Export: '导出',
'Export Object': '导出物体',
'Export Scene': '导出场景',
'Offline package': '离线包',
'Single packet': '单包',
'Multiple packet': '多包',
'PLY (Binary)': 'PLY(二进制)',
'STL (Binary)': 'STL(二进制)',
/* File 下的选项 End */

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import {ref, onMounted, nextTick,provide} from 'vue';
import {Hooks,App,defaultProjectInfo} from "@astral3d/engine";
import {Hooks,App,Package,defaultProjectInfo} from "@astral3d/engine";
import * as Layout from './layouts';
import {connectWebSocket} from "@/hooks/useWebSocket";
import {useRoute} from "vue-router";
@ -82,7 +82,8 @@ function getScene(sceneInfo) {
closable: false,
})
window.viewer.package.unpack({
const p = new Package(window.viewer);
p.unpack({
url: sceneInfo.zip,
onSceneLoad: () => {
drawingInfo.value = App.project.getKey("drawing");
@ -93,6 +94,8 @@ function getScene(sceneInfo) {
Hooks.useDispatchSignal("sceneLoadComplete");
notice.destroy();
p.dispose();
}
})
return;

View File

@ -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,defaultProjectInfo} from "@astral3d/engine";
import {App,Viewer,Hooks,Package,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,7 +97,8 @@ function getScene(sceneInfo) {
closable: false,
})
window.viewer.package.unpack({
const p = new Package(window.viewer);
p.unpack({
url: sceneInfo.zip,
onSceneLoad: (sceneJson: ISceneJson) => {
if (sceneJson.controls?.state) {

View File

@ -7,7 +7,6 @@
*/
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";
@ -17,70 +16,18 @@ import App from "@/core/app/App";
import type Viewer from "@/core/viewer/Viewer";
interface IPackConfig {
// 首包名称
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 为单 zipmultiple 为多 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;
name: string; // 首包名称
layer?: number; // 拆分的最深层级 0:拆分至最深层
zipUploadFun: (zip: File) => Promise<any>; // 压缩包上传接口函数,多压缩包
onProgress?: (progress: number) => void; // 打包进度回调
onComplete?: (_: { firstUploadResult: any, totalSize: number, totalZipNumber: number }) => void; // 打包完成回调
}
interface IUnpackConfig {
// 首包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;
url: string, // 首包url
onSceneLoad?: (sceneJson: ISceneJson, configJson: IAppProject.Config) => void, // 场景首包加载完成回调
onProgress?: (progress: number) => void; // 场景加载进度回调
onComplete?: () => void // 场景加载完成回调.
}
interface SourceData {
@ -124,19 +71,6 @@ 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;
@ -457,7 +391,7 @@ export class Package {
});
// 首包上传
const firstUploadResult = await this.zip(sceneZipData, packConfig.name, packConfig.zipUploadFun, packConfig.rawUploadFun);
const firstUploadResult = await this.zip(sceneZipData, packConfig.name, packConfig.zipUploadFun);
// 进度
let progress = 0;
@ -520,7 +454,7 @@ export class Package {
const content = JSON.stringify(json);
zipData.push({ name, json: content });
await this.zip(zipData, group.uuid, packConfig.zipUploadFun, packConfig.rawUploadFun);
await this.zip(zipData, group.uuid, packConfig.zipUploadFun);
progress++;
packConfig.onProgress && packConfig.onProgress(parseFloat((progress / groupArr.length * 100).toFixed(2)));
@ -544,43 +478,7 @@ export class Package {
* @param {string | number} zipName
* @return {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 });
}
private async zip(sourceData: SourceData[], zipName: string | number, zipUploadFun: (zip: File) => Promise<any>): Promise<any> {
const jszip = new JSZip();
const imgFolder = jszip.folder("Textures") as JSZip; // 贴图文件夹
const geometriesFolder = jszip.folder("Geometries") as JSZip; // 几何数据文件夹
@ -1301,140 +1199,4 @@ 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;
}
}

View File

@ -32,7 +32,6 @@ 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 {
// 场景加载完成时执行,仅执行一次
@ -199,7 +198,6 @@ 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();
@ -218,7 +216,6 @@ 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) {
@ -1151,8 +1148,6 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
this.dispatchEvent({type: "afterDestroy"});
this.unInstallScripts();
this.package?.dispose();
this.package = null as any;
}
/* -----------------暂时放在Viewer下的工具方法-------------------- */

View File

@ -28,8 +28,7 @@ export default defineConfig(async ({mode, command}) => {
root,
compress: {
compress: VITE_BUILD_COMPRESS,
// SDK 作为 workspace 依赖需要保留原始产物package.json 的 main/module/exports 依赖这些文件
deleteOriginFile: false,
deleteOriginFile: VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE,
},
enableAnalyze: VITE_ENABLE_ANALYZE,
enableConfig:VITE_ENABLE_CONFIG_GENERATE