Compare commits
No commits in common. "e8615c67969f123be65861056b6c9edefb4fd3de" and "151bc7c8a2cb0902d400624364ba25a2a7bd9743" have entirely different histories.
e8615c6796
...
151bc7c8a2
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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>
|
|
||||||
@ -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>
|
|
||||||
425
Test/index.html
425
Test/index.html
@ -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>
|
|
||||||
@ -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({
|
||||||
|
|||||||
@ -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 分组各打包为1个zip文件
|
* 按 group 分组各打包为1个zip文件
|
||||||
* @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);
|
|
||||||
|
|
||||||
if (!this.offlineZipMap && !this.offlineFlatPackages) {
|
|
||||||
const match = unpackConfig.url.match( /(.*[\\/])?([a-zA-Z0-9]+-V\d+)(?=[\\/]|$)/);
|
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("/")));
|
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,7 +1132,11 @@ export class Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseGroupZip = async (file: Blob | Uint8Array | ArrayBuffer) => {
|
const getByNetwork = () => {
|
||||||
|
Package._fetch(`${this.prefix_url}/${uuid}.zip`, {
|
||||||
|
onSuccess: (zipRes) => {
|
||||||
|
const file = zipRes.blob();
|
||||||
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
let json: GroupJson;
|
let json: GroupJson;
|
||||||
|
|
||||||
@ -1500,22 +1162,27 @@ export class Package {
|
|||||||
|
|
||||||
// 遍历children,拉取group zip还原
|
// 遍历children,拉取group zip还原
|
||||||
const children: any = [];
|
const children: any = [];
|
||||||
json.object.children.forEach((childUuid) => {
|
json.object.children.forEach((uuid) => {
|
||||||
if (typeof childUuid === "string") {
|
if (typeof uuid === "string") {
|
||||||
// 保存uuid对应的function
|
// 保存uuid对应的function
|
||||||
funcMap.set(childUuid, this.unpackGroup);
|
funcMap.set(uuid, this.unpackGroup);
|
||||||
|
|
||||||
this.callFunNum.value++;
|
this.callFunNum.value++;
|
||||||
} else {
|
} else {
|
||||||
children.push(childUuid)
|
children.push(uuid)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
json.object.children = children;
|
json.object.children = children;
|
||||||
|
|
||||||
|
//json.object.groupChildren = [...funcMap.keys()];
|
||||||
|
|
||||||
|
// 解压处理好的数据添加至 indexDB -> Msy3D -> ${dbTable}
|
||||||
|
// db.setItem(`${uuid}.zip`, json,dbTable);
|
||||||
|
|
||||||
parse(json);
|
parse(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await zip.loadAsync(file as any);
|
zip.loadAsync(file as Blob).then(res => {
|
||||||
let num = new Proxy({ value: Object.keys(res.files).length }, {
|
let num = new Proxy({ value: Object.keys(res.files).length }, {
|
||||||
set(target, p, value) {
|
set(target, p, value) {
|
||||||
target[p] = value;
|
target[p] = value;
|
||||||
@ -1525,7 +1192,6 @@ export class Package {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
for (let key in res.files) {
|
for (let key in res.files) {
|
||||||
//判断是否是目录
|
//判断是否是目录
|
||||||
if (!res.files[key].dir) {
|
if (!res.files[key].dir) {
|
||||||
@ -1534,75 +1200,44 @@ export class Package {
|
|||||||
//找到我们压缩包所需要的json文件
|
//找到我们压缩包所需要的json文件
|
||||||
if (fileName === `${uuid}.json`) { // 场景json
|
if (fileName === `${uuid}.json`) { // 场景json
|
||||||
res.file(fileName)?.async('string').then(content => {
|
res.file(fileName)?.async('string').then(content => {
|
||||||
|
//得到scene.json文件的内容
|
||||||
json = JSON.parse(content);
|
json = JSON.parse(content);
|
||||||
|
|
||||||
num.value--;
|
num.value--;
|
||||||
})
|
})
|
||||||
} else if (fileName.substring(0, 9) === "Textures/") {
|
} else if (fileName.substring(0, 9) === "Textures/") {
|
||||||
|
/**
|
||||||
|
* 贴图
|
||||||
|
* 分为两种情况:
|
||||||
|
* 1.贴图为env格式(type!width!height!uuid.env),转换为arraybuffer格式,存入map
|
||||||
|
* 2.贴图为普通图片格式,直接存入map
|
||||||
|
**/
|
||||||
if (/\.env$/.test(fileName)) {
|
if (/\.env$/.test(fileName)) {
|
||||||
// 转换回贴图原始信息,存入map
|
// 转换回贴图原始信息,存入map
|
||||||
res.file(fileName)?.async('arraybuffer').then(content => {
|
res.file(fileName)?.async('arraybuffer').then(content => {
|
||||||
this.unGzipImage(fileName.replace("Textures/", ""), 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--;
|
num.value--;
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
res.file(fileName)?.async('string').then(content => {
|
res.file(fileName)?.async('string').then(content => {
|
||||||
this.unGzipImage(fileName.replace("Textures/", ""), content);
|
this.unGzipImage(fileName.replace("Textures/", ""), content);
|
||||||
|
|
||||||
num.value--;
|
num.value--;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (/^Geometries\/geometries_\d*\.json$/.test(fileName)) {
|
} else if (/^Geometries\/geometries_\d*\.json$/.test(fileName)) {
|
||||||
res.file(fileName)?.async('string').then(content => {
|
res.file(fileName)?.async('string').then(content => {
|
||||||
geometries.push(...this.unGzipGeometryJson(JSON.parse(content)));
|
geometries.push(...this.unGzipGeometryJson(JSON.parse(content)));
|
||||||
|
|
||||||
num.value--;
|
num.value--;
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
num.value--;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
num.value--;
|
num.value--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
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`, {
|
|
||||||
onSuccess: (zipRes) => {
|
|
||||||
parseGroupZip(zipRes.blob());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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)(data, config, index);
|
|
||||||
} else {
|
|
||||||
(fn as Function).bind(object)(intersect as THREE.Intersection);
|
(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) {
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user