Compare commits

..

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

10 changed files with 242 additions and 1877 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,364 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TkAstral3D 非分包离线测试</title>
<style>
:root {
--bg: #0b1020;
--panel: rgba(15, 23, 42, 0.88);
--line: rgba(148, 163, 184, 0.24);
--text: #e2e8f0;
--muted: #94a3b8;
--accent: #22c55e;
--accent-2: #38bdf8;
--danger: #f87171;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background:
radial-gradient(circle at top left, rgba(56, 189, 248, 0.12), transparent 32%),
radial-gradient(circle at top right, rgba(34, 197, 94, 0.14), transparent 28%),
linear-gradient(180deg, #0b1020, #050814 72%);
color: var(--text);
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.app {
display: grid;
grid-template-columns: 360px 1fr;
width: 100%;
height: 100%;
}
.sidebar {
padding: 20px;
border-right: 1px solid var(--line);
background: var(--panel);
backdrop-filter: blur(10px);
overflow: auto;
}
.viewer-wrap {
position: relative;
min-width: 0;
min-height: 0;
}
#viewer {
width: 100%;
height: 100%;
}
h1 {
margin: 0 0 10px;
font-size: 24px;
line-height: 1.2;
}
.desc,
.hint,
.meta,
.log {
color: var(--muted);
line-height: 1.6;
}
.desc {
margin-bottom: 16px;
font-size: 14px;
}
.card {
margin-top: 16px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(2, 6, 23, 0.36);
}
.card h2 {
margin: 0 0 10px;
font-size: 15px;
color: #f8fafc;
}
.row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-top: 12px;
}
button,
input[type="file"] {
font: inherit;
}
button {
padding: 10px 14px;
border: 1px solid var(--line);
border-radius: 10px;
background: linear-gradient(180deg, rgba(56, 189, 248, 0.16), rgba(34, 197, 94, 0.16));
color: var(--text);
cursor: pointer;
}
button.secondary {
background: rgba(15, 23, 42, 0.9);
}
input[type="file"] {
width: 100%;
color: var(--muted);
}
.status {
margin-top: 12px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--line);
background: rgba(15, 23, 42, 0.72);
font-size: 13px;
white-space: pre-wrap;
}
.status.success {
border-color: rgba(34, 197, 94, 0.35);
color: #bbf7d0;
}
.status.error {
border-color: rgba(248, 113, 113, 0.35);
color: #fecaca;
}
.file-list {
margin: 10px 0 0;
padding-left: 18px;
font-size: 13px;
color: var(--muted);
}
.tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 999px;
background: rgba(56, 189, 248, 0.1);
border: 1px solid rgba(56, 189, 248, 0.18);
font-size: 12px;
color: #bae6fd;
}
@media (max-width: 900px) {
.app {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid var(--line);
}
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<h1>Tk 非分包离线测试</h1>
<div class="desc">
这个页面直接使用 Tk 构建产物来验证离线包加载。
仅支持非分包单文件模式:选择 1 个 .astral 文件加载。
</div>
<div class="row">
<span class="tag">SDK: TkAstral3D/Test/dist/astral3d.umd.js</span>
</div>
<div class="card">
<h2>选择离线包</h2>
<input id="fileInput" type="file" accept=".astral" />
<div class="row">
<button id="loadBtn">加载离线包</button>
<button id="loadLocalBtn" class="secondary">直接加载本目录</button>
<button id="resetBtn" class="secondary">重置场景</button>
</div>
<div id="status" class="status">等待选择 .astral 文件,或点击“直接加载本目录”</div>
<ul id="fileList" class="file-list"></ul>
</div>
<div class="card">
<h2>说明(非分包)</h2>
<div class="hint">
1. 仅选择一个非分包导出的 .astral 文件。\n
2. 页面使用 SDK 的 Package.unpack 直接加载,不依赖页面层 JSZip。\n
3. 也可点击“直接加载本目录”,默认读取 1121212.astral。\n
4. 建议通过本地静态服务器打开此页面,而不是直接 file:// 打开。
</div>
</div>
</aside>
<main class="viewer-wrap">
<div id="viewer"></div>
</main>
</div>
<script src="./dist/astral3d.umd.js"></script>
<script>
(function () {
const SDK = globalThis.Astral3D;
if (!SDK) {
document.getElementById("status").textContent = "Tk SDK 加载失败,请检查 astral3d.umd.js 路径";
document.getElementById("status").className = "status error";
return;
}
const viewerEl = document.getElementById("viewer");
const fileInput = document.getElementById("fileInput");
const loadBtn = document.getElementById("loadBtn");
const loadLocalBtn = document.getElementById("loadLocalBtn");
const resetBtn = document.getElementById("resetBtn");
const statusEl = document.getElementById("status");
const fileListEl = document.getElementById("fileList");
const LOCAL_SINGLE_FILE = "1121212.astral";
let viewer = null;
function setStatus(text, type) {
statusEl.textContent = text;
statusEl.className = "status" + (type ? ` ${type}` : "");
}
function ensureViewer() {
if (viewer) return viewer;
viewer = new SDK.Viewer({
container: viewerEl,
edit: { enabled: false },
grid: { enabled: true },
request: { baseUrl: "" },
});
globalThis.viewer = viewer;
return viewer;
}
function resetViewer() {
if (viewer && typeof viewer.dispose === "function") {
viewer.dispose();
}
viewer = null;
viewerEl.innerHTML = "";
setStatus("场景已重置", "success");
}
function renderFileList(files) {
fileListEl.innerHTML = "";
Array.from(files || []).forEach(file => {
const li = document.createElement("li");
li.textContent = `${file.name} (${Math.round(file.size / 1024)} KB)`;
fileListEl.appendChild(li);
});
}
async function loadByOfflineBundle(bundle, entryName) {
setStatus("正在准备离线包...", "");
const currentViewer = ensureViewer();
const packer = currentViewer.package || new SDK.Package(currentViewer);
currentViewer.package = packer;
setStatus(`开始加载离线包:${entryName}`, "");
await packer.unpack({
url: entryName,
offlineBundle: bundle,
offlineEntry: entryName,
onProgress: progress => {
setStatus(`离线包加载中... ${Number(progress || 0).toFixed(2)}%`, "");
},
onSceneLoad: () => {
setStatus("场景首包已解析,正在继续加载资源...", "");
},
onComplete: () => {
setStatus(`离线包加载完成:${entryName}`, "success");
},
});
}
async function fetchLocalAstralFile(name) {
const response = await fetch(`./${encodeURIComponent(name)}`);
if (!response.ok) {
throw new Error(`读取本地文件失败:${name}`);
}
const blob = await response.blob();
return new File([blob], name, { type: "application/octet-stream" });
}
function getSingleAstralFromFileList(fileList) {
const files = Array.from(fileList || []).filter(file => file.name.toLowerCase().endsWith(".astral"));
if (files.length === 0) {
throw new Error("请选择 .astral 文件");
}
if (files.length !== 1) {
throw new Error("非分包模式请只选择 1 个 .astral 文件");
}
return files[0];
}
async function loadOfflinePackage() {
try {
const file = getSingleAstralFromFileList(fileInput.files);
renderFileList([file]);
await loadByOfflineBundle(file, file.name);
} catch (error) {
console.error(error);
setStatus(`加载失败:${error && error.message ? error.message : error}`, "error");
}
}
async function loadLocalPackage() {
try {
renderFileList([{ name: LOCAL_SINGLE_FILE, size: 0 }]);
const file = await fetchLocalAstralFile(LOCAL_SINGLE_FILE);
await loadByOfflineBundle(file, LOCAL_SINGLE_FILE);
} catch (error) {
console.error(error);
setStatus(`本目录加载失败:${error && error.message ? error.message : error}`, "error");
}
}
fileInput.addEventListener("change", () => {
renderFileList(fileInput.files);
setStatus("文件已选择,点击“加载离线包”开始测试", "");
});
loadBtn.addEventListener("click", () => {
loadOfflinePackage();
});
loadLocalBtn.addEventListener("click", () => {
loadLocalPackage();
});
resetBtn.addEventListener("click", () => {
resetViewer();
});
ensureViewer();
setStatus("Tk SDK 已加载(非分包模式),等待选择离线包", "success");
})();
</script>
</body>
</html>

View File

@ -1,369 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TkAstral3D 分包离线测试</title>
<style>
:root {
--bg: #0b1020;
--panel: rgba(15, 23, 42, 0.88);
--line: rgba(148, 163, 184, 0.24);
--text: #e2e8f0;
--muted: #94a3b8;
--accent: #22c55e;
--accent-2: #38bdf8;
--danger: #f87171;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background:
radial-gradient(circle at top left, rgba(56, 189, 248, 0.12), transparent 32%),
radial-gradient(circle at top right, rgba(34, 197, 94, 0.14), transparent 28%),
linear-gradient(180deg, #0b1020, #050814 72%);
color: var(--text);
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.app {
display: grid;
grid-template-columns: 360px 1fr;
width: 100%;
height: 100%;
}
.sidebar {
padding: 20px;
border-right: 1px solid var(--line);
background: var(--panel);
backdrop-filter: blur(10px);
overflow: auto;
}
.viewer-wrap {
position: relative;
min-width: 0;
min-height: 0;
}
#viewer {
width: 100%;
height: 100%;
}
h1 {
margin: 0 0 10px;
font-size: 24px;
line-height: 1.2;
}
.desc,
.hint,
.meta,
.log {
color: var(--muted);
line-height: 1.6;
}
.desc {
margin-bottom: 16px;
font-size: 14px;
}
.card {
margin-top: 16px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(2, 6, 23, 0.36);
}
.card h2 {
margin: 0 0 10px;
font-size: 15px;
color: #f8fafc;
}
.row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-top: 12px;
}
button,
input[type="file"] {
font: inherit;
}
button {
padding: 10px 14px;
border: 1px solid var(--line);
border-radius: 10px;
background: linear-gradient(180deg, rgba(56, 189, 248, 0.16), rgba(34, 197, 94, 0.16));
color: var(--text);
cursor: pointer;
}
button.secondary {
background: rgba(15, 23, 42, 0.9);
}
input[type="file"] {
width: 100%;
color: var(--muted);
}
.status {
margin-top: 12px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--line);
background: rgba(15, 23, 42, 0.72);
font-size: 13px;
white-space: pre-wrap;
}
.status.success {
border-color: rgba(34, 197, 94, 0.35);
color: #bbf7d0;
}
.status.error {
border-color: rgba(248, 113, 113, 0.35);
color: #fecaca;
}
.file-list {
margin: 10px 0 0;
padding-left: 18px;
font-size: 13px;
color: var(--muted);
}
.tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 999px;
background: rgba(56, 189, 248, 0.1);
border: 1px solid rgba(56, 189, 248, 0.18);
font-size: 12px;
color: #bae6fd;
}
@media (max-width: 900px) {
.app {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid var(--line);
}
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<h1>Tk 分包离线测试</h1>
<div class="desc">
这个页面直接使用 Tk 构建产物来验证离线包加载。
仅支持分包模式:请一次选择多个 .astral 文件并加载。
</div>
<div class="row">
<span class="tag">SDK: TkAstral3D/Test/dist/astral3d.umd.js</span>
</div>
<div class="card">
<h2>选择离线包</h2>
<input id="fileInput" type="file" multiple accept=".astral" />
<div class="row">
<button id="loadBtn">加载离线包</button>
<button id="loadLocalBtn" class="secondary">直接加载本目录</button>
<button id="resetBtn" class="secondary">重置场景</button>
</div>
<div id="status" class="status">等待选择 .astral 文件,或点击“直接加载本目录”</div>
<ul id="fileList" class="file-list"></ul>
</div>
<div class="card">
<h2>说明(分包)</h2>
<div class="hint">
1. 请选择导出的全部分包 .astral 文件。\n
2. 至少包含入口包和依赖包后再点击加载。\n
3. 也可点击“直接加载本目录”,默认读取 1121212.astral + 7ff6dd32-25b3-4eea-abdc-6f316ecd668e.astral。\n
4. 建议通过本地静态服务器打开此页面,而不是直接 file:// 打开。
</div>
</div>
</aside>
<main class="viewer-wrap">
<div id="viewer"></div>
</main>
</div>
<script src="./dist/astral3d.umd.js"></script>
<script>
(function () {
const SDK = globalThis.Astral3D;
if (!SDK) {
document.getElementById("status").textContent = "Tk SDK 加载失败,请检查 astral3d.umd.js 路径";
document.getElementById("status").className = "status error";
return;
}
const viewerEl = document.getElementById("viewer");
const fileInput = document.getElementById("fileInput");
const loadBtn = document.getElementById("loadBtn");
const loadLocalBtn = document.getElementById("loadLocalBtn");
const resetBtn = document.getElementById("resetBtn");
const statusEl = document.getElementById("status");
const fileListEl = document.getElementById("fileList");
const LOCAL_SPLIT_FILES = [
"1121212.astral",
"7ff6dd32-25b3-4eea-abdc-6f316ecd668e.astral",
];
let viewer = null;
function setStatus(text, type) {
statusEl.textContent = text;
statusEl.className = "status" + (type ? ` ${type}` : "");
}
function guessEntryFile(files) {
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 => name.replace(/\.[^.]+$/, "");
return files.find(file => !uuidReg.test(findBaseName(file.name))) || files[0];
}
function ensureViewer() {
if (viewer) return viewer;
viewer = new SDK.Viewer({
container: viewerEl,
edit: { enabled: false },
grid: { enabled: true },
request: { baseUrl: "" },
});
globalThis.viewer = viewer;
return viewer;
}
function resetViewer() {
if (viewer && typeof viewer.dispose === "function") {
viewer.dispose();
}
viewer = null;
viewerEl.innerHTML = "";
setStatus("场景已重置", "success");
}
function renderFileList(files) {
fileListEl.innerHTML = "";
Array.from(files || []).forEach(file => {
const li = document.createElement("li");
li.textContent = `${file.name} (${Math.round(file.size / 1024)} KB)`;
fileListEl.appendChild(li);
});
}
async function loadByOfflineFiles(files, entryName) {
setStatus(`开始加载离线包:${entryName}`, "");
const currentViewer = ensureViewer();
const packer = currentViewer.package || new SDK.Package(currentViewer);
currentViewer.package = packer;
await packer.unpack({
url: entryName,
offlineFiles: files,
offlineEntry: entryName,
onProgress: progress => {
setStatus(`离线包加载中... ${Number(progress || 0).toFixed(2)}%`, "");
},
onSceneLoad: () => {
setStatus("场景首包已解析,正在继续加载资源...", "");
},
onComplete: () => {
setStatus(`离线包加载完成:${entryName}`, "success");
},
});
}
async function fetchLocalAstralFile(name) {
const response = await fetch(`./${encodeURIComponent(name)}`);
if (!response.ok) {
throw new Error(`读取本地文件失败:${name}`);
}
const blob = await response.blob();
return new File([blob], name, { type: "application/octet-stream" });
}
async function loadOfflinePackage() {
const files = Array.from(fileInput.files || []).filter(f => f.name.toLowerCase().endsWith(".astral"));
if (files.length < 2) {
setStatus("分包模式请至少选择 2 个 .astral 文件", "error");
return;
}
renderFileList(files);
try {
const entry = guessEntryFile(files);
await loadByOfflineFiles(files, entry.name);
} catch (error) {
console.error(error);
setStatus(`加载失败:${error && error.message ? error.message : error}`, "error");
}
}
async function loadLocalPackages() {
try {
renderFileList(LOCAL_SPLIT_FILES.map(name => ({ name, size: 0 })));
const files = [];
for (const name of LOCAL_SPLIT_FILES) {
files.push(await fetchLocalAstralFile(name));
}
await loadByOfflineFiles(files, LOCAL_SPLIT_FILES[0]);
} catch (error) {
console.error(error);
setStatus(`本目录加载失败:${error && error.message ? error.message : error}`, "error");
}
}
fileInput.addEventListener("change", () => {
renderFileList(fileInput.files);
setStatus("文件已选择,点击“加载离线包”开始测试", "");
});
loadBtn.addEventListener("click", () => {
loadOfflinePackage();
});
loadLocalBtn.addEventListener("click", () => {
loadLocalPackages();
});
resetBtn.addEventListener("click", () => {
resetViewer();
});
ensureViewer();
setStatus("Tk SDK 已加载(分包模式),等待选择离线包", "success");
})();
</script>
</body>
</html>

View File

@ -1,425 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TkAstral3D Offline Package Test</title>
<style>
:root {
--bg: #0b1020;
--panel: rgba(15, 23, 42, 0.88);
--line: rgba(148, 163, 184, 0.24);
--text: #e2e8f0;
--muted: #94a3b8;
--accent: #22c55e;
--accent-2: #38bdf8;
--danger: #f87171;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background:
radial-gradient(circle at top left, rgba(56, 189, 248, 0.12), transparent 32%),
radial-gradient(circle at top right, rgba(34, 197, 94, 0.14), transparent 28%),
linear-gradient(180deg, #0b1020, #050814 72%);
color: var(--text);
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.app {
display: grid;
grid-template-columns: 360px 1fr;
width: 100%;
height: 100%;
}
.sidebar {
padding: 20px;
border-right: 1px solid var(--line);
background: var(--panel);
backdrop-filter: blur(10px);
overflow: auto;
}
.viewer-wrap {
position: relative;
min-width: 0;
min-height: 0;
}
#viewer {
width: 100%;
height: 100%;
}
h1 {
margin: 0 0 10px;
font-size: 24px;
line-height: 1.2;
}
.desc,
.hint,
.meta,
.log {
color: var(--muted);
line-height: 1.6;
}
.desc {
margin-bottom: 16px;
font-size: 14px;
}
.card {
margin-top: 16px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(2, 6, 23, 0.36);
}
.card h2 {
margin: 0 0 10px;
font-size: 15px;
color: #f8fafc;
}
.row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-top: 12px;
}
button,
input[type="file"] {
font: inherit;
}
button {
padding: 10px 14px;
border: 1px solid var(--line);
border-radius: 10px;
background: linear-gradient(180deg, rgba(56, 189, 248, 0.16), rgba(34, 197, 94, 0.16));
color: var(--text);
cursor: pointer;
}
button.secondary {
background: rgba(15, 23, 42, 0.9);
}
input[type="file"] {
width: 100%;
color: var(--muted);
}
.status {
margin-top: 12px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--line);
background: rgba(15, 23, 42, 0.72);
font-size: 13px;
white-space: pre-wrap;
}
.status.success {
border-color: rgba(34, 197, 94, 0.35);
color: #bbf7d0;
}
.status.error {
border-color: rgba(248, 113, 113, 0.35);
color: #fecaca;
}
.file-list {
margin: 10px 0 0;
padding-left: 18px;
font-size: 13px;
color: var(--muted);
}
.tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 999px;
background: rgba(56, 189, 248, 0.1);
border: 1px solid rgba(56, 189, 248, 0.18);
font-size: 12px;
color: #bae6fd;
}
@media (max-width: 900px) {
.app {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid var(--line);
}
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<h1>Tk 离线包测试</h1>
<div class="desc">
这个页面直接使用 Tk 构建产物来验证离线包加载。
支持单包与多包 .astral 文件,直接通过 SDK 的离线参数加载,不依赖编辑器页面。
</div>
<div class="row">
<span class="tag">SDK: TkAstral3D/Test/dist/astral3d.umd.js</span>
</div>
<div class="card">
<h2>选择离线包</h2>
<input id="fileInput" type="file" multiple accept=".astral" />
<div class="row">
<button id="loadBtn">加载离线包</button>
<button id="loadLocalSplitBtn" class="secondary">直接加载本目录(分包)</button>
<button id="loadLocalSingleBtn" class="secondary">直接加载本目录(单包)</button>
<button id="resetBtn" class="secondary">重置场景</button>
</div>
<div id="status" class="status">等待选择 .astral 文件,或使用“直接加载本目录”</div>
<ul id="fileList" class="file-list"></ul>
</div>
<div class="card">
<h2>说明</h2>
<div class="hint">
1. 多包模式:直接选中全部 .astral 文件后加载。\n
2. 单包模式:只需要选择导出的单个 .astral 文件。\n
3. 本目录快捷加载:分包=1121212.astral + 7ff6dd32-25b3-4eea-abdc-6f316ecd668e.astral单包=1121212.astral。\n
4. 建议通过本地静态服务器打开此页面,而不是直接 file:// 打开。
</div>
</div>
</aside>
<main class="viewer-wrap">
<div id="viewer"></div>
</main>
</div>
<script src="./dist/astral3d.umd.js"></script>
<script>
(function () {
const SDK = globalThis.Astral3D;
if (!SDK) {
document.getElementById("status").textContent = "Tk SDK 加载失败,请检查 astral3d.umd.js 路径";
document.getElementById("status").className = "status error";
return;
}
const viewerEl = document.getElementById("viewer");
const fileInput = document.getElementById("fileInput");
const loadBtn = document.getElementById("loadBtn");
const loadLocalSplitBtn = document.getElementById("loadLocalSplitBtn");
const loadLocalSingleBtn = document.getElementById("loadLocalSingleBtn");
const resetBtn = document.getElementById("resetBtn");
const statusEl = document.getElementById("status");
const fileListEl = document.getElementById("fileList");
const LOCAL_SPLIT_FILES = [
"1121212.astral",
"7ff6dd32-25b3-4eea-abdc-6f316ecd668e.astral",
];
const LOCAL_SINGLE_FILE = "1121212-offline.astral";
let viewer = null;
function setStatus(text, type) {
statusEl.textContent = text;
statusEl.className = "status" + (type ? ` ${type}` : "");
}
function guessEntryFile(files) {
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}$/;
return files.find(f => !uuidReg.test(f.name.replace(/\.[^.]+$/, ""))) || files[0];
}
function ensureViewer() {
if (viewer) return viewer;
viewer = new SDK.Viewer({
container: viewerEl,
edit: { enabled: false },
grid: { enabled: true },
request: { baseUrl: "" },
});
globalThis.viewer = viewer;
return viewer;
}
function resetViewer() {
if (viewer && typeof viewer.dispose === "function") {
viewer.dispose();
}
viewer = null;
viewerEl.innerHTML = "";
setStatus("场景已重置", "success");
}
function renderFileList(files) {
fileListEl.innerHTML = "";
Array.from(files || []).forEach(file => {
const li = document.createElement("li");
li.textContent = `${file.name} (${Math.round(file.size / 1024)} KB)`;
fileListEl.appendChild(li);
});
}
async function loadByOfflineFiles(files, entryName) {
setStatus(`开始加载离线包:${entryName}`, "");
const currentViewer = ensureViewer();
const packer = currentViewer.package || new SDK.Package(currentViewer);
currentViewer.package = packer;
await packer.unpack({
url: entryName,
offlineFiles: files,
offlineEntry: entryName,
onProgress: progress => {
setStatus(`离线包加载中... ${Number(progress || 0).toFixed(2)}%`, "");
},
onSceneLoad: () => {
setStatus("场景首包已解析,正在继续加载资源...", "");
},
onComplete: () => {
setStatus(`离线包加载完成:${entryName}`, "success");
},
});
}
async function loadByOfflineBundle(bundle, entryName) {
setStatus(`开始加载离线包:${entryName}`, "");
const currentViewer = ensureViewer();
const packer = currentViewer.package || new SDK.Package(currentViewer);
currentViewer.package = packer;
console.log({
url: "/",
offlineBundle: bundle,
offlineEntry: entryName,
onProgress: progress => {
setStatus(`离线包加载中... ${Number(progress || 0).toFixed(2)}%`, "");
},
onSceneLoad: () => {
setStatus("场景首包已解析,正在继续加载资源...", "");
},
onComplete: () => {
setStatus(`离线包加载完成:${entryName}`, "success");
},
})
await packer.unpack({
url: "/",
offlineBundle: bundle,
offlineEntry: entryName,
onProgress: progress => {
setStatus(`离线包加载中... ${Number(progress || 0).toFixed(2)}%`, "");
},
onSceneLoad: () => {
setStatus("场景首包已解析,正在继续加载资源...", "");
},
onComplete: () => {
setStatus(`离线包加载完成:${entryName}`, "success");
},
});
}
async function fetchLocalAstralFile(name) {
const response = await fetch(`./${encodeURIComponent(name)}`);
if (!response.ok) {
throw new Error(`读取本地文件失败:${name}`);
}
const blob = await response.blob();
return new File([blob], name, { type: "application/octet-stream" });
}
async function loadOfflinePackage() {
const files = Array.from(fileInput.files || []).filter(f => f.name.toLowerCase().endsWith(".astral"));
if (files.length === 0) {
setStatus("请选择 .astral 文件", "error");
return;
}
renderFileList(files);
try {
if (files.length === 1) {
await loadByOfflineBundle(files[0], files[0].name);
} else {
const entry = guessEntryFile(files);
await loadByOfflineFiles(files, entry.name);
}
} catch (error) {
console.error(error);
setStatus(`加载失败:${error && error.message ? error.message : error}`, "error");
}
}
async function loadLocalSplitPackage() {
try {
renderFileList(LOCAL_SPLIT_FILES.map(name => ({ name, size: 0 })));
const files = [];
for (const name of LOCAL_SPLIT_FILES) {
files.push(await fetchLocalAstralFile(name));
}
await loadByOfflineFiles(files, LOCAL_SPLIT_FILES[0]);
} catch (error) {
console.error(error);
setStatus(`本目录分包加载失败:${error && error.message ? error.message : error}`, "error");
}
}
async function loadLocalSinglePackage() {
try {
renderFileList([{ name: LOCAL_SINGLE_FILE, size: 0 }]);
const file = await fetchLocalAstralFile(LOCAL_SINGLE_FILE);
await loadByOfflineBundle(file, LOCAL_SINGLE_FILE);
} catch (error) {
console.error(error);
setStatus(`本目录单包加载失败:${error && error.message ? error.message : error}`, "error");
}
}
fileInput.addEventListener("change", () => {
renderFileList(fileInput.files);
setStatus("文件已选择,点击“加载离线包”开始测试", "");
});
loadBtn.addEventListener("click", () => {
loadOfflinePackage();
});
loadLocalSplitBtn.addEventListener("click", () => {
loadLocalSplitPackage();
});
loadLocalSingleBtn.addEventListener("click", () => {
loadLocalSinglePackage();
});
resetBtn.addEventListener("click", () => {
resetViewer();
});
ensureViewer();
setStatus("Tk SDK 已加载,等待选择离线包", "success");
})();
</script>
</body>
</html>

View File

@ -78,7 +78,6 @@ async function initMonaco() {
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false, noSemanticValidation: false,
noSyntaxValidation: false, noSyntaxValidation: false,
diagnosticCodesToIgnore: [80002],
}); });
// 使 Webpack 使 importScripts // 使 Webpack 使 importScripts
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ monaco.languages.typescript.javascriptDefaults.setCompilerOptions({

View File

@ -5,7 +5,7 @@
* @update 2025-02-14 * @update 2025-02-14
* @version 5.0.0 * @version 5.0.0
*/ */
import { Mesh, Group, Bone, Object3D, Texture } from "three"; import { Mesh, Group, Bone } from "three";
import JSZip from "jszip"; import JSZip from "jszip";
import { strToU8 } from 'three/examples/jsm/libs/fflate.module.js'; import { strToU8 } from 'three/examples/jsm/libs/fflate.module.js';
import { BASE64_TYPES, TYPED_ARRAYS } from "@/constant"; import { BASE64_TYPES, TYPED_ARRAYS } from "@/constant";
@ -123,7 +123,6 @@ export class Package {
private textureMap: Map<string, any>; private textureMap: Map<string, any>;
private callFunNum: { value: number; }; private callFunNum: { value: number; };
private skeletonClass: PackageSkeleton; private skeletonClass: PackageSkeleton;
private dataComponentMap: Record<string, any> | null = null;
// 离线包相关属性 // 离线包相关属性
// @ts-ignore // @ts-ignore
@ -203,15 +202,6 @@ export class Package {
* @returns {string} * @returns {string}
*/ */
handleImage(imageJson:ITHREEScene.ImageJSON, zipData:SourceData[]): 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") { if (typeof imageJson.url === "string") {
const name = imageJson.uuid + `.${BASE64_TYPES[imageJson.url.split(",")[0]]}`; const name = imageJson.uuid + `.${BASE64_TYPES[imageJson.url.split(",")[0]]}`;
zipData.push({ name, texture: imageJson.url }); zipData.push({ name, texture: imageJson.url });
@ -231,22 +221,6 @@ export class Package {
return name; 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 * mesh json
* @param mesh * @param mesh
@ -278,12 +252,6 @@ export class Package {
!json.images && (json.images = []); !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) const name = this.handleImage(image, zipData)
if(name){ if(name){
json.images.push(name); json.images.push(name);
@ -346,60 +314,6 @@ 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 1zip文件 * group 1zip文件
* @param {IPackConfig} packConfig * @param {IPackConfig} packConfig
@ -508,14 +422,6 @@ export class Package {
console.log(sceneZipData,drawingInfo) 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({ sceneZipData.push({
name: "config.json", name: "config.json",
@ -524,8 +430,6 @@ export class Package {
xr: App.project.getKey("xr"), xr: App.project.getKey("xr"),
// 项目渲染器配置 // 项目渲染器配置
renderer: App.project.getKey("renderer"), renderer: App.project.getKey("renderer"),
// BVH 配置
bvh: App.project.getKey("bvh"),
// 项目级联阴影映射 // 项目级联阴影映射
csm: App.project.getKey("csm"), csm: App.project.getKey("csm"),
// 项目后处理配置 // 项目后处理配置
@ -808,170 +712,16 @@ 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 * @param {IUnpackConfig} unpackConfig
*/ */
public async unpack(unpackConfig: IUnpackConfig) { public unpack(unpackConfig: IUnpackConfig) {
unpackConfig.onProgress && unpackConfig.onProgress(0); unpackConfig.onProgress && unpackConfig.onProgress(0);
let totalZipNumber = 0, progress = 0; let totalZipNumber = 0, progress = 0;
await this.initOfflineSource(unpackConfig); 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("/")));
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存储 // indexDb存储
// const db = window.VIEWPORT.modules["db"]; // const db = window.VIEWPORT.modules["db"];
@ -993,11 +743,6 @@ export class Package {
that.imagesMap.clear(); that.imagesMap.clear();
that.materialsMap.clear(); that.materialsMap.clear();
that.textureMap.clear(); that.textureMap.clear();
that.offlineZipMap = null;
that.offlineEntryZip = null;
that.offlineFlatPackages = null;
that.offlineFlatEntryBase = null;
that.dataComponentMap = null;
// @ts-ignore 清除loader // @ts-ignore 清除loader
that.loader = undefined; that.loader = undefined;
} }
@ -1061,11 +806,6 @@ export class Package {
App.FPS = Number(configJson.renderer.fps); App.FPS = Number(configJson.renderer.fps);
} }
const bvhConfig = (configJson as any)?.bvh;
if (bvhConfig) {
App.project.setKey("bvh", bvhConfig);
}
if(configJson.csm){ if(configJson.csm){
const projectCSM = App.project.getKey("csm"); const projectCSM = App.project.getKey("csm");
let _csmNotChange = true; let _csmNotChange = true;
@ -1119,9 +859,6 @@ export class Package {
unpackConfig.onSceneLoad && unpackConfig.onSceneLoad(sceneJson, configJson); unpackConfig.onSceneLoad && unpackConfig.onSceneLoad(sceneJson, configJson);
this.applyDataComponentMap(App.scene as unknown as Object3D);
this.dataComponentMap = null;
// 防止项目只有一个包的情况造成不触发proxy set // 防止项目只有一个包的情况造成不触发proxy set
if (this.callFunNum.value === 0) { if (this.callFunNum.value === 0) {
this.callFunNum.value = 0; this.callFunNum.value = 0;
@ -1135,12 +872,15 @@ export class Package {
}) })
} }
const parseSceneZip = async (file: Blob | Uint8Array | ArrayBuffer) => { const networkGet = () => {
// 下载场景包
fetch(this.viewer.options.request?.baseUrl + unpackConfig.url)
.then(zipRes => zipRes.blob())
.then(async (file) => {
unpackConfig.onProgress && unpackConfig.onProgress(1); unpackConfig.onProgress && unpackConfig.onProgress(1);
// @ts-ignore // @ts-ignore
let sceneJson: ISceneJson = undefined, configJson: IAppProject.Config = undefined; let sceneJson: ISceneJson = undefined, configJson: IAppProject.Config = undefined;
let dataComponentMap: Record<string, any> | null = null;
// 开始解压首包 // 开始解压首包
const zip = new JSZip(); const zip = new JSZip();
@ -1158,7 +898,7 @@ export class Package {
} }
} }
const res = await zip.loadAsync(file as any); const res = await zip.loadAsync(file);
/** /**
* res.files里包含整个zip里的文件描述 * res.files里包含整个zip里的文件描述
@ -1177,9 +917,6 @@ export class Package {
} else if (fileName === "config.json") { // 项目配置json } else if (fileName === "config.json") { // 项目配置json
const content = await res.file(fileName)?.async('string') as string; const content = await res.file(fileName)?.async('string') as string;
configJson = JSON.parse(content); 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/") { } else if (fileName.substring(0, 9) === "Textures/") {
/** /**
* *
@ -1191,11 +928,6 @@ export class Package {
// 转换回贴图原始信息存入map // 转换回贴图原始信息存入map
const content = await res.file(fileName)?.async('arraybuffer'); const content = await res.file(fileName)?.async('arraybuffer');
this.unGzipImage(fileName.replace("Textures/", ""), content); 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 { } else {
const content = await res.file(fileName)?.async('string') const content = await res.file(fileName)?.async('string')
this.unGzipImage(fileName.replace("Textures/", ""), content); this.unGzipImage(fileName.replace("Textures/", ""), content);
@ -1232,7 +964,6 @@ export class Package {
} }
totalZipNumber = sceneJson.totalZipNumber || 0; totalZipNumber = sceneJson.totalZipNumber || 0;
this.dataComponentMap = dataComponentMap;
// 贴图还原至sceneJson // 贴图还原至sceneJson
sceneJson.scene.images = sceneJson.scene.images.map((item) => { sceneJson.scene.images = sceneJson.scene.images.map((item) => {
@ -1272,42 +1003,12 @@ export class Package {
// 解压处理好的数据添加至 indexDB -> Msy3D -> scene // 解压处理好的数据添加至 indexDB -> Msy3D -> scene
// db.setItem(dbKey, sceneJson); // 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) => { // db.getItem(dbKey).then((dbData) => {
// if (dbData === undefined) { // if (dbData === undefined) {
await networkGet(); networkGet();
// }else{ // }else{
// this.recordUuid(dbData.scene); // this.recordUuid(dbData.scene);
// //
@ -1343,8 +1044,6 @@ export class Package {
const check = (object, group) => { const check = (object, group) => {
// 检查数据是否已完善 // 检查数据是否已完善
let isDone = true; let isDone = true;
const useMaterials = new Set<string>();
object.children.forEach((child) => { object.children.forEach((child) => {
// 检查几何数据是否都已拥有 // 检查几何数据是否都已拥有
if (child.geometry && group.geometries?.findIndex((geometry) => geometry.uuid === child.geometry) === -1) { if (child.geometry && group.geometries?.findIndex((geometry) => geometry.uuid === child.geometry) === -1) {
@ -1356,9 +1055,30 @@ export class Package {
} }
// material->texture->image // material->texture->image
if (child.material) { if (child.material && group.materials?.findIndex((material) => material.uuid === child.material) === -1) {
const materialUUIDs = Array.isArray(child.material) ? child.material : [child.material]; if (!this.materialsMap.has(child.material)) {
materialUUIDs.forEach((materialUuid) => useMaterials.add(materialUuid)); 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.children?.length > 0 && isDone) { if (child.children?.length > 0 && isDone) {
@ -1366,68 +1086,6 @@ 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; return isDone;
} }
@ -1474,135 +1132,112 @@ 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 = () => { 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`, { Package._fetch(`${this.prefix_url}/${uuid}.zip`, {
onSuccess: (zipRes) => { onSuccess: (zipRes) => {
parseGroupZip(zipRes.blob()); 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.envarraybuffer格式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--;
}
}
})
} }
}); });
} }
@ -1662,11 +1297,6 @@ export class Package {
this.prefix_url = ""; this.prefix_url = "";
this.callFunNum = { value: 0 }; // 重置为初始状态 this.callFunNum = { value: 0 }; // 重置为初始状态
this.totalSize = 0; this.totalSize = 0;
this.offlineZipMap = null;
this.offlineEntryZip = null;
this.offlineFlatPackages = null;
this.offlineFlatEntryBase = null;
this.dataComponentMap = null;
// 5. 释放 viewer 引用(注意:不销毁 viewer仅移除引用 // 5. 释放 viewer 引用(注意:不销毁 viewer仅移除引用
this.viewer = null as any; this.viewer = null as any;

View File

@ -119,11 +119,6 @@ export interface ViewerModules {
tilesManage:TilesManage, tilesManage:TilesManage,
} }
export interface InstallScriptsOptions {
invokeLoaded?: boolean;
loadedScriptNames?: string[];
}
CameraControls.install({ CameraControls.install({
THREE: { THREE: {
Vector2: THREE.Vector2, Vector2: THREE.Vector2,
@ -145,14 +140,25 @@ const onDoubleClickPosition = new THREE.Vector2();
// 表示animate()函数被多次调用累积时间,用于限制FPS // 表示animate()函数被多次调用累积时间,用于限制FPS
let timeStamp = 0; let timeStamp = 0;
// 事件绑定
const Fn: any = {
pointerdown: null,
pointerup: null,
pointermove: null,
keydown: null,
keyup: null,
touchstart: null,
dblclick: null,
}
// 脚本管理数据结构 // 脚本管理数据结构
type EventHandlers = { type EventHandlers = {
[eventName: string]: { [eventName: string]: {
[uuid: string]: Function[]; [uuid: string]: Function[];
}; };
}; };
// 脚本中可写的所有事件
const createScriptEvents = (): EventHandlers => ({ let events: EventHandlers = {
loaded: {}, loaded: {},
beforeAnimation: {}, beforeAnimation: {},
afterAnimation: {}, afterAnimation: {},
@ -162,7 +168,6 @@ const createScriptEvents = (): EventHandlers => ({
afterDestroy: {}, afterDestroy: {},
onPick: {}, onPick: {},
onDoubleClick: {}, onDoubleClick: {},
bindDataChange: {},
onKeyDown: {}, onKeyDown: {},
onKeyUp: {}, onKeyUp: {},
onPointerDown: {}, onPointerDown: {},
@ -170,7 +175,9 @@ const createScriptEvents = (): EventHandlers => ({
onPointerMove: {}, onPointerMove: {},
onTouchStart: {}, onTouchStart: {},
onTouchEnd: {}, onTouchEnd: {},
}); };
// UUID 到事件的映射
const uuidEventMap: Map<string, Map<string, { name: string, fn: Function }[]>> = new Map();
export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> { export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
public container: HTMLElement; public container: HTMLElement;
@ -190,18 +197,6 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
public css2DRenderer: CSS2DRenderer = new CSS2DRenderer(); public css2DRenderer: CSS2DRenderer = new CSS2DRenderer();
public css3DRenderer: CSS3DRenderer = new CSS3DRenderer(); public css3DRenderer: CSS3DRenderer = new CSS3DRenderer();
public timer = new Timer(); public timer = new Timer();
public events: EventHandlers = createScriptEvents();
public uuidEventMap: Map<string, Map<string, { name: string, fn: Function }[]>> = new Map();
public fns: any = {
mousedown: null,
pointerdown: null,
pointerup: null,
pointermove: null,
keydown: null,
keyup: null,
touchstart: null,
dblclick: null,
};
//整个主场景的box3 //整个主场景的box3
public sceneBox3 = new THREE.Box3(); public sceneBox3 = new THREE.Box3();
public package: Package; public package: Package;
@ -367,8 +362,6 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
set enableScript(enable: boolean) { set enableScript(enable: boolean) {
if (enable === this.enableScript) return; if (enable === this.enableScript) return;
this.options.enableScript = enable;
if (enable) { if (enable) {
this.installScripts(); this.installScripts();
} else { } else {
@ -598,14 +591,14 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
* *
*/ */
initEvent() { initEvent() {
this.fns.pointerdown = this.onPointerDown.bind(this); Fn.pointerdown = this.onPointerDown.bind(this);
this.container.addEventListener('pointerdown', this.fns.pointerdown); this.container.addEventListener('pointerdown', Fn.pointerdown);
this.fns.pointermove = this.onPointerMove.bind(this); Fn.pointermove = this.onPointerMove.bind(this);
this.container.addEventListener('pointermove', this.fns.pointermove); this.container.addEventListener('pointermove', Fn.pointermove);
this.fns.touchstart = this.onTouchStart.bind(this); Fn.touchstart = this.onTouchStart.bind(this);
this.container.addEventListener('touchstart', this.fns.touchstart); this.container.addEventListener('touchstart', Fn.touchstart);
this.fns.dblclick = this.onDoubleClick.bind(this) Fn.dblclick = this.onDoubleClick.bind(this)
this.container.addEventListener('dblclick', this.fns.dblclick); this.container.addEventListener('dblclick', Fn.dblclick);
} }
/** /**
@ -613,13 +606,9 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
* @param uuids Object.uuid的脚本 * @param uuids Object.uuid的脚本
* @param filterName Object.uuid的脚本中name匹配的脚本 * @param filterName Object.uuid的脚本中name匹配的脚本
*/ */
installScripts(uuids?: string | string[], filterName: string = "", options: InstallScriptsOptions = {}) { installScripts(uuids?: string | string[], filterName: string = "") {
if (!this.enableScript) return; if (!this.enableScript) return;
const { invokeLoaded = false, loadedScriptNames = [] } = options;
const loadedScriptNameSet = new Set(loadedScriptNames.filter(Boolean));
const shouldInvokeLoadedAfterInstall = invokeLoaded || loadedScriptNameSet.size > 0;
// 注册 Helper // 注册 Helper
const helper = new ScriptHelper(this.scene); const helper = new ScriptHelper(this.scene);
@ -633,7 +622,7 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
} }
// 拼接下方闭包函数返回的结构,即返回脚本中写的支持的事件函数 // 拼接下方闭包函数返回的结构,即返回脚本中写的支持的事件函数
const validEvents = Object.keys(this.events); const validEvents = Object.keys(events);
// 准备返回结构 // 准备返回结构
validEvents.forEach(eventName => { validEvents.forEach(eventName => {
@ -657,8 +646,7 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
// 一个模型允许存在多个脚本 // 一个模型允许存在多个脚本
const scripts = App.scripts[uuid] || []; const scripts = App.scripts[uuid] || [];
const uuidEvents = this.uuidEventMap.get(uuid) || new Map<string, { name: string, fn: Function }[]>(); const uuidEvents = uuidEventMap.get(uuid) || new Map<string, { name: string, fn: Function }[]>();
const scriptsNeedLoaded = new Set<string>();
scripts.forEach(script => { scripts.forEach(script => {
// 如果存在需要按照name过滤 // 如果存在需要按照name过滤
@ -672,11 +660,6 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
this.camera, this.modules.controls, this.timer, fns.render this.camera, this.modules.controls, this.timer, fns.render
); );
const shouldInvokeLoaded = loadedScriptNameSet.size > 0 ? loadedScriptNameSet.has(script.name) : invokeLoaded;
if (shouldInvokeLoaded && typeof functions.loaded === "function") {
scriptsNeedLoaded.add(script.name);
}
Object.entries(functions).forEach(([name, fn]) => { Object.entries(functions).forEach(([name, fn]) => {
if (!fn || !validEvents.includes(name)) { if (!fn || !validEvents.includes(name)) {
if (fn && !validEvents.includes(name)) { if (fn && !validEvents.includes(name)) {
@ -689,16 +672,12 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
const {type, target, ...params} = e; const {type, target, ...params} = e;
// 点击事件只分发给对应模型 // 点击事件只分发给对应模型
if (["onPick", "onDoubleClick", "bindDataChange"].includes(name)) { if (["onPick", "onDoubleClick"].includes(name)) {
const {intersect, object: _object, data, config, index} = params; const {intersect, object: _object} = params;
if (_object.uuid !== object.uuid) return; if (_object.uuid !== object.uuid) return;
if (name === "bindDataChange") { (fn as Function).bind(object)(intersect as THREE.Intersection);
(fn as Function).bind(object)(data, config, index);
} else {
(fn as Function).bind(object)(intersect as THREE.Intersection);
}
} else { } else {
if (isEmptyObject(params)) { if (isEmptyObject(params)) {
(fn as Function).bind(object)(); (fn as Function).bind(object)();
@ -709,8 +688,8 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
} }
// 添加到全局事件集合 // 添加到全局事件集合
if (!this.events[name][uuid]) this.events[name][uuid] = []; if (!events[name][uuid]) events[name][uuid] = [];
this.events[name][uuid].push(boundFn); events[name][uuid].push(boundFn);
// 添加到 UUID 事件映射 // 添加到 UUID 事件映射
if (!uuidEvents.has(name)) uuidEvents.set(name, []); if (!uuidEvents.has(name)) uuidEvents.set(name, []);
@ -725,13 +704,7 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
}); });
// 更新 UUID 映射 // 更新 UUID 映射
this.uuidEventMap.set(uuid, uuidEvents); uuidEventMap.set(uuid, uuidEvents);
if (shouldInvokeLoadedAfterInstall) {
scriptsNeedLoaded.forEach(scriptName => {
this.invokeInstalledScriptEvent(uuid, scriptName, "loaded");
});
}
}; };
// 处理指定 UUID 或全部 // 处理指定 UUID 或全部
@ -741,15 +714,15 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
Object.keys(App.scripts).forEach(processUuid); Object.keys(App.scripts).forEach(processUuid);
} }
if (!this.fns.keydown) { if (!Fn.keydown) {
this.fns.keydown = (event: KeyboardEvent) => { Fn.keydown = (event: KeyboardEvent) => {
this.dispatchEvent({type: "onKeyDown", event}) this.dispatchEvent({type: "onKeyDown", event})
} }
window.addEventListener('keydown', this.fns.keydown); window.addEventListener('keydown', Fn.keydown);
this.fns.keyup = (event: KeyboardEvent) => { Fn.keyup = (event: KeyboardEvent) => {
this.dispatchEvent({type: "onKeyUp", event}) this.dispatchEvent({type: "onKeyUp", event})
} }
window.addEventListener('keyup', this.fns.keyup); window.addEventListener('keyup', Fn.keyup);
} }
} }
@ -759,9 +732,9 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
* @param filterName Object.uuid的脚本中name匹配的脚本 * @param filterName Object.uuid的脚本中name匹配的脚本
*/ */
uninstallScriptsByUuid(uuid: string, filterName: string = "") { uninstallScriptsByUuid(uuid: string, filterName: string = "") {
if (!this.uuidEventMap.has(uuid)) return; if (!uuidEventMap.has(uuid)) return;
const uuidEvents = this.uuidEventMap.get(uuid)!; const uuidEvents = uuidEventMap.get(uuid)!;
const uuidEventsArray = Array.from(uuidEvents); const uuidEventsArray = Array.from(uuidEvents);
for (let i = uuidEventsArray.length - 1; i >= 0; i--) { for (let i = uuidEventsArray.length - 1; i >= 0; i--) {
@ -783,7 +756,7 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
} }
// 全局事件集合 // 全局事件集合
const es = this.events[eventName][uuid]; const es = events[eventName][uuid];
// 移除相应函数 // 移除相应函数
const ei = es.findIndex(f => f === sc.fn); const ei = es.findIndex(f => f === sc.fn);
if (ei !== -1) { if (ei !== -1) {
@ -791,79 +764,40 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
} }
if (es.length === 0) { if (es.length === 0) {
delete this.events[eventName][uuid]; delete events[eventName][uuid];
} }
} }
} }
// 清理 UUID 映射 // 清理 UUID 映射
if (Array.from(uuidEvents.keys()).length === 0) { if (Array.from(uuidEvents.keys()).length === 0) {
this.uuidEventMap.delete(uuid); uuidEventMap.delete(uuid);
} }
} }
invokeInstalledScriptEvent<T extends keyof ViewerEventMap>(
uuid: string,
scriptName: string,
eventName: T,
payload: Partial<ViewerEventMap[T]> = {}
): boolean {
const uuidEvents = this.uuidEventMap.get(uuid);
if (!uuidEvents) return false;
const handlers = uuidEvents.get(String(eventName));
if (!handlers || handlers.length === 0) return false;
let executed = false;
handlers.forEach(handler => {
if (handler.name !== scriptName) return;
try {
handler.fn({
...payload,
type: eventName,
target: this,
});
executed = true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
App.log.error(`[Script] 执行 ${scriptName}.${String(eventName)} 失败: ${message}`);
}
});
return executed;
}
reinstallObjectScripts(uuid: string, loadedScriptNames: string[] = []): void {
if (!uuid) return;
const uniqueLoadedScriptNames = Array.from(new Set(loadedScriptNames.filter(Boolean)));
this.uninstallScriptsByUuid(uuid);
this.installScripts([uuid], "", uniqueLoadedScriptNames.length > 0 ? { loadedScriptNames: uniqueLoadedScriptNames } : undefined);
}
/** /**
* *
*/ */
unInstallScripts() { unInstallScripts() {
if (this.enableScript) return;
// 直接遍历 UUID 映射,复杂度 O(n) // 直接遍历 UUID 映射,复杂度 O(n)
this.uuidEventMap.forEach((_, uuid) => { uuidEventMap.forEach((_, uuid) => {
this.uninstallScriptsByUuid(uuid); this.uninstallScriptsByUuid(uuid);
}); });
// 重置数据结构 // 重置数据结构
this.uuidEventMap.clear(); uuidEventMap.clear();
Object.keys(this.events).forEach(event => { Object.keys(events).forEach(event => {
this.events[event] = {}; events[event] = {};
}); });
if (this.fns.keydown) { if (Fn.keydown) {
window.removeEventListener('keydown', this.fns.keydown); window.removeEventListener('keydown', Fn.keydown);
this.fns.keydown = null; Fn.keydown = null;
window.removeEventListener('keyup', this.fns.keyup); window.removeEventListener('keyup', Fn.keyup);
this.fns.keyup = null; Fn.keyup = null;
} }
} }
@ -970,8 +904,8 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
event.preventDefault(); event.preventDefault();
const array = getMousePosition(this.container, event.clientX, event.clientY); const array = getMousePosition(this.container, event.clientX, event.clientY);
onDownPosition.fromArray(array); onDownPosition.fromArray(array);
this.fns.pointerup = this.onPointerUp.bind(this); Fn.pointerup = this.onPointerUp.bind(this);
document.addEventListener('pointerup', this.fns.pointerup); document.addEventListener('pointerup', Fn.pointerup);
} }
/** /**
@ -984,8 +918,8 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
const array = getMousePosition(this.container, event.clientX, event.clientY); const array = getMousePosition(this.container, event.clientX, event.clientY);
onUpPosition.fromArray(array); onUpPosition.fromArray(array);
this.handleClick(); this.handleClick();
document.removeEventListener('pointerup', this.fns.pointerup); document.removeEventListener('pointerup', Fn.pointerup);
this.fns.pointerup = null; Fn.pointerup = null;
} }
/** /**
@ -1006,8 +940,8 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
const touch = event.changedTouches[0]; const touch = event.changedTouches[0];
const array = getMousePosition(this.container, touch.clientX, touch.clientY); const array = getMousePosition(this.container, touch.clientX, touch.clientY);
onDownPosition.fromArray(array); onDownPosition.fromArray(array);
this.fns.pointerup = this.onTouchEnd.bind(this); Fn.pointerup = this.onTouchEnd.bind(this);
document.addEventListener('touchend', this.fns.pointerup); document.addEventListener('touchend', Fn.pointerup);
} }
/** /**
@ -1021,8 +955,8 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
const array = getMousePosition(this.container, touch.clientX, touch.clientY); const array = getMousePosition(this.container, touch.clientX, touch.clientY);
onUpPosition.fromArray(array); onUpPosition.fromArray(array);
this.handleClick(); this.handleClick();
document.removeEventListener('touchend', this.fns.pointerup); document.removeEventListener('touchend', Fn.pointerup);
this.fns.pointerup = null; Fn.pointerup = null;
} }
/** /**
@ -1199,14 +1133,14 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
dispose() { dispose() {
this.dispatchEvent({type: "beforeDestroy"}); this.dispatchEvent({type: "beforeDestroy"});
this.container.removeEventListener('mousedown', this.fns.mousedown); this.container.removeEventListener('mousedown', Fn.mousedown);
this.fns.mousedown = null; Fn.mousedown = null;
this.container.removeEventListener('pointermove', this.fns.pointermove); this.container.removeEventListener('pointermove', Fn.pointermove);
this.fns.pointermove = null; Fn.pointermove = null;
this.container.removeEventListener('touchstart', this.fns.touchstart); this.container.removeEventListener('touchstart', Fn.touchstart);
this.fns.touchstart = null; Fn.touchstart = null;
this.container.removeEventListener('dblclick', this.fns.dblclick); this.container.removeEventListener('dblclick', Fn.dblclick);
this.fns.dblclick = null; Fn.dblclick = null;
Object.keys(this.modules).forEach(key => { Object.keys(this.modules).forEach(key => {
if (this.modules[key].dispose) { if (this.modules[key].dispose) {

View File

@ -2,21 +2,15 @@ import * as THREE from "three";
import {RoomEnvironment} from "three/examples/jsm/environments/RoomEnvironment.js"; import {RoomEnvironment} from "three/examples/jsm/environments/RoomEnvironment.js";
import {ShaderPass} from "three/examples/jsm/postprocessing/ShaderPass.js"; import {ShaderPass} from "three/examples/jsm/postprocessing/ShaderPass.js";
import {Effect} from "./Effect"; import {Effect} from "./Effect";
import {useAddSignal, useRemoveSignal} from "@/hooks"; import {useAddSignal} from "@/hooks";
import Viewer from "../Viewer"; import Viewer from "../Viewer";
import App from "@/core/app/App"; import App from "@/core/app/App";
import {focusObject} from "@/utils/scene/controls.ts"; import {focusObject} from "@/utils/scene/controls.ts";
type SignalListenerRecord = {
name: string;
listener: (...params: any[]) => void;
};
export class Signals { export class Signals {
private readonly viewer: Viewer; private readonly viewer: Viewer;
private useBackgroundAsEnvironment = false; private useBackgroundAsEnvironment = false;
private readonly signalListeners: SignalListenerRecord[] = [];
constructor(viewer:Viewer) { constructor(viewer:Viewer) {
this.viewer = viewer; this.viewer = viewer;
@ -24,46 +18,41 @@ export class Signals {
this.init(); this.init();
} }
private registerSignal(name: string, listener: (...params: any[]) => void) {
this.signalListeners.push({ name, listener });
useAddSignal(name, listener);
}
init() { init() {
this.registerSignal("sceneCleared", this.sceneCleared.bind(this)); useAddSignal("sceneCleared", this.sceneCleared.bind(this));
this.registerSignal("transformModeChanged", this.transformModeChanged.bind(this)); useAddSignal("transformModeChanged", this.transformModeChanged.bind(this));
this.registerSignal("snapChanged", this.snapChanged.bind(this)); useAddSignal("snapChanged", this.snapChanged.bind(this));
this.registerSignal("spaceChanged", this.spaceChanged.bind(this)); useAddSignal("spaceChanged", this.spaceChanged.bind(this));
this.registerSignal("effectEnabledChange", this.effectEnabledChange.bind(this)); useAddSignal("effectEnabledChange", this.effectEnabledChange.bind(this));
this.registerSignal("rendererUpdated", this.rendererUpdated.bind(this)); useAddSignal("rendererUpdated", this.rendererUpdated.bind(this));
this.registerSignal("rendererCreated", this.rendererCreated.bind(this)); useAddSignal("rendererCreated", this.rendererCreated.bind(this));
this.registerSignal("rendererConfigUpdate", this.rendererConfigUpdate.bind(this)); useAddSignal("rendererConfigUpdate", this.rendererConfigUpdate.bind(this));
this.registerSignal("rendererDetectKTX2Support", this.rendererDetectKTX2Support.bind(this)); useAddSignal("rendererDetectKTX2Support", this.rendererDetectKTX2Support.bind(this));
this.registerSignal("sceneBackgroundChanged", this.sceneBackgroundChanged.bind(this)); useAddSignal("sceneBackgroundChanged", this.sceneBackgroundChanged.bind(this));
this.registerSignal("sceneEnvironmentChanged", this.sceneEnvironmentChanged.bind(this)); useAddSignal("sceneEnvironmentChanged", this.sceneEnvironmentChanged.bind(this));
this.registerSignal("sceneGraphChanged", this.sceneGraphChanged.bind(this)); useAddSignal("sceneGraphChanged", this.sceneGraphChanged.bind(this));
this.registerSignal("cameraChanged", this.cameraChanged.bind(this)); useAddSignal("cameraChanged", this.cameraChanged.bind(this));
this.registerSignal("cameraReset", this.viewer.updateAspectRatio.bind(this.viewer)); useAddSignal("cameraReset", this.viewer.updateAspectRatio.bind(this.viewer));
this.registerSignal("viewportCameraChanged", this.viewportCameraChanged.bind(this)); useAddSignal("viewportCameraChanged", this.viewportCameraChanged.bind(this));
this.registerSignal("viewportShadingChanged", this.viewportShadingChanged.bind(this)); useAddSignal("viewportShadingChanged", this.viewportShadingChanged.bind(this));
this.registerSignal("objectSelected", this.objectSelected.bind(this)); useAddSignal("objectSelected", this.objectSelected.bind(this));
this.registerSignal("objectFocused", this.objectFocused.bind(this)); useAddSignal("objectFocused", this.objectFocused.bind(this));
this.registerSignal("objectAdded", this.objectAdded.bind(this)); useAddSignal("objectAdded", this.objectAdded.bind(this));
this.registerSignal("objectChanged", this.objectChanged.bind(this)); useAddSignal("objectChanged", this.objectChanged.bind(this));
this.registerSignal("objectRemoved", this.objectRemoved.bind(this)); useAddSignal("objectRemoved", this.objectRemoved.bind(this));
this.registerSignal("geometryChanged", this.geometryChanged.bind(this)); useAddSignal("geometryChanged", this.geometryChanged.bind(this));
this.registerSignal("materialChanged", this.materialChanged.bind(this)); useAddSignal("materialChanged", this.materialChanged.bind(this));
this.registerSignal("sceneResize", this.sceneResize.bind(this)); useAddSignal("sceneResize", this.sceneResize.bind(this));
this.registerSignal("showGridChanged", this.showGridChanged.bind(this)); useAddSignal("showGridChanged", this.showGridChanged.bind(this));
this.registerSignal("scriptAdded",this.scriptAdded.bind(this)); useAddSignal("scriptAdded",this.scriptAdded.bind(this));
this.registerSignal("scriptRemoved",this.scriptRemoved.bind(this)); useAddSignal("scriptRemoved",this.scriptRemoved.bind(this));
this.registerSignal("scriptChanged",this.scriptChanged.bind(this)); useAddSignal("scriptChanged",this.scriptChanged.bind(this));
} }
/** /**
@ -77,8 +66,6 @@ export class Signals {
* *
*/ */
sceneCleared() { sceneCleared() {
this.viewer.unInstallScripts();
this.viewer.modules.controls.setTarget(0, 0, 0,true); this.viewer.modules.controls.setTarget(0, 0, 0,true);
this.viewer.pathtracer?.reset(); this.viewer.pathtracer?.reset();
@ -466,29 +453,15 @@ export class Signals {
/** /**
* *
*/ */
scriptAdded(object:THREE.Object3D, sc:ISceneScript){ scriptAdded(object:THREE.Object3D, _:ISceneScript){
if (!object?.uuid || !sc?.name) return; this.viewer.installScripts([object.uuid]);
try {
this.viewer.reinstallObjectScripts(object.uuid, [sc.name]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
App.log.error(`[Script] 热安装脚本 ${sc.name} 失败: ${message}`);
}
} }
/** /**
* *
*/ */
scriptRemoved(object:THREE.Object3D, sc:ISceneScript){ scriptRemoved(object:THREE.Object3D, sc:ISceneScript){
if (!object?.uuid) return; this.viewer.uninstallScriptsByUuid(object.uuid,sc.name);
try {
this.viewer.reinstallObjectScripts(object.uuid);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
App.log.error(`[Script] 卸载脚本 ${sc?.name || "unknown"} 失败: ${message}`);
}
} }
/** /**
@ -496,14 +469,8 @@ export class Signals {
*/ */
scriptChanged(attributeName:string,object:THREE.Object3D, sc:ISceneScript){ scriptChanged(attributeName:string,object:THREE.Object3D, sc:ISceneScript){
if(attributeName !== "source") return; if(attributeName !== "source") return;
if (!object?.uuid || !sc?.name) return;
try { this.viewer.installScripts([object.uuid],sc.name);
this.viewer.reinstallObjectScripts(object.uuid, [sc.name]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
App.log.error(`[Script] 热更新脚本 ${sc.name} 失败: ${message}`);
}
} }
/** /**
@ -512,11 +479,4 @@ export class Signals {
render(){ render(){
this.viewer.render(); this.viewer.render();
} }
dispose() {
this.signalListeners.forEach(({ name, listener }) => {
useRemoveSignal(name, listener);
});
this.signalListeners.length = 0;
}
} }