Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecd24acf19 | ||
|
|
37e1e58883 | ||
|
|
a8947696af | ||
|
|
9662f41eec | ||
|
|
fbfaa59b14 | ||
|
|
27b4b720ed | ||
|
|
e62f2b54d5 | ||
|
|
d7c36e31f7 | ||
|
|
985c8d39fc | ||
|
|
5368e52f0d | ||
|
|
83684b4c7b | ||
|
|
6720a3e730 | ||
|
|
e78afc73f3 | ||
|
|
2c62987281 | ||
|
|
ecddca00c9 | ||
|
|
8b363e8eb4 | ||
|
|
43c2be9525 | ||
|
|
838939855e | ||
|
|
e8615c6796 | ||
|
|
d8f98ee295 | ||
|
|
151bc7c8a2 | ||
|
|
404e804143 | ||
|
|
309bb0a5e6 | ||
|
|
8385c08ec7 | ||
|
|
4cc239d660 | ||
|
|
986da8ce42 | ||
|
|
2605656dc4 | ||
|
|
d7c0dba569 |
@@ -0,0 +1,364 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
<!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>
|
||||||
@@ -7,10 +7,10 @@ export const navbar = defineNavbarConfig([
|
|||||||
icon: 'icon-park-outline:guide-board',
|
icon: 'icon-park-outline:guide-board',
|
||||||
link: '/notes/guide/quick-start/介绍.md',
|
link: '/notes/guide/quick-start/介绍.md',
|
||||||
},
|
},
|
||||||
{ text: 'SDK', link: '/notes/sdk/README.md', icon: 'carbon:api' },
|
{ text: 'SDK', link: '/notes/sdk/迁移目标.md', icon: 'carbon:api' },
|
||||||
{ text: '示例', link: 'https://examples.astraljs.com/', icon: 'carbon:carbon-for-ibm-dotcom' },
|
{ text: '示例', link: 'https://examples.astraljs.com/', icon: 'carbon:carbon-for-ibm-dotcom' },
|
||||||
{ text: '推广中心', link: '/notes/promotion/README.md', icon: 'mdi:star-shooting-outline' },
|
{ text: '推广中心', link: '/notes/promotion/迁移目标.md', icon: 'mdi:star-shooting-outline' },
|
||||||
{ text: '商务合作', link: '/notes/cooperation/README.md', icon: 'carbon:partnership' },
|
{ text: '商务合作', link: '/notes/cooperation/迁移目标.md', icon: 'carbon:partnership' },
|
||||||
{
|
{
|
||||||
text: '更多',
|
text: '更多',
|
||||||
icon: 'icon-park-outline:more-three',
|
icon: 'icon-park-outline:more-three',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ VITE_ENABLE_ANALYZE=true
|
|||||||
VITE_ENABLE_CONFIG_GENERATE=true
|
VITE_ENABLE_CONFIG_GENERATE=true
|
||||||
|
|
||||||
# 当前站点URL
|
# 当前站点URL
|
||||||
VITE_GLOB_ORIGIN=https://editor.astraljs.com/
|
VITE_GLOB_ORIGIN=https://tk.astraltwin.cn/
|
||||||
|
|
||||||
#ws接口
|
#ws接口
|
||||||
VITE_GLOB_SOCKET_URL=wss://editor.astraljs.com/socket
|
VITE_GLOB_SOCKET_URL=wss://tk.astraltwin.cn/api/sys/ws
|
||||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 75 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
|
||||||
|
<rect width="256" height="256" fill="#121212"/>
|
||||||
|
<rect x="24" y="24" width="208" height="208" rx="18" fill="#1f1f1f" stroke="#2d2d2d" stroke-width="4"/>
|
||||||
|
<rect x="40" y="40" width="176" height="56" rx="12" fill="#00b6a4"/>
|
||||||
|
<rect x="40" y="108" width="176" height="84" rx="12" fill="#262626"/>
|
||||||
|
<text x="128" y="78" text-anchor="middle" font-size="28" font-family="Arial, sans-serif" fill="#ffffff">UIPanel</text>
|
||||||
|
<text x="128" y="156" text-anchor="middle" font-size="18" font-family="Arial, sans-serif" fill="#cfd8dc">WYSIWYG</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -3,7 +3,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
import * as monaco from 'monaco-editor';
|
import * as monaco from 'monaco-editor';
|
||||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||||
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||||
@@ -74,10 +74,22 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.source,
|
||||||
|
value => {
|
||||||
|
if (!editor) return;
|
||||||
|
const currentValue = editor.getValue();
|
||||||
|
if (value !== currentValue) {
|
||||||
|
editor.setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
async function initMonaco() {
|
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({
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {onMounted, ref} from "vue";
|
|||||||
import {Copy} from "@vicons/carbon";
|
import {Copy} from "@vicons/carbon";
|
||||||
import {NIcon, NTooltip} from "naive-ui";
|
import {NIcon, NTooltip} from "naive-ui";
|
||||||
import {t} from "@/language";
|
import {t} from "@/language";
|
||||||
import {App,Hooks,AddObjectCommand} from "@astral3d/engine";
|
import {App,Hooks,Loader,Utils,AddObjectCommand} from "@astral3d/engine";
|
||||||
|
|
||||||
const disabled = ref(true);
|
const disabled = ref(true);
|
||||||
|
|
||||||
@@ -36,6 +36,15 @@ function handleClone() {
|
|||||||
//避免复制相机或场景
|
//避免复制相机或场景
|
||||||
if (object === null || object.parent === null) return;
|
if (object === null || object.parent === null) return;
|
||||||
|
|
||||||
|
if (Utils.isHtmlPanelObject(object)) {
|
||||||
|
const _json = object.toJSON() as any;
|
||||||
|
|
||||||
|
Loader.objectLoader.parseAsync(_json).then(newObject3D => {
|
||||||
|
App.execute(new AddObjectCommand(newObject3D));
|
||||||
|
}).catch((e: Error) => window.$message?.error(e.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
object = object.clone();
|
object = object.clone();
|
||||||
|
|
||||||
App.execute(new AddObjectCommand(object));
|
App.execute(new AddObjectCommand(object));
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<n-button class="mr-2">
|
<n-button class="mr-2">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon size="22">
|
<n-icon size="22">
|
||||||
<DocumentExport/>
|
<DocumentExport />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
{{ t("layout.header.Export") }}
|
{{ t("layout.header.Export") }}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<n-button type="primary" @click="handleImport">
|
<n-button type="primary" @click="handleImport">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<DocumentImport/>
|
<DocumentImport />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
{{ t("layout.header.Import") }}
|
{{ t("layout.header.Import") }}
|
||||||
@@ -21,55 +21,57 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted} from "vue";
|
import { onMounted } from "vue";
|
||||||
import {DocumentImport, DocumentExport} from "@vicons/carbon";
|
import { DocumentImport, DocumentExport } from "@vicons/carbon";
|
||||||
import {t} from "@/language";
|
import { t } from "@/language";
|
||||||
import {App, Export, Loader} from "@astral3d/engine";
|
import { App, Export, Loader } from "@astral3d/engine";
|
||||||
|
import { useGlobalConfigStore } from "@/store/modules/globalConfig";
|
||||||
|
|
||||||
const exportClass = new Export();
|
const exportClass = new Export();
|
||||||
|
const globalConfigStore = useGlobalConfigStore();
|
||||||
|
|
||||||
const exportOptions = [
|
const exportOptions = [
|
||||||
{
|
{
|
||||||
label: t("layout.header['Export Object']"),
|
label: t("layout.header['Export Object']"),
|
||||||
key: "exportObject",
|
key: "exportObject",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: "JSON",
|
label: "JSON",
|
||||||
key: "exportObjectToJSON"
|
key: "exportObjectToJSON",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "GLB",
|
label: "GLB",
|
||||||
key: "exportObjectToGlb"
|
key: "exportObjectToGlb",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "GLTF",
|
label: "GLTF",
|
||||||
key: "exportObjectToGltf"
|
key: "exportObjectToGltf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "OBJ",
|
label: "OBJ",
|
||||||
key: "exportObjectToObj"
|
key: "exportObjectToObj",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "PLY",
|
label: "PLY",
|
||||||
key: "exportObjectToPly"
|
key: "exportObjectToPly",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("layout.header['PLY (Binary)']"),
|
label: t("layout.header['PLY (Binary)']"),
|
||||||
key: "exportObjectToPlyBinary"
|
key: "exportObjectToPlyBinary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "STL",
|
label: "STL",
|
||||||
key: "exportObjectToStl"
|
key: "exportObjectToStl",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("layout.header['STL (Binary)']"),
|
label: t("layout.header['STL (Binary)']"),
|
||||||
key: "exportObjectToStlBinary"
|
key: "exportObjectToStlBinary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "USDZ",
|
label: "USDZ",
|
||||||
key: "exportObjectToUSDZ"
|
key: "exportObjectToUSDZ",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("layout.header['Export Scene']"),
|
label: t("layout.header['Export Scene']"),
|
||||||
@@ -77,55 +79,82 @@ const exportOptions = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: "JSON",
|
label: "JSON",
|
||||||
key: "exportSceneToJSON"
|
key: "exportSceneToJSON",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "GLB",
|
label: "GLB",
|
||||||
key: "exportSceneToGlb"
|
key: "exportSceneToGlb",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "GLTF",
|
label: "GLTF",
|
||||||
key: "exportSceneToGltf"
|
key: "exportSceneToGltf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "OBJ",
|
label: "OBJ",
|
||||||
key: "exportSceneToObj"
|
key: "exportSceneToObj",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "PLY",
|
label: "PLY",
|
||||||
key: "exportSceneToPly"
|
key: "exportSceneToPly",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("layout.header['PLY (Binary)']"),
|
label: t("layout.header['PLY (Binary)']"),
|
||||||
key: "exportSceneToPlyBinary"
|
key: "exportSceneToPlyBinary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "STL",
|
label: "STL",
|
||||||
key: "exportSceneToStl"
|
key: "exportSceneToStl",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("layout.header['STL (Binary)']"),
|
label: t("layout.header['STL (Binary)']"),
|
||||||
key: "exportSceneToStlBinary"
|
key: "exportSceneToStlBinary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "USDZ",
|
label: "USDZ",
|
||||||
key: "exportSceneToUSDZ"
|
key: "exportSceneToUSDZ",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
label: t("layout.header['Offline package']"),
|
||||||
|
key: "exportOffline",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: t("layout.header['Single packet']"),
|
||||||
|
key: "exportOfflineSingle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("layout.header['Multiple packet']"),
|
||||||
|
key: "exportOfflineMultiple",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function handleImport() {
|
function handleImport() {
|
||||||
const form = document.createElement('form');
|
const form = document.createElement("form");
|
||||||
form.style.display = 'none';
|
form.style.display = "none";
|
||||||
document.body.appendChild(form);
|
document.body.appendChild(form);
|
||||||
|
|
||||||
const fileInput = document.createElement('input');
|
const fileInput = document.createElement("input");
|
||||||
fileInput.multiple = true;
|
fileInput.multiple = true;
|
||||||
fileInput.type = 'file';
|
fileInput.type = "file";
|
||||||
fileInput.addEventListener('change', function () {
|
fileInput.addEventListener("change", function () {
|
||||||
Loader.loadFiles(fileInput.files, undefined)
|
const files = fileInput.files;
|
||||||
.catch((err) => {
|
if (!files || files.length === 0) {
|
||||||
|
form.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const astralFiles = Array.from(files).filter(file => file.name.toLowerCase().endsWith(".astral"));
|
||||||
|
if (astralFiles.length > 0) {
|
||||||
|
handleOfflineFiles(files);
|
||||||
|
form.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader.loadFiles(files, undefined)
|
||||||
|
.catch(err => {
|
||||||
window.$message?.error(err);
|
window.$message?.error(err);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -135,9 +164,18 @@ function handleImport() {
|
|||||||
form.appendChild(fileInput);
|
form.appendChild(fileInput);
|
||||||
|
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleExportSelect(key: string) {
|
||||||
|
if (key === "exportOfflineSingle") {
|
||||||
|
exportOffline("single");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === "exportOfflineMultiple") {
|
||||||
|
exportOffline("multiple");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function handleExportSelect(key: string) {
|
|
||||||
if (key.startsWith("exportObject")) {
|
if (key.startsWith("exportObject")) {
|
||||||
if (App.selected === null) {
|
if (App.selected === null) {
|
||||||
window.$message?.error(window.$t("prompt['No object selected.']"));
|
window.$message?.error(window.$t("prompt['No object selected.']"));
|
||||||
@@ -145,11 +183,121 @@ function handleExportSelect(key: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exportClass[key]();
|
exportClass[key] && exportClass[key]();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
function downloadFile(file: File) {
|
||||||
})
|
const url = URL.createObjectURL(file);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = file.name;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFilesInBatches(files: File[], batchSize = 9, delayMs = 1100) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
downloadFile(files[i]);
|
||||||
|
if ((i + 1) % batchSize === 0 && i < files.length - 1) {
|
||||||
|
await sleep(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportOffline(mode: "single" | "multiple") {
|
||||||
|
const packer = (window.viewer as any)?.package;
|
||||||
|
if (!packer || typeof packer.packOffline !== "function") {
|
||||||
|
window.$message?.error("当前SDK暂不支持离线包导出");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sceneInfo = App.project.getKey("sceneInfo");
|
||||||
|
const sceneName = sceneInfo?.sceneName || "scene";
|
||||||
|
|
||||||
|
globalConfigStore.loading = true;
|
||||||
|
globalConfigStore.loadingText = "正在导出离线包...";
|
||||||
|
|
||||||
|
packer
|
||||||
|
.packOffline({
|
||||||
|
name: sceneName,
|
||||||
|
layer: 2,
|
||||||
|
mode,
|
||||||
|
onProgress: (progress: number) => {
|
||||||
|
globalConfigStore.loadingText = `离线包导出 ${progress}%`;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (result: { files: File[] }) => {
|
||||||
|
await downloadFilesInBatches(result.files);
|
||||||
|
window.$message?.success("离线包导出完成");
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
if (err?.code === "SDK_FEATURE_DISABLED") {
|
||||||
|
window.$message?.error("当前授权未开通离线包导出能力");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof err?.message === "string" && err.message.trim()) {
|
||||||
|
window.$message?.error(err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.$message?.error("离线包导出失败");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
globalConfigStore.loading = false;
|
||||||
|
globalConfigStore.loadingText = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOfflineFiles(files: FileList | null) {
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
const astralFiles = Array.from(files).filter(file => file.name.toLowerCase().endsWith(".astral"));
|
||||||
|
if (astralFiles.length === 0) {
|
||||||
|
window.$message?.error("请选择离线包文件(.astral)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packer = (window.viewer as any)?.package;
|
||||||
|
if (!packer || typeof packer.unpack !== "function") {
|
||||||
|
window.$message?.error("当前SDK暂不支持离线包导入");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryFile = guessEntryFile(astralFiles);
|
||||||
|
|
||||||
|
globalConfigStore.loading = true;
|
||||||
|
globalConfigStore.loadingText = "离线包加载中...";
|
||||||
|
|
||||||
|
packer.unpack({
|
||||||
|
url: entryFile.name,
|
||||||
|
offlineFiles: astralFiles,
|
||||||
|
offlineEntry: entryFile.name,
|
||||||
|
onProgress: (progress: number) => {
|
||||||
|
globalConfigStore.loadingText = `离线包加载 ${progress.toFixed(2)}%`;
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
globalConfigStore.loading = false;
|
||||||
|
globalConfigStore.loadingText = "";
|
||||||
|
window.$message?.success("离线包加载完成");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessEntryFile(files: File[]): File {
|
||||||
|
const uuidReg = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||||
|
const findBaseName = (name: string) => name.replace(/\.[^.]+$/, "");
|
||||||
|
|
||||||
|
const candidate = files.find(file => !uuidReg.test(findBaseName(file.name)));
|
||||||
|
return candidate || files[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less"></style>
|
<style scoped lang="less"></style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import {nextTick} from "vue";
|
import {nextTick} from "vue";
|
||||||
import {Save} from "@vicons/carbon";
|
import {Save} from "@vicons/carbon";
|
||||||
import {t} from "@/language";
|
import {t} from "@/language";
|
||||||
import {App,Package} from "@astral3d/engine";
|
import {App} from "@astral3d/engine";
|
||||||
import { useGlobalConfigStore } from '@/store/modules/globalConfig';
|
import { useGlobalConfigStore } from '@/store/modules/globalConfig';
|
||||||
import {fetchUpload} from "@/http/api/sys";
|
import {fetchUpload} from "@/http/api/sys";
|
||||||
import {filterSize} from "@/utils/common/file";
|
import {filterSize} from "@/utils/common/file";
|
||||||
@@ -59,7 +59,7 @@ function save(){
|
|||||||
|
|
||||||
globalConfigStore.loadingText = window.$t("scene['Scene is being compressed...']");
|
globalConfigStore.loadingText = window.$t("scene['Scene is being compressed...']");
|
||||||
|
|
||||||
const p = new Package(window.viewer);
|
const p = window.viewer.package;
|
||||||
p.pack({
|
p.pack({
|
||||||
// 首包名称
|
// 首包名称
|
||||||
name:`${sceneInfo.sceneName}`,
|
name:`${sceneInfo.sceneName}`,
|
||||||
@@ -96,8 +96,6 @@ function save(){
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
globalConfigStore.loading = false;
|
globalConfigStore.loading = false;
|
||||||
|
|
||||||
p.dispose();
|
|
||||||
}, 500)
|
}, 500)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
ChoroplethMap
|
ChoroplethMap
|
||||||
} from '@vicons/carbon';
|
} from '@vicons/carbon';
|
||||||
import {t} from "@/language";
|
import {t} from "@/language";
|
||||||
import {App,Hooks, MoveObjectCommand, RemoveObjectCommand, AddObjectCommand} from "@astral3d/engine";
|
import {App,Hooks,Loader,Utils, MoveObjectCommand, RemoveObjectCommand, AddObjectCommand} from "@astral3d/engine";
|
||||||
import {escapeHTML, findSiblingsAndIndex} from "@/utils/common/utils";
|
import {escapeHTML, findSiblingsAndIndex} from "@/utils/common/utils";
|
||||||
import {getMaterialName} from "@/utils/common/scenes";
|
import {getMaterialName} from "@/utils/common/scenes";
|
||||||
import EsContextmenu from "@/components/es/EsContextmenu.vue";
|
import EsContextmenu from "@/components/es/EsContextmenu.vue";
|
||||||
@@ -367,6 +367,15 @@ function handleContextmenuSelect(key: string) {
|
|||||||
if (parent !== null) App.execute(new RemoveObjectCommand(object));
|
if (parent !== null) App.execute(new RemoveObjectCommand(object));
|
||||||
break;
|
break;
|
||||||
case "clone":
|
case "clone":
|
||||||
|
if (Utils.isHtmlPanelObject(object)) {
|
||||||
|
const _json = object.toJSON() as any;
|
||||||
|
|
||||||
|
Loader.objectLoader.parseAsync(_json).then(newObject3D => {
|
||||||
|
App.execute(new AddObjectCommand(newObject3D));
|
||||||
|
}).catch((e: Error) => window.$message?.error(e.message));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const _object = object.clone();
|
const _object = object.clone();
|
||||||
|
|
||||||
App.execute(new AddObjectCommand(_object));
|
App.execute(new AddObjectCommand(_object));
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import {request} from "@/http/request";
|
||||||
|
|
||||||
|
export interface DataSetQueryParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
name?: string;
|
||||||
|
type?: string;
|
||||||
|
groupId?: IDataSet.Item["groupId"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataSetPayload {
|
||||||
|
id?: IDataSet.Item["id"];
|
||||||
|
name: string;
|
||||||
|
groupId: IDataSet.Item["groupId"];
|
||||||
|
type: "API" | "SQL" | "JSON" | string;
|
||||||
|
method?: IDataSet.Item["method"];
|
||||||
|
api?: string;
|
||||||
|
dataSourceId?: IDataSet.Item["id"];
|
||||||
|
sql?: string;
|
||||||
|
json?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataSetExecutePayload {
|
||||||
|
params?: any[];
|
||||||
|
query?: Record<string, any>;
|
||||||
|
body?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDataSetPage(params: DataSetQueryParams) {
|
||||||
|
return request.get<Service.ListPageResult<IDataSet.Item>>(`/data-set/page`, {params});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDataSetDetail(id: IDataSet.Item["id"]) {
|
||||||
|
return request.get<IDataSet.Item>(`/data-set/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchCreateDataSet(data: DataSetPayload) {
|
||||||
|
return request.post(`/data-set`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateDataSet(data: DataSetPayload) {
|
||||||
|
return request.put(`/data-set`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteDataSet(id: IDataSet.Item["id"]) {
|
||||||
|
return request.delete(`/data-set/${id}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExecuteDataSet(id: IDataSet.Item["id"], data?: DataSetExecutePayload) {
|
||||||
|
return request.post(`/data-set/${id}/execute`, data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {request} from "@/http/request";
|
||||||
|
|
||||||
|
export interface DataSetGroupPayload {
|
||||||
|
id?: IDataSet.IGroup["id"];
|
||||||
|
name: string;
|
||||||
|
pid: IDataSet.IGroup["pid"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDataSetGroupTree() {
|
||||||
|
return request.get<IDataSet.IGroup[]>(`/data-set-group/tree`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchCreateDataSetGroup(data: DataSetGroupPayload) {
|
||||||
|
return request.post(`/data-set-group`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateDataSetGroup(data: DataSetGroupPayload) {
|
||||||
|
return request.put(`/data-set-group`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteDataSetGroup(id: IDataSet.IGroup["id"]) {
|
||||||
|
return request.delete(`/data-set-group/${id}`, {});
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {request} from "@/http/request";
|
||||||
|
|
||||||
|
export interface DataSourcePayload {
|
||||||
|
id?: IDataSource.Item["id"];
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
connectionString: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDataSourceList() {
|
||||||
|
return request.get<IDataSource.Item[]>(`/data-source/list`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchCreateDataSource(data: DataSourcePayload) {
|
||||||
|
return request.post<IDataSource.Item>(`/data-source`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateDataSource(data: DataSourcePayload) {
|
||||||
|
return request.put<IDataSource.Item>(`/data-source`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteDataSource(id: IDataSource.Item["id"]) {
|
||||||
|
return request.delete(`/data-source/${id}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchTestDataSource(data: DataSourcePayload) {
|
||||||
|
return request.post(`/data-source/test`, data);
|
||||||
|
}
|
||||||
@@ -77,6 +77,9 @@ export default {
|
|||||||
Export: '导出',
|
Export: '导出',
|
||||||
'Export Object': '导出物体',
|
'Export Object': '导出物体',
|
||||||
'Export Scene': '导出场景',
|
'Export Scene': '导出场景',
|
||||||
|
'Offline package': '离线包',
|
||||||
|
'Single packet': '单包',
|
||||||
|
'Multiple packet': '多包',
|
||||||
'PLY (Binary)': 'PLY(二进制)',
|
'PLY (Binary)': 'PLY(二进制)',
|
||||||
'STL (Binary)': 'STL(二进制)',
|
'STL (Binary)': 'STL(二进制)',
|
||||||
/* File 下的选项 End */
|
/* File 下的选项 End */
|
||||||
@@ -122,6 +125,9 @@ export default {
|
|||||||
world: '世界坐标',
|
world: '世界坐标',
|
||||||
"Status monitoring":"状态监控",
|
"Status monitoring":"状态监控",
|
||||||
},
|
},
|
||||||
|
path: {
|
||||||
|
"Path drawing: Left-click to add a point, and double-click to end the drawing": "路径绘制:左键单击添加点,双击结束绘制",
|
||||||
|
},
|
||||||
viewportInfo: {
|
viewportInfo: {
|
||||||
Objects: '物体',
|
Objects: '物体',
|
||||||
Vertices: '顶点',
|
Vertices: '顶点',
|
||||||
@@ -223,6 +229,27 @@ export default {
|
|||||||
"Post processing": "后期处理",
|
"Post processing": "后期处理",
|
||||||
postProcessing: {
|
postProcessing: {
|
||||||
"Anti-aliasing": "抗锯齿",
|
"Anti-aliasing": "抗锯齿",
|
||||||
|
"Quality Preset": "质量预设",
|
||||||
|
"Tone Mapping": "色调映射",
|
||||||
|
"Exposure": "曝光度",
|
||||||
|
"SSAO": "环境光遮蔽",
|
||||||
|
"Samples": "采样数",
|
||||||
|
"Rings": "环数",
|
||||||
|
"Bias": "偏置",
|
||||||
|
"Fade": "衰减",
|
||||||
|
"Luminance Influence": "亮度影响",
|
||||||
|
"Min Radius Scale": "最小半径缩放",
|
||||||
|
"Depth-aware Upsampling": "深度感知上采样",
|
||||||
|
"Distance Threshold": "距离阈值",
|
||||||
|
"Distance Falloff": "距离衰减",
|
||||||
|
"Range Threshold": "范围阈值",
|
||||||
|
"Range Falloff": "范围衰减",
|
||||||
|
"World Distance Threshold": "世界距离阈值",
|
||||||
|
"World Distance Falloff": "世界距离衰减",
|
||||||
|
"World Proximity Threshold": "世界邻近阈值",
|
||||||
|
"World Proximity Falloff": "世界邻近衰减",
|
||||||
|
"Colorize": "颜色化",
|
||||||
|
"AO Color": "遮蔽颜色",
|
||||||
"Outline": "描边线",
|
"Outline": "描边线",
|
||||||
"Edge Strength": "边缘强度",
|
"Edge Strength": "边缘强度",
|
||||||
"Edge Glow": "边缘发光",
|
"Edge Glow": "边缘发光",
|
||||||
@@ -234,6 +261,57 @@ export default {
|
|||||||
"Radius": "半径",
|
"Radius": "半径",
|
||||||
"Threshold": "阈值",
|
"Threshold": "阈值",
|
||||||
"Strength": "强度",
|
"Strength": "强度",
|
||||||
|
"Brightness Contrast": "亮度对比度",
|
||||||
|
"Brightness": "亮度",
|
||||||
|
"Contrast": "对比度",
|
||||||
|
"Chromatic Aberration": "色差",
|
||||||
|
"Offset X": "偏移 X",
|
||||||
|
"Offset Y": "偏移 Y",
|
||||||
|
"Radial Modulation": "径向调制",
|
||||||
|
"Modulation Offset": "调制偏移",
|
||||||
|
"Color Depth": "色深",
|
||||||
|
"Bits": "位数",
|
||||||
|
"Hue Saturation": "色相饱和度",
|
||||||
|
"Hue": "色相",
|
||||||
|
"Saturation": "饱和度",
|
||||||
|
"Tilt Shift": "移轴模糊",
|
||||||
|
"Offset": "偏移",
|
||||||
|
"Rotation": "旋转",
|
||||||
|
"Focus Area": "焦点区域",
|
||||||
|
"Feather": "羽化",
|
||||||
|
"Scanline": "扫描线",
|
||||||
|
"Density": "密度",
|
||||||
|
"Scroll Speed": "滚动速度",
|
||||||
|
"Glitch": "故障",
|
||||||
|
"Glitch Mode": "故障模式",
|
||||||
|
"Sporadic": "偶发",
|
||||||
|
"Constant Mild": "持续轻微",
|
||||||
|
"Constant Wild": "持续剧烈",
|
||||||
|
"Delay Min": "延迟最小值",
|
||||||
|
"Delay Max": "延迟最大值",
|
||||||
|
"Duration Min": "持续最小值",
|
||||||
|
"Duration Max": "持续最大值",
|
||||||
|
"Strength Min": "强度最小值",
|
||||||
|
"Strength Max": "强度最大值",
|
||||||
|
"Ratio": "出现概率",
|
||||||
|
"Lens Distortion": "镜头畸变",
|
||||||
|
"Distortion X": "畸变 X",
|
||||||
|
"Distortion Y": "畸变 Y",
|
||||||
|
"Principal Point X": "主点 X",
|
||||||
|
"Principal Point Y": "主点 Y",
|
||||||
|
"Focal Length X": "焦距 X",
|
||||||
|
"Focal Length Y": "焦距 Y",
|
||||||
|
"Skew": "倾斜",
|
||||||
|
"Shock Wave": "冲击波",
|
||||||
|
"Click on scene to trigger shock wave effect": "点击场景触发冲击波效果",
|
||||||
|
"Amplitude": "振幅",
|
||||||
|
"Wave Size": "波浪大小",
|
||||||
|
"Speed": "传播速度",
|
||||||
|
"Max Radius": "最大半径",
|
||||||
|
"Click Trigger": "点击触发",
|
||||||
|
"Vignette": "暗角",
|
||||||
|
"Darkness": "暗度",
|
||||||
|
"Blend Function": "混合模式",
|
||||||
"LUT Color filter":"LUT 颜色滤镜",
|
"LUT Color filter":"LUT 颜色滤镜",
|
||||||
"Intensity":"强度",
|
"Intensity":"强度",
|
||||||
"Afterimage":"运动残影",
|
"Afterimage":"运动残影",
|
||||||
@@ -242,6 +320,7 @@ export default {
|
|||||||
"Focus": "焦距",
|
"Focus": "焦距",
|
||||||
"Aperture": "孔径",
|
"Aperture": "孔径",
|
||||||
"MaxBlur": "最大模糊",
|
"MaxBlur": "最大模糊",
|
||||||
|
"Resolution Scale": "分辨率缩放",
|
||||||
"Pixelate": "像素风",
|
"Pixelate": "像素风",
|
||||||
"PixelSize": "像素大小",
|
"PixelSize": "像素大小",
|
||||||
"NormalEdgeStrength": "法向边缘强度",
|
"NormalEdgeStrength": "法向边缘强度",
|
||||||
@@ -539,6 +618,173 @@ export default {
|
|||||||
htmlPanel: {
|
htmlPanel: {
|
||||||
Content: "内容"
|
Content: "内容"
|
||||||
},
|
},
|
||||||
|
Heatmap: "热力图",
|
||||||
|
Path: "路径",
|
||||||
|
"UI panel": "UI面板",
|
||||||
|
"Data component": "数据组件",
|
||||||
|
"Water pool": "水池",
|
||||||
|
dataComponent: {
|
||||||
|
"Data set": "数据集",
|
||||||
|
"Select data set": "选择数据集",
|
||||||
|
"Data filter": "数据过滤器",
|
||||||
|
"Enable filter": "启用过滤",
|
||||||
|
"Disabled filter": "禁用过滤",
|
||||||
|
"Popup editor": "弹出编辑",
|
||||||
|
"Filter editor": "过滤器编辑",
|
||||||
|
"Auto refresh": "自动刷新",
|
||||||
|
"Seconds once": "秒一次",
|
||||||
|
"Apply to model": "应用于模型",
|
||||||
|
"Transition": "过渡时间",
|
||||||
|
"Milliseconds": "毫秒",
|
||||||
|
"Data result": "数据结果",
|
||||||
|
"Result editor": "结果编辑",
|
||||||
|
"Manual refresh": "手动刷新",
|
||||||
|
"Result error": "结果错误",
|
||||||
|
},
|
||||||
|
heatmap: {
|
||||||
|
"Heatmap Texture": "热力图纹理",
|
||||||
|
Blur: "模糊",
|
||||||
|
Gradient: "色阶",
|
||||||
|
Add: "添加",
|
||||||
|
Remove: "移除",
|
||||||
|
Data: "数据",
|
||||||
|
Points: "点",
|
||||||
|
"Controls the opacity of the darkest and brightest parts of a thermal texture": "控制热力纹理最暗处和最亮处的不透明度",
|
||||||
|
"Height Scale": "高度缩放",
|
||||||
|
"Heatmap Points": "热力图点",
|
||||||
|
Error: "错误",
|
||||||
|
"Points must be an array.": "点数据必须是数组。",
|
||||||
|
"Point[{index}] must be an object.": "点[{index}]必须是对象。",
|
||||||
|
"Point[{index}].x must be a number.": "点[{index}].x必须是数字。",
|
||||||
|
"Point[{index}].y must be a number.": "点[{index}].y必须是数字。",
|
||||||
|
"Point[{index}].value must be a number.": "点[{index}].value必须是数字。",
|
||||||
|
"Point[{index}].radius must be a number.": "点[{index}].radius必须是数字。",
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
"Path Params": "路径参数",
|
||||||
|
"Ribbon Params": "平面路径",
|
||||||
|
"Tube Params": "管状路径",
|
||||||
|
"Flow Params": "流动效果",
|
||||||
|
Type: "类型",
|
||||||
|
Ribbon: "平面",
|
||||||
|
Tube: "管状",
|
||||||
|
"Corner Radius": "拐角半径",
|
||||||
|
"Corner Split": "拐角分段",
|
||||||
|
Progress: "进度",
|
||||||
|
Arrow: "箭头",
|
||||||
|
Side: "绘制侧",
|
||||||
|
Both: "双侧",
|
||||||
|
Left: "左侧",
|
||||||
|
Right: "右侧",
|
||||||
|
"Start Rad": "起始弧度",
|
||||||
|
Flow: "流动",
|
||||||
|
"Flow Speed": "流动速度",
|
||||||
|
"Flow Direction": "流动方向",
|
||||||
|
},
|
||||||
|
uipanel: {
|
||||||
|
Node: "节点",
|
||||||
|
"Select a node to edit": "选择节点以编辑",
|
||||||
|
Type: "类型",
|
||||||
|
Text: "文本",
|
||||||
|
Color: "颜色",
|
||||||
|
Padding: "内边距",
|
||||||
|
Margin: "外边距",
|
||||||
|
Offset: "偏移",
|
||||||
|
"Font opacity": "字体透明度",
|
||||||
|
"Font supersampling": "字体超采样",
|
||||||
|
"Letter spacing": "字间距",
|
||||||
|
"Line height": "行高",
|
||||||
|
"White space": "空白处理",
|
||||||
|
"Break on": "换行字符",
|
||||||
|
"Background color": "背景色",
|
||||||
|
"Background opacity": "背景透明度",
|
||||||
|
"Background image URL": "背景图链接",
|
||||||
|
"Select local image": "选择本地图片",
|
||||||
|
"Image size must be <= {size}MB": "图片大小不能超过{size}MB",
|
||||||
|
"Background size": "背景图大小",
|
||||||
|
Cover: "覆盖",
|
||||||
|
Contain: "包含",
|
||||||
|
Stretch: "拉伸",
|
||||||
|
"Border radius": "圆角",
|
||||||
|
"Border width": "边框宽度",
|
||||||
|
"Border color": "边框颜色",
|
||||||
|
"Hidden overflow": "隐藏溢出",
|
||||||
|
"Flex direction": "布局方向",
|
||||||
|
Row: "横向",
|
||||||
|
"Row reverse": "横向反转",
|
||||||
|
Column: "纵向",
|
||||||
|
"Column reverse": "纵向反转",
|
||||||
|
"Justify content": "主轴对齐",
|
||||||
|
"Align items": "交叉轴对齐",
|
||||||
|
States: "状态样式",
|
||||||
|
Hover: "悬停",
|
||||||
|
Active: "点击",
|
||||||
|
"Hover background": "悬停背景色",
|
||||||
|
"Hover opacity": "悬停透明度",
|
||||||
|
"Active background": "点击背景色",
|
||||||
|
"Active opacity": "点击透明度",
|
||||||
|
"Hover color": "悬停颜色",
|
||||||
|
"Active color": "点击颜色",
|
||||||
|
Start: "开始",
|
||||||
|
Center: "居中",
|
||||||
|
End: "结束",
|
||||||
|
"Space between": "两端对齐",
|
||||||
|
"Space around": "环绕",
|
||||||
|
"Space evenly": "均分",
|
||||||
|
"Font size": "字号",
|
||||||
|
"Text align": "文字对齐",
|
||||||
|
Left: "左对齐",
|
||||||
|
Right: "右对齐",
|
||||||
|
"Add Block": "添加容器",
|
||||||
|
"Add Text": "添加文本",
|
||||||
|
},
|
||||||
|
waterPool: {
|
||||||
|
Basic: "基础",
|
||||||
|
Type: "水池类型",
|
||||||
|
Cylinder: "圆柱",
|
||||||
|
Square: "方形",
|
||||||
|
WallMode: "墙体模式",
|
||||||
|
WallNone: "无墙体",
|
||||||
|
Wall: "墙体",
|
||||||
|
Volume: "体积水",
|
||||||
|
WallOpacity: "墙体透明度",
|
||||||
|
ScreenRefraction: "屏幕空间折射",
|
||||||
|
Size: "尺寸",
|
||||||
|
Diameter: "直径",
|
||||||
|
Width: "宽度",
|
||||||
|
Depth: "深度",
|
||||||
|
Height: "高度",
|
||||||
|
Light: "光照",
|
||||||
|
Direction: "方向",
|
||||||
|
Color: "颜色",
|
||||||
|
VolumeColor: "体积水颜色",
|
||||||
|
SurfaceColor: "水面颜色",
|
||||||
|
Disturbance: "水波",
|
||||||
|
Enabled: "启用",
|
||||||
|
Mode: "模式",
|
||||||
|
ModeDrag: "拖动",
|
||||||
|
ModeUniform: "均匀",
|
||||||
|
DropsPerStep: "每步水滴数",
|
||||||
|
RadiusMin: "半径最小",
|
||||||
|
RadiusMax: "半径最大",
|
||||||
|
StrengthMin: "强度最小",
|
||||||
|
StrengthMax: "强度最大",
|
||||||
|
TravelRadius: "活动半径",
|
||||||
|
DriftSpeed: "漂移速度",
|
||||||
|
Jitter: "抖动强度",
|
||||||
|
Spread: "扩散范围",
|
||||||
|
Refraction: "折射与波纹",
|
||||||
|
SurfaceTransmittance: "透视强度",
|
||||||
|
NormalStrength: "法线强度",
|
||||||
|
RefractionStrength: "折射强度",
|
||||||
|
Texture: "贴图",
|
||||||
|
WallTexture: "墙体纹理",
|
||||||
|
Quality: "模拟精度",
|
||||||
|
SimulationSize: "模拟尺寸",
|
||||||
|
CausticsSize: "焦散尺寸",
|
||||||
|
WaterSegments: "水面分段",
|
||||||
|
WallSegments: "墙体分段",
|
||||||
|
},
|
||||||
"3D Tiles": "3D Tiles",
|
"3D Tiles": "3D Tiles",
|
||||||
tiles: {
|
tiles: {
|
||||||
"Color mode": "颜色模式",
|
"Color mode": "颜色模式",
|
||||||
@@ -596,6 +842,7 @@ export default {
|
|||||||
Open: "开启",
|
Open: "开启",
|
||||||
Close: "关闭",
|
Close: "关闭",
|
||||||
Enable: "启用",
|
Enable: "启用",
|
||||||
|
Enabled: "启用",
|
||||||
Minimum: "最小值",
|
Minimum: "最小值",
|
||||||
Maximum: "最大值",
|
Maximum: "最大值",
|
||||||
Width: "宽度",
|
Width: "宽度",
|
||||||
@@ -606,6 +853,7 @@ export default {
|
|||||||
'Query failed': "查询失败",
|
'Query failed': "查询失败",
|
||||||
'Related document': "相关文档",
|
'Related document': "相关文档",
|
||||||
Copy: "复制",
|
Copy: "复制",
|
||||||
|
Finish: "完成",
|
||||||
Focus: '聚焦',
|
Focus: '聚焦',
|
||||||
Support: '支持',
|
Support: '支持',
|
||||||
Upload: '上传',
|
Upload: '上传',
|
||||||
@@ -1081,6 +1329,24 @@ export default {
|
|||||||
// 其他
|
// 其他
|
||||||
'Dragon':"龙",
|
'Dragon':"龙",
|
||||||
},
|
},
|
||||||
|
"Expansion": "扩展",
|
||||||
|
expansion: {
|
||||||
|
"Heat map": "热力图",
|
||||||
|
"Path": "路径",
|
||||||
|
"Flat heatmap": "平面热力图",
|
||||||
|
"Elevation heatmap": "高程热力图",
|
||||||
|
"Flow path": "流动路径",
|
||||||
|
"Flowing light path": "流光路径",
|
||||||
|
"Tube path": "管道路径",
|
||||||
|
"UI Panel": "UI面板",
|
||||||
|
"Water": "水体",
|
||||||
|
"Circular Water Surface": "圆形水面",
|
||||||
|
"Cylinder Water Pool": "圆柱水池",
|
||||||
|
"Cylinder Water Pool Volume": "圆柱水池体",
|
||||||
|
"Square Water Surface": "方形水面",
|
||||||
|
"Square Water Pool": "方形水池",
|
||||||
|
"Square Water Pool Volume": "方形水池体",
|
||||||
|
},
|
||||||
"Light": '灯光',
|
"Light": '灯光',
|
||||||
"Camera": '相机',
|
"Camera": '相机',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ async function main() {
|
|||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|
||||||
if (import.meta.env.PROD){
|
if (import.meta.env.PROD){
|
||||||
DisableDevtool({
|
// DisableDevtool({
|
||||||
clearLog:true
|
// md5: "32981a13284db7a021131df49e6cd203",
|
||||||
});
|
// clearLog: true,
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { store } from "@/store";
|
||||||
|
|
||||||
|
interface IPathDrawingState {
|
||||||
|
active: boolean;
|
||||||
|
template: Record<string, any> | null;
|
||||||
|
submit: ((payload: IPathDrawingResult) => void) | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPathDrawingResult {
|
||||||
|
worldPoints: Array<{ x: number; y: number; z: number }>;
|
||||||
|
origin: { x: number; y: number; z: number };
|
||||||
|
options: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPathDrawingRequest {
|
||||||
|
template?: Record<string, any> | null;
|
||||||
|
submit?: (payload: IPathDrawingResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPathDrawingRequest(payload: Record<string, any> | IPathDrawingRequest) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(payload, "template") || Object.prototype.hasOwnProperty.call(payload, "submit");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePathDrawingStore = defineStore({
|
||||||
|
id: "pathDrawing",
|
||||||
|
state: (): IPathDrawingState => ({
|
||||||
|
active: false,
|
||||||
|
template: null,
|
||||||
|
submit: null,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
isActive: state => state.active,
|
||||||
|
getTemplate: state => state.template,
|
||||||
|
getSubmit: state => state.submit,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
start(payload: Record<string, any> | IPathDrawingRequest) {
|
||||||
|
const request = isPathDrawingRequest(payload) ? payload : { template: payload };
|
||||||
|
|
||||||
|
this.template = request.template ? JSON.parse(JSON.stringify(request.template)) : null;
|
||||||
|
this.submit = typeof request.submit === "function" ? request.submit : null;
|
||||||
|
this.active = true;
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.active = false;
|
||||||
|
this.template = null;
|
||||||
|
this.submit = null;
|
||||||
|
},
|
||||||
|
finish() {
|
||||||
|
this.active = false;
|
||||||
|
this.template = null;
|
||||||
|
this.submit = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function usePathDrawingStoreWithOut() {
|
||||||
|
return usePathDrawingStore(store);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { t } from "@/language";
|
||||||
|
|
||||||
|
// Blend mode options for post-processing blendFunction config.
|
||||||
|
const BLEND_FUNCTION_KEYS = [
|
||||||
|
"NORMAL",
|
||||||
|
"ADD",
|
||||||
|
"ALPHA",
|
||||||
|
"AVERAGE",
|
||||||
|
"COLOR",
|
||||||
|
"COLOR_BURN",
|
||||||
|
"COLOR_DODGE",
|
||||||
|
"DARKEN",
|
||||||
|
"DIFFERENCE",
|
||||||
|
"DIVIDE",
|
||||||
|
"DST",
|
||||||
|
"EXCLUSION",
|
||||||
|
"HARD_LIGHT",
|
||||||
|
"HARD_MIX",
|
||||||
|
"HUE",
|
||||||
|
"INVERT",
|
||||||
|
"INVERT_RGB",
|
||||||
|
"LIGHTEN",
|
||||||
|
"LINEAR_BURN",
|
||||||
|
"LINEAR_DODGE",
|
||||||
|
"LINEAR_LIGHT",
|
||||||
|
"LUMINOSITY",
|
||||||
|
"MULTIPLY",
|
||||||
|
"NEGATION",
|
||||||
|
"OVERLAY",
|
||||||
|
"PIN_LIGHT",
|
||||||
|
"REFLECT",
|
||||||
|
"SATURATION",
|
||||||
|
"SCREEN",
|
||||||
|
"SET",
|
||||||
|
"SOFT_LIGHT",
|
||||||
|
"SRC",
|
||||||
|
"SUBTRACT",
|
||||||
|
"VIVID_LIGHT",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const blendFunctionOptions = BLEND_FUNCTION_KEYS.map(k => ({ label: k, value: k }));
|
||||||
|
|
||||||
|
// Glitch mode options.
|
||||||
|
export const glitchModeOptions = [
|
||||||
|
{ label: t("layout.sider.postProcessing.Sporadic"), value: "SPORADIC" },
|
||||||
|
{ label: t("layout.sider.postProcessing.Constant Mild"), value: "CONSTANT_MILD" },
|
||||||
|
{ label: t("layout.sider.postProcessing.Constant Wild"), value: "CONSTANT_WILD" },
|
||||||
|
];
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { injectWasm } from "@/utils/wasm/inject";
|
|
||||||
|
|
||||||
export function clearBuffer() {
|
|
||||||
injectWasm({ wasmUrl: "/wasm/Astral3DEditor.wasm" }).then(() => {
|
|
||||||
// 加载完wasm后自动注册了清除缓存的函数调用
|
|
||||||
window.clearBuffer();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,659 @@
|
|||||||
|
<template>
|
||||||
|
<div id="extra-resource-expansion" class="flex flex-col">
|
||||||
|
<div class="mb-3 flex flex-wrap items-center gap-2">
|
||||||
|
<n-button
|
||||||
|
v-for="subCategory in subCategories"
|
||||||
|
:key="subCategory.key"
|
||||||
|
strong
|
||||||
|
secondary
|
||||||
|
round
|
||||||
|
:type="activeSubCategory === subCategory.key ? 'primary' : 'default'"
|
||||||
|
@click="selectSubCategory(subCategory.key)"
|
||||||
|
>
|
||||||
|
{{ subCategory.name }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<div class="grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-3">
|
||||||
|
<n-card
|
||||||
|
size="small"
|
||||||
|
hoverable
|
||||||
|
v-for="item in filteredList"
|
||||||
|
:key="item.key"
|
||||||
|
@contextmenu.prevent="handlePreview(item)"
|
||||||
|
@dblclick="addToScene(item)"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="dragStart(item)"
|
||||||
|
@dragend="dragEnd"
|
||||||
|
>
|
||||||
|
<template #cover>
|
||||||
|
<img :src="item.image" :alt="item.key" draggable="false" class="w-full object-fill" />
|
||||||
|
</template>
|
||||||
|
<n-tooltip placement="bottom" trigger="hover">
|
||||||
|
<template #trigger>{{ getItemName(item) }}</template>
|
||||||
|
<span>{{ getItemName(item) }}</span>
|
||||||
|
</n-tooltip>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, inject, computed, nextTick } from "vue";
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
import { Box3, Vector3, type Object3D } from "three";
|
||||||
|
import { cpt } from "@/language";
|
||||||
|
import { useDragStore } from "@/store/modules/drag";
|
||||||
|
import { usePathDrawingStore } from "@/store/modules/pathDrawing";
|
||||||
|
import { screenToWorld } from "@/utils/common/scenes";
|
||||||
|
import { App, AddObjectCommand, Heatmap, Path, UIPanel, Water, type Preview } from "@astral3d/engine";
|
||||||
|
|
||||||
|
interface ExpansionItem {
|
||||||
|
key: string;
|
||||||
|
image: string;
|
||||||
|
name: any;
|
||||||
|
options: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchText = inject("searchText") as Ref<string>;
|
||||||
|
const previewInfo = inject("previewInfo") as any;
|
||||||
|
const previewRef = inject("previewRef") as any;
|
||||||
|
const pathDrawingStore = usePathDrawingStore();
|
||||||
|
|
||||||
|
const activeSubCategory = ref("heatmap");
|
||||||
|
const subCategories = ref([
|
||||||
|
{key: "heatmap", name: cpt("extra.resource.expansion.Heat map")},
|
||||||
|
{key: "path", name: cpt("extra.resource.expansion.Path")},
|
||||||
|
{key: "uipanel", name: cpt("extra.resource.expansion.UI Panel")},
|
||||||
|
{key: "water", name: cpt("extra.resource.expansion.Water")},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pathPreviewPoints = [
|
||||||
|
{x: -3.5, y: 0, z: -1.8},
|
||||||
|
{x: -1.8, y: 0, z: 1.2},
|
||||||
|
{x: 0.4, y: 0, z: -0.8},
|
||||||
|
{x: 2.4, y: 0, z: 1.6},
|
||||||
|
{x: 3.6, y: 0, z: -0.4},
|
||||||
|
];
|
||||||
|
const pathPreviewPoints3D = [
|
||||||
|
{x: -3.2, y: 0.2, z: -1.4},
|
||||||
|
{x: -1.6, y: 1.1, z: 1.0},
|
||||||
|
{x: 0.3, y: 0.4, z: -0.6},
|
||||||
|
{x: 2.1, y: 1.3, z: 1.4},
|
||||||
|
{x: 3.1, y: 0.5, z: -0.3},
|
||||||
|
];
|
||||||
|
|
||||||
|
const allList: Record<string, ExpansionItem[]> = {
|
||||||
|
heatmap: [
|
||||||
|
{
|
||||||
|
key: "flatHeatmap",
|
||||||
|
image: "/static/images/resource/expansion/heatmap/flatHeatmap.jpg",
|
||||||
|
name: cpt("extra.resource.expansion.Flat heatmap"),
|
||||||
|
options: {
|
||||||
|
mode: "flat",
|
||||||
|
heatmap: {
|
||||||
|
maxOpacity: 0.8,
|
||||||
|
minOpacity: 0,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
max: 5,
|
||||||
|
min: 0,
|
||||||
|
points: [
|
||||||
|
{x: 0, y: 0, value: 5, radius: 1.6},
|
||||||
|
{x: 0.9, y: -0.5, value: 4.4, radius: 1.2},
|
||||||
|
{x: -0.8, y: 0.9, value: 4.0, radius: 1.1},
|
||||||
|
{x: 1.6, y: 0.8, value: 3.6, radius: 0.9},
|
||||||
|
{x: -1.7, y: -0.9, value: 3.2, radius: 0.9},
|
||||||
|
{x: 0.2, y: 1.8, value: 2.8, radius: 0.8},
|
||||||
|
{x: -0.4, y: -2.1, value: 4.6, radius: 0.8},
|
||||||
|
{x: 2.6, y: -1.9, value: 2.9, radius: 0.9},
|
||||||
|
{x: -2.4, y: 2.1, value: 3.7, radius: 0.9},
|
||||||
|
{x: 3.2, y: 2.9, value: 3.8, radius: 1.1},
|
||||||
|
{x: 4.1, y: 2.1, value: 3.0, radius: 0.9},
|
||||||
|
{x: 2.4, y: 3.9, value: 2.4, radius: 0.8},
|
||||||
|
{x: 4.5, y: 3.4, value: 3.8, radius: 0.7},
|
||||||
|
{x: -3.4, y: -2.7, value: 3.1, radius: 0.9},
|
||||||
|
{x: -4.2, y: -2.1, value: 2.3, radius: 0.8},
|
||||||
|
{x: -2.6, y: -3.9, value: 2.0, radius: 0.7},
|
||||||
|
{x: -4.6, y: -3.6, value: 1.4, radius: 0.6},
|
||||||
|
{x: -1.3, y: 3.1, value: 2.2, radius: 0.8},
|
||||||
|
{x: 1.5, y: 2.6, value: 2.7, radius: 0.8},
|
||||||
|
{x: 3.0, y: -3.7, value: 1.9, radius: 0.7},
|
||||||
|
{x: 4.2, y: -3.4, value: 4.4, radius: 0.6},
|
||||||
|
{x: -4.4, y: 1.4, value: 4.6, radius: 0.6},
|
||||||
|
{x: -3.7, y: 2.5, value: 2.1, radius: 0.7},
|
||||||
|
{x: -2.0, y: 4.4, value: 3.3, radius: 0.6},
|
||||||
|
{x: 0.0, y: 4.6, value: 1.2, radius: 0.6},
|
||||||
|
{x: 4.6, y: 0.2, value: 3.5, radius: 0.6},
|
||||||
|
{x: -4.8, y: 0.1, value: 4.0, radius: 0.5},
|
||||||
|
{x: 0.3, y: -4.5, value: 3.1, radius: 0.6},
|
||||||
|
{x: 1.8, y: -4.1, value: 1.4, radius: 0.6},
|
||||||
|
{x: -1.7, y: -4.2, value: 4.6, radius: 0.6},
|
||||||
|
{x: 3.6, y: -1.2, value: 2.5, radius: 0.8},
|
||||||
|
{x: -3.2, y: 0.6, value: 2.7, radius: 0.8},
|
||||||
|
{x: 0.0, y: -2.6, value: 4.3, radius: 0.8},
|
||||||
|
{x: -0.6, y: 2.2, value: 2.9, radius: 0.8},
|
||||||
|
{x: 2.4, y: 0.4, value: 3.1, radius: 0.9},
|
||||||
|
{x: -2.8, y: -0.6, value: 2.4, radius: 0.8},
|
||||||
|
{x: 1.0, y: 4.2, value: 4.6, radius: 0.6},
|
||||||
|
{x: -4.1, y: 4.1, value: 5.0, radius: 0.5},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "heightHeatmap",
|
||||||
|
image: "/static/images/resource/expansion/heatmap/heightHeatmap.jpg",
|
||||||
|
name: cpt("extra.resource.expansion.Elevation heatmap"),
|
||||||
|
options: {
|
||||||
|
mode: "height",
|
||||||
|
data: {
|
||||||
|
max: 5,
|
||||||
|
min: 0,
|
||||||
|
points: [
|
||||||
|
{x: 0, y: 0, value: 5, radius: 1.6},
|
||||||
|
{x: 0.9, y: -0.5, value: 4.4, radius: 1.2},
|
||||||
|
{x: -0.8, y: 0.9, value: 4.0, radius: 1.1},
|
||||||
|
{x: 1.6, y: 0.8, value: 3.6, radius: 0.9},
|
||||||
|
{x: -1.7, y: -0.9, value: 3.2, radius: 0.9},
|
||||||
|
{x: 0.2, y: 1.8, value: 2.8, radius: 0.8},
|
||||||
|
{x: -0.4, y: -2.1, value: 4.6, radius: 0.8},
|
||||||
|
{x: 2.6, y: -1.9, value: 2.9, radius: 0.9},
|
||||||
|
{x: -2.4, y: 2.1, value: 3.7, radius: 0.9},
|
||||||
|
{x: 3.2, y: 2.9, value: 3.8, radius: 1.1},
|
||||||
|
{x: 4.1, y: 2.1, value: 3.0, radius: 0.9},
|
||||||
|
{x: 2.4, y: 3.9, value: 2.4, radius: 0.8},
|
||||||
|
{x: 4.5, y: 3.4, value: 3.8, radius: 0.7},
|
||||||
|
{x: -3.4, y: -2.7, value: 3.1, radius: 0.9},
|
||||||
|
{x: -4.2, y: -2.1, value: 2.3, radius: 0.8},
|
||||||
|
{x: -2.6, y: -3.9, value: 2.0, radius: 0.7},
|
||||||
|
{x: -4.6, y: -3.6, value: 1.4, radius: 0.6},
|
||||||
|
{x: -1.3, y: 3.1, value: 2.2, radius: 0.8},
|
||||||
|
{x: 1.5, y: 2.6, value: 2.7, radius: 0.8},
|
||||||
|
{x: 3.0, y: -3.7, value: 1.9, radius: 0.7},
|
||||||
|
{x: 4.2, y: -3.4, value: 4.4, radius: 0.6},
|
||||||
|
{x: -4.4, y: 1.4, value: 4.6, radius: 0.6},
|
||||||
|
{x: -3.7, y: 2.5, value: 2.1, radius: 0.7},
|
||||||
|
{x: -2.0, y: 4.4, value: 3.3, radius: 0.6},
|
||||||
|
{x: 0.0, y: 4.6, value: 1.2, radius: 0.6},
|
||||||
|
{x: 4.6, y: 0.2, value: 3.5, radius: 0.6},
|
||||||
|
{x: -4.8, y: 0.1, value: 4.0, radius: 0.5},
|
||||||
|
{x: 0.3, y: -4.5, value: 3.1, radius: 0.6},
|
||||||
|
{x: 1.8, y: -4.1, value: 1.4, radius: 0.6},
|
||||||
|
{x: -1.7, y: -4.2, value: 4.6, radius: 0.6},
|
||||||
|
{x: 3.6, y: -1.2, value: 2.5, radius: 0.8},
|
||||||
|
{x: -3.2, y: 0.6, value: 2.7, radius: 0.8},
|
||||||
|
{x: 0.0, y: -2.6, value: 4.3, radius: 0.8},
|
||||||
|
{x: -0.6, y: 2.2, value: 2.9, radius: 0.8},
|
||||||
|
{x: 2.4, y: 0.4, value: 3.1, radius: 0.9},
|
||||||
|
{x: -2.8, y: -0.6, value: 2.4, radius: 0.8},
|
||||||
|
{x: 1.0, y: 4.2, value: 4.6, radius: 0.6},
|
||||||
|
{x: -4.1, y: 4.1, value: 5.0, radius: 0.5},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
path: [
|
||||||
|
{
|
||||||
|
key: "flowPath",
|
||||||
|
image: "/static/images/resource/expansion/path/flow.png",
|
||||||
|
name: cpt("extra.resource.expansion.Flow path"),
|
||||||
|
options: {
|
||||||
|
mode: "path",
|
||||||
|
cornerRadius: 1,
|
||||||
|
cornerSplit: 30,
|
||||||
|
path: {
|
||||||
|
width: 1,
|
||||||
|
arrow: false,
|
||||||
|
progress: 1,
|
||||||
|
side: "both",
|
||||||
|
},
|
||||||
|
flow: {
|
||||||
|
enabled: true,
|
||||||
|
speed: 0.35,
|
||||||
|
direction: [1, 0],
|
||||||
|
},
|
||||||
|
material: {
|
||||||
|
map: "/static/images/resource/expansion/path/flow.png",
|
||||||
|
},
|
||||||
|
points: pathPreviewPoints,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "flowingLightPath",
|
||||||
|
image: "/static/images/resource/expansion/path/light.png",
|
||||||
|
name: cpt("extra.resource.expansion.Flowing light path"),
|
||||||
|
options: {
|
||||||
|
mode: "path",
|
||||||
|
cornerRadius: 1,
|
||||||
|
cornerSplit: 30,
|
||||||
|
path: {
|
||||||
|
width: 1,
|
||||||
|
arrow: false,
|
||||||
|
progress: 1,
|
||||||
|
side: "both",
|
||||||
|
},
|
||||||
|
flow: {
|
||||||
|
enabled: true,
|
||||||
|
speed: 0.5,
|
||||||
|
direction: [1, 0],
|
||||||
|
},
|
||||||
|
material: {
|
||||||
|
map: "/static/images/resource/expansion/path/light.png",
|
||||||
|
},
|
||||||
|
points: pathPreviewPoints,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tubePath",
|
||||||
|
image: "/static/images/resource/expansion/path/diffuse.jpg",
|
||||||
|
name: cpt("extra.resource.expansion.Tube path"),
|
||||||
|
options: {
|
||||||
|
mode: "tube",
|
||||||
|
cornerRadius: 0.15,
|
||||||
|
cornerSplit: 6,
|
||||||
|
tube: {
|
||||||
|
radius: 0.18,
|
||||||
|
radialSegments: 12,
|
||||||
|
progress: 1,
|
||||||
|
startRad: 0,
|
||||||
|
},
|
||||||
|
material: {
|
||||||
|
color: "#4c90f5",
|
||||||
|
map: "/static/images/resource/expansion/path/diffuse.jpg",
|
||||||
|
transparent: true,
|
||||||
|
repeat: [1, 1],
|
||||||
|
},
|
||||||
|
points: pathPreviewPoints3D,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
uipanel: [
|
||||||
|
{
|
||||||
|
key: "uiPanel",
|
||||||
|
image: "/static/images/resource/expansion/uipanel/uipanel.svg",
|
||||||
|
name: cpt("extra.resource.expansion.UI Panel"),
|
||||||
|
options: {
|
||||||
|
name: "UIPanel",
|
||||||
|
type: "block",
|
||||||
|
props: {
|
||||||
|
width: 1.6,
|
||||||
|
height: 0.9,
|
||||||
|
padding: 0.04,
|
||||||
|
backgroundColor: "#151515",
|
||||||
|
backgroundOpacity: 0.9,
|
||||||
|
borderRadius: 0.05,
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "block",
|
||||||
|
name: "Header",
|
||||||
|
props: {
|
||||||
|
width: 1.5,
|
||||||
|
height: 0.18,
|
||||||
|
backgroundColor: "#00b6a4",
|
||||||
|
backgroundOpacity: 0.95,
|
||||||
|
borderRadius: 0.04,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "HeaderTitle",
|
||||||
|
props: {
|
||||||
|
textContent: "UIPanel",
|
||||||
|
fontSize: 0.07,
|
||||||
|
color: "#ffffff",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "block",
|
||||||
|
name: "Body",
|
||||||
|
props: {
|
||||||
|
width: 1.5,
|
||||||
|
height: 0.5,
|
||||||
|
padding: 0.03,
|
||||||
|
backgroundColor: "#007e82",
|
||||||
|
backgroundOpacity: 0.9,
|
||||||
|
borderRadius: 0.04,
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "BodyTitle",
|
||||||
|
props: {
|
||||||
|
textContent: "WYSIWYG",
|
||||||
|
fontSize: 0.06,
|
||||||
|
color: "#cfd8dc",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "BodySubTitle",
|
||||||
|
props: {
|
||||||
|
textContent: "Drag nodes to layout",
|
||||||
|
fontSize: 0.045,
|
||||||
|
color: "#9aa4ad",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "block",
|
||||||
|
name: "Footer",
|
||||||
|
props: {
|
||||||
|
width: 1.5,
|
||||||
|
height: 0.12,
|
||||||
|
backgroundColor: "#101010",
|
||||||
|
backgroundOpacity: 0.9,
|
||||||
|
borderRadius: 0.04,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "FooterText",
|
||||||
|
props: {
|
||||||
|
textContent: "Astral3D UI",
|
||||||
|
fontSize: 0.04,
|
||||||
|
color: "#7bd5cd",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
water: [
|
||||||
|
{
|
||||||
|
key: "circularWaterSurface",
|
||||||
|
image: "/static/images/resource/expansion/water/circularWaterSurface.jpg",
|
||||||
|
name: cpt("extra.resource.expansion.Circular Water Surface"),
|
||||||
|
options: {
|
||||||
|
sky: window.viewer?.scene.environment,
|
||||||
|
name: "CircularWaterSurface",
|
||||||
|
type: "cylinder",
|
||||||
|
light: [0.7, 1, -0.3],
|
||||||
|
diameter: 5,
|
||||||
|
height: 5,
|
||||||
|
wallMode: "none",
|
||||||
|
wallOpacity: 0,
|
||||||
|
useSceneRefraction: 1,
|
||||||
|
surfaceTransmittance: 0.6,
|
||||||
|
normalStrength: 0.5,
|
||||||
|
refractionStrength: 0.035,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cylinderWaterPool",
|
||||||
|
image: "/static/images/resource/expansion/water/cylinderWaterPool.jpg",
|
||||||
|
name: cpt("extra.resource.expansion.Cylinder Water Pool"),
|
||||||
|
options: {
|
||||||
|
sky: window.viewer?.scene.environment,
|
||||||
|
name: "CylinderWaterPool",
|
||||||
|
type: "cylinder",
|
||||||
|
light: [0.7, 1, -0.3],
|
||||||
|
diameter: 5,
|
||||||
|
height: 5,
|
||||||
|
wallMode: "wall",
|
||||||
|
wallOpacity: 1,
|
||||||
|
useSceneRefraction: 1,
|
||||||
|
surfaceTransmittance: 0.6,
|
||||||
|
normalStrength: 0.5,
|
||||||
|
refractionStrength: 0.035,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cylinderWaterPoolVolume",
|
||||||
|
image: "/static/images/resource/expansion/water/cylinderWaterPoolVolume.jpg",
|
||||||
|
name: cpt("extra.resource.expansion.Cylinder Water Pool Volume"),
|
||||||
|
options: {
|
||||||
|
sky: window.viewer?.scene.environment,
|
||||||
|
name: "CylinderWaterPoolVolume",
|
||||||
|
type: "cylinder",
|
||||||
|
light: [0.7, 1, -0.3],
|
||||||
|
diameter: 5,
|
||||||
|
height: 5,
|
||||||
|
wallMode: "volume",
|
||||||
|
wallOpacity: 1,
|
||||||
|
useSceneRefraction: 1,
|
||||||
|
surfaceTransmittance: 0.6,
|
||||||
|
normalStrength: 0.5,
|
||||||
|
refractionStrength: 0.035,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "squareWaterSurface",
|
||||||
|
image: "/static/images/resource/expansion/water/squareWaterSurface.jpg",
|
||||||
|
name: cpt("extra.resource.expansion.Square Water Surface"),
|
||||||
|
options: {
|
||||||
|
sky: window.viewer?.scene.environment,
|
||||||
|
name: "SquareWaterSurface",
|
||||||
|
type: "square",
|
||||||
|
light: [0.7, 1, -0.3],
|
||||||
|
diameter: 5,
|
||||||
|
height: 5,
|
||||||
|
wallMode: "none",
|
||||||
|
wallOpacity: 0,
|
||||||
|
useSceneRefraction: 1,
|
||||||
|
surfaceTransmittance: 0.6,
|
||||||
|
normalStrength: 0.5,
|
||||||
|
refractionStrength: 0.035,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "squareWaterPool",
|
||||||
|
image: "/static/images/resource/expansion/water/squareWaterPool.jpg",
|
||||||
|
name: cpt("extra.resource.expansion.Square Water Pool"),
|
||||||
|
options: {
|
||||||
|
sky: window.viewer?.scene.environment,
|
||||||
|
name: "SquareWaterPool",
|
||||||
|
type: "square",
|
||||||
|
light: [0.7, 1, -0.3],
|
||||||
|
diameter: 5,
|
||||||
|
height: 5,
|
||||||
|
wallMode: "wall",
|
||||||
|
wallOpacity: 1,
|
||||||
|
useSceneRefraction: 1,
|
||||||
|
surfaceTransmittance: 0.6,
|
||||||
|
normalStrength: 0.5,
|
||||||
|
refractionStrength: 0.035,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "squareWaterPoolVolume",
|
||||||
|
image: "/static/images/resource/expansion/water/squareWaterPoolVolume.jpg",
|
||||||
|
name: cpt("extra.resource.expansion.Square Water Pool Volume"),
|
||||||
|
options: {
|
||||||
|
sky: window.viewer?.scene.environment,
|
||||||
|
name: "SquareWaterPoolVolume",
|
||||||
|
type: "square",
|
||||||
|
light: [0.7, 1, -0.3],
|
||||||
|
diameter: 5,
|
||||||
|
height: 5,
|
||||||
|
wallMode: "volume",
|
||||||
|
wallOpacity: 1,
|
||||||
|
useSceneRefraction: 1,
|
||||||
|
surfaceTransmittance: 0.6,
|
||||||
|
normalStrength: 0.5,
|
||||||
|
refractionStrength: 0.035,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragStore = useDragStore();
|
||||||
|
|
||||||
|
function getItemName(item: ExpansionItem): string {
|
||||||
|
return item.name?.value || item.name || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredList = computed(() => {
|
||||||
|
const key = activeSubCategory.value;
|
||||||
|
const list = allList[key] || [];
|
||||||
|
return list.filter(item => getItemName(item).toLowerCase().includes(searchText.value.toLowerCase()));
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectSubCategory(key: string) {
|
||||||
|
activeSubCategory.value = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPathDrawing(item: ExpansionItem) {
|
||||||
|
const options = JSON.parse(JSON.stringify(item.options || {}));
|
||||||
|
if (!options.name) {
|
||||||
|
options.name = getItemName(item);
|
||||||
|
}
|
||||||
|
if (pathDrawingStore.isActive) {
|
||||||
|
pathDrawingStore.cancel();
|
||||||
|
}
|
||||||
|
pathDrawingStore.start(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultAddPosition(): number[] | undefined {
|
||||||
|
const container = window.viewer?.container;
|
||||||
|
if (!container) return undefined;
|
||||||
|
|
||||||
|
const centerX = container.offsetWidth / 2;
|
||||||
|
const centerY = container.offsetHeight / 2;
|
||||||
|
const centerPosition = screenToWorld(centerX, centerY);
|
||||||
|
return centerPosition.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePreview(item: ExpansionItem) {
|
||||||
|
previewInfo.name = getItemName(item);
|
||||||
|
previewInfo.visible = true;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
previewRef.value?.getPreviewer().then((previewer: Preview) => {
|
||||||
|
if (!previewer) return;
|
||||||
|
|
||||||
|
let object3d: Object3D | null = null;
|
||||||
|
|
||||||
|
switch (activeSubCategory.value) {
|
||||||
|
case "heatmap":
|
||||||
|
object3d = new Heatmap(item.options);
|
||||||
|
break;
|
||||||
|
case "path":
|
||||||
|
object3d = new Path(item.options);
|
||||||
|
break;
|
||||||
|
case "uipanel":
|
||||||
|
object3d = new UIPanel(item.options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!object3d) return;
|
||||||
|
|
||||||
|
previewer.scene.add(object3d);
|
||||||
|
|
||||||
|
const box3 = new Box3();
|
||||||
|
if (activeSubCategory.value === "heatmap") {
|
||||||
|
box3.set(new Vector3(-5, 0, -5), new Vector3(5, 5, 5));
|
||||||
|
} else {
|
||||||
|
box3.setFromObject(object3d);
|
||||||
|
}
|
||||||
|
|
||||||
|
previewer.modules.controls.fitToBox(box3, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToScene(item: ExpansionItem, position?: number[]) {
|
||||||
|
const options = JSON.parse(JSON.stringify(item.options || {}));
|
||||||
|
if (!options.name) {
|
||||||
|
options.name = getItemName(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initPosition = position || getDefaultAddPosition();
|
||||||
|
if (initPosition) {
|
||||||
|
options.position = initPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (activeSubCategory.value) {
|
||||||
|
case "heatmap": {
|
||||||
|
const heatmap = new Heatmap(options);
|
||||||
|
App.execute(new AddObjectCommand(heatmap), `Add Heatmap: ${options.name}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "path": {
|
||||||
|
startPathDrawing(item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "uipanel": {
|
||||||
|
const uipanel = new UIPanel(options);
|
||||||
|
App.execute(new AddObjectCommand(uipanel), `Add UIPanel: ${options.name}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "water": {
|
||||||
|
Water.waterPool(options, options.position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragStart(item: ExpansionItem) {
|
||||||
|
dragStore.setData(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragEnd() {
|
||||||
|
if (dragStore.getActionTarget !== "addToScene" || dragStore.endArea !== "Scene") return;
|
||||||
|
|
||||||
|
if (activeSubCategory.value === "path") {
|
||||||
|
startPathDrawing(dragStore.getData);
|
||||||
|
dragStore.setActionTarget("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = screenToWorld(dragStore.endPosition.x, dragStore.endPosition.y).toArray();
|
||||||
|
addToScene(dragStore.getData, position);
|
||||||
|
|
||||||
|
dragStore.setActionTarget("");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
#extra-resource-expansion {
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.n-card {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
:deep(.n-card-cover) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-card__content) {
|
||||||
|
padding: 0.3rem 0;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
font-size: 0.65rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, markRaw, reactive, ref, provide, useTemplateRef} from "vue";
|
import {computed, markRaw, reactive, ref, provide, useTemplateRef} from "vue";
|
||||||
import {cpt} from "@/language";
|
import {cpt} from "@/language";
|
||||||
import {Buoy, CameraAction, HeatMap, LocationCompany, LocationHeart,ChoroplethMap, ModelAlt, Opacity} from "@vicons/carbon";
|
import {Buoy, CameraAction, HeatMap, LocationCompany, LocationHeart,ChoroplethMap, EdtLoop, ModelAlt, Opacity} from "@vicons/carbon";
|
||||||
import CommonPreview from "@/components/preview/CommonPreview.vue";
|
import CommonPreview from "@/components/preview/CommonPreview.vue";
|
||||||
import Models from "@/views/editor/components/extraPane/resource/builtin/Models.vue";
|
import Models from "@/views/editor/components/extraPane/resource/builtin/Models.vue";
|
||||||
import Materials from "@/views/editor/components/extraPane/resource/builtin/Materials.vue";
|
import Materials from "@/views/editor/components/extraPane/resource/builtin/Materials.vue";
|
||||||
@@ -37,6 +37,7 @@ import Particles from "@/views/editor/components/extraPane/resource/builtin/Part
|
|||||||
import Billboard from "@/views/editor/components/extraPane/resource/builtin/Billboard.vue";
|
import Billboard from "@/views/editor/components/extraPane/resource/builtin/Billboard.vue";
|
||||||
import HtmlPanel from "@/views/editor/components/extraPane/resource/builtin/HtmlPanel.vue";
|
import HtmlPanel from "@/views/editor/components/extraPane/resource/builtin/HtmlPanel.vue";
|
||||||
import Tiles from "@/views/editor/components/extraPane/resource/builtin/Tiles.vue";
|
import Tiles from "@/views/editor/components/extraPane/resource/builtin/Tiles.vue";
|
||||||
|
import Expansion from "@/views/editor/components/extraPane/resource/builtin/Expansion.vue";
|
||||||
import Lights from "@/views/editor/components/extraPane/resource/builtin/Lights.vue";
|
import Lights from "@/views/editor/components/extraPane/resource/builtin/Lights.vue";
|
||||||
import Cameras from "@/views/editor/components/extraPane/resource/builtin/Cameras.vue";
|
import Cameras from "@/views/editor/components/extraPane/resource/builtin/Cameras.vue";
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ const categories = ref([
|
|||||||
{ key: 'billboard', name: cpt("home.assets.Billboard"), icon: markRaw(LocationHeart), component: markRaw(Billboard) },
|
{ key: 'billboard', name: cpt("home.assets.Billboard"), icon: markRaw(LocationHeart), component: markRaw(Billboard) },
|
||||||
{ key: 'htmlPanel', name: cpt("extra.resource.Html panel"), icon: markRaw(LocationCompany), component: markRaw(HtmlPanel) },
|
{ key: 'htmlPanel', name: cpt("extra.resource.Html panel"), icon: markRaw(LocationCompany), component: markRaw(HtmlPanel) },
|
||||||
{ key: 'tiles', name: "3D Tiles", icon: markRaw(ChoroplethMap), component: markRaw(Tiles) },
|
{ key: 'tiles', name: "3D Tiles", icon: markRaw(ChoroplethMap), component: markRaw(Tiles) },
|
||||||
|
{ key: 'expansion', name: cpt("extra.resource.Expansion"), icon: markRaw(EdtLoop), component: markRaw(Expansion) },
|
||||||
{ key: 'lights', name: cpt("extra.resource.Light"), icon: markRaw(Buoy), component: markRaw(Lights) },
|
{ key: 'lights', name: cpt("extra.resource.Light"), icon: markRaw(Buoy), component: markRaw(Lights) },
|
||||||
{ key: 'cameras', name: cpt("extra.resource.Camera"), icon: markRaw(CameraAction), component: markRaw(Cameras) },
|
{ key: 'cameras', name: cpt("extra.resource.Camera"), icon: markRaw(CameraAction), component: markRaw(Cameras) },
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {ref, onMounted, nextTick,provide} from 'vue';
|
import {ref, onMounted, nextTick,provide} from 'vue';
|
||||||
import {Hooks,App,Package,defaultProjectInfo} from "@astral3d/engine";
|
import {Hooks,App,defaultProjectInfo} from "@astral3d/engine";
|
||||||
import * as Layout from './layouts';
|
import * as Layout from './layouts';
|
||||||
import {connectWebSocket} from "@/hooks/useWebSocket";
|
import {connectWebSocket} from "@/hooks/useWebSocket";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
@@ -82,8 +82,7 @@ function getScene(sceneInfo) {
|
|||||||
closable: false,
|
closable: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const p = new Package(window.viewer);
|
window.viewer.package.unpack({
|
||||||
p.unpack({
|
|
||||||
url: sceneInfo.zip,
|
url: sceneInfo.zip,
|
||||||
onSceneLoad: () => {
|
onSceneLoad: () => {
|
||||||
drawingInfo.value = App.project.getKey("drawing");
|
drawingInfo.value = App.project.getKey("drawing");
|
||||||
@@ -94,8 +93,6 @@ function getScene(sceneInfo) {
|
|||||||
Hooks.useDispatchSignal("sceneLoadComplete");
|
Hooks.useDispatchSignal("sceneLoadComplete");
|
||||||
|
|
||||||
notice.destroy();
|
notice.destroy();
|
||||||
|
|
||||||
p.dispose();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,20 +4,16 @@
|
|||||||
<SceneTree />
|
<SceneTree />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-tabs default-value="bim" animated type="line" justify-content="space-around" class="!h-60%"
|
<n-tabs default-value="cad" animated type="line" justify-content="space-around" class="!h-60%"
|
||||||
pane-wrapper-class="layout-assets-tab-pane-wrapper">
|
pane-wrapper-class="layout-assets-tab-pane-wrapper">
|
||||||
<n-tab-pane name="cad" tab="CAD" display-directive="show">
|
<n-tab-pane name="cad" tab="CAD" display-directive="show">
|
||||||
<CadLibrary />
|
<CadLibrary />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="bim" tab="BIM" display-directive="show">
|
|
||||||
<BIMLibrary />
|
|
||||||
</n-tab-pane>
|
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import BIMLibrary from "./assets/BIMLibrary.vue";
|
|
||||||
import SceneTree from "@/components/tree/SceneTree.vue";
|
import SceneTree from "@/components/tree/SceneTree.vue";
|
||||||
import CadLibrary from "./assets/CadLibrary.vue";
|
import CadLibrary from "./assets/CadLibrary.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ import {
|
|||||||
MagicWandFilled,
|
MagicWandFilled,
|
||||||
CloudSnow,
|
CloudSnow,
|
||||||
HeatMap,
|
HeatMap,
|
||||||
|
DataBase,
|
||||||
Opacity,
|
Opacity,
|
||||||
ImageReference,
|
ImageReference,
|
||||||
LocationHeart,
|
LocationHeart,
|
||||||
LocationCompany,
|
LocationCompany,
|
||||||
ChoroplethMap
|
ChoroplethMap,
|
||||||
|
DirectionLoopLeft,
|
||||||
|
Gui,
|
||||||
|
ChartWaterfall
|
||||||
} from "@vicons/carbon";
|
} from "@vicons/carbon";
|
||||||
|
|
||||||
import { t } from "@/language";
|
import { t } from "@/language";
|
||||||
@@ -38,6 +42,11 @@ import SidebarParticle from "./sidebar/SidebarParticle.vue";
|
|||||||
import SidebarBillboard from "./sidebar/SidebarBillboard.vue";
|
import SidebarBillboard from "./sidebar/SidebarBillboard.vue";
|
||||||
import SidebarHtmlPanel from "./sidebar/SidebarHtmlPanel.vue";
|
import SidebarHtmlPanel from "./sidebar/SidebarHtmlPanel.vue";
|
||||||
import Sidebar3DTiles from "./sidebar/Sidebar3DTiles.vue";
|
import Sidebar3DTiles from "./sidebar/Sidebar3DTiles.vue";
|
||||||
|
import SidebarHeatmap from "./sidebar/SidebarHeatmap.vue";
|
||||||
|
import SidebarPath from "./sidebar/SidebarPath.vue";
|
||||||
|
import SidebarUIPanel from "./sidebar/SidebarUIPanel.vue";
|
||||||
|
import SidebarWaterPool from "./sidebar/SidebarWaterPool.vue";
|
||||||
|
import SidebarDataComponent from "./sidebar/SidebarDataComponent.vue";
|
||||||
|
|
||||||
const tabsInstRef = ref<TabsInst | null>(null);
|
const tabsInstRef = ref<TabsInst | null>(null);
|
||||||
const tabs = ref<Array<any>>([]);
|
const tabs = ref<Array<any>>([]);
|
||||||
@@ -67,6 +76,7 @@ function setTabs(object){
|
|||||||
{ name: "geometry", icon: { text: 'Geometry',color:"#6287D1", component: markRaw(GroupObjects) }, component: markRaw(SidebarGeometry) },
|
{ name: "geometry", icon: { text: 'Geometry',color:"#6287D1", component: markRaw(GroupObjects) }, component: markRaw(SidebarGeometry) },
|
||||||
{ name: "material", icon: { text: 'Material',color:"#6287D1", component: markRaw(Opacity) }, component: markRaw(SidebarMaterial) },
|
{ name: "material", icon: { text: 'Material',color:"#6287D1", component: markRaw(Opacity) }, component: markRaw(SidebarMaterial) },
|
||||||
{ name: "animations", icon: { text: 'Animations',color:"#06AF88", component: markRaw(Draw) }, component: markRaw(SidebarAnimations) },
|
{ name: "animations", icon: { text: 'Animations',color:"#06AF88", component: markRaw(Draw) }, component: markRaw(SidebarAnimations) },
|
||||||
|
{ name: "dataComponent", icon: { text: 'Data component', color:"#0bbc14", component: markRaw(DataBase) }, component: markRaw(SidebarDataComponent) },
|
||||||
{ name: "script", icon: { text: 'Script',color:"#06AF88", component: markRaw(Script) }, component: markRaw(SidebarScript) },
|
{ name: "script", icon: { text: 'Script',color:"#06AF88", component: markRaw(Script) }, component: markRaw(SidebarScript) },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -96,6 +106,30 @@ function setTabs(object){
|
|||||||
|
|
||||||
current.value = '3DTiles';
|
current.value = '3DTiles';
|
||||||
break;
|
break;
|
||||||
|
case "Heatmap":
|
||||||
|
object3DTabs.push({ name: 'heatmap', icon: { text: 'Heatmap',color:"#A9575F", component: markRaw(HeatMap) }, component: markRaw(SidebarHeatmap) })
|
||||||
|
|
||||||
|
current.value = 'heatmap';
|
||||||
|
break;
|
||||||
|
case "Path":
|
||||||
|
object3DTabs.push({ name: 'path', icon: { text: 'Path',color:"#A9575F", component: markRaw(DirectionLoopLeft) }, component: markRaw(SidebarPath) })
|
||||||
|
|
||||||
|
current.value = 'path';
|
||||||
|
break;
|
||||||
|
case "WaterPool":
|
||||||
|
object3DTabs.push({ name: 'waterPool', icon: { text: 'Water pool',color:"#4aa3b5", component: markRaw(ChartWaterfall) }, component: markRaw(SidebarWaterPool) })
|
||||||
|
|
||||||
|
current.value = 'waterPool';
|
||||||
|
break;
|
||||||
|
case "UIPanel":
|
||||||
|
case "UIPanelBlock":
|
||||||
|
case "UIPanelText":
|
||||||
|
case "UIPanelInline":
|
||||||
|
case "UIPanelInlineBlock":
|
||||||
|
object3DTabs.push({ name: 'uipanel', icon: { text: 'UI panel',color:"#A9575F", component: markRaw(Gui) }, component: markRaw(SidebarUIPanel) })
|
||||||
|
|
||||||
|
current.value = 'uipanel';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +164,30 @@ function setTabs(object){
|
|||||||
current.value = 'object';
|
current.value = 'object';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (current.value === 'heatmap') {
|
||||||
|
if (!(object && object.type === 'Heatmap')){
|
||||||
|
current.value = 'object';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.value === 'path') {
|
||||||
|
if (!(object && object.type === 'Path')){
|
||||||
|
current.value = 'object';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.value === 'waterPool') {
|
||||||
|
if (!(object && object.type === 'WaterPool')){
|
||||||
|
current.value = 'object';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.value === 'uipanel') {
|
||||||
|
if (!(object && ["UIPanel", "UIPanelBlock", "UIPanelText", "UIPanelInline", "UIPanelInlineBlock"].includes(object.type))){
|
||||||
|
current.value = 'object';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
|
|||||||
@@ -1,339 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="bim-library" class="h-full flex flex-col">
|
|
||||||
<n-alert type="info" :show-icon="false" :bordered="false" class="mb-2 mx-2">
|
|
||||||
<n-button quaternary round type="primary" @click="showHistoryModal = true">
|
|
||||||
{{ t("layout.sider.History") }}
|
|
||||||
</n-button>
|
|
||||||
|
|
||||||
<n-button quaternary circle type="primary" @click="showBIMUpload = true">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon>
|
|
||||||
<CloudUpload/>
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</n-alert>
|
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto px-0.3rem">
|
|
||||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(80px,1fr))] gap-2">
|
|
||||||
<n-card size="small" hoverable v-for="item in objectList" :key="item.id" @dblclick="addToScene(item)"
|
|
||||||
draggable="true" @dragstart="dragStart($event, item)" @dragend="dragEnd">
|
|
||||||
<template #cover>
|
|
||||||
<n-spin :show="item.conversionStatus === 0">
|
|
||||||
<template #description>
|
|
||||||
正在轻量化...
|
|
||||||
</template>
|
|
||||||
<img :src="item.thumbnail ? item.thumbnail : '/static/images/placeholder/占位图.png'"
|
|
||||||
:alt="item.fileName" draggable="false">
|
|
||||||
<n-tag :color="{ color: '#F1C3CC', textColor: '#D03050' }" :bordered="false" size="small"
|
|
||||||
class="absolute top-33px w-full" v-if="item.conversionStatus === 2">
|
|
||||||
轻量化失败
|
|
||||||
<template #icon>
|
|
||||||
<n-icon>
|
|
||||||
<CloseCircleSharp/>
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
</n-tag>
|
|
||||||
</n-spin>
|
|
||||||
</template>
|
|
||||||
<n-tooltip placement="bottom" trigger="hover">
|
|
||||||
<template #trigger> {{ item.fileName }}</template>
|
|
||||||
<span> {{ item.fileName }} </span>
|
|
||||||
</n-tooltip>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center mt-0.5rem">
|
|
||||||
<n-pagination v-bind="paginationReactive"></n-pagination>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-modal v-model:show="showHistoryModal" class="!w-60vw" preset="dialog" display-directive="show"
|
|
||||||
:title="t('bim[\'BIM lightweight\']') + t('layout.sider.History')">
|
|
||||||
<n-data-table class="mt-20px" size="small" :loading="tableLoading" :columns="columns"
|
|
||||||
:data="objectList"></n-data-table>
|
|
||||||
<div class="flex justify-end mt-0.5rem">
|
|
||||||
<n-pagination v-bind="paginationReactive"></n-pagination>
|
|
||||||
</div>
|
|
||||||
</n-modal>
|
|
||||||
|
|
||||||
<!-- BIM文件上传 -->
|
|
||||||
<BimUploadDialog v-model:show="showBIMUpload" ref="uploadDialogRef"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {ref, reactive, onMounted, h, onBeforeUnmount} from "vue";
|
|
||||||
import type {DataTableColumns} from 'naive-ui';
|
|
||||||
import {NButton, NTag, NIcon} from 'naive-ui'
|
|
||||||
import {CloudUpload, Reload, CheckmarkCircle, CloseOutline, CloseCircleSharp} from '@vicons/ionicons5';
|
|
||||||
import {t} from "@/language";
|
|
||||||
import {useDragStore} from "@/store/modules/drag";
|
|
||||||
import {fetchGetBim2GltfList} from "@/http/api/bim";
|
|
||||||
import {Bim2GltfWsData, WebSocketMessage} from "~/network";
|
|
||||||
import {Loader,Hooks} from "@astral3d/engine";
|
|
||||||
import {filterSize} from "@/utils/common/file";
|
|
||||||
import {onWebSocket, offWebSocket} from "@/hooks/useWebSocket";
|
|
||||||
import {useWebsocketStore} from "@/store/modules/websocket";
|
|
||||||
import {dateTimeFormat} from "@/utils/common/dateTime";
|
|
||||||
import BimUploadDialog from "./bimLibrary/BimUploadDialog.vue";
|
|
||||||
|
|
||||||
const websocketStore = useWebsocketStore();
|
|
||||||
|
|
||||||
let objectList = ref<IBIMData[]>([]);
|
|
||||||
const showHistoryModal = ref(false);
|
|
||||||
const tableLoading = ref(false);
|
|
||||||
const columns: DataTableColumns<IBIMData> = [
|
|
||||||
{
|
|
||||||
title: '文件名',
|
|
||||||
key: 'fileName'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'bim文件体积',
|
|
||||||
key: 'bimFileSize',
|
|
||||||
render(row) {
|
|
||||||
return filterSize(row.bimFileSize);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'gltf文件体积',
|
|
||||||
key: 'gltfFileSize',
|
|
||||||
render(row) {
|
|
||||||
return filterSize(row.gltfFileSize);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '转换时长',
|
|
||||||
key: 'conversionDuration',
|
|
||||||
render(row) {
|
|
||||||
return row.conversionDuration.toFixed(2) + "s";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
key: 'conversionStatus',
|
|
||||||
render(row) {
|
|
||||||
return h(
|
|
||||||
NTag,
|
|
||||||
{
|
|
||||||
bordered: false,
|
|
||||||
type: row.conversionStatus === 0 ? "warning" : row.conversionStatus === 1 ? "success" : "error",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => row.conversionStatus === 0 ? "转换中" : row.conversionStatus === 1 ? "成功" : "失败",
|
|
||||||
icon: () => h(
|
|
||||||
NIcon, {
|
|
||||||
component: row.conversionStatus === 0 ? Reload : row.conversionStatus === 1 ? CheckmarkCircle : CloseOutline
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'actions',
|
|
||||||
render(row) {
|
|
||||||
if (row.conversionStatus !== 1) return "";
|
|
||||||
return h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
size: 'small',
|
|
||||||
onClick: () => addToScene(row)
|
|
||||||
},
|
|
||||||
{default: () => t("other.Load")}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
let paginationReactive = reactive({
|
|
||||||
page: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
pageCount: 1,
|
|
||||||
"on-update:page": (page: number) => {
|
|
||||||
paginationReactive.page = page;
|
|
||||||
getBim2GltfList();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const showBIMUpload = ref(false);
|
|
||||||
const uploadDialogRef = ref();
|
|
||||||
|
|
||||||
// 获取BIM转换列表
|
|
||||||
async function getBim2GltfList() {
|
|
||||||
const res = await fetchGetBim2GltfList({
|
|
||||||
offset: (paginationReactive.page - 1) * paginationReactive.pageSize,
|
|
||||||
limit: paginationReactive.pageSize
|
|
||||||
});
|
|
||||||
|
|
||||||
objectList.value = res.data?.items || [];
|
|
||||||
paginationReactive.pageCount = res.data?.pages || 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addToScene(item) {
|
|
||||||
showHistoryModal.value = false;
|
|
||||||
|
|
||||||
let notice = window.$notification.info({
|
|
||||||
title: window.$t("scene['Get the scene data']") + "...",
|
|
||||||
content: window.$t("other.Loading") + "...",
|
|
||||||
closable: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 从gltfFilePath字段去服务器获取gltf文件
|
|
||||||
fetch(`file/static/${item.gltfFilePath}`)
|
|
||||||
.then(res => res.blob())
|
|
||||||
.then(data => {
|
|
||||||
const file = new File([data as Blob], `${item.fileName}.glb`, {type: 'model/gltf-binary'});
|
|
||||||
|
|
||||||
notice.content = window.$t("scene['Parsing to editor']");
|
|
||||||
|
|
||||||
Loader.loadFiles([file], undefined).finally(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
notice.destroy();
|
|
||||||
Hooks.useDispatchSignal("sceneGraphChanged");
|
|
||||||
}, 800)
|
|
||||||
Hooks.useDispatchSignal("sceneGraphChanged");
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
notice.content = window.$t("scene['Failed to get scene data']");
|
|
||||||
setTimeout(() => {
|
|
||||||
notice.destroy();
|
|
||||||
}, 500)
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始拖拽事件
|
|
||||||
const dragStore = useDragStore();
|
|
||||||
|
|
||||||
function dragStart(_, item) {
|
|
||||||
if (item.conversionStatus !== 1) return;
|
|
||||||
dragStore.setData(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragEnd() {
|
|
||||||
if (dragStore.getActionTarget !== "addToScene") return;
|
|
||||||
|
|
||||||
const data = dragStore.getData
|
|
||||||
if (data.conversionStatus !== 1) return;
|
|
||||||
|
|
||||||
addToScene(data);
|
|
||||||
|
|
||||||
dragStore.setActionTarget("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// websocket监听 bim2gltf 消息
|
|
||||||
function Bim2GltfWsHandle(data: WebSocketMessage<Bim2GltfWsData>) {
|
|
||||||
if (data.type === "bim2gltf") {
|
|
||||||
// 判断订阅者,以确定是自己还是别人轻量化的结果
|
|
||||||
if (data.subscriber === websocketStore.uname) {
|
|
||||||
let wsNotice = uploadDialogRef.value?.getNotice();
|
|
||||||
// 如果是自己的轻量化结果,使用用户登录结果作为uname,则用户可能关闭页面再打开时接收到消息,应继续显示
|
|
||||||
if (!wsNotice) {
|
|
||||||
wsNotice = window.$notification.info({
|
|
||||||
title: t("bim['BIM lightweight']"),
|
|
||||||
content: "",
|
|
||||||
closable: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.data.conversionStatus === "progress") {
|
|
||||||
wsNotice.content = t("bim['BIM lightweight is in progress']") + "...";
|
|
||||||
} else if (data.data.conversionStatus === "completed") {
|
|
||||||
wsNotice.content = `${t("bim['BIM lightweight completed']")},${t("bim.In")} ${data.data.item.conversionDuration} ${t("bim.seconds")}`;
|
|
||||||
setTimeout(() => {
|
|
||||||
wsNotice?.destroy();
|
|
||||||
window.$dialog.info({
|
|
||||||
title: t("bim['BIM lightweight completed']"),
|
|
||||||
content: t("bim['Whether to load the BIM model into the scene?']"),
|
|
||||||
positiveText: window.$t('other.Load'),
|
|
||||||
negativeText: window.$t('other.Cancel'),
|
|
||||||
onPositiveClick: () => {
|
|
||||||
addToScene(data.data.item);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, 800)
|
|
||||||
getBim2GltfList();
|
|
||||||
} else if (data.data.conversionStatus === "failed") {
|
|
||||||
wsNotice.content = t("bim['BIM lightweight failed']");
|
|
||||||
setTimeout(() => {
|
|
||||||
wsNotice?.destroy();
|
|
||||||
}, 1500)
|
|
||||||
getBim2GltfList();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (data.data.conversionStatus !== "completed") return;
|
|
||||||
|
|
||||||
// 别人的轻量化结果
|
|
||||||
const n = window.$notification.info({
|
|
||||||
title: t("bim['BIM lightweight']"),
|
|
||||||
content: t("bim['New lightweight BIM model received, do you want to view it?']"),
|
|
||||||
duration: 5000,
|
|
||||||
closable: true,
|
|
||||||
meta: dateTimeFormat("yyyy-MM-dd HH:mm:ss"),
|
|
||||||
action: () =>
|
|
||||||
h(NButton, {
|
|
||||||
text: true,
|
|
||||||
type: 'primary',
|
|
||||||
onClick: () => {
|
|
||||||
addToScene(data.data.item);
|
|
||||||
n.destroy();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => t("other.Load")
|
|
||||||
}
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getBim2GltfList();
|
|
||||||
|
|
||||||
// 注册websocket消息监听
|
|
||||||
onWebSocket(Bim2GltfWsHandle);
|
|
||||||
})
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
offWebSocket(Bim2GltfWsHandle)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
#bim-library {
|
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
.n-alert {
|
|
||||||
:deep(.n-alert-body) {
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-card {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
:deep(.n-card-cover) {
|
|
||||||
img {
|
|
||||||
height: 89px;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-card__content) {
|
|
||||||
padding: 3px 5px;
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-modal :show="show" @update:show="(b) => emits('update:show',b)" class="!w-500px" preset="card" :title="t('bim.BIM lightweight')">
|
|
||||||
<n-form label-placement="left" :model="BIMModel" :rules="BIMRules"
|
|
||||||
label-width="100px" label-align="right" ref="formRef"
|
|
||||||
require-mark-placement="right-hanging">
|
|
||||||
<!-- <n-form-item :label="t('bim[\'File name\']')" path="fileName">-->
|
|
||||||
<!-- <n-input v-model:value="BIMModel.fileName"-->
|
|
||||||
<!-- :placeholder="t('bim[\'Please enter the BIM file name\']')"/>-->
|
|
||||||
<!-- </n-form-item>-->
|
|
||||||
<n-form-item :label="t('bim.Thumbnail')">
|
|
||||||
<n-upload :default-upload="false" list-type="image-card" :max="1"
|
|
||||||
@change="thumbnailChange"/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="t('bim[\'BIM file\']')" path="bimFile">
|
|
||||||
<n-upload ref="uploadBIMRef" :default-upload="false" directory-dnd :max="1"
|
|
||||||
:accept="'.' + NEED_CONVERT_BIM_MODEL.join(',.')" @change="bimChange">
|
|
||||||
<n-upload-dragger>
|
|
||||||
<div>
|
|
||||||
<n-icon size="48" :depth="3">
|
|
||||||
<ArchiveOutline/>
|
|
||||||
</n-icon>
|
|
||||||
</div>
|
|
||||||
<n-text style="font-size: 14px">
|
|
||||||
{{
|
|
||||||
t("bim['Click or drag the file to this area.Supported formats are:']") + ` ${NEED_CONVERT_BIM_MODEL.join("、")}`
|
|
||||||
}}
|
|
||||||
</n-text>
|
|
||||||
</n-upload-dragger>
|
|
||||||
</n-upload>
|
|
||||||
</n-form-item>
|
|
||||||
<n-collapse>
|
|
||||||
<template #arrow>
|
|
||||||
<n-icon>
|
|
||||||
<CaretForwardOutline />
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<n-collapse-item :title="t('bim[\'Conversion configuration\']')">
|
|
||||||
<!-- 极致轻量化 -->
|
|
||||||
<n-form-item :label="t('bim[\'Extreme lightweight\']')">
|
|
||||||
<n-switch v-model:value="BIMModel.options.Optimize" @change="handleOptimizeChange" />
|
|
||||||
</n-form-item>
|
|
||||||
<!-- 导出属性 -->
|
|
||||||
<n-form-item :label="t('bim[\'Export Property\']')">
|
|
||||||
<n-switch v-model:value="BIMModel.options.ExportProperty" :disabled="exportPropertyDisabled" />
|
|
||||||
</n-form-item>
|
|
||||||
<!-- 转换视图 -->
|
|
||||||
<n-form-item :label="t('bim[\'Conversion view\']')">
|
|
||||||
<n-radio-group v-model:value="BIMModel.options.View" name="View">
|
|
||||||
<n-radio-button value="Default">默认3D视图</n-radio-button>
|
|
||||||
<n-radio-button value="Name">按名称</n-radio-button>
|
|
||||||
</n-radio-group>
|
|
||||||
</n-form-item>
|
|
||||||
<!-- 视图名称 -->
|
|
||||||
<n-form-item :label="t('bim[\'View name\']')" v-if="BIMModel.options.View === 'Name'">
|
|
||||||
<n-input v-model:value="BIMModel.options.ViewName" :placeholder="t('bim[\'Please enter the conversion view name\']')"/>
|
|
||||||
</n-form-item>
|
|
||||||
<!-- 视觉样式 -->
|
|
||||||
<n-form-item :label="t('bim[\'Display style\']')">
|
|
||||||
<n-radio-group v-model:value="BIMModel.options.DisplayStyle" name="DisplayStyle">
|
|
||||||
<n-radio-button value="Colour">{{t("bim.Colour")}}</n-radio-button>
|
|
||||||
<n-radio-button value="Realistic">{{t("bim.Realistic")}}</n-radio-button>
|
|
||||||
<n-radio-button value="ViewDefault">{{t("bim['View default']")}}</n-radio-button>
|
|
||||||
</n-radio-group>
|
|
||||||
</n-form-item>
|
|
||||||
<!-- 坐标参考 -->
|
|
||||||
<n-form-item :label="t('bim[\'Coordinate reference\']')">
|
|
||||||
<n-radio-group v-model:value="BIMModel.options.CoordinateReference" name="CoordinateReference">
|
|
||||||
<n-radio-button value="Origin">{{t("bim.Origin")}}</n-radio-button>
|
|
||||||
<n-radio-button value="ProjectBasePoint">{{t("bim['Project base point']")}}</n-radio-button>
|
|
||||||
<n-radio-button value="MeasuringPoint">{{t("bim['Measuring point']")}}</n-radio-button>
|
|
||||||
</n-radio-group>
|
|
||||||
</n-form-item>
|
|
||||||
</n-collapse-item>
|
|
||||||
</n-collapse>
|
|
||||||
</n-form>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<n-button round type="primary" @click="submit">{{ t("bim['Upload and lightweight']") }}</n-button>
|
|
||||||
</div>
|
|
||||||
</n-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {reactive, ref} from "vue";
|
|
||||||
import {NotificationReactive} from "naive-ui";
|
|
||||||
import {t} from "@/language";
|
|
||||||
import {ArchiveOutline, CaretForwardOutline} from "@vicons/ionicons5";
|
|
||||||
import {fetchUpload} from "@/http/api/sys";
|
|
||||||
import {fetchAddBim2Gltf, fetchUploadRvt} from "@/http/api/bim";
|
|
||||||
import {NEED_CONVERT_BIM_MODEL} from "@/utils/common/constant";
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
show:boolean
|
|
||||||
}>(),{
|
|
||||||
show:false
|
|
||||||
})
|
|
||||||
|
|
||||||
const emits = defineEmits(["update:show"]);
|
|
||||||
|
|
||||||
const formRef = ref();
|
|
||||||
const uploadBIMRef = ref();
|
|
||||||
const exportPropertyDisabled = ref(false);
|
|
||||||
|
|
||||||
/* bim文件上传转换 */
|
|
||||||
const BIMModel = reactive<{
|
|
||||||
fileName: string,
|
|
||||||
thumbnail: File | null,
|
|
||||||
bimFile: File | null,
|
|
||||||
options:{
|
|
||||||
// 极致压缩率(会执行实例化网格,破坏模型结构)
|
|
||||||
Optimize:boolean,
|
|
||||||
// 是否导出属性
|
|
||||||
ExportProperty:boolean,
|
|
||||||
// 转换视图 - Default:默认3D视图 | Name:按传入名称查找视图
|
|
||||||
View:string,
|
|
||||||
// 转换视图名称 - View = "Name"时输入有效
|
|
||||||
ViewName:string,
|
|
||||||
// 转换视觉样式 - Colour:着色模式 | Realistic-真实模式 | ViewDefault-视图默认
|
|
||||||
DisplayStyle:string,
|
|
||||||
// 坐标参考 - Origin:默认原点 | ProjectBasePoint:项目基点 | MeasuringPoint:测量点
|
|
||||||
CoordinateReference:string
|
|
||||||
}
|
|
||||||
}>({
|
|
||||||
fileName: "",
|
|
||||||
thumbnail: null,
|
|
||||||
bimFile: null,
|
|
||||||
options:{
|
|
||||||
Optimize:false,
|
|
||||||
ExportProperty:true,
|
|
||||||
View:"Default",
|
|
||||||
ViewName:"",
|
|
||||||
DisplayStyle:"Colour",
|
|
||||||
CoordinateReference:"Origin"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const BIMRules = {
|
|
||||||
bimFile: {
|
|
||||||
required: true,
|
|
||||||
trigger: 'change',
|
|
||||||
validator: (_, value) => {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
// 判断value是否是File类型
|
|
||||||
if (value === '' || !(value instanceof File)) {
|
|
||||||
reject(Error(t("bim['Please upload the BIM file']")))
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预览图变化
|
|
||||||
function thumbnailChange({file}) {
|
|
||||||
if (file.status === "removed") {
|
|
||||||
BIMModel.thumbnail = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BIMModel.thumbnail = file.file as File;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bim文件选择变化
|
|
||||||
function bimChange({file}) {
|
|
||||||
if (file.status === "removed") {
|
|
||||||
BIMModel.bimFile = null;
|
|
||||||
formRef.value?.validate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NEED_CONVERT_BIM_MODEL.includes(file.name.split(".").at(-1).toLowerCase())) {
|
|
||||||
window.$message?.error(t("prompt['This format is not supported, please upload again! Supported formats are:']") + ` ${NEED_CONVERT_BIM_MODEL.join("、")}`);
|
|
||||||
uploadBIMRef.value.clear();
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
BIMModel.bimFile = file.file as File;
|
|
||||||
BIMModel.fileName = BIMModel.bimFile.name;
|
|
||||||
formRef.value?.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 极致轻量化选项变化(导出属性选项跟随变化)
|
|
||||||
function handleOptimizeChange() {
|
|
||||||
if(BIMModel.options.Optimize){
|
|
||||||
BIMModel.options.ExportProperty = false;
|
|
||||||
exportPropertyDisabled.value = true;
|
|
||||||
}else{
|
|
||||||
exportPropertyDisabled.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ws 监听使用的notice
|
|
||||||
let wsNotice: null | NotificationReactive = null;
|
|
||||||
const getNotice = () => wsNotice;
|
|
||||||
// 提交
|
|
||||||
function submit(e) {
|
|
||||||
if (import.meta.env.PROD) {
|
|
||||||
window.$message?.error(window.$t("prompt['Disable this function in the demonstration environment!']"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
formRef.value?.validate(async (errors) => {
|
|
||||||
if (!errors) {
|
|
||||||
emits("update:show",false);
|
|
||||||
|
|
||||||
wsNotice = window.$notification.info({
|
|
||||||
title: t("bim['BIM lightweight']"),
|
|
||||||
content: t("prompt.Uploading") + "...",
|
|
||||||
closable: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 1. 上传缩略图
|
|
||||||
let thumbnail = "";
|
|
||||||
if (BIMModel.thumbnail) {
|
|
||||||
const res = await fetchUpload({
|
|
||||||
file: BIMModel.thumbnail,
|
|
||||||
biz: "upload/bim/thumbnail"
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.data === null) {
|
|
||||||
window.$message?.error(t("bim['Failed to upload thumbnail']"));
|
|
||||||
} else {
|
|
||||||
thumbnail = res.data as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 上传bim文件
|
|
||||||
const bimRes = await fetchUploadRvt({
|
|
||||||
file: BIMModel.bimFile,
|
|
||||||
});
|
|
||||||
if (bimRes.data === null) {
|
|
||||||
window.$message?.error(t("bim['Failed to upload BIM file']"));
|
|
||||||
wsNotice.content = `${t("bim['Failed to upload BIM file']")},${t("prompt['Please try again later!']")}`;
|
|
||||||
setTimeout(() => {
|
|
||||||
wsNotice?.destroy();
|
|
||||||
}, 1000)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//3 启动Revit轻量化服务
|
|
||||||
wsNotice.content = t("bim['BIM lightweight is in progress']") + "...";
|
|
||||||
await fetchAddBim2Gltf({
|
|
||||||
bimFilePath: bimRes.data,
|
|
||||||
bimFileSize: (BIMModel.bimFile as File).size,
|
|
||||||
fileName: BIMModel.fileName,
|
|
||||||
fileSourceIp: "",
|
|
||||||
thumbnail,
|
|
||||||
conversionStatus: 0,
|
|
||||||
// 转换配置
|
|
||||||
options:BIMModel.options
|
|
||||||
})
|
|
||||||
|
|
||||||
// reset
|
|
||||||
BIMModel.fileName = "";
|
|
||||||
BIMModel.thumbnail = null;
|
|
||||||
BIMModel.bimFile = null;
|
|
||||||
BIMModel.options = {
|
|
||||||
Optimize:false,
|
|
||||||
ExportProperty:true,
|
|
||||||
View:"Default",
|
|
||||||
ViewName:"",
|
|
||||||
DisplayStyle:"Colour",
|
|
||||||
CoordinateReference:"Origin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({getNotice})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.n-form-item{
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||||
|
import type { SelectOption } from "naive-ui";
|
||||||
|
import { Popup } from "@vicons/carbon";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Hooks, Utils, DataBindingManager } from "@astral3d/engine";
|
||||||
|
import { fetchDataSetPage, fetchExecuteDataSet } from "@/http/api/dataSet";
|
||||||
|
import CodeEditor from "@/components/code/CodeEditor.vue";
|
||||||
|
import EsInputNumber from "@/components/es/EsInputNumber.vue";
|
||||||
|
|
||||||
|
type DataSetId = IDataSet.Item["id"];
|
||||||
|
|
||||||
|
type DataComponentConfig = {
|
||||||
|
dataSetId?: DataSetId | null;
|
||||||
|
filterEnabled?: boolean;
|
||||||
|
filterBody?: string;
|
||||||
|
autoRefreshEnabled?: boolean;
|
||||||
|
autoRefreshInterval?: number;
|
||||||
|
applyToModel?: boolean;
|
||||||
|
transition?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultFilterBody = [" // Editable area", " // Example: return data.filter(item => item.value > 100);", " return data;"].join("\n");
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
dataSetId: null as DataSetId | null,
|
||||||
|
filterEnabled: false,
|
||||||
|
filterBody: defaultFilterBody,
|
||||||
|
autoRefreshEnabled: false,
|
||||||
|
autoRefreshInterval: 5,
|
||||||
|
applyToModel: false,
|
||||||
|
transition: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectObject3D = ref(false);
|
||||||
|
const dataSetOptions = ref<SelectOption[]>([]);
|
||||||
|
const dataSetLoading = ref(false);
|
||||||
|
const resultSource = ref("");
|
||||||
|
const resultLoading = ref(false);
|
||||||
|
const fetchError = ref("");
|
||||||
|
const filterError = ref("");
|
||||||
|
const applyError = ref("");
|
||||||
|
const showFilterModal = ref(false);
|
||||||
|
const showResultModal = ref(false);
|
||||||
|
const resultError = computed(() => [fetchError.value, filterError.value, applyError.value].filter(Boolean).join(" | "));
|
||||||
|
const rawResult = ref<unknown>(undefined);
|
||||||
|
const processedResult = ref<unknown>(undefined);
|
||||||
|
let selectedObject: any = null;
|
||||||
|
|
||||||
|
const config = reactive({ ...defaultConfig });
|
||||||
|
|
||||||
|
const filterEditorKey = computed(() => (config.filterEnabled ? "filter-enabled" : "filter-disabled"));
|
||||||
|
const filterEditorConfig = computed(() => ({
|
||||||
|
readOnly: !config.filterEnabled,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
renderValidationDecorations: "off",
|
||||||
|
automaticLayout: true,
|
||||||
|
lineNumbers: "off",
|
||||||
|
}));
|
||||||
|
const filterModalEditorConfig = computed(() => ({
|
||||||
|
readOnly: !config.filterEnabled,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
renderValidationDecorations: "off",
|
||||||
|
automaticLayout: true,
|
||||||
|
lineNumbers: "on",
|
||||||
|
}));
|
||||||
|
const resultEditorConfig = {
|
||||||
|
readOnly: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
renderValidationDecorations: "off",
|
||||||
|
automaticLayout: true,
|
||||||
|
lineNumbers: "off",
|
||||||
|
};
|
||||||
|
const resultModalEditorConfig = {
|
||||||
|
readOnly: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
renderValidationDecorations: "off",
|
||||||
|
automaticLayout: true,
|
||||||
|
lineNumbers: "on",
|
||||||
|
};
|
||||||
|
|
||||||
|
let isHydrating = false;
|
||||||
|
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let executeToken = 0;
|
||||||
|
|
||||||
|
async function loadDataSets() {
|
||||||
|
dataSetLoading.value = true;
|
||||||
|
const res = await fetchDataSetPage({ page: 1, pageSize: 1000 });
|
||||||
|
dataSetLoading.value = false;
|
||||||
|
if (res.error) return;
|
||||||
|
dataSetOptions.value =
|
||||||
|
res.data?.items?.map(item => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
})) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearResult() {
|
||||||
|
executeToken += 1;
|
||||||
|
rawResult.value = undefined;
|
||||||
|
processedResult.value = undefined;
|
||||||
|
resultSource.value = "";
|
||||||
|
resultLoading.value = false;
|
||||||
|
fetchError.value = "";
|
||||||
|
filterError.value = "";
|
||||||
|
applyError.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeInterval(value: number | null | undefined) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric)) {
|
||||||
|
return defaultConfig.autoRefreshInterval;
|
||||||
|
}
|
||||||
|
return Math.min(600, Math.max(1, Math.round(numeric)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeResult(value: unknown) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatResult(value: unknown) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(value ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilter(value: unknown) {
|
||||||
|
if (!config.filterEnabled) {
|
||||||
|
return { value, error: "" };
|
||||||
|
}
|
||||||
|
const body = (config.filterBody || "").trim();
|
||||||
|
if (!body) {
|
||||||
|
return { value, error: "" };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const filterFn = new Function("data", body);
|
||||||
|
return { value: filterFn(value), error: "" };
|
||||||
|
} catch (error) {
|
||||||
|
const message = (error as Error).message || t("prompt.Parse failed");
|
||||||
|
window.$message?.error(message);
|
||||||
|
return { value, error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeComponentData(value: unknown, options: { applyToModel?: boolean } = {}) {
|
||||||
|
const object = selectedObject;
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
const result = DataBindingManager.setData(object, value, {
|
||||||
|
index: 0,
|
||||||
|
applyToModel: options.applyToModel,
|
||||||
|
transition: config.transition,
|
||||||
|
onUpdate: () => Hooks.useDispatchSignal("objectChanged", object),
|
||||||
|
});
|
||||||
|
if (options.applyToModel && result.errors.length > 0) {
|
||||||
|
applyError.value = result.errors.join(" | ");
|
||||||
|
} else {
|
||||||
|
applyError.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncProcessedData(options: { applyToModel?: boolean } = {}) {
|
||||||
|
if (rawResult.value === undefined) {
|
||||||
|
processedResult.value = undefined;
|
||||||
|
resultSource.value = "";
|
||||||
|
filterError.value = "";
|
||||||
|
applyError.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { value, error } = applyFilter(rawResult.value);
|
||||||
|
processedResult.value = value;
|
||||||
|
filterError.value = error;
|
||||||
|
resultSource.value = formatResult(value);
|
||||||
|
writeComponentData(value, { applyToModel: options.applyToModel });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeDataSet() {
|
||||||
|
if (!config.dataSetId) {
|
||||||
|
clearResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dataSetId = config.dataSetId;
|
||||||
|
const token = ++executeToken;
|
||||||
|
resultLoading.value = true;
|
||||||
|
fetchError.value = "";
|
||||||
|
filterError.value = "";
|
||||||
|
applyError.value = "";
|
||||||
|
const res = await fetchExecuteDataSet(dataSetId);
|
||||||
|
if (token !== executeToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resultLoading.value = false;
|
||||||
|
if (res.error) {
|
||||||
|
fetchError.value = res.error.msg || t("other['Query failed']");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dataSetId !== config.dataSetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rawResult.value = normalizeResult(res.data);
|
||||||
|
syncProcessedData({ applyToModel: config.applyToModel });
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAutoRefresh() {
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
refreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAutoRefresh() {
|
||||||
|
stopAutoRefresh();
|
||||||
|
if (!config.autoRefreshEnabled || !config.dataSetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const interval = normalizeInterval(config.autoRefreshInterval);
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
executeDataSet();
|
||||||
|
}, interval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConfig(): DataComponentConfig {
|
||||||
|
return {
|
||||||
|
dataSetId: config.dataSetId ?? null,
|
||||||
|
filterEnabled: Boolean(config.filterEnabled),
|
||||||
|
filterBody: config.filterBody ?? "",
|
||||||
|
autoRefreshEnabled: Boolean(config.autoRefreshEnabled),
|
||||||
|
autoRefreshInterval: normalizeInterval(config.autoRefreshInterval),
|
||||||
|
applyToModel: Boolean(config.applyToModel),
|
||||||
|
transition: Number.isFinite(Number(config.transition)) ? Number(config.transition) : defaultConfig.transition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameConfig(left: unknown, right: unknown) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(left) === JSON.stringify(right);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfigToObject() {
|
||||||
|
const object = selectedObject;
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
const nextConfig = buildConfig();
|
||||||
|
const currentList = Array.isArray(object.dataComponent) ? object.dataComponent : [];
|
||||||
|
const currentConfig = currentList[0]?.config;
|
||||||
|
if (isSameConfig(currentConfig, nextConfig)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DataBindingManager.setConfig(object, nextConfig, { index: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleSave = Utils.debounce(() => {
|
||||||
|
if (isHydrating) return;
|
||||||
|
saveConfigToObject();
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
const schedulePreview = Utils.debounce(() => {
|
||||||
|
syncProcessedData({ applyToModel: config.applyToModel });
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
function applyConfigFromObject(object: any) {
|
||||||
|
const saved = Array.isArray(object?.dataComponent) ? (object.dataComponent[0]?.config as DataComponentConfig | undefined) : undefined;
|
||||||
|
isHydrating = true;
|
||||||
|
config.dataSetId = saved?.dataSetId ?? defaultConfig.dataSetId;
|
||||||
|
config.filterEnabled = saved?.filterEnabled ?? defaultConfig.filterEnabled;
|
||||||
|
config.filterBody = typeof saved?.filterBody === "string" ? saved.filterBody : defaultConfig.filterBody;
|
||||||
|
config.autoRefreshEnabled = saved?.autoRefreshEnabled ?? defaultConfig.autoRefreshEnabled;
|
||||||
|
config.autoRefreshInterval = normalizeInterval(saved?.autoRefreshInterval ?? defaultConfig.autoRefreshInterval);
|
||||||
|
config.applyToModel = saved?.applyToModel ?? defaultConfig.applyToModel;
|
||||||
|
config.transition = Number.isFinite(Number(saved?.transition)) ? Number(saved?.transition) : defaultConfig.transition;
|
||||||
|
nextTick(() => {
|
||||||
|
isHydrating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectObject(object: any) {
|
||||||
|
stopAutoRefresh();
|
||||||
|
selectedObject = object || null;
|
||||||
|
if (object) {
|
||||||
|
isSelectObject3D.value = true;
|
||||||
|
applyConfigFromObject(object);
|
||||||
|
if (config.dataSetId) {
|
||||||
|
executeDataSet();
|
||||||
|
} else {
|
||||||
|
clearResult();
|
||||||
|
}
|
||||||
|
syncAutoRefresh();
|
||||||
|
} else {
|
||||||
|
isSelectObject3D.value = false;
|
||||||
|
clearResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => config.dataSetId,
|
||||||
|
value => {
|
||||||
|
if (isHydrating) return;
|
||||||
|
scheduleSave();
|
||||||
|
if (value) {
|
||||||
|
executeDataSet();
|
||||||
|
} else {
|
||||||
|
clearResult();
|
||||||
|
stopAutoRefresh();
|
||||||
|
}
|
||||||
|
syncAutoRefresh();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => config.filterEnabled,
|
||||||
|
() => {
|
||||||
|
if (isHydrating) return;
|
||||||
|
scheduleSave();
|
||||||
|
syncProcessedData({ applyToModel: config.applyToModel });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => config.filterBody,
|
||||||
|
() => {
|
||||||
|
if (isHydrating) return;
|
||||||
|
scheduleSave();
|
||||||
|
schedulePreview();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => config.autoRefreshEnabled,
|
||||||
|
value => {
|
||||||
|
if (isHydrating) return;
|
||||||
|
scheduleSave();
|
||||||
|
syncAutoRefresh();
|
||||||
|
if (value) {
|
||||||
|
executeDataSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => config.autoRefreshInterval,
|
||||||
|
() => {
|
||||||
|
if (isHydrating) return;
|
||||||
|
scheduleSave();
|
||||||
|
syncAutoRefresh();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => config.transition,
|
||||||
|
() => {
|
||||||
|
if (isHydrating) return;
|
||||||
|
scheduleSave();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => config.applyToModel,
|
||||||
|
value => {
|
||||||
|
if (isHydrating) return;
|
||||||
|
scheduleSave();
|
||||||
|
if (!value) {
|
||||||
|
applyError.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (processedResult.value !== undefined) {
|
||||||
|
writeComponentData(processedResult.value, { applyToModel: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Hooks.useAddSignal("objectSelected", handleSelectObject);
|
||||||
|
loadDataSets();
|
||||||
|
handleSelectObject(App.selected);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
Hooks.useRemoveSignal("objectSelected", handleSelectObject);
|
||||||
|
stopAutoRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="isSelectObject3D" class="data-component-panel">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.dataComponent.Data set") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="config.dataSetId"
|
||||||
|
size="small"
|
||||||
|
:options="dataSetOptions"
|
||||||
|
:loading="dataSetLoading"
|
||||||
|
:placeholder="t('layout.sider.dataComponent.Select data set')"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider class="!mt-2 !mb-4" />
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.dataComponent.Data filter") }}</span>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<n-switch v-model:value="config.filterEnabled">
|
||||||
|
<template #checked>
|
||||||
|
{{ t("layout.sider.dataComponent.Enable filter") }}
|
||||||
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
{{ t("layout.sider.dataComponent.Disabled filter") }}
|
||||||
|
</template>
|
||||||
|
</n-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-editor relative" :class="{ 'is-disabled': !config.filterEnabled }">
|
||||||
|
<div class="filter-editor-line">function filter(data) {</div>
|
||||||
|
<CodeEditor :key="filterEditorKey" v-model:source="config.filterBody" mode="javascript" :config="filterEditorConfig" class="filter-editor-code" />
|
||||||
|
<div class="filter-editor-line">}</div>
|
||||||
|
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" tertiary @click="showFilterModal = true" class="absolute top-1 right-1">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><Popup /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t("layout.sider.dataComponent.Popup editor") }}
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider class="!mt-2 !mb-4" />
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.dataComponent.Apply to model") }}</span>
|
||||||
|
<div class="apply-controls">
|
||||||
|
<n-checkbox v-model:checked="config.applyToModel" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.dataComponent.Auto refresh") }}</span>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 items-center">
|
||||||
|
<n-checkbox v-model:checked="config.autoRefreshEnabled" size="small" />
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="config.autoRefreshInterval"
|
||||||
|
size="small"
|
||||||
|
:min="1"
|
||||||
|
:max="86400"
|
||||||
|
:decimal="0"
|
||||||
|
:step="1"
|
||||||
|
:show-button="false"
|
||||||
|
:disabled="!config.autoRefreshEnabled"
|
||||||
|
:unit="t('layout.sider.dataComponent.Seconds once')"
|
||||||
|
class="!w-60%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.dataComponent.Transition") }}</span>
|
||||||
|
<div class="flex justify-end items-center">
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="config.transition"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="600000"
|
||||||
|
:step="100"
|
||||||
|
:decimal="0"
|
||||||
|
:show-button="false"
|
||||||
|
:unit="t('layout.sider.dataComponent.Milliseconds')"
|
||||||
|
class="!w-60%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider class="!mt-2 !mb-4" />
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.dataComponent.Data result") }}</span>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<n-text depth="3" v-if="resultLoading">{{ t("other.Loading") }}</n-text>
|
||||||
|
|
||||||
|
<n-button size="tiny" tertiary :disabled="!config.dataSetId" :loading="resultLoading" @click="executeDataSet">
|
||||||
|
{{ t("layout.sider.dataComponent.Manual refresh") }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-editor relative">
|
||||||
|
<n-alert v-if="resultError" type="error" :title="t('layout.sider.dataComponent.Result error')" class="mb-2">
|
||||||
|
{{ resultError }}
|
||||||
|
</n-alert>
|
||||||
|
<CodeEditor v-model:source="resultSource" mode="json" :config="resultEditorConfig" class="flex-1 min-h-200px" />
|
||||||
|
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" tertiary @click="showResultModal = true" class="absolute top-1 right-1">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><Popup /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t("layout.sider.dataComponent.Popup editor") }}
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-result v-else status="418" title="Empty" :description="t('prompt[\'No object selected.\']')" />
|
||||||
|
|
||||||
|
<n-modal v-model:show="showFilterModal" preset="dialog" :showIcon="false" :title="t('layout.sider.dataComponent.Filter editor')" class="!w-90vw">
|
||||||
|
<div class="filter-editor filter-editor-modal" :class="{ 'is-disabled': !config.filterEnabled }">
|
||||||
|
<div class="filter-editor-line">function filter(data) {</div>
|
||||||
|
<CodeEditor
|
||||||
|
:key="`modal-${filterEditorKey}`"
|
||||||
|
v-model:source="config.filterBody"
|
||||||
|
mode="javascript"
|
||||||
|
:config="filterModalEditorConfig"
|
||||||
|
class="filter-editor-code filter-editor-code--modal"
|
||||||
|
/>
|
||||||
|
<div class="filter-editor-line">}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-3">
|
||||||
|
<n-button size="small" @click="showFilterModal = false">{{ t("other.Close") }}</n-button>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
|
||||||
|
<n-modal v-model:show="showResultModal" preset="dialog" :showIcon="false" :title="t('layout.sider.dataComponent.Result editor')" class="!w-90vw">
|
||||||
|
<CodeEditor v-model:source="resultSource" mode="json" :config="resultModalEditorConfig" class="flex-1 min-h-200px !h-70vh" />
|
||||||
|
<div class="flex justify-end mt-3">
|
||||||
|
<n-button size="small" @click="showResultModal = false">{{ t("other.Close") }}</n-button>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.data-component-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-editor {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--n-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-editor.is-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-editor-line {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c5c5c5;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-editor-code {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-editor-code--modal {
|
||||||
|
height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apply-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,37 +1,122 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref} from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import {CaretForwardOutline} from "@vicons/ionicons5";
|
import { CaretForwardOutline } from "@vicons/ionicons5";
|
||||||
import {t} from "@/language";
|
import { t } from "@/language";
|
||||||
import {App} from "@astral3d/engine";
|
import { App } from "@astral3d/engine";
|
||||||
import OutLine from './effect/Sidebar.Effect.Outline.vue';
|
import OutLine from "./effect/Sidebar.Effect.Outline.vue";
|
||||||
import FXAA from './effect/Sidebar.Effect.FXAA.vue';
|
import SMAA from "./effect/Sidebar.Effect.SMAA.vue";
|
||||||
import UnrealBloom from './effect/Sidebar.Effect.UnrealBloom.vue';
|
import SSAO from "./effect/Sidebar.Effect.SSAO.vue";
|
||||||
import LUT from './effect/Sidebar.Effect.LUT.vue';
|
import UnrealBloom from "./effect/Sidebar.Effect.UnrealBloom.vue";
|
||||||
import Afterimage from './effect/Sidebar.Effect.Afterimage.vue';
|
import Bokeh from "./effect/Sidebar.Effect.Bokeh.vue";
|
||||||
import Bokeh from './effect/Sidebar.Effect.Bokeh.vue';
|
import Pixelate from "./effect/Sidebar.Effect.Pixelate.vue";
|
||||||
import Pixelate from './effect/Sidebar.Effect.Pixelate.vue';
|
import TiltShift from "./effect/Sidebar.Effect.TiltShift.vue";
|
||||||
import Halftone from './effect/Sidebar.Effect.Halftone.vue';
|
import Scanline from "./effect/Sidebar.Effect.Scanline.vue";
|
||||||
import EsTip from "@/components/es/EsTip.vue";
|
import BrightnessContrast from "./effect/Sidebar.Effect.BrightnessContrast.vue";
|
||||||
|
import ChromaticAberration from "./effect/Sidebar.Effect.ChromaticAberration.vue";
|
||||||
|
import ColorDepth from "./effect/Sidebar.Effect.ColorDepth.vue";
|
||||||
|
import Glitch from "./effect/Sidebar.Effect.Glitch.vue";
|
||||||
|
import HueSaturation from "./effect/Sidebar.Effect.HueSaturation.vue";
|
||||||
|
import LensDistortion from "./effect/Sidebar.Effect.LensDistortion.vue";
|
||||||
|
import ShockWave from "./effect/Sidebar.Effect.ShockWave.vue";
|
||||||
|
import ToneMapping from "./effect/Sidebar.Effect.ToneMapping.vue";
|
||||||
|
import Vignette from "./effect/Sidebar.Effect.Vignette.vue";
|
||||||
|
import EsTip from "@/components/es/EsTip.vue";
|
||||||
|
|
||||||
const effectEnabled = ref(App.project.getKey("effect.enabled"));
|
const effectDefaults: Record<string, any> = {
|
||||||
|
ToneMapping: { mode: "ACES_FILMIC", exposure: 1.0, blendFunction: "NORMAL" },
|
||||||
|
SMAA: { enabled: true, preset: "ULTRA" },
|
||||||
|
SSAO: {
|
||||||
|
enabled: true,
|
||||||
|
blendFunction: "MULTIPLY",
|
||||||
|
samples: 9,
|
||||||
|
rings: 7,
|
||||||
|
radius: 0.1825,
|
||||||
|
intensity: 1.0,
|
||||||
|
bias: 0.025,
|
||||||
|
fade: 0.01,
|
||||||
|
luminanceInfluence: 0.7,
|
||||||
|
minRadiusScale: 0.1,
|
||||||
|
depthAwareUpsampling: true,
|
||||||
|
resolutionScale: 0.5,
|
||||||
|
distanceThreshold: 0.97,
|
||||||
|
distanceFalloff: 0.03,
|
||||||
|
rangeThreshold: 0.0005,
|
||||||
|
rangeFalloff: 0.001,
|
||||||
|
worldDistanceThreshold: null,
|
||||||
|
worldDistanceFalloff: null,
|
||||||
|
worldProximityThreshold: null,
|
||||||
|
worldProximityFalloff: null,
|
||||||
|
colorEnabled: false,
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
TiltShift: { enabled: false, offset: 0.0, rotation: 0.0, focusArea: 0.4, feather: 0.3, blendFunction: "NORMAL" },
|
||||||
|
Scanline: { enabled: false, density: 1.25, scrollSpeed: 0.0, blendFunction: "OVERLAY" },
|
||||||
|
BrightnessContrast: { enabled: false, brightness: 0.0, contrast: 0.0, blendFunction: "SRC" },
|
||||||
|
ChromaticAberration: {
|
||||||
|
enabled: false,
|
||||||
|
offset: { x: 0.002, y: 0.002 },
|
||||||
|
radialModulation: false,
|
||||||
|
modulationOffset: 0.15,
|
||||||
|
blendFunction: "NORMAL",
|
||||||
|
},
|
||||||
|
ColorDepth: { enabled: false, bits: 16, blendFunction: "NORMAL" },
|
||||||
|
Glitch: {
|
||||||
|
enabled: false,
|
||||||
|
chromaticAberrationOffset: null,
|
||||||
|
delay: { min: 1.5, max: 3.5 },
|
||||||
|
duration: { min: 0.6, max: 1.0 },
|
||||||
|
strength: { min: 0.3, max: 1.0 },
|
||||||
|
mode: "SPORADIC",
|
||||||
|
ratio: 0.85,
|
||||||
|
blendFunction: "NORMAL",
|
||||||
|
},
|
||||||
|
HueSaturation: { enabled: false, hue: 0.0, saturation: 0.0, blendFunction: "SRC" },
|
||||||
|
LensDistortion: {
|
||||||
|
enabled: false,
|
||||||
|
distortion: { x: 0.0, y: 0.0 },
|
||||||
|
principalPoint: { x: 0.0, y: 0.0 },
|
||||||
|
focalLength: { x: 1.0, y: 1.0 },
|
||||||
|
skew: 0.0,
|
||||||
|
},
|
||||||
|
ShockWave: { enabled: false, amplitude: 0.05, waveSize: 0.2, speed: 2.0, maxRadius: 1.0, clickTrigger: true },
|
||||||
|
Vignette: { enabled: false, offset: 0.5, darkness: 0.5, blendFunction: "NORMAL" },
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
// 兼容旧项目配置缺失 effect 子项,避免子组件 setup 阶段 JSON.parse(undefined)
|
||||||
|
function ensureEffectDefaults() {
|
||||||
|
if (App.project.getKey("effect") === undefined) {
|
||||||
|
App.project.setKey("effect", { enabled: false }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(effectDefaults).forEach(([name, config]) => {
|
||||||
|
const key = `effect.${name}`;
|
||||||
|
if (App.project.getKey(key) === undefined) {
|
||||||
|
App.project.setKey(key, JSON.parse(JSON.stringify(config)), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureEffectDefaults();
|
||||||
|
|
||||||
|
const effectEnabled = ref(App.project.getKey("effect.enabled"));
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
const viewerLoaded = () => {
|
const viewerLoaded = () => {
|
||||||
effectEnabled.value = App.project.getKey("effect.enabled");
|
effectEnabled.value = App.project.getKey("effect.enabled");
|
||||||
|
|
||||||
window.viewer.removeEventListener('loaded', viewerLoaded);
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
}
|
};
|
||||||
window.viewer.addEventListener('loaded', viewerLoaded);
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
})
|
});
|
||||||
|
|
||||||
function handleEffectEnabledChange(value:boolean){
|
function handleEffectEnabledChange(value: boolean) {
|
||||||
App.project.setKey("effect.enabled",value);
|
App.project.setKey("effect.enabled", value);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h4>{{ t('layout.sider.Post processing') }}</h4>
|
<h4>{{ t("layout.sider.Post processing") }}</h4>
|
||||||
<n-switch v-model:value="effectEnabled" @update:value="handleEffectEnabledChange">
|
<n-switch v-model:value="effectEnabled" @update:value="handleEffectEnabledChange">
|
||||||
<template #checked>
|
<template #checked>
|
||||||
{{ t("other.Open") }}
|
{{ t("other.Open") }}
|
||||||
@@ -44,7 +129,12 @@ function handleEffectEnabledChange(value:boolean){
|
|||||||
|
|
||||||
<n-divider class="!my-3" />
|
<n-divider class="!my-3" />
|
||||||
|
|
||||||
<n-collapse display-directive="show" :default-expanded-names="['Anti-aliasing','Outline','UnrealBloom']">
|
<!-- ToneMapping:色调映射 -->
|
||||||
|
<div class="px-2 mb-3">
|
||||||
|
<ToneMapping :effect-enabled="effectEnabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-collapse display-directive="show" :default-expanded-names="['Anti-aliasing', 'Outline']">
|
||||||
<template #arrow>
|
<template #arrow>
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<CaretForwardOutline />
|
<CaretForwardOutline />
|
||||||
@@ -53,10 +143,14 @@ function handleEffectEnabledChange(value:boolean){
|
|||||||
|
|
||||||
<!-- 抗锯齿 -->
|
<!-- 抗锯齿 -->
|
||||||
<n-collapse-item :title="t('layout.sider.postProcessing[\'Anti-aliasing\']')" name="Anti-aliasing">
|
<n-collapse-item :title="t('layout.sider.postProcessing[\'Anti-aliasing\']')" name="Anti-aliasing">
|
||||||
<FXAA :effect-enabled="effectEnabled" />
|
<SMAA :effect-enabled="effectEnabled" />
|
||||||
</n-collapse-item>
|
</n-collapse-item>
|
||||||
|
|
||||||
<!-- Outline:描边线 -->
|
<!-- Outline:描边线 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.postProcessing.SSAO')" name="SSAO">
|
||||||
|
<SSAO :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
<n-collapse-item :title="t('layout.sider.postProcessing.Outline')" name="Outline">
|
<n-collapse-item :title="t('layout.sider.postProcessing.Outline')" name="Outline">
|
||||||
<OutLine :effect-enabled="effectEnabled" />
|
<OutLine :effect-enabled="effectEnabled" />
|
||||||
</n-collapse-item>
|
</n-collapse-item>
|
||||||
@@ -66,40 +160,74 @@ function handleEffectEnabledChange(value:boolean){
|
|||||||
<UnrealBloom :effect-enabled="effectEnabled" />
|
<UnrealBloom :effect-enabled="effectEnabled" />
|
||||||
</n-collapse-item>
|
</n-collapse-item>
|
||||||
|
|
||||||
<!-- LUT:颜色滤镜 -->
|
|
||||||
<n-collapse-item :title="t('layout.sider.postProcessing.LUT Color filter')" name="LUT">
|
|
||||||
<LUT :effect-enabled="effectEnabled" />
|
|
||||||
</n-collapse-item>
|
|
||||||
|
|
||||||
<!-- 运动残影 -->
|
|
||||||
<n-collapse-item :title="t('layout.sider.postProcessing.Afterimage')" name="Afterimage">
|
|
||||||
<Afterimage :effect-enabled="effectEnabled" />
|
|
||||||
</n-collapse-item>
|
|
||||||
|
|
||||||
<!-- Bokeh:变焦 -->
|
<!-- Bokeh:变焦 -->
|
||||||
<n-collapse-item :title="t('layout.sider.postProcessing.Bokeh')" name="Bokeh">
|
<n-collapse-item :title="t('layout.sider.postProcessing.Bokeh')" name="Bokeh">
|
||||||
<Bokeh :effect-enabled="effectEnabled" />
|
<Bokeh :effect-enabled="effectEnabled" />
|
||||||
</n-collapse-item>
|
</n-collapse-item>
|
||||||
|
|
||||||
<!-- Pixelate:像素风 -->
|
<!-- Pixelate:像素风 -->
|
||||||
<n-collapse-item name="Pixelate">
|
<n-collapse-item :title="t(`layout.sider.postProcessing.Pixelate`)" name="Pixelate">
|
||||||
|
<Pixelate :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- TiltShift:移轴模糊 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.postProcessing.Tilt Shift')" name="TiltShift">
|
||||||
|
<TiltShift :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- Scanline:扫描线 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.postProcessing.Scanline')" name="Scanline">
|
||||||
|
<Scanline :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- BrightnessContrast:亮度对比度 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.postProcessing.Brightness Contrast')" name="BrightnessContrast">
|
||||||
|
<BrightnessContrast :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- ChromaticAberration:色差 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.postProcessing.Chromatic Aberration')" name="ChromaticAberration">
|
||||||
|
<ChromaticAberration :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- ColorDepth:色深 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.postProcessing.Color Depth')" name="ColorDepth">
|
||||||
|
<ColorDepth :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- Glitch:故障 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.postProcessing.Glitch')" name="Glitch">
|
||||||
|
<Glitch :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- HueSaturation:色相饱和度 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.postProcessing.Hue Saturation')" name="HueSaturation">
|
||||||
|
<HueSaturation :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- LensDistortion:镜头畸变 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.postProcessing.Lens Distortion')" name="LensDistortion">
|
||||||
|
<LensDistortion :effect-enabled="effectEnabled" />
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- ShockWave:冲击波 -->
|
||||||
|
<n-collapse-item name="ShockWave">
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-text>
|
<n-text>
|
||||||
<EsTip class="!justify-start" :content="t(`layout.sider.postProcessing.Pixelate`)">
|
<EsTip class="!justify-start" :content="t(`layout.sider.postProcessing.Shock Wave`)">
|
||||||
{{ t(`layout.sider.postProcessing.Use a solid color background to achieve the best rendering effect`) }}
|
{{ t(`layout.sider.postProcessing.Click on scene to trigger shock wave effect`) }}
|
||||||
</EsTip>
|
</EsTip>
|
||||||
</n-text>
|
</n-text>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Pixelate :effect-enabled="effectEnabled" />
|
<ShockWave :effect-enabled="effectEnabled" />
|
||||||
</n-collapse-item>
|
</n-collapse-item>
|
||||||
|
|
||||||
<!-- Halftone:半色调 -->
|
<!-- Vignette:晕影 -->
|
||||||
<n-collapse-item :title="t('layout.sider.postProcessing.Halftoning')" name="Halftoning">
|
<n-collapse-item :title="t('layout.sider.postProcessing.Vignette')" name="Vignette">
|
||||||
<Halftone :effect-enabled="effectEnabled" />
|
<Vignette :effect-enabled="effectEnabled" />
|
||||||
</n-collapse-item>
|
</n-collapse-item>
|
||||||
</n-collapse>
|
</n-collapse>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less"></style>
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,534 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||||
|
import { CaretForwardOutline } from "@vicons/ionicons5";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Hooks, Utils, getDefaultHeatmapOptions } from "@astral3d/engine";
|
||||||
|
import CodeEditor from "@/components/code/CodeEditor.vue";
|
||||||
|
import EsTip from "@/components/es/EsTip.vue";
|
||||||
|
|
||||||
|
type GradientStep = {
|
||||||
|
id: string;
|
||||||
|
step: number;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions = getDefaultHeatmapOptions();
|
||||||
|
const heatmapData = reactive(getDefaultHeatmapOptions() as any);
|
||||||
|
const opacityRange = ref<[number, number]>([heatmapData.heatmap.minOpacity, heatmapData.heatmap.maxOpacity]);
|
||||||
|
const isHeightMode = computed(() => heatmapData.mode === "height");
|
||||||
|
const pointsPreview = computed(() => JSON.stringify(heatmapData.data?.points || [], null, 2));
|
||||||
|
const pointsEditorShow = ref(false);
|
||||||
|
const pointsEditorRef = ref();
|
||||||
|
const pointsSource = ref("[]");
|
||||||
|
const pointsErrors = ref<string[]>([]);
|
||||||
|
const gradientSteps = ref<GradientStep[]>([]);
|
||||||
|
|
||||||
|
function getHeatmapObject() {
|
||||||
|
const object = App.selected;
|
||||||
|
if (!object) return null;
|
||||||
|
if (object.type !== "Heatmap" || !object.options) return null;
|
||||||
|
return object as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
const object = getHeatmapObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
Utils.deepAssign(heatmapData, object.options);
|
||||||
|
if (object.options.heatmap?.gradient) {
|
||||||
|
heatmapData.heatmap.gradient = { ...object.options.heatmap.gradient };
|
||||||
|
}
|
||||||
|
opacityRange.value = [heatmapData.heatmap.minOpacity, heatmapData.heatmap.maxOpacity];
|
||||||
|
|
||||||
|
ensureDataShape();
|
||||||
|
syncGradientSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDataShape() {
|
||||||
|
if (!heatmapData.data) {
|
||||||
|
heatmapData.data = JSON.parse(JSON.stringify(defaultOptions.data));
|
||||||
|
}
|
||||||
|
if (!Array.isArray(heatmapData.data.points)) {
|
||||||
|
heatmapData.data.points = [];
|
||||||
|
}
|
||||||
|
if (!heatmapData.height) {
|
||||||
|
heatmapData.height = JSON.parse(JSON.stringify(defaultOptions.height));
|
||||||
|
}
|
||||||
|
if (!heatmapData.heatmap) {
|
||||||
|
heatmapData.heatmap = JSON.parse(JSON.stringify(defaultOptions.heatmap));
|
||||||
|
}
|
||||||
|
if (!heatmapData.heatmap.gradient || typeof heatmapData.heatmap.gradient !== "object" || Array.isArray(heatmapData.heatmap.gradient)) {
|
||||||
|
heatmapData.heatmap.gradient = JSON.parse(JSON.stringify(defaultOptions.heatmap.gradient));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampStep(value: number) {
|
||||||
|
if (!Number.isFinite(value)) return 0;
|
||||||
|
return Math.min(1, Math.max(0, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGradientSteps(gradient: Record<string, string>) {
|
||||||
|
const steps: GradientStep[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
Object.entries(gradient).forEach(([step, color]) => {
|
||||||
|
const numericStep = Number(step);
|
||||||
|
if (!Number.isFinite(numericStep)) return;
|
||||||
|
steps.push({
|
||||||
|
id: `step-${index}-${numericStep}`,
|
||||||
|
step: clampStep(numericStep),
|
||||||
|
color: typeof color === "string" && color.trim() !== "" ? color : "#ffffff",
|
||||||
|
});
|
||||||
|
index += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (steps.length === 0) {
|
||||||
|
steps.push({ id: `step-${Date.now()}`, step: 0, color: "#ffffff" });
|
||||||
|
}
|
||||||
|
|
||||||
|
steps.sort((a, b) => a.step - b.step);
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncGradientSteps() {
|
||||||
|
ensureDataShape();
|
||||||
|
gradientSteps.value = normalizeGradientSteps(heatmapData.heatmap.gradient || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGradientFromSteps() {
|
||||||
|
const gradient: Record<string, string> = {};
|
||||||
|
const sortedSteps = [...gradientSteps.value].sort((a, b) => a.step - b.step);
|
||||||
|
|
||||||
|
sortedSteps.forEach(step => {
|
||||||
|
const clamped = clampStep(Number(step.step));
|
||||||
|
gradient[clamped.toFixed(2)] = step.color || "#ffffff";
|
||||||
|
});
|
||||||
|
|
||||||
|
return gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGradient() {
|
||||||
|
ensureDataShape();
|
||||||
|
heatmapData.heatmap.gradient = buildGradientFromSteps();
|
||||||
|
update("heatmap");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextGradientStep() {
|
||||||
|
const steps = gradientSteps.value
|
||||||
|
.map(step => step.step)
|
||||||
|
.filter(Number.isFinite)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
if (steps.length === 0) return 0;
|
||||||
|
for (let i = 0; i < steps.length - 1; i += 1) {
|
||||||
|
const gap = steps[i + 1] - steps[i];
|
||||||
|
if (gap > 0.1) {
|
||||||
|
return Number(((steps[i] + steps[i + 1]) / 2).toFixed(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const last = steps[steps.length - 1];
|
||||||
|
if (last < 1) {
|
||||||
|
return Number(Math.min(1, last + 0.1).toFixed(2));
|
||||||
|
}
|
||||||
|
const first = steps[0];
|
||||||
|
return Number(Math.max(0, first - 0.1).toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGradientStep() {
|
||||||
|
const step = getNextGradientStep();
|
||||||
|
const fallbackColor = gradientSteps.value.length ? gradientSteps.value[gradientSteps.value.length - 1].color : "#ffffff";
|
||||||
|
gradientSteps.value.push({ id: `step-${Date.now()}-${Math.random().toString(16).slice(2)}`, step, color: fallbackColor });
|
||||||
|
updateGradient();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGradientStep(index: number) {
|
||||||
|
if (gradientSteps.value.length <= 1) return;
|
||||||
|
gradientSteps.value.splice(index, 1);
|
||||||
|
updateGradient();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGradientStep(index: number) {
|
||||||
|
const item = gradientSteps.value[index];
|
||||||
|
if (!item) return;
|
||||||
|
const step = clampStep(Number(item.step ?? 0));
|
||||||
|
item.step = step;
|
||||||
|
updateGradient();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGradientColor(index: number, color: string | null) {
|
||||||
|
const item = gradientSteps.value[index];
|
||||||
|
if (!item) return;
|
||||||
|
item.color = color || "#ffffff";
|
||||||
|
updateGradient();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestRender(object) {
|
||||||
|
Hooks.useDispatchSignal("objectChanged", object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(key: string) {
|
||||||
|
const object = getHeatmapObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
const call = {
|
||||||
|
mode: () => {
|
||||||
|
object.setMode(heatmapData.mode);
|
||||||
|
},
|
||||||
|
size: () => {
|
||||||
|
object.setSize({ width: heatmapData.size.width, height: heatmapData.size.height });
|
||||||
|
},
|
||||||
|
heatmap: () => {
|
||||||
|
heatmapData.heatmap.minOpacity = opacityRange.value[0];
|
||||||
|
heatmapData.heatmap.maxOpacity = opacityRange.value[1];
|
||||||
|
|
||||||
|
object.updateHeatmapConfig(heatmapData.heatmap);
|
||||||
|
},
|
||||||
|
height: () => {
|
||||||
|
object.options.height = {
|
||||||
|
scale: heatmapData.height.scale,
|
||||||
|
segments: {
|
||||||
|
width: heatmapData.height.segments.width,
|
||||||
|
height: heatmapData.height.segments.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
object.setSize({ width: heatmapData.size.width, height: heatmapData.size.height });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
call[key]?.();
|
||||||
|
requestRender(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateData() {
|
||||||
|
const object = getHeatmapObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
ensureDataShape();
|
||||||
|
object.setData({
|
||||||
|
max: heatmapData.data.max,
|
||||||
|
min: heatmapData.data.min,
|
||||||
|
points: heatmapData.data.points || [],
|
||||||
|
});
|
||||||
|
requestRender(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPointsEditor() {
|
||||||
|
pointsEditorShow.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePoints(value: unknown) {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const points: Array<{ x: number; y: number; value: number; radius?: number }> = [];
|
||||||
|
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return { errors: [t("layout.sider.heatmap.Points must be an array.")], points };
|
||||||
|
}
|
||||||
|
|
||||||
|
value.forEach((item, index) => {
|
||||||
|
if (!item || typeof item !== "object") {
|
||||||
|
errors.push(t("layout.sider.heatmap.Point[{index}] must be an object.", { index }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = item as Record<string, unknown>;
|
||||||
|
const x = Number(point.x);
|
||||||
|
const y = Number(point.y);
|
||||||
|
const v = Number(point.value);
|
||||||
|
const radius = point.radius === undefined ? undefined : Number(point.radius);
|
||||||
|
|
||||||
|
if (!Number.isFinite(x)) errors.push(t("layout.sider.heatmap.Point[{index}].x must be a number.", { index }));
|
||||||
|
if (!Number.isFinite(y)) errors.push(t("layout.sider.heatmap.Point[{index}].y must be a number.", { index }));
|
||||||
|
if (!Number.isFinite(v)) errors.push(t("layout.sider.heatmap.Point[{index}].value must be a number.", { index }));
|
||||||
|
if (radius !== undefined && !Number.isFinite(radius)) {
|
||||||
|
errors.push(t("layout.sider.heatmap.Point[{index}].radius must be a number.", { index }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(v)) {
|
||||||
|
const normalized: { x: number; y: number; value: number; radius?: number } = { x, y, value: v };
|
||||||
|
if (radius !== undefined && Number.isFinite(radius)) {
|
||||||
|
normalized.radius = radius;
|
||||||
|
}
|
||||||
|
points.push(normalized);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { errors, points };
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitPoints(e: Event) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
pointsErrors.value = pointsEditorRef.value?.getErrors?.() || [];
|
||||||
|
if (pointsErrors.value.length > 0) return;
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(pointsSource.value);
|
||||||
|
} catch (error) {
|
||||||
|
pointsErrors.value = [String((error as Error).message || error)];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { errors, points } = validatePoints(parsed);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
pointsErrors.value = errors;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDataShape();
|
||||||
|
heatmapData.data.points = points;
|
||||||
|
updateData();
|
||||||
|
pointsEditorShow.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => pointsEditorShow.value,
|
||||||
|
show => {
|
||||||
|
if (!show) return;
|
||||||
|
ensureDataShape();
|
||||||
|
pointsSource.value = JSON.stringify(heatmapData.data.points || [], null, 2);
|
||||||
|
pointsErrors.value = [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Hooks.useAddSignal("objectSelected", updateUI);
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
Hooks.useRemoveSignal("objectSelected", updateUI);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-collapse display-directive="show" :default-expanded-names="['basic', 'heatmap', 'data', 'height']">
|
||||||
|
<template #arrow>
|
||||||
|
<n-icon>
|
||||||
|
<CaretForwardOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 基础 -->
|
||||||
|
<n-collapse-item :title="t('extra.resource.expansion.Heat map')" name="basic">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.particle.Type") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="heatmapData.mode"
|
||||||
|
size="small"
|
||||||
|
:options="[
|
||||||
|
{ label: t('extra.resource.expansion.Flat heatmap'), value: 'flat' },
|
||||||
|
{ label: t('extra.resource.expansion.Elevation heatmap'), value: 'height' },
|
||||||
|
]"
|
||||||
|
@update:value="update('mode')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("other.Width") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="heatmapData.size.width"
|
||||||
|
size="small"
|
||||||
|
:min="0.1"
|
||||||
|
:max="Infinity"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="update('size')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("other.Height") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="heatmapData.size.height"
|
||||||
|
size="small"
|
||||||
|
:min="0.1"
|
||||||
|
:max="Infinity"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="update('size')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- 高程 -->
|
||||||
|
<n-collapse-item v-if="isHeightMode" :title="t('extra.resource.expansion.Elevation heatmap')" name="height">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.heatmap.Height Scale") }}</span>
|
||||||
|
<EsInputNumber v-model:value="heatmapData.height.scale" size="small" :min="0" :max="100" :decimal="2" :show-button="false" @change="update('height')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.scene.Width segments") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="heatmapData.height.segments.width"
|
||||||
|
size="small"
|
||||||
|
:min="1"
|
||||||
|
:max="512"
|
||||||
|
:decimal="0"
|
||||||
|
:show-button="false"
|
||||||
|
@change="update('height')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.scene.Height segments") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="heatmapData.height.segments.height"
|
||||||
|
size="small"
|
||||||
|
:min="1"
|
||||||
|
:max="512"
|
||||||
|
:decimal="0"
|
||||||
|
:show-button="false"
|
||||||
|
@change="update('height')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- 热力图纹理参数 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.heatmap.Heatmap Texture')" name="heatmap">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.postProcessing.Radius") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="heatmapData.heatmap.radius"
|
||||||
|
size="small"
|
||||||
|
:min="1"
|
||||||
|
:max="Infinity"
|
||||||
|
:decimal="0"
|
||||||
|
:show-button="false"
|
||||||
|
@change="update('heatmap')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.heatmap.Blur") }}</span>
|
||||||
|
<n-slider v-model:value="heatmapData.heatmap.blur" :min="0" :max="1" :step="0.01" @update:value="update('heatmap')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<n-text>
|
||||||
|
<EsTip class="!justify-start" content="Alpha">
|
||||||
|
{{ t("layout.sider.heatmap.Controls the opacity of the darkest and brightest parts of a thermal texture") }}
|
||||||
|
</EsTip>
|
||||||
|
</n-text>
|
||||||
|
<n-slider v-model:value="opacityRange" range :min="0" :max="1" :step="0.01" @update:value="update('heatmap')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.heatmap.Gradient") }}</span>
|
||||||
|
<div class="heatmap-gradient-actions">
|
||||||
|
<n-button size="tiny" tertiary @click="addGradientStep">{{ t("layout.sider.heatmap.Add") }}</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="heatmap-gradient-item" v-for="(step, index) in gradientSteps" :key="step.id">
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="step.step"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
class="heatmap-gradient-step"
|
||||||
|
@change="updateGradientStep(index)"
|
||||||
|
/>
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="step.color"
|
||||||
|
:show-alpha="false"
|
||||||
|
:modes="['hex']"
|
||||||
|
size="small"
|
||||||
|
class="heatmap-gradient-color"
|
||||||
|
:render-label="() => ''"
|
||||||
|
@update:value="updateGradientColor(index, $event)"
|
||||||
|
/>
|
||||||
|
<n-button size="tiny" quaternary :disabled="gradientSteps.length <= 1" @click="removeGradientStep(index)">
|
||||||
|
{{ t("layout.sider.heatmap.Remove") }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<!-- 数据 -->
|
||||||
|
<n-collapse-item :title="t('layout.sider.heatmap.Data')" name="data">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("other.Maximum") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="heatmapData.data.max"
|
||||||
|
size="small"
|
||||||
|
:min="-Infinity"
|
||||||
|
:max="Infinity"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="updateData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("other.Minimum") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="heatmapData.data.min"
|
||||||
|
size="small"
|
||||||
|
:min="-Infinity"
|
||||||
|
:max="Infinity"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="updateData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<n-input class="!w-full" type="textarea" :value="pointsPreview" :rows="6" readonly round @click.stop="openPointsEditor" />
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
|
||||||
|
<n-modal
|
||||||
|
:show="pointsEditorShow"
|
||||||
|
@update:show="show => (pointsEditorShow = show)"
|
||||||
|
class="!w-60vw"
|
||||||
|
preset="dialog"
|
||||||
|
:title="t('layout.sider.heatmap.Heatmap Points')"
|
||||||
|
:showIcon="false"
|
||||||
|
>
|
||||||
|
<CodeEditor ref="pointsEditorRef" v-model:source="pointsSource" mode="json" class="!h-400px" />
|
||||||
|
|
||||||
|
<n-alert
|
||||||
|
:title="t('layout.sider.heatmap.Error')"
|
||||||
|
type="error"
|
||||||
|
v-if="pointsErrors.length"
|
||||||
|
closable
|
||||||
|
@close="pointsErrors = []"
|
||||||
|
class="absolute bottom-0 w-full z-9999"
|
||||||
|
>
|
||||||
|
<n-text depth="1" v-for="(error, index) in pointsErrors" :key="index" class="block">{{ error }}</n-text>
|
||||||
|
</n-alert>
|
||||||
|
<div class="float-right mt-10px">
|
||||||
|
<n-button size="small" @click="pointsEditorShow = false">{{ t("other.Cancel") }}</n-button>
|
||||||
|
<n-button class="ml-5px" type="primary" size="small" @click="submitPoints">{{ t("other.Ok") }}</n-button>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.heatmap-gradient-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heatmap-gradient-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0 1rem 0.5rem 1rem;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heatmap-gradient-step {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heatmap-gradient-color {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input__textarea) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -29,6 +29,22 @@ onBeforeUnmount(() => {
|
|||||||
Hooks.useRemoveSignal("objectSelected", updateUI);
|
Hooks.useRemoveSignal("objectSelected", updateUI);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function ensureHtmlPanelOptions(object: any) {
|
||||||
|
const options = object?.options || {};
|
||||||
|
const codes = Array.isArray(options.codes) ? options.codes : [];
|
||||||
|
const isSingleHtml = typeof options.isSingleHtml === 'boolean' ? options.isSingleHtml : true;
|
||||||
|
const isSprite = typeof options.isSprite === 'boolean' ? options.isSprite : !!object?.isHtmlSprite;
|
||||||
|
|
||||||
|
object.options = {
|
||||||
|
...options,
|
||||||
|
codes,
|
||||||
|
isSingleHtml,
|
||||||
|
isSprite
|
||||||
|
};
|
||||||
|
|
||||||
|
return object.options;
|
||||||
|
}
|
||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
const object = App.selected;
|
const object = App.selected;
|
||||||
if(!object) return;
|
if(!object) return;
|
||||||
@@ -39,7 +55,8 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data.type = object.isHtmlPanel? 'panel' :'sprite';
|
data.type = object.isHtmlPanel? 'panel' :'sprite';
|
||||||
data.codes = object.options.codes;
|
const options = ensureHtmlPanelOptions(object);
|
||||||
|
data.codes = options.codes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(method: string){
|
function update(method: string){
|
||||||
@@ -53,8 +70,9 @@ function update(method: string){
|
|||||||
if(data.type === 'panel' && object.isHtmlPanel) return;
|
if(data.type === 'panel' && object.isHtmlPanel) return;
|
||||||
if(data.type === 'sprite' && object.isHtmlSprite) return;
|
if(data.type === 'sprite' && object.isHtmlSprite) return;
|
||||||
|
|
||||||
const _json = object.toJSON();
|
const _json = object.toJSON() as any;
|
||||||
_json.object.type = data.type === 'panel' ? 'HtmlSprite' : 'HtmlPanel';
|
_json.object.type = data.type === 'panel' ? 'HtmlSprite' : 'HtmlPanel';
|
||||||
|
_json.object.options = _json.object.options || {};
|
||||||
_json.object.options.isSprite = !_json.object.options.isSprite;
|
_json.object.options.isSprite = !_json.object.options.isSprite;
|
||||||
|
|
||||||
Loader.objectLoader.parseAsync(_json).then(newObject3D => {
|
Loader.objectLoader.parseAsync(_json).then(newObject3D => {
|
||||||
@@ -73,9 +91,11 @@ function update(method: string){
|
|||||||
|
|
||||||
_code.content = editorCode.code;
|
_code.content = editorCode.code;
|
||||||
|
|
||||||
|
const options = ensureHtmlPanelOptions(object);
|
||||||
|
|
||||||
const htmlPanelOption = {
|
const htmlPanelOption = {
|
||||||
isSprite: object.options.isSprite,
|
isSprite: options.isSprite,
|
||||||
isSingleHtml:object.options.isSingleHtml,
|
isSingleHtml: options.isSingleHtml,
|
||||||
codes: data.codes
|
codes: data.codes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,19 +155,26 @@ function updateFileList(fList: UploadFileInfo[]) {
|
|||||||
isSprite: data.type ==='sprite',
|
isSprite: data.type ==='sprite',
|
||||||
fileName: file.name
|
fileName: file.name
|
||||||
}).then(htmlPlaneObj => {
|
}).then(htmlPlaneObj => {
|
||||||
data.codes = htmlPlaneObj.options.codes;
|
const options = ensureHtmlPanelOptions(object);
|
||||||
|
|
||||||
const _json = object.toJSON();
|
options.codes = htmlPlaneObj.options.codes;
|
||||||
Loader.objectLoader.copyAttrByData(htmlPlaneObj, _json.object)
|
options.isSingleHtml = htmlPlaneObj.options.isSingleHtml;
|
||||||
|
|
||||||
object.parent?.add(htmlPlaneObj);
|
data.codes = options.codes;
|
||||||
|
|
||||||
object.parent?.remove(object);
|
// Keep object identity (id/uuid) so scene tree and context menu references stay valid.
|
||||||
|
while (object.element.firstChild) {
|
||||||
|
object.element.removeChild(object.element.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
App.selected = htmlPlaneObj;
|
while (htmlPlaneObj.element.firstChild) {
|
||||||
|
object.element.appendChild(htmlPlaneObj.element.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
Hooks.useDispatchSignal("objectSelected", htmlPlaneObj);
|
App.selected = object;
|
||||||
}).catch((e:Error) => window.$message?.error(e.message));
|
Hooks.useDispatchSignal("objectSelected", object);
|
||||||
|
}).catch((e:Error) => window.$message?.error(e.message))
|
||||||
|
.finally(() => URL.revokeObjectURL(tempURL));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCanEdit(name: string) {
|
function getCanEdit(name: string) {
|
||||||
|
|||||||
@@ -121,15 +121,15 @@ const updateUI = Utils.throttle(function(object) {
|
|||||||
objectData.type = object.type;
|
objectData.type = object.type;
|
||||||
objectData.uuid = object.uuid;
|
objectData.uuid = object.uuid;
|
||||||
objectData.name = object.name;
|
objectData.name = object.name;
|
||||||
objectData.position.x = Number(object.position.x.toFixed(3));
|
objectData.position.x = Number(object.position.x.toFixed(6));
|
||||||
objectData.position.y = Number(object.position.y.toFixed(3));
|
objectData.position.y = Number(object.position.y.toFixed(6));
|
||||||
objectData.position.z = Number(object.position.z.toFixed(3));
|
objectData.position.z = Number(object.position.z.toFixed(6));
|
||||||
objectData.rotation.x = Number((object.rotation.x * THREE.MathUtils.RAD2DEG).toFixed(3));
|
objectData.rotation.x = Number((object.rotation.x * THREE.MathUtils.RAD2DEG).toFixed(6));
|
||||||
objectData.rotation.y = Number((object.rotation.y * THREE.MathUtils.RAD2DEG).toFixed(3));
|
objectData.rotation.y = Number((object.rotation.y * THREE.MathUtils.RAD2DEG).toFixed(6));
|
||||||
objectData.rotation.z = Number((object.rotation.z * THREE.MathUtils.RAD2DEG).toFixed(3));
|
objectData.rotation.z = Number((object.rotation.z * THREE.MathUtils.RAD2DEG).toFixed(6));
|
||||||
objectData.scale.x = Number(object.scale.x.toFixed(3));
|
objectData.scale.x = Number(object.scale.x.toFixed(6));
|
||||||
objectData.scale.y = Number(object.scale.y.toFixed(3));
|
objectData.scale.y = Number(object.scale.y.toFixed(6));
|
||||||
objectData.scale.z = Number(object.scale.z.toFixed(3));
|
objectData.scale.z = Number(object.scale.z.toFixed(6));
|
||||||
|
|
||||||
if (object.fov !== undefined) {
|
if (object.fov !== undefined) {
|
||||||
objectData.fov = object.fov;
|
objectData.fov = object.fov;
|
||||||
@@ -225,19 +225,19 @@ const update = (method: string) => {
|
|||||||
},
|
},
|
||||||
position: () => {
|
position: () => {
|
||||||
const newPosition = new THREE.Vector3(objectData.position.x, objectData.position.y, objectData.position.z);
|
const newPosition = new THREE.Vector3(objectData.position.x, objectData.position.y, objectData.position.z);
|
||||||
if (object.position.distanceTo(newPosition) >= 0.01) {
|
if (object.position.distanceTo(newPosition) >= 0.0001) {
|
||||||
App.execute(new SetPositionCommand(object, newPosition));
|
App.execute(new SetPositionCommand(object, newPosition));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rotation: () => {
|
rotation: () => {
|
||||||
const newRotation = new THREE.Euler(objectData.rotation.x * THREE.MathUtils.DEG2RAD, objectData.rotation.y * THREE.MathUtils.DEG2RAD, objectData.rotation.z * THREE.MathUtils.DEG2RAD);
|
const newRotation = new THREE.Euler(objectData.rotation.x * THREE.MathUtils.DEG2RAD, objectData.rotation.y * THREE.MathUtils.DEG2RAD, objectData.rotation.z * THREE.MathUtils.DEG2RAD);
|
||||||
if (new THREE.Vector3().setFromEuler(object.rotation).distanceTo(new THREE.Vector3().setFromEuler(newRotation)) >= 0.01) {
|
if (new THREE.Vector3().setFromEuler(object.rotation).distanceTo(new THREE.Vector3().setFromEuler(newRotation)) >= 0.0001) {
|
||||||
App.execute(new SetRotationCommand(object, newRotation, undefined));
|
App.execute(new SetRotationCommand(object, newRotation, undefined));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scale: () => {
|
scale: () => {
|
||||||
const newScale = new THREE.Vector3(objectData.scale.x, objectData.scale.y, objectData.scale.z);
|
const newScale = new THREE.Vector3(objectData.scale.x, objectData.scale.y, objectData.scale.z);
|
||||||
if (object.scale.distanceTo(newScale) >= 0.01) {
|
if (object.scale.distanceTo(newScale) >= 0.0001) {
|
||||||
App.execute(new SetScaleCommand(object, newScale, undefined));
|
App.execute(new SetScaleCommand(object, newScale, undefined));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -433,11 +433,11 @@ const handleUserDataClick = () => {
|
|||||||
<div class="sider-scene-attr-item">
|
<div class="sider-scene-attr-item">
|
||||||
<EsKeyFrame :label="t('layout.sider.object.position')" attr="position" />
|
<EsKeyFrame :label="t('layout.sider.object.position')" attr="position" />
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<EsInputNumber v-model:value="objectData.position.x" size="tiny" :show-button="false" :decimal="3" :step="1"
|
<EsInputNumber v-model:value="objectData.position.x" size="tiny" :show-button="false" :decimal="6" :step="1"
|
||||||
@change="update('position')" />
|
@change="update('position')" />
|
||||||
<EsInputNumber v-model:value="objectData.position.y" size="tiny" :show-button="false" :decimal="3" :step="1"
|
<EsInputNumber v-model:value="objectData.position.y" size="tiny" :show-button="false" :decimal="6" :step="1"
|
||||||
@change="update('position')" />
|
@change="update('position')" />
|
||||||
<EsInputNumber v-model:value="objectData.position.z" size="tiny" :show-button="false" :decimal="3" :step="1"
|
<EsInputNumber v-model:value="objectData.position.z" size="tiny" :show-button="false" :decimal="6" :step="1"
|
||||||
@change="update('position')" />
|
@change="update('position')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,11 +445,11 @@ const handleUserDataClick = () => {
|
|||||||
<div class="sider-scene-attr-item" v-if="transformRowsVisible.rotation">
|
<div class="sider-scene-attr-item" v-if="transformRowsVisible.rotation">
|
||||||
<EsKeyFrame :label="t('layout.sider.object.rotation')" attr="quaternion" />
|
<EsKeyFrame :label="t('layout.sider.object.rotation')" attr="quaternion" />
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<EsInputNumber v-model:value="objectData.rotation.x" size="tiny" :decimal="2" :step="1" :show-button="false"
|
<EsInputNumber v-model:value="objectData.rotation.x" size="tiny" :decimal="6" :step="1" :show-button="false"
|
||||||
@change="update('rotation')" unit="°" />
|
@change="update('rotation')" unit="°" />
|
||||||
<EsInputNumber v-model:value="objectData.rotation.y" size="tiny" :decimal="2" :step="1" :show-button="false"
|
<EsInputNumber v-model:value="objectData.rotation.y" size="tiny" :decimal="6" :step="1" :show-button="false"
|
||||||
@change="update('rotation')" unit="°" />
|
@change="update('rotation')" unit="°" />
|
||||||
<EsInputNumber v-model:value="objectData.rotation.z" size="tiny" :decimal="2" :step="1" :show-button="false"
|
<EsInputNumber v-model:value="objectData.rotation.z" size="tiny" :decimal="6" :step="1" :show-button="false"
|
||||||
@change="update('rotation')" unit="°" />
|
@change="update('rotation')" unit="°" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, reactive } from "vue";
|
||||||
|
import { CaretForwardOutline } from "@vicons/ionicons5";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Hooks, Utils, getDefaultPathOptions } from "@astral3d/engine";
|
||||||
|
import EsInputNumber from "@/components/es/EsInputNumber.vue";
|
||||||
|
|
||||||
|
const defaultOptions = getDefaultPathOptions();
|
||||||
|
const pathData = reactive(getDefaultPathOptions() as any);
|
||||||
|
const isTubeMode = computed(() => pathData.mode === "tube");
|
||||||
|
|
||||||
|
function getPathObject() {
|
||||||
|
const object = App.selected;
|
||||||
|
if (!object) return null;
|
||||||
|
if (object.type !== "Path") return null;
|
||||||
|
return object as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDataShape() {
|
||||||
|
if (!pathData.path) {
|
||||||
|
pathData.path = JSON.parse(JSON.stringify(defaultOptions.path));
|
||||||
|
}
|
||||||
|
if (!pathData.tube) {
|
||||||
|
pathData.tube = JSON.parse(JSON.stringify(defaultOptions.tube));
|
||||||
|
}
|
||||||
|
if (!pathData.flow) {
|
||||||
|
pathData.flow = JSON.parse(JSON.stringify(defaultOptions.flow));
|
||||||
|
}
|
||||||
|
if (!Array.isArray(pathData.flow.direction)) {
|
||||||
|
pathData.flow.direction = defaultOptions.flow.direction.slice();
|
||||||
|
}
|
||||||
|
if (typeof pathData.cornerRadius !== "number") {
|
||||||
|
pathData.cornerRadius = defaultOptions.cornerRadius;
|
||||||
|
}
|
||||||
|
if (typeof pathData.cornerSplit !== "number") {
|
||||||
|
pathData.cornerSplit = defaultOptions.cornerSplit;
|
||||||
|
}
|
||||||
|
if (typeof pathData.closed !== "boolean") {
|
||||||
|
pathData.closed = defaultOptions.closed;
|
||||||
|
}
|
||||||
|
if (!pathData.mode) {
|
||||||
|
pathData.mode = defaultOptions.mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
const object = getPathObject();
|
||||||
|
if (!object) return;
|
||||||
|
Utils.deepAssign(pathData, object.options);
|
||||||
|
ensureDataShape();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOptions() {
|
||||||
|
const object = getPathObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
object.updateOptions({
|
||||||
|
mode: pathData.mode,
|
||||||
|
closed: pathData.closed,
|
||||||
|
cornerRadius: pathData.cornerRadius,
|
||||||
|
cornerSplit: pathData.cornerSplit,
|
||||||
|
path: { ...pathData.path },
|
||||||
|
tube: { ...pathData.tube },
|
||||||
|
flow: {
|
||||||
|
...pathData.flow,
|
||||||
|
direction: Array.isArray(pathData.flow.direction) ? pathData.flow.direction.slice() : defaultOptions.flow.direction.slice(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Hooks.useDispatchSignal("objectChanged", object);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Hooks.useAddSignal("objectSelected", updateUI);
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
Hooks.useRemoveSignal("objectSelected", updateUI);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-collapse display-directive="show" :default-expanded-names="['base', 'shape', 'flow']">
|
||||||
|
<template #arrow>
|
||||||
|
<n-icon>
|
||||||
|
<CaretForwardOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.path.Path Params')" name="base">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Type") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="pathData.mode"
|
||||||
|
size="small"
|
||||||
|
:options="[
|
||||||
|
{ label: t('layout.sider.path.Ribbon'), value: 'path' },
|
||||||
|
{ label: t('layout.sider.path.Tube'), value: 'tube' },
|
||||||
|
]"
|
||||||
|
@update:value="updateOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Corner Radius") }}</span>
|
||||||
|
<EsInputNumber v-model:value="pathData.cornerRadius" size="small" :min="0" :max="100" :decimal="2" :show-button="false" @change="updateOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Corner Split") }}</span>
|
||||||
|
<EsInputNumber v-model:value="pathData.cornerSplit" size="small" :min="0" :max="50" :decimal="0" :show-button="false" @change="updateOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.scene.Closed") }}</span>
|
||||||
|
<n-checkbox size="small" v-model:checked="pathData.closed" @update:checked="updateOptions" />
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item v-if="!isTubeMode" :title="t('layout.sider.path.Ribbon Params')" name="shape">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("other.Width") }}</span>
|
||||||
|
<EsInputNumber v-model:value="pathData.path.width" size="small" :min="0.01" :max="100" :decimal="2" :show-button="false" @change="updateOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Progress") }}</span>
|
||||||
|
<n-slider v-model:value="pathData.path.progress" :min="0" :max="1" :step="0.01" @update:value="updateOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Arrow") }}</span>
|
||||||
|
<n-switch size="small" v-model:value="pathData.path.arrow" @update:value="updateOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Side") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="pathData.path.side"
|
||||||
|
size="small"
|
||||||
|
:options="[
|
||||||
|
{ label: t('layout.sider.path.Both'), value: 'both' },
|
||||||
|
{ label: t('layout.sider.path.Left'), value: 'left' },
|
||||||
|
{ label: t('layout.sider.path.Right'), value: 'right' },
|
||||||
|
]"
|
||||||
|
@update:value="updateOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item v-else :title="t('layout.sider.path.Tube Params')" name="shape">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.scene.Radius") }}</span>
|
||||||
|
<EsInputNumber v-model:value="pathData.tube.radius" size="small" :min="0.01" :max="100" :decimal="2" :show-button="false" @change="updateOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.scene['Radial segments']") }}</span>
|
||||||
|
<EsInputNumber v-model:value="pathData.tube.radialSegments" size="small" :min="2" :max="64" :decimal="0" :show-button="false" @change="updateOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Progress") }}</span>
|
||||||
|
<n-slider v-model:value="pathData.tube.progress" :min="0" :max="1" :step="0.01" @update:value="updateOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Start Rad") }}</span>
|
||||||
|
<EsInputNumber v-model:value="pathData.tube.startRad" size="small" :min="-6.28" :max="6.28" :decimal="2" :show-button="false" @change="updateOptions" />
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.path.Flow Params')" name="flow">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Flow") }}</span>
|
||||||
|
<n-switch size="small" v-model:value="pathData.flow.enabled" @update:value="updateOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Flow Speed") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="pathData.flow.speed"
|
||||||
|
size="small"
|
||||||
|
:min="-10"
|
||||||
|
:max="10"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
:disabled="!pathData.flow.enabled"
|
||||||
|
@change="updateOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.path.Flow Direction") }}</span>
|
||||||
|
<div class="flex">
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="pathData.flow.direction[0]"
|
||||||
|
size="small"
|
||||||
|
:min="-1"
|
||||||
|
:max="1"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
:disabled="!pathData.flow.enabled"
|
||||||
|
@change="updateOptions"
|
||||||
|
/>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="pathData.flow.direction[1]"
|
||||||
|
size="small"
|
||||||
|
:min="-1"
|
||||||
|
:max="1"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
:disabled="!pathData.flow.enabled"
|
||||||
|
@change="updateOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||||
|
import { CaretForwardOutline } from "@vicons/ionicons5";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Hooks, UIPanelBlock, UIPanelText, Utils } from "@astral3d/engine";
|
||||||
|
import SidebarUIPanelBlock from "./uipanel/Sidebar.UIPanel.Block.vue";
|
||||||
|
import SidebarUIPanelText from "./uipanel/Sidebar.UIPanel.Text.vue";
|
||||||
|
|
||||||
|
type UIPanelFormType = "Block" | "Text";
|
||||||
|
type UIPanelStateMap = Record<string, Record<string, any>>;
|
||||||
|
|
||||||
|
const UIPANEL_TYPES = new Set(["UIPanel", "UIPanelBlock", "UIPanelText", "UIPanelInline", "UIPanelInlineBlock"]);
|
||||||
|
// 只存储是否有选中对象的状态,3D 对象完全脱离 Vue 响应式系统
|
||||||
|
const hasSelection = ref(false);
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
type: "Block" as UIPanelFormType,
|
||||||
|
props: {} as Record<string, any>,
|
||||||
|
states: {} as UIPanelStateMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
function getUIPanelContext() {
|
||||||
|
const object = App.selected as any;
|
||||||
|
if (!object || !UIPANEL_TYPES.has(object.type)) return null;
|
||||||
|
const panel = Utils.findUIPanelRoot(object);
|
||||||
|
if (!panel) return null;
|
||||||
|
return { panel, object };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFormData() {
|
||||||
|
if (!formData.props || typeof formData.props !== "object") formData.props = {};
|
||||||
|
if (!formData.states || typeof formData.states !== "object") formData.states = {};
|
||||||
|
|
||||||
|
if (formData.type === "Block") {
|
||||||
|
if (!Number.isFinite(formData.props.width)) formData.props.width = 1;
|
||||||
|
if (!Number.isFinite(formData.props.height)) formData.props.height = 0.4;
|
||||||
|
if (!Number.isFinite(formData.props.padding)) formData.props.padding = 0.04;
|
||||||
|
if (!Number.isFinite(formData.props.margin)) formData.props.margin = 0;
|
||||||
|
if (!Number.isFinite(formData.props.offset)) formData.props.offset = 0.005;
|
||||||
|
if (!Number.isFinite(formData.props.backgroundOpacity)) formData.props.backgroundOpacity = 0.8;
|
||||||
|
if (!Number.isFinite(formData.props.borderRadius)) formData.props.borderRadius = 0.04;
|
||||||
|
if (!Number.isFinite(formData.props.borderWidth)) formData.props.borderWidth = 0;
|
||||||
|
if (!formData.props.backgroundColor) formData.props.backgroundColor = "#2a2a2a";
|
||||||
|
if (!formData.props.borderColor) formData.props.borderColor = "#ffffff";
|
||||||
|
if (!formData.props.flexDirection) formData.props.flexDirection = "column";
|
||||||
|
if (!formData.props.justifyContent) formData.props.justifyContent = "center";
|
||||||
|
if (!formData.props.alignItems) formData.props.alignItems = "center";
|
||||||
|
if (!formData.props.backgroundSize) formData.props.backgroundSize = "cover";
|
||||||
|
if (!formData.props.overflow) formData.props.overflow = "visible";
|
||||||
|
if (!formData.props.bestFit) formData.props.bestFit = "none";
|
||||||
|
} else {
|
||||||
|
if (!formData.props.textContent) formData.props.textContent = "";
|
||||||
|
// Text 类型的 width/height 使用 "auto" 表示自动尺寸,
|
||||||
|
// 若 props 中未定义或为 "auto",则保持 "auto";仅当为有限数值时显示具体值
|
||||||
|
if (formData.props.width !== "auto" && !Number.isFinite(formData.props.width)) formData.props.width = "auto";
|
||||||
|
if (formData.props.height !== "auto" && !Number.isFinite(formData.props.height)) formData.props.height = "auto";
|
||||||
|
if (!Number.isFinite(formData.props.fontSize)) formData.props.fontSize = 0.05;
|
||||||
|
if (!Number.isFinite(formData.props.fontOpacity)) formData.props.fontOpacity = 1;
|
||||||
|
if (!Number.isFinite(formData.props.letterSpacing)) formData.props.letterSpacing = 0;
|
||||||
|
if (!Number.isFinite(formData.props.lineHeight)) formData.props.lineHeight = 1.2;
|
||||||
|
if (!Number.isFinite(formData.props.padding)) formData.props.padding = 0;
|
||||||
|
if (!Number.isFinite(formData.props.margin)) formData.props.margin = 0;
|
||||||
|
if (!Number.isFinite(formData.props.offset)) formData.props.offset = 0.005;
|
||||||
|
if (!formData.props.color) formData.props.color = "#ffffff";
|
||||||
|
if (!formData.props.textAlign) formData.props.textAlign = "center";
|
||||||
|
if (!formData.props.fontKerning) formData.props.fontKerning = "normal";
|
||||||
|
if (!formData.props.whiteSpace) formData.props.whiteSpace = "pre-line";
|
||||||
|
if (!formData.props.fontSmooth) formData.props.fontSmooth = "antialiased";
|
||||||
|
if (!formData.props.breakOn) formData.props.breakOn = "- ,.:?!\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
const context = getUIPanelContext();
|
||||||
|
if (!context) {
|
||||||
|
hasSelection.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelection.value = true;
|
||||||
|
|
||||||
|
const options = context.object.options || {};
|
||||||
|
formData.type = context.object.type === "UIPanelText" || context.object.type === "UIPanelInline" ? "Text" : "Block";
|
||||||
|
formData.props = JSON.parse(JSON.stringify(options.props || {}));
|
||||||
|
formData.states = JSON.parse(JSON.stringify(options.states || {}));
|
||||||
|
normalizeFormData();
|
||||||
|
}
|
||||||
|
|
||||||
|
const canAddNodes = computed(() => {
|
||||||
|
if (!hasSelection.value) return false;
|
||||||
|
const object = App.selected as any;
|
||||||
|
return object && (object.type === "UIPanel" || object.type === "UIPanelBlock");
|
||||||
|
});
|
||||||
|
|
||||||
|
function getAddParent() {
|
||||||
|
const object = App.selected as any;
|
||||||
|
if (object && (object.type === "UIPanel" || object.type === "UIPanelBlock")) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
return Utils.findUIPanelRoot(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBlock() {
|
||||||
|
return new UIPanelBlock({
|
||||||
|
name: "Block",
|
||||||
|
props: {
|
||||||
|
width: 1,
|
||||||
|
height: 0.4,
|
||||||
|
padding: 0.04,
|
||||||
|
backgroundColor: "#2a2a2a",
|
||||||
|
backgroundOpacity: 0.8,
|
||||||
|
borderRadius: 0.04,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createText() {
|
||||||
|
return new UIPanelText({
|
||||||
|
name: "Text",
|
||||||
|
props: {
|
||||||
|
textContent: "",
|
||||||
|
fontSize: 0.05,
|
||||||
|
color: "#ffffff",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNode(type: "Block" | "Text") {
|
||||||
|
const parent = getAddParent();
|
||||||
|
if (!parent) return;
|
||||||
|
const node = type === "Block" ? createBlock() : createText();
|
||||||
|
parent.add?.(node);
|
||||||
|
Hooks.useDispatchSignal("objectChanged", parent);
|
||||||
|
App.select(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProps(patch: Record<string, any>) {
|
||||||
|
const object = App.selected as any;
|
||||||
|
if (!object) return;
|
||||||
|
object.setProps?.(patch);
|
||||||
|
Hooks.useDispatchSignal("objectChanged", object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStates() {
|
||||||
|
const object = App.selected as any;
|
||||||
|
if (!object) return;
|
||||||
|
object.setStates?.(formData.states);
|
||||||
|
Hooks.useDispatchSignal("objectChanged", object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStateEnabled(key: string) {
|
||||||
|
return Boolean(formData.states && formData.states[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleState(key: string, enabled: boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
if (!formData.states) formData.states = {};
|
||||||
|
if (!formData.states[key]) {
|
||||||
|
// 首次启用状态时,根据类型设置不同的默认值
|
||||||
|
if (formData.type === "Block") {
|
||||||
|
// Block: 使用当前背景色和透明度 1
|
||||||
|
formData.states[key] = {
|
||||||
|
backgroundColor: formData.props.backgroundColor || "#2a2a2a",
|
||||||
|
backgroundOpacity: 1,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Text: 使用当前文字颜色
|
||||||
|
formData.states[key] = {
|
||||||
|
color: formData.props.color || "#ffffff",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (formData.states) {
|
||||||
|
delete formData.states[key];
|
||||||
|
if (Object.keys(formData.states).length === 0) {
|
||||||
|
formData.states = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStateProps(key: string, patch: Record<string, any>) {
|
||||||
|
if (!formData.states) formData.states = {};
|
||||||
|
if (!formData.states[key]) formData.states[key] = {};
|
||||||
|
formData.states[key] = { ...formData.states[key], ...patch };
|
||||||
|
updateStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Hooks.useAddSignal("objectSelected", updateUI);
|
||||||
|
Hooks.useAddSignal("objectChanged", updateUI);
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
Hooks.useRemoveSignal("objectSelected", updateUI);
|
||||||
|
Hooks.useRemoveSignal("objectChanged", updateUI);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="uipanel-actions">
|
||||||
|
<n-space size="small" align="center" justify="center">
|
||||||
|
<n-button v-if="canAddNodes" @click="addNode('Block')">{{ t("layout.sider.uipanel['Add Block']") }}</n-button>
|
||||||
|
<n-button v-if="canAddNodes" @click="addNode('Text')">{{ t("layout.sider.uipanel['Add Text']") }}</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-collapse display-directive="show" :default-expanded-names="['node']">
|
||||||
|
<template #arrow>
|
||||||
|
<n-icon>
|
||||||
|
<CaretForwardOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.uipanel.Node')" name="node">
|
||||||
|
<div v-if="!hasSelection" class="text-12px opacity-70">
|
||||||
|
{{ t("layout.sider.uipanel['Select a node to edit']") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Type") }}</span>
|
||||||
|
<div class="text-center">{{ formData.type }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SidebarUIPanelBlock
|
||||||
|
v-if="formData.type === 'Block'"
|
||||||
|
:form-data="formData"
|
||||||
|
:is-state-enabled="isStateEnabled"
|
||||||
|
:toggle-state="toggleState"
|
||||||
|
:update-props="updateProps"
|
||||||
|
:update-state-props="updateStateProps"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SidebarUIPanelText
|
||||||
|
v-else
|
||||||
|
:form-data="formData"
|
||||||
|
:is-state-enabled="isStateEnabled"
|
||||||
|
:toggle-state="toggleState"
|
||||||
|
:update-props="updateProps"
|
||||||
|
:update-state-props="updateStateProps"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.uipanel-actions {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,687 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, toRaw } from "vue";
|
||||||
|
import { CaretForwardOutline } from "@vicons/ionicons5";
|
||||||
|
import { Color, CubeTexture, Texture } from "three";
|
||||||
|
import { App, Hooks, Water, WaterPool } from "@astral3d/engine";
|
||||||
|
import EsInputNumber from "@/components/es/EsInputNumber.vue";
|
||||||
|
import EsTexture from "@/components/es/EsTexture.vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const poolData = reactive({
|
||||||
|
type: "cylinder",
|
||||||
|
light: { x: 0.7, y: 1, z: -0.3 },
|
||||||
|
wallMode: "volume",
|
||||||
|
wallOpacity: 0.3,
|
||||||
|
disturbanceEnabled: true,
|
||||||
|
disturbanceMode: "drag",
|
||||||
|
disturbanceDropsPerStep: 1,
|
||||||
|
disturbanceRadiusMin: 0.01,
|
||||||
|
disturbanceRadiusMax: 0.05,
|
||||||
|
disturbanceStrengthMin: 0.005,
|
||||||
|
disturbanceStrengthMax: 0.02,
|
||||||
|
disturbanceTravelRadius: 0.85,
|
||||||
|
disturbanceDriftSpeed: 0.015,
|
||||||
|
disturbanceJitter: 0.01,
|
||||||
|
disturbanceSpread: 0.08,
|
||||||
|
useSceneRefraction: true,
|
||||||
|
surfaceTransmittance: 0.65,
|
||||||
|
normalStrength: 0.6,
|
||||||
|
refractionStrength: 0.035,
|
||||||
|
simulationSize: 256,
|
||||||
|
causticsSize: 1024,
|
||||||
|
waterSegments: 200,
|
||||||
|
wallSegments: 64,
|
||||||
|
diameter: 2,
|
||||||
|
width: 2,
|
||||||
|
depth: 2,
|
||||||
|
height: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tilesTexture = ref<Color | Texture | CubeTexture | null>(null);
|
||||||
|
const volumeColor = ref("#2c5965");
|
||||||
|
const surfaceColor = ref("#4a8aa0");
|
||||||
|
const rebuilding = ref(false);
|
||||||
|
let qualityTimer: number | null = null;
|
||||||
|
|
||||||
|
const isCylinder = computed(() => poolData.type === "cylinder");
|
||||||
|
|
||||||
|
function getWaterPoolObject() {
|
||||||
|
const object = App.selected;
|
||||||
|
if (!object) return null;
|
||||||
|
if (object.type !== "WaterPool") return null;
|
||||||
|
return object as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncColors(object) {
|
||||||
|
const volume = object.volumeColor instanceof Color ? object.volumeColor : new Color(object.volumeColor ?? 0x2c5965);
|
||||||
|
const surface = object.surfaceColor instanceof Color ? object.surfaceColor : new Color(object.surfaceColor ?? volume.getHex());
|
||||||
|
volumeColor.value = `#${volume.getHexString()}`;
|
||||||
|
surfaceColor.value = `#${surface.getHexString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
const object = getWaterPoolObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
poolData.type = object.poolType === "cylinder" ? "cylinder" : "square";
|
||||||
|
poolData.light.x = object.light?.[0] ?? 0.7;
|
||||||
|
poolData.light.y = object.light?.[1] ?? 1;
|
||||||
|
poolData.light.z = object.light?.[2] ?? -0.3;
|
||||||
|
poolData.wallMode = object.wallMode ?? (object.hasWall ? "wall" : "none");
|
||||||
|
poolData.wallOpacity = Number.isFinite(object.wallOpacity) ? object.wallOpacity : 1;
|
||||||
|
const disturbance = object._disturbance || null;
|
||||||
|
if (disturbance) {
|
||||||
|
poolData.disturbanceEnabled = true;
|
||||||
|
poolData.disturbanceMode = disturbance.mode === "uniform" ? "uniform" : "drag";
|
||||||
|
poolData.disturbanceDropsPerStep = disturbance.dropsPerStep ?? 1;
|
||||||
|
poolData.disturbanceRadiusMin = disturbance.radiusMin ?? 0.01;
|
||||||
|
poolData.disturbanceRadiusMax = disturbance.radiusMax ?? 0.05;
|
||||||
|
poolData.disturbanceStrengthMin = disturbance.strengthMin ?? 0.005;
|
||||||
|
poolData.disturbanceStrengthMax = disturbance.strengthMax ?? 0.02;
|
||||||
|
poolData.disturbanceTravelRadius = disturbance.travelRadius ?? 0.85;
|
||||||
|
poolData.disturbanceDriftSpeed = disturbance.driftSpeed ?? 0.015;
|
||||||
|
poolData.disturbanceJitter = disturbance.jitter ?? 0.01;
|
||||||
|
poolData.disturbanceSpread = disturbance.spread ?? 0.08;
|
||||||
|
} else {
|
||||||
|
poolData.disturbanceEnabled = true;
|
||||||
|
poolData.disturbanceMode = "drag";
|
||||||
|
poolData.disturbanceDropsPerStep = 1;
|
||||||
|
poolData.disturbanceRadiusMin = 0.01;
|
||||||
|
poolData.disturbanceRadiusMax = 0.05;
|
||||||
|
poolData.disturbanceStrengthMin = 0.005;
|
||||||
|
poolData.disturbanceStrengthMax = 0.02;
|
||||||
|
poolData.disturbanceTravelRadius = 0.85;
|
||||||
|
poolData.disturbanceDriftSpeed = 0.015;
|
||||||
|
poolData.disturbanceJitter = 0.01;
|
||||||
|
poolData.disturbanceSpread = 0.08;
|
||||||
|
}
|
||||||
|
poolData.useSceneRefraction = !!object.useSceneRefraction;
|
||||||
|
poolData.surfaceTransmittance = Number.isFinite(object.surfaceTransmittance) ? object.surfaceTransmittance : 0.65;
|
||||||
|
poolData.normalStrength = Number.isFinite(object.normalStrength) ? object.normalStrength : 0.6;
|
||||||
|
poolData.refractionStrength = Number.isFinite(object.refractionStrength) ? object.refractionStrength : 0.035;
|
||||||
|
poolData.simulationSize = object.simulationSize ?? 256;
|
||||||
|
poolData.causticsSize = object.causticsSize ?? 1024;
|
||||||
|
poolData.waterSegments = object.waterSegments ?? 200;
|
||||||
|
poolData.wallSegments = object.wallSegments ?? 64;
|
||||||
|
poolData.height = Number.isFinite(object.height) ? object.height : (object._poolHeight ?? 2);
|
||||||
|
|
||||||
|
if (poolData.type === "cylinder") {
|
||||||
|
poolData.diameter = Number.isFinite(object.diameter) ? object.diameter : object._radius ? object._radius * 2 : 2;
|
||||||
|
} else {
|
||||||
|
poolData.width = Number.isFinite(object.width) ? object.width : 2;
|
||||||
|
poolData.depth = Number.isFinite(object.depth) ? object.depth : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
tilesTexture.value = object.tiles || null;
|
||||||
|
syncColors(object);
|
||||||
|
applyDisturbance();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestRender(object) {
|
||||||
|
Hooks.useDispatchSignal("objectChanged", object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLight() {
|
||||||
|
const object = getWaterPoolObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
const light = [poolData.light.x, poolData.light.y, poolData.light.z];
|
||||||
|
object.light = light;
|
||||||
|
|
||||||
|
const materials = typeof object._getWaterMaterials === "function" ? object._getWaterMaterials() : [];
|
||||||
|
materials.forEach(material => {
|
||||||
|
if (material?.uniforms?.light) material.uniforms.light.value = light;
|
||||||
|
});
|
||||||
|
|
||||||
|
const wallMaterial = object.wall?._material;
|
||||||
|
if (wallMaterial?.uniforms?.light) wallMaterial.uniforms.light.value = light;
|
||||||
|
|
||||||
|
const causticsMaterial = typeof object._getCausticsMaterial === "function" ? object._getCausticsMaterial() : null;
|
||||||
|
if (causticsMaterial?.uniforms?.light) causticsMaterial.uniforms.light.value = light;
|
||||||
|
|
||||||
|
requestRender(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySize() {
|
||||||
|
const object = getWaterPoolObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
if (poolData.type === "cylinder") {
|
||||||
|
object.setSize({ diameter: poolData.diameter, height: poolData.height });
|
||||||
|
} else {
|
||||||
|
object.setSize({ width: poolData.width, depth: poolData.depth, height: poolData.height });
|
||||||
|
}
|
||||||
|
|
||||||
|
requestRender(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTiles() {
|
||||||
|
const object = getWaterPoolObject();
|
||||||
|
if (!object) return;
|
||||||
|
if (poolData.wallMode !== "wall") return;
|
||||||
|
|
||||||
|
const texture = (toRaw(tilesTexture.value) as any) || null;
|
||||||
|
object.tiles = texture;
|
||||||
|
|
||||||
|
const materials = typeof object._getWaterMaterials === "function" ? object._getWaterMaterials() : [];
|
||||||
|
materials.forEach(material => {
|
||||||
|
if (material?.uniforms?.tiles) material.uniforms.tiles.value = texture;
|
||||||
|
if (material?.uniforms?.useTiles) material.uniforms.useTiles.value = texture ? 1 : 0;
|
||||||
|
if (material?.uniforms?.tilesColor) material.uniforms.tilesColor.value = object.volumeColor;
|
||||||
|
});
|
||||||
|
|
||||||
|
const wallMaterial = object.wall?._material;
|
||||||
|
if (wallMaterial?.uniforms?.tiles) wallMaterial.uniforms.tiles.value = texture;
|
||||||
|
if (wallMaterial?.uniforms?.useTiles) wallMaterial.uniforms.useTiles.value = texture ? 1 : 0;
|
||||||
|
if (wallMaterial?.uniforms?.tilesColor) wallMaterial.uniforms.tilesColor.value = object.volumeColor;
|
||||||
|
|
||||||
|
requestRender(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyColors() {
|
||||||
|
const object = getWaterPoolObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
object.volumeColor = new Color(volumeColor.value || "#2c5965");
|
||||||
|
object.surfaceColor = new Color(surfaceColor.value || volumeColor.value || "#2c5965");
|
||||||
|
|
||||||
|
if (typeof object._applyUniforms === "function") {
|
||||||
|
object._applyUniforms();
|
||||||
|
}
|
||||||
|
|
||||||
|
requestRender(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRefraction() {
|
||||||
|
const object = getWaterPoolObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
object.useSceneRefraction = poolData.useSceneRefraction ? 1 : 0;
|
||||||
|
object.surfaceTransmittance = poolData.surfaceTransmittance;
|
||||||
|
object.normalStrength = poolData.normalStrength;
|
||||||
|
object.refractionStrength = poolData.refractionStrength;
|
||||||
|
|
||||||
|
if (typeof object._applyUniforms === "function") {
|
||||||
|
object._applyUniforms();
|
||||||
|
}
|
||||||
|
|
||||||
|
requestRender(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDisturbance() {
|
||||||
|
const object = getWaterPoolObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
if (!poolData.disturbanceEnabled) {
|
||||||
|
object.stopDisturbance?.();
|
||||||
|
requestRender(object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object.startDisturbance?.({
|
||||||
|
mode: poolData.disturbanceMode === "uniform" ? "uniform" : "drag",
|
||||||
|
dropsPerStep: Math.max(1, Math.round(poolData.disturbanceDropsPerStep)),
|
||||||
|
radiusMin: Math.max(0, poolData.disturbanceRadiusMin),
|
||||||
|
radiusMax: Math.max(poolData.disturbanceRadiusMin, poolData.disturbanceRadiusMax),
|
||||||
|
strengthMin: poolData.disturbanceStrengthMin,
|
||||||
|
strengthMax: poolData.disturbanceStrengthMax,
|
||||||
|
travelRadius: Math.max(0, poolData.disturbanceTravelRadius),
|
||||||
|
driftSpeed: Math.max(0, poolData.disturbanceDriftSpeed),
|
||||||
|
jitter: Math.max(0, poolData.disturbanceJitter),
|
||||||
|
spread: Math.max(0, poolData.disturbanceSpread),
|
||||||
|
});
|
||||||
|
|
||||||
|
requestRender(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWallOpacity() {
|
||||||
|
const object = getWaterPoolObject();
|
||||||
|
if (!object) return;
|
||||||
|
|
||||||
|
object.wallOpacity = poolData.wallOpacity;
|
||||||
|
if (typeof object._applyUniforms === "function") {
|
||||||
|
object._applyUniforms();
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallMaterial = object.wall?._material;
|
||||||
|
if (wallMaterial) {
|
||||||
|
const transparent = object.wallOpacity < 1;
|
||||||
|
wallMaterial.transparent = transparent;
|
||||||
|
wallMaterial.depthWrite = !transparent;
|
||||||
|
wallMaterial.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestRender(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOptionsFromUI(object) {
|
||||||
|
const options: any = {
|
||||||
|
name: object?.name || "WaterPool",
|
||||||
|
type: poolData.type,
|
||||||
|
light: [poolData.light.x, poolData.light.y, poolData.light.z],
|
||||||
|
tiles: poolData.wallMode === "wall" ? (toRaw(tilesTexture.value) as any) || null : object?.tiles || null,
|
||||||
|
sky: object?.sky || null,
|
||||||
|
wallMode: poolData.wallMode,
|
||||||
|
wall: poolData.wallMode !== "none",
|
||||||
|
wallOpacity: poolData.wallOpacity,
|
||||||
|
useSceneRefraction: poolData.useSceneRefraction ? 1 : 0,
|
||||||
|
surfaceTransmittance: poolData.surfaceTransmittance,
|
||||||
|
normalStrength: poolData.normalStrength,
|
||||||
|
refractionStrength: poolData.refractionStrength,
|
||||||
|
volumeColor: new Color(volumeColor.value || "#2c5965"),
|
||||||
|
surfaceColor: new Color(surfaceColor.value || volumeColor.value || "#2c5965"),
|
||||||
|
simulationSize: Math.max(16, Math.round(poolData.simulationSize)),
|
||||||
|
causticsSize: Math.max(64, Math.round(poolData.causticsSize)),
|
||||||
|
waterSegments: Math.max(1, Math.round(poolData.waterSegments)),
|
||||||
|
wallSegments: Math.max(3, Math.round(poolData.wallSegments)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (poolData.type === "cylinder") {
|
||||||
|
options.diameter = poolData.diameter;
|
||||||
|
} else {
|
||||||
|
options.width = poolData.width;
|
||||||
|
options.depth = poolData.depth;
|
||||||
|
}
|
||||||
|
options.height = poolData.height;
|
||||||
|
|
||||||
|
if (object && Number.isFinite(object._renderOrderBase)) {
|
||||||
|
options.renderOrder = object._renderOrderBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rebuildPool() {
|
||||||
|
const object = getWaterPoolObject();
|
||||||
|
if (!object || rebuilding.value) return;
|
||||||
|
|
||||||
|
rebuilding.value = true;
|
||||||
|
try {
|
||||||
|
const options = buildOptionsFromUI(object);
|
||||||
|
|
||||||
|
const newPool = new WaterPool(options);
|
||||||
|
await newPool.loaded;
|
||||||
|
|
||||||
|
newPool.name = object.name;
|
||||||
|
newPool.position.copy(object.position);
|
||||||
|
newPool.quaternion.copy(object.quaternion);
|
||||||
|
newPool.scale.copy(object.scale);
|
||||||
|
newPool.visible = object.visible;
|
||||||
|
newPool.layers.mask = object.layers.mask;
|
||||||
|
newPool.userData = { ...object.userData };
|
||||||
|
|
||||||
|
const parent = object.parent ?? App.scene;
|
||||||
|
if (parent) {
|
||||||
|
const oldIndex = parent.children.indexOf(object);
|
||||||
|
parent.add(newPool);
|
||||||
|
const newIndex = parent.children.indexOf(newPool);
|
||||||
|
if (oldIndex >= 0 && newIndex >= 0 && newIndex !== oldIndex) {
|
||||||
|
parent.children.splice(newIndex, 1);
|
||||||
|
parent.children.splice(oldIndex, 0, newPool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Water.registerRendering();
|
||||||
|
|
||||||
|
object.parent?.remove(object);
|
||||||
|
|
||||||
|
App.select(newPool);
|
||||||
|
requestRender(newPool);
|
||||||
|
} catch (error) {
|
||||||
|
// 避免阻断侧边栏操作
|
||||||
|
console.warn("WaterPool 重建失败:", error);
|
||||||
|
} finally {
|
||||||
|
rebuilding.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWallMode() {
|
||||||
|
rebuildPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyType() {
|
||||||
|
rebuildPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyQuality() {
|
||||||
|
rebuildPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleQualityRebuild() {
|
||||||
|
if (qualityTimer !== null) {
|
||||||
|
clearTimeout(qualityTimer);
|
||||||
|
}
|
||||||
|
qualityTimer = window.setTimeout(() => {
|
||||||
|
qualityTimer = null;
|
||||||
|
applyQuality();
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Hooks.useAddSignal("objectSelected", updateUI);
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (qualityTimer !== null) {
|
||||||
|
clearTimeout(qualityTimer);
|
||||||
|
qualityTimer = null;
|
||||||
|
}
|
||||||
|
Hooks.useRemoveSignal("objectSelected", updateUI);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-collapse display-directive="show" :default-expanded-names="['basic', 'size', 'light', 'color', 'disturbance', 'refraction', 'texture', 'quality']">
|
||||||
|
<template #arrow>
|
||||||
|
<n-icon>
|
||||||
|
<CaretForwardOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.waterPool.Basic')" name="basic">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.Type") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="poolData.type"
|
||||||
|
size="small"
|
||||||
|
:options="[
|
||||||
|
{ label: t('layout.sider.waterPool.Cylinder'), value: 'cylinder' },
|
||||||
|
{ label: t('layout.sider.waterPool.Square'), value: 'square' },
|
||||||
|
]"
|
||||||
|
:disabled="rebuilding"
|
||||||
|
@update:value="applyType"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.WallMode") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="poolData.wallMode"
|
||||||
|
size="small"
|
||||||
|
:options="[
|
||||||
|
{ label: t('layout.sider.waterPool.WallNone'), value: 'none' },
|
||||||
|
{ label: t('layout.sider.waterPool.Wall'), value: 'wall' },
|
||||||
|
{ label: t('layout.sider.waterPool.Volume'), value: 'volume' },
|
||||||
|
]"
|
||||||
|
:disabled="rebuilding"
|
||||||
|
@update:value="applyWallMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.WallOpacity") }}</span>
|
||||||
|
<n-slider
|
||||||
|
v-model:value="poolData.wallOpacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:disabled="poolData.wallMode === 'none'"
|
||||||
|
@update:value="applyWallOpacity"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.ScreenRefraction") }}</span>
|
||||||
|
<n-switch size="small" v-model:value="poolData.useSceneRefraction" @update:value="applyRefraction" />
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.waterPool.Size')" name="size">
|
||||||
|
<div class="sidebar-config-item" v-if="isCylinder">
|
||||||
|
<span>{{ t("layout.sider.waterPool.Diameter") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.diameter"
|
||||||
|
size="small"
|
||||||
|
:min="0.1"
|
||||||
|
:max="Infinity"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applySize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item" v-else>
|
||||||
|
<span>{{ t("layout.sider.waterPool.Width") }}</span>
|
||||||
|
<EsInputNumber v-model:value="poolData.width" size="small" :min="0.1" :max="Infinity" :decimal="2" :show-button="false" @change="applySize" />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item" v-if="!isCylinder">
|
||||||
|
<span>{{ t("layout.sider.waterPool.Depth") }}</span>
|
||||||
|
<EsInputNumber v-model:value="poolData.depth" size="small" :min="0.1" :max="Infinity" :decimal="2" :show-button="false" @change="applySize" />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.Height") }}</span>
|
||||||
|
<EsInputNumber v-model:value="poolData.height" size="small" :min="0.1" :max="Infinity" :decimal="2" :show-button="false" @change="applySize" />
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.waterPool.Light')" name="light">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.Direction") }}</span>
|
||||||
|
<div class="flex gap-6px">
|
||||||
|
<EsInputNumber v-model:value="poolData.light.x" size="small" :min="-1" :max="1" :decimal="2" :show-button="false" @change="applyLight" />
|
||||||
|
<EsInputNumber v-model:value="poolData.light.y" size="small" :min="-1" :max="1" :decimal="2" :show-button="false" @change="applyLight" />
|
||||||
|
<EsInputNumber v-model:value="poolData.light.z" size="small" :min="-1" :max="1" :decimal="2" :show-button="false" @change="applyLight" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.waterPool.Color')" name="color">
|
||||||
|
<div class="sidebar-config-item" v-if="poolData.wallMode !== 'none'">
|
||||||
|
<span>{{ t("layout.sider.waterPool.VolumeColor") }}</span>
|
||||||
|
<n-color-picker v-model:value="volumeColor" :show-alpha="false" :modes="['hex']" size="small" @update:value="applyColors" />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.SurfaceColor") }}</span>
|
||||||
|
<n-color-picker v-model:value="surfaceColor" :show-alpha="false" :modes="['hex']" size="small" @update:value="applyColors" />
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.waterPool.Disturbance')" name="disturbance">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.Enabled") }}</span>
|
||||||
|
<n-switch size="small" v-model:value="poolData.disturbanceEnabled" @update:value="applyDisturbance" />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.Mode") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="poolData.disturbanceMode"
|
||||||
|
size="small"
|
||||||
|
:options="[
|
||||||
|
{ label: t('layout.sider.waterPool.ModeDrag'), value: 'drag' },
|
||||||
|
{ label: t('layout.sider.waterPool.ModeUniform'), value: 'uniform' },
|
||||||
|
]"
|
||||||
|
@update:value="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.DropsPerStep") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.disturbanceDropsPerStep"
|
||||||
|
size="small"
|
||||||
|
:min="1"
|
||||||
|
:max="20"
|
||||||
|
:decimal="0"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.RadiusMin") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.disturbanceRadiusMin"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.RadiusMax") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.disturbanceRadiusMax"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.StrengthMin") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.disturbanceStrengthMin"
|
||||||
|
size="small"
|
||||||
|
:min="-1"
|
||||||
|
:max="1"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.StrengthMax") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.disturbanceStrengthMax"
|
||||||
|
size="small"
|
||||||
|
:min="-1"
|
||||||
|
:max="1"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.TravelRadius") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.disturbanceTravelRadius"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="2"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.DriftSpeed") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.disturbanceDriftSpeed"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.Jitter") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.disturbanceJitter"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.Spread") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.disturbanceSpread"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="applyDisturbance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.waterPool.Refraction')" name="refraction">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.SurfaceTransmittance") }}</span>
|
||||||
|
<n-slider v-model:value="poolData.surfaceTransmittance" :min="0" :max="1" :step="0.01" @update:value="applyRefraction" />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.NormalStrength") }}</span>
|
||||||
|
<n-slider v-model:value="poolData.normalStrength" :min="0" :max="1" :step="0.01" @update:value="applyRefraction" />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.RefractionStrength") }}</span>
|
||||||
|
<n-slider v-model:value="poolData.refractionStrength" :min="0" :max="1" :step="0.001" @update:value="applyRefraction" />
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item v-if="poolData.wallMode === 'wall'" :title="t('layout.sider.waterPool.Texture')" name="texture">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.WallTexture") }}</span>
|
||||||
|
<EsTexture v-model:texture="tilesTexture" width="26px" height="26px" class="ml-5px" @change="applyTiles" />
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item :title="t('layout.sider.waterPool.Quality')" name="quality">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.SimulationSize") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.simulationSize"
|
||||||
|
size="small"
|
||||||
|
:min="16"
|
||||||
|
:max="2048"
|
||||||
|
:decimal="0"
|
||||||
|
:show-button="false"
|
||||||
|
:disabled="rebuilding"
|
||||||
|
@update:value="scheduleQualityRebuild"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.CausticsSize") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.causticsSize"
|
||||||
|
size="small"
|
||||||
|
:min="64"
|
||||||
|
:max="4096"
|
||||||
|
:decimal="0"
|
||||||
|
:show-button="false"
|
||||||
|
:disabled="rebuilding"
|
||||||
|
@update:value="scheduleQualityRebuild"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.WaterSegments") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.waterSegments"
|
||||||
|
size="small"
|
||||||
|
:min="1"
|
||||||
|
:max="1024"
|
||||||
|
:decimal="0"
|
||||||
|
:show-button="false"
|
||||||
|
:disabled="rebuilding"
|
||||||
|
@update:value="scheduleQualityRebuild"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.waterPool.WallSegments") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="poolData.wallSegments"
|
||||||
|
size="small"
|
||||||
|
:min="3"
|
||||||
|
:max="512"
|
||||||
|
:decimal="0"
|
||||||
|
:show-button="false"
|
||||||
|
:disabled="rebuilding"
|
||||||
|
@update:value="scheduleQualityRebuild"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
@@ -28,7 +28,7 @@ function handleAfterimageConfigChange(){
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-config-item">
|
<div class="sidebar-config-item">
|
||||||
<span>{{ t(`other.Enable`) }}</span>
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
<div>
|
<div>
|
||||||
<n-checkbox size="small" v-model:checked="afterimage.enabled" :disabled="!effectEnabled" @update:checked="handleAfterimageConfigChange"/>
|
<n-checkbox size="small" v-model:checked="afterimage.enabled" :disabled="!effectEnabled" @update:checked="handleAfterimageConfigChange"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,3 +46,4 @@ function handleAfterimageConfigChange(){
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function handleBokehConfigChange(){
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-config-item">
|
<div class="sidebar-config-item">
|
||||||
<span>{{ t(`other.Enable`) }}</span>
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
<div>
|
<div>
|
||||||
<n-checkbox size="small" v-model:checked="bokeh.enabled" :disabled="!effectEnabled" @update:checked="handleBokehConfigChange"/>
|
<n-checkbox size="small" v-model:checked="bokeh.enabled" :disabled="!effectEnabled" @update:checked="handleBokehConfigChange"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,3 +63,4 @@ function handleBokehConfigChange(){
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const bc = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.BrightnessContrast"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !bc.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(bc, App.project.getKey("effect.BrightnessContrast"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.BrightnessContrast`, toRaw(bc));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="bc.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 亮度 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Brightness`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="bc.brightness" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对比度 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Contrast`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="bc.contrast" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select size="small" v-model:value="bc.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const ca = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.ChromaticAberration"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !ca.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(ca, App.project.getKey("effect.ChromaticAberration"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.ChromaticAberration`, toRaw(ca));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="ca.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 偏移 X -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Offset X`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ca.offset.x" :step="0.001" :min="0" :max="0.05" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 偏移 Y -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Offset Y`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ca.offset.y" :step="0.001" :min="0" :max="0.05" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 径向调制 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Radial Modulation`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="ca.radialModulation" :disabled="disabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 调制偏移 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Modulation Offset`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ca.modulationOffset" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select size="small" v-model:value="ca.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const colorDepth = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.ColorDepth"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !colorDepth.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(colorDepth, App.project.getKey("effect.ColorDepth"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.ColorDepth`, toRaw(colorDepth));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="colorDepth.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 色深位数 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Bits`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="colorDepth.bits" :step="1" :min="1" :max="32" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select v-model:value="colorDepth.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ function handleFXAAConfigChange(){
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-config-item">
|
<div class="sidebar-config-item">
|
||||||
<span>{{ t(`other.Enable`) }}</span>
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
<div>
|
<div>
|
||||||
<n-checkbox size="small" v-model:checked="faxx.enabled" :disabled="!effectEnabled" @update:checked="handleFXAAConfigChange"/>
|
<n-checkbox size="small" v-model:checked="faxx.enabled" :disabled="!effectEnabled" @update:checked="handleFXAAConfigChange"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,3 +37,4 @@ function handleFXAAConfigChange(){
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions, glitchModeOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const glitch = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.Glitch"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !glitch.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(glitch, App.project.getKey("effect.Glitch"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.Glitch`, toRaw(glitch));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="glitch.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 故障模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Glitch Mode`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select v-model:value="glitch.mode" :options="glitchModeOptions" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 延迟最小值 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Delay Min`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="glitch.delay.min" :step="0.1" :min="0" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 延迟最大值 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Delay Max`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="glitch.delay.max" :step="0.1" :min="0" :max="10" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 持续时间最小值 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Duration Min`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="glitch.duration.min" :step="0.1" :min="0" :max="2" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 持续时间最大值 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Duration Max`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="glitch.duration.max" :step="0.1" :min="0" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 强度最小值 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Strength Min`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="glitch.strength.min" :step="0.1" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 强度最大值 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Strength Max`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="glitch.strength.max" :step="0.1" :min="0" :max="2" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 故障比率 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Ratio`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="glitch.ratio" :step="0.05" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select
|
||||||
|
size="small"
|
||||||
|
v-model:value="glitch.blendFunction"
|
||||||
|
:options="blendFunctionOptions"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ function handleHalftoneConfigChange(){
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-config-item">
|
<div class="sidebar-config-item">
|
||||||
<span>{{ t(`other.Enable`) }}</span>
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
<div>
|
<div>
|
||||||
<n-checkbox size="small" v-model:checked="halftone.enabled" :disabled="!effectEnabled" @update:checked="handleHalftoneConfigChange"/>
|
<n-checkbox size="small" v-model:checked="halftone.enabled" :disabled="!effectEnabled" @update:checked="handleHalftoneConfigChange"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,3 +121,4 @@ function handleHalftoneConfigChange(){
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const hueSat = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.HueSaturation"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !hueSat.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(hueSat, App.project.getKey("effect.HueSaturation"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.HueSaturation`, toRaw(hueSat));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="hueSat.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 色相 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Hue`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="hueSat.hue" :step="0.01" :min="-3.14" :max="3.14" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 饱和度 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Saturation`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="hueSat.saturation" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select v-model:value="hueSat.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ function handleLUTConfigChange(){
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-config-item">
|
<div class="sidebar-config-item">
|
||||||
<span>{{ t(`other.Enable`) }}</span>
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
<div>
|
<div>
|
||||||
<n-checkbox size="small" v-model:checked="lut.enabled" :disabled="!effectEnabled" @update:checked="handleLUTConfigChange"/>
|
<n-checkbox size="small" v-model:checked="lut.enabled" :disabled="!effectEnabled" @update:checked="handleLUTConfigChange"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,3 +56,4 @@ function handleLUTConfigChange(){
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const lens = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.LensDistortion"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !lens.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(lens, App.project.getKey("effect.LensDistortion"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.LensDistortion`, toRaw(lens));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="lens.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 畸变 X -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Distortion X`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="lens.distortion.x" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 畸变 Y -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Distortion Y`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="lens.distortion.y" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主点 X -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Principal Point X`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="lens.principalPoint.x" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主点 Y -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Principal Point Y`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="lens.principalPoint.y" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 焦距 X -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Focal Length X`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="lens.focalLength.x" :step="0.1" :min="0.1" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 焦距 Y -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Focal Length Y`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="lens.focalLength.y" :step="0.1" :min="0.1" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 倾斜 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Skew`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="lens.skew" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ function handleOutlineConfigChange(){
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-config-item">
|
<div class="sidebar-config-item">
|
||||||
<span>{{ t(`other.Enable`) }}</span>
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
<div>
|
<div>
|
||||||
<n-checkbox size="small" v-model:checked="outline.enabled" :disabled="!effectEnabled" @update:checked="handleOutlineConfigChange"/>
|
<n-checkbox size="small" v-model:checked="outline.enabled" :disabled="!effectEnabled" @update:checked="handleOutlineConfigChange"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,3 +86,4 @@ function handleOutlineConfigChange(){
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function handlePixelateConfigChange(){
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-config-item">
|
<div class="sidebar-config-item">
|
||||||
<span>{{ t(`other.Enable`) }}</span>
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
<div>
|
<div>
|
||||||
<n-checkbox size="small" v-model:checked="pixelate.enabled" :disabled="!effectEnabled" @update:checked="handlePixelateConfigChange"/>
|
<n-checkbox size="small" v-model:checked="pixelate.enabled" :disabled="!effectEnabled" @update:checked="handlePixelateConfigChange"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,3 +62,4 @@ function handlePixelateConfigChange(){
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// SMAA (Subpixel Morphological Anti-Aliasing) 替代 FXAA
|
||||||
|
const smaa = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.SMAA"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !smaa.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(smaa, App.project.getKey("effect.SMAA"));
|
||||||
|
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSMAAConfigChange() {
|
||||||
|
App.project.setKey(`effect.SMAA`, toRaw(smaa));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="smaa.enabled" :disabled="!effectEnabled" @update:checked="handleSMAAConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 质量预设 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Quality Preset`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="smaa.preset"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:value="handleSMAAConfigChange"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Low', value: 'LOW' },
|
||||||
|
{ label: 'Medium', value: 'MEDIUM' },
|
||||||
|
{ label: 'High', value: 'HIGH' },
|
||||||
|
{ label: 'Ultra', value: 'ULTRA' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const ssao = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.SSAO"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !ssao.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(ssao, App.project.getKey("effect.SSAO"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 世界空间阈值允许为空,这里在提交前归一化,避免 NaN 污染配置
|
||||||
|
function normalizeOptionalNumber(value: unknown): number | null {
|
||||||
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
ssao.worldDistanceThreshold = normalizeOptionalNumber(ssao.worldDistanceThreshold);
|
||||||
|
ssao.worldDistanceFalloff = normalizeOptionalNumber(ssao.worldDistanceFalloff);
|
||||||
|
ssao.worldProximityThreshold = normalizeOptionalNumber(ssao.worldProximityThreshold);
|
||||||
|
ssao.worldProximityFalloff = normalizeOptionalNumber(ssao.worldProximityFalloff);
|
||||||
|
|
||||||
|
App.project.setKey("effect.SSAO", toRaw(ssao));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="ssao.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select size="small" v-model:value="ssao.blendFunction" :options="blendFunctionOptions" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Samples`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.samples" :step="1" :min="1" :max="64" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Rings`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.rings" :step="1" :min="1" :max="32" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Radius`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.radius" :step="0.001" :min="0.01" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Strength`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.intensity" :step="0.01" :min="0" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Bias`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.bias" :step="0.001" :min="0" :max="0.1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Fade`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.fade" :step="0.001" :min="0" :max="0.1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Luminance Influence`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.luminanceInfluence" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Min Radius Scale`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.minRadiusScale" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Depth-aware Upsampling`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="ssao.depthAwareUpsampling" :disabled="disabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Resolution Scale`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.resolutionScale" :step="0.05" :min="0.1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Distance Threshold`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.distanceThreshold" :step="0.001" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Distance Falloff`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.distanceFalloff" :step="0.001" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Range Threshold`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.rangeThreshold" :step="0.0001" :min="0" :max="0.02" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Range Falloff`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="ssao.rangeFalloff" :step="0.0001" :min="0" :max="0.02" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.World Distance Threshold`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-input-number v-model:value="ssao.worldDistanceThreshold" size="small" :min="0" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.World Distance Falloff`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-input-number v-model:value="ssao.worldDistanceFalloff" size="small" :min="0" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.World Proximity Threshold`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-input-number v-model:value="ssao.worldProximityThreshold" size="small" :min="0" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.World Proximity Falloff`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-input-number v-model:value="ssao.worldProximityFalloff" size="small" :min="0" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Colorize`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="ssao.colorEnabled" :disabled="disabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.AO Color`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="ssao.color"
|
||||||
|
:show-alpha="false"
|
||||||
|
:modes="['hex']"
|
||||||
|
size="small"
|
||||||
|
:disabled="disabled || !ssao.colorEnabled"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const scanline = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.Scanline"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !scanline.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(scanline, App.project.getKey("effect.Scanline"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.Scanline`, toRaw(scanline));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="scanline.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密度 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Density`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="scanline.density" :step="0.05" :min="0.1" :max="5" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 滚动速度 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Scroll Speed`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="scanline.scrollSpeed" :step="0.01" :min="0" :max="2" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select
|
||||||
|
size="small"
|
||||||
|
v-model:value="scanline.blendFunction"
|
||||||
|
:options="blendFunctionOptions"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const shockWave = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.ShockWave"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !shockWave.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(shockWave, App.project.getKey("effect.ShockWave"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.ShockWave`, toRaw(shockWave));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="shockWave.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 振幅 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Amplitude`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="shockWave.amplitude" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 波浪大小 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Wave Size`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="shockWave.waveSize" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 速度 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Speed`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="shockWave.speed" :step="0.1" :min="0.1" :max="10" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最大半径 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Max Radius`) }}</span>
|
||||||
|
<div>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="shockWave.maxRadius"
|
||||||
|
:show-button="false"
|
||||||
|
:decimal="1"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
size="small"
|
||||||
|
:disabled="disabled"
|
||||||
|
@change="handleConfigChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 点击触发 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Click Trigger`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="shockWave.clickTrigger" :disabled="disabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
effectEnabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const tiltShift = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.TiltShift"))));
|
||||||
|
const disabled = computed(() => !props.effectEnabled || !tiltShift.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(tiltShift, App.project.getKey("effect.TiltShift"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.TiltShift`, toRaw(tiltShift));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="tiltShift.enabled" :disabled="!effectEnabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 焦点偏移 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Offset`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="tiltShift.offset" :step="0.01" :min="-1" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 旋转角度 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Rotation`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="tiltShift.rotation" :step="0.01" :min="0" :max="6.28" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 焦点区域 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Focus Area`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="tiltShift.focusArea" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 羽化 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Feather`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="tiltShift.feather" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select
|
||||||
|
size="small"
|
||||||
|
v-model:value="tiltShift.blendFunction"
|
||||||
|
:options="blendFunctionOptions"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { h, onMounted, reactive, toRaw, VNode } from "vue";
|
||||||
|
import { SelectOption, NTooltip } from "naive-ui";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
const defaultToneMapping = {
|
||||||
|
mode: "ACES_FILMIC",
|
||||||
|
exposure: 1,
|
||||||
|
blendFunction: "NORMAL",
|
||||||
|
};
|
||||||
|
|
||||||
|
const toneMapping = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.ToneMapping") || defaultToneMapping)));
|
||||||
|
|
||||||
|
// 色调映射模式选项
|
||||||
|
const toneMappingModeOptions = [
|
||||||
|
{ label: "Linear", value: "LINEAR", tooltip: "线性映射,保持原始颜色" },
|
||||||
|
{ label: "Reinhard", value: "REINHARD", tooltip: "Reinhard 映射,柔和的高光压缩" },
|
||||||
|
{ label: "Reinhard2", value: "REINHARD2", tooltip: "改进的 Reinhard 映射" },
|
||||||
|
{ label: "Optimized Cineon", value: "OPTIMIZED_CINEON", tooltip: "优化的电影风格映射" },
|
||||||
|
{ label: "ACES Filmic", value: "ACES_FILMIC", tooltip: "电影工业标准 ACES 映射" },
|
||||||
|
{ label: "AgX", value: "AGX", tooltip: "AgX 映射,适合户外场景" },
|
||||||
|
{ label: "Neutral", value: "NEUTRAL", tooltip: "中性映射,平衡的色调" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderToneMappingOption = ({ node, option }: { node: VNode; option: SelectOption }) => {
|
||||||
|
return h(NTooltip, null, {
|
||||||
|
trigger: () => node,
|
||||||
|
default: () => (option as any).tooltip || option.label,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(toneMapping, App.project.getKey("effect.ToneMapping") || defaultToneMapping);
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.ToneMapping`, toRaw(toneMapping));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 色调映射模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.rendererConfig.Tone mapping`) || "色调映射" }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="toneMapping.mode"
|
||||||
|
:options="toneMappingModeOptions"
|
||||||
|
:render-option="renderToneMappingOption"
|
||||||
|
size="small"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 曝光度 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Exposure`) || "曝光度" }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="toneMapping.exposure" :step="0.01" :min="0.1" :max="3" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select size="small" v-model:value="toneMapping.blendFunction" :options="blendFunctionOptions" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ function handleUnrealBloomConfigChange(){
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-config-item">
|
<div class="sidebar-config-item">
|
||||||
<span>{{ t(`other.Enable`) }}</span>
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
<div>
|
<div>
|
||||||
<n-checkbox size="small" v-model:checked="unrealBloom.enabled" :disabled="!effectEnabled" @update:checked="handleUnrealBloomConfigChange"/>
|
<n-checkbox size="small" v-model:checked="unrealBloom.enabled" :disabled="!effectEnabled" @update:checked="handleUnrealBloomConfigChange"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,3 +62,4 @@ function handleUnrealBloomConfigChange(){
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, toRaw } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import { App, Utils } from "@astral3d/engine";
|
||||||
|
import { blendFunctionOptions } from "../../../../../utils/common/postprocessing";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
effectEnabled: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const vignette = reactive(JSON.parse(JSON.stringify(App.project.getKey("effect.Vignette"))));
|
||||||
|
const disabled = computed(() => !vignette.enabled);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewerLoaded = () => {
|
||||||
|
Utils.deepAssign(vignette, App.project.getKey("effect.Vignette"));
|
||||||
|
window.viewer.removeEventListener("loaded", viewerLoaded);
|
||||||
|
};
|
||||||
|
window.viewer.addEventListener("loaded", viewerLoaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
App.project.setKey(`effect.Vignette`, toRaw(vignette));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`other.Enabled`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-checkbox size="small" v-model:checked="vignette.enabled" @update:checked="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 偏移 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.postProcessing.Offset") }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="vignette.offset" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 暗度 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.postProcessing.Darkness") }}</span>
|
||||||
|
<div>
|
||||||
|
<n-slider v-model:value="vignette.darkness" :step="0.01" :min="0" :max="1" :disabled="disabled" @update:value="handleConfigChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t(`layout.sider.postProcessing.Blend Function`) }}</span>
|
||||||
|
<div>
|
||||||
|
<n-select
|
||||||
|
size="small"
|
||||||
|
v-model:value="vignette.blendFunction"
|
||||||
|
:options="blendFunctionOptions"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, toRefs } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import EsInputNumber from "@/components/es/EsInputNumber.vue";
|
||||||
|
|
||||||
|
type UIPanelStateMap = Record<string, Record<string, any>>;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
formData: { props: Record<string, any>; states: UIPanelStateMap };
|
||||||
|
updateProps: (patch: Record<string, any>) => void;
|
||||||
|
updateStateProps: (key: string, patch: Record<string, any>) => void;
|
||||||
|
isStateEnabled: (key: string) => boolean;
|
||||||
|
toggleState: (key: string, enabled: boolean) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { formData } = toRefs(props);
|
||||||
|
const backgroundImageInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const overflowHidden = computed({
|
||||||
|
get: () => formData.value.props.overflow === "hidden",
|
||||||
|
set: (value: boolean) => {
|
||||||
|
formData.value.props.overflow = value ? "hidden" : "visible";
|
||||||
|
props.updateProps({ overflow: formData.value.props.overflow });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const flexOptions = computed(() => [
|
||||||
|
{ label: t("layout.sider.uipanel.Row"), value: "row" },
|
||||||
|
{ label: t("layout.sider.uipanel.Row reverse"), value: "row-reverse" },
|
||||||
|
{ label: t("layout.sider.uipanel.Column"), value: "column" },
|
||||||
|
{ label: t("layout.sider.uipanel.Column reverse"), value: "column-reverse" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const justifyOptions = computed(() => [
|
||||||
|
{ label: t("layout.sider.uipanel.Start"), value: "start" },
|
||||||
|
{ label: t("layout.sider.uipanel.Center"), value: "center" },
|
||||||
|
{ label: t("layout.sider.uipanel.End"), value: "end" },
|
||||||
|
{ label: t("layout.sider.uipanel['Space between']"), value: "space-between" },
|
||||||
|
{ label: t("layout.sider.uipanel['Space around']"), value: "space-around" },
|
||||||
|
{ label: t("layout.sider.uipanel['Space evenly']"), value: "space-evenly" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const alignOptions = computed(() => [
|
||||||
|
{ label: t("layout.sider.uipanel.Start"), value: "start" },
|
||||||
|
{ label: t("layout.sider.uipanel.Center"), value: "center" },
|
||||||
|
{ label: t("layout.sider.uipanel.End"), value: "end" },
|
||||||
|
{ label: t("layout.sider.uipanel.Stretch"), value: "stretch" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const backgroundSizeOptions = computed(() => [
|
||||||
|
{ label: t("layout.sider.uipanel.Cover"), value: "cover" },
|
||||||
|
{ label: t("layout.sider.uipanel.Contain"), value: "contain" },
|
||||||
|
{ label: t("layout.sider.uipanel.Stretch"), value: "stretch" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
function handleBackgroundImageUrl(value: string) {
|
||||||
|
formData.value.props.backgroundImage = value;
|
||||||
|
props.updateProps({ backgroundImage: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBackgroundImagePicker() {
|
||||||
|
backgroundImageInput.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBackgroundImageFile(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const maxSizeMb = 3;
|
||||||
|
if (file.size / (1024 * 1024) > maxSizeMb) {
|
||||||
|
console.log(file.size / (1024 * 1024));
|
||||||
|
const message = t("layout.sider.uipanel['Image size must be <= {size}MB']").replace("{size}", String(maxSizeMb));
|
||||||
|
window.$message?.warning(message);
|
||||||
|
input.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
if (typeof reader.result !== "string") return;
|
||||||
|
formData.value.props.backgroundImage = reader.result;
|
||||||
|
props.updateProps({ backgroundImage: reader.result });
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.scene.Width") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.width"
|
||||||
|
size="small"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ width: formData.props.width })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.scene.Height") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.height"
|
||||||
|
size="small"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ height: formData.props.height })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Padding") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.padding"
|
||||||
|
size="small"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ padding: formData.props.padding })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Margin") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.margin"
|
||||||
|
size="small"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ margin: formData.props.margin })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Offset") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.offset"
|
||||||
|
size="small"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ offset: formData.props.offset })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider class="!my-2" />
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Background color']") }}</span>
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="formData.props.backgroundColor"
|
||||||
|
:show-alpha="false"
|
||||||
|
size="small"
|
||||||
|
@update:value="props.updateProps({ backgroundColor: formData.props.backgroundColor })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Background opacity']") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.backgroundOpacity"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ backgroundOpacity: formData.props.backgroundOpacity })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Background image URL']") }}</span>
|
||||||
|
<n-input v-model:value="formData.props.backgroundImage" size="small" @update:value="handleBackgroundImageUrl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Select local image']") }}</span>
|
||||||
|
<div class="uipanel-bg-image">
|
||||||
|
<n-button size="small" @click="openBackgroundImagePicker">
|
||||||
|
{{ t("layout.sider.uipanel['Select local image']") }}
|
||||||
|
</n-button>
|
||||||
|
<input ref="backgroundImageInput" class="uipanel-bg-image-input" type="file" accept="image/*" @change="handleBackgroundImageFile" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Background size']") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.props.backgroundSize"
|
||||||
|
size="small"
|
||||||
|
:options="backgroundSizeOptions"
|
||||||
|
@update:value="props.updateProps({ backgroundSize: formData.props.backgroundSize })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Border radius']") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.borderRadius"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ borderRadius: formData.props.borderRadius })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Border width']") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.borderWidth"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ borderWidth: formData.props.borderWidth })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Border color']") }}</span>
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="formData.props.borderColor"
|
||||||
|
:show-alpha="false"
|
||||||
|
size="small"
|
||||||
|
@update:value="props.updateProps({ borderColor: formData.props.borderColor })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Hidden overflow']") }}</span>
|
||||||
|
<n-switch v-model:value="overflowHidden" size="small" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider class="!my-2" />
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Flex direction']") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.props.flexDirection"
|
||||||
|
size="small"
|
||||||
|
:options="flexOptions"
|
||||||
|
@update:value="props.updateProps({ flexDirection: formData.props.flexDirection })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Justify content']") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.props.justifyContent"
|
||||||
|
size="small"
|
||||||
|
:options="justifyOptions"
|
||||||
|
@update:value="props.updateProps({ justifyContent: formData.props.justifyContent })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Align items']") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.props.alignItems"
|
||||||
|
size="small"
|
||||||
|
:options="alignOptions"
|
||||||
|
@update:value="props.updateProps({ alignItems: formData.props.alignItems })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider class="!my-2" />
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.States") }}</span>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Hover") }}</span>
|
||||||
|
<n-switch size="small" :value="props.isStateEnabled('hover')" @update:value="(value: boolean) => props.toggleState('hover', value)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="props.isStateEnabled('hover')">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Hover background']") }}</span>
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="formData.states.hover.backgroundColor"
|
||||||
|
:show-alpha="false"
|
||||||
|
size="small"
|
||||||
|
@update:value="props.updateStateProps('hover', { backgroundColor: formData.states.hover.backgroundColor })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Hover opacity']") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.states.hover.backgroundOpacity"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateStateProps('hover', { backgroundOpacity: formData.states.hover.backgroundOpacity })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Active") }}</span>
|
||||||
|
<n-switch size="small" :value="props.isStateEnabled('active')" @update:value="(value: boolean) => props.toggleState('active', value)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="props.isStateEnabled('active')">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Active background']") }}</span>
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="formData.states.active.backgroundColor"
|
||||||
|
:show-alpha="false"
|
||||||
|
size="small"
|
||||||
|
@update:value="props.updateStateProps('active', { backgroundColor: formData.states.active.backgroundColor })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Active opacity']") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.states.active.backgroundOpacity"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateStateProps('active', { backgroundOpacity: formData.states.active.backgroundOpacity })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.uipanel-bg-image {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uipanel-bg-image-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, toRefs } from "vue";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import EsInputNumber from "@/components/es/EsInputNumber.vue";
|
||||||
|
|
||||||
|
type UIPanelStateMap = Record<string, Record<string, any>>;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
formData: { props: Record<string, any>; states: UIPanelStateMap };
|
||||||
|
updateProps: (patch: Record<string, any>) => void;
|
||||||
|
updateStateProps: (key: string, patch: Record<string, any>) => void;
|
||||||
|
isStateEnabled: (key: string) => boolean;
|
||||||
|
toggleState: (key: string, enabled: boolean) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { formData } = toRefs(props);
|
||||||
|
|
||||||
|
const fontSupersampling = computed({
|
||||||
|
get: () => formData.value.props.fontSmooth !== "none",
|
||||||
|
set: (value: boolean) => {
|
||||||
|
formData.value.props.fontSmooth = value ? "antialiased" : "none";
|
||||||
|
props.updateProps({ fontSmooth: formData.value.props.fontSmooth });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 宽度是否使用 auto
|
||||||
|
const widthIsAuto = computed({
|
||||||
|
get: () => formData.value.props.width === "auto",
|
||||||
|
set: (value: boolean) => {
|
||||||
|
formData.value.props.width = value ? "auto" : 0.5;
|
||||||
|
props.updateProps({ width: formData.value.props.width });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 高度是否使用 auto
|
||||||
|
const heightIsAuto = computed({
|
||||||
|
get: () => formData.value.props.height === "auto",
|
||||||
|
set: (value: boolean) => {
|
||||||
|
formData.value.props.height = value ? "auto" : 0.1;
|
||||||
|
props.updateProps({ height: formData.value.props.height });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数值化的宽度(用于绑定数字输入)
|
||||||
|
const numericWidth = computed({
|
||||||
|
get: () => (typeof formData.value.props.width === "number" ? formData.value.props.width : 0),
|
||||||
|
set: (value: number) => {
|
||||||
|
formData.value.props.width = value;
|
||||||
|
props.updateProps({ width: value });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数值化的高度(用于绑定数字输入)
|
||||||
|
const numericHeight = computed({
|
||||||
|
get: () => (typeof formData.value.props.height === "number" ? formData.value.props.height : 0),
|
||||||
|
set: (value: number) => {
|
||||||
|
formData.value.props.height = value;
|
||||||
|
props.updateProps({ height: value });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const textAlignOptions = computed(() => [
|
||||||
|
{ label: t("layout.sider.uipanel.Left"), value: "left" },
|
||||||
|
{ label: t("layout.sider.uipanel.Center"), value: "center" },
|
||||||
|
{ label: t("layout.sider.uipanel.Right"), value: "right" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const whiteSpaceOptions = [
|
||||||
|
{ label: "normal", value: "normal" },
|
||||||
|
{ label: "pre-line", value: "pre-line" },
|
||||||
|
{ label: "pre-wrap", value: "pre-wrap" },
|
||||||
|
{ label: "pre", value: "pre" },
|
||||||
|
{ label: "nowrap", value: "nowrap" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleTextContent(value: string) {
|
||||||
|
formData.value.props.textContent = value;
|
||||||
|
props.updateProps({ textContent: value });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Text") }}</span>
|
||||||
|
<n-input v-model:value="formData.props.textContent" type="textarea" size="small" :autosize="{ minRows: 1, maxRows: 3 }" @update:value="handleTextContent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.scene.Width") }}</span>
|
||||||
|
<div class="uipanel-auto-size">
|
||||||
|
<n-switch v-model:value="widthIsAuto" size="small" />
|
||||||
|
<span class="uipanel-auto-label">auto</span>
|
||||||
|
<EsInputNumber v-if="!widthIsAuto" v-model:value="numericWidth" size="small" :min="0" :decimal="3" :show-button="false" @change="numericWidth = $event" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.scene.Height") }}</span>
|
||||||
|
<div class="uipanel-auto-size">
|
||||||
|
<n-switch v-model:value="heightIsAuto" size="small" />
|
||||||
|
<span class="uipanel-auto-label">auto</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-if="!heightIsAuto"
|
||||||
|
v-model:value="numericHeight"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="numericHeight = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Font size']") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.fontSize"
|
||||||
|
size="small"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ fontSize: formData.props.fontSize })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Color") }}</span>
|
||||||
|
<n-color-picker v-model:value="formData.props.color" :show-alpha="false" size="small" @update:value="props.updateProps({ color: formData.props.color })" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Font opacity']") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.fontOpacity"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ fontOpacity: formData.props.fontOpacity })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Font supersampling']") }}</span>
|
||||||
|
<n-switch v-model:value="fontSupersampling" size="small" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Letter spacing']") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.letterSpacing"
|
||||||
|
size="small"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ letterSpacing: formData.props.letterSpacing })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Line height']") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.lineHeight"
|
||||||
|
size="small"
|
||||||
|
:decimal="2"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ lineHeight: formData.props.lineHeight })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Padding") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.padding"
|
||||||
|
size="small"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ padding: formData.props.padding })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Margin") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.margin"
|
||||||
|
size="small"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ margin: formData.props.margin })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Offset") }}</span>
|
||||||
|
<EsInputNumber
|
||||||
|
v-model:value="formData.props.offset"
|
||||||
|
size="small"
|
||||||
|
:decimal="3"
|
||||||
|
:show-button="false"
|
||||||
|
@change="props.updateProps({ offset: formData.props.offset })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider class="!my-2" />
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Text align']") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.props.textAlign"
|
||||||
|
size="small"
|
||||||
|
:options="textAlignOptions"
|
||||||
|
@update:value="props.updateProps({ textAlign: formData.props.textAlign })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['White space']") }}</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.props.whiteSpace"
|
||||||
|
size="small"
|
||||||
|
:options="whiteSpaceOptions"
|
||||||
|
@update:value="props.updateProps({ whiteSpace: formData.props.whiteSpace })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Break on']") }}</span>
|
||||||
|
<n-input v-model:value="formData.props.breakOn" size="small" @update:value="(value: string) => props.updateProps({ breakOn: value })" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider class="!my-2" />
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.States") }}</span>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Hover") }}</span>
|
||||||
|
<n-switch size="small" :value="props.isStateEnabled('hover')" @update:value="(value: boolean) => props.toggleState('hover', value)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="props.isStateEnabled('hover')">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Hover color']") }}</span>
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="formData.states.hover.color"
|
||||||
|
:show-alpha="false"
|
||||||
|
size="small"
|
||||||
|
@update:value="props.updateStateProps('hover', { color: formData.states.hover.color })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel.Active") }}</span>
|
||||||
|
<n-switch size="small" :value="props.isStateEnabled('active')" @update:value="(value: boolean) => props.toggleState('active', value)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="props.isStateEnabled('active')">
|
||||||
|
<div class="sidebar-config-item">
|
||||||
|
<span>{{ t("layout.sider.uipanel['Active color']") }}</span>
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="formData.states.active.color"
|
||||||
|
:show-alpha="false"
|
||||||
|
size="small"
|
||||||
|
@update:value="props.updateStateProps('active', { color: formData.states.active.color })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.uipanel-auto-size {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uipanel-auto-label {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
min-width: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-card v-if="visible" class="absolute top-40px right-1 max-w-300px" content-style="padding: 5px 10px;">
|
|
||||||
<n-collapse accordion default-expanded-names="bim">
|
|
||||||
<template #arrow="{ collapsed }">
|
|
||||||
<n-icon v-if="collapsed">
|
|
||||||
<PlanetOutline/>
|
|
||||||
</n-icon>
|
|
||||||
<n-icon v-else>
|
|
||||||
<SunnyOutline/>
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<n-collapse-item title=" BIM 构件信息" name="bim">
|
|
||||||
<div class="max-h-360px overflow-y-auto">
|
|
||||||
<n-descriptions v-for="(item, index) in info.parameters" :key="index" label-placement="left"
|
|
||||||
bordered size="small" :column="1">
|
|
||||||
<template #header>
|
|
||||||
<p class="py-2">{{ item.GroupName }}</p>
|
|
||||||
</template>
|
|
||||||
<n-descriptions-item v-for="(it, i) in item.Parameters" :key="i + it.name" :label="it.name">
|
|
||||||
{{ it.value }}
|
|
||||||
</n-descriptions-item>
|
|
||||||
</n-descriptions>
|
|
||||||
</div>
|
|
||||||
</n-collapse-item>
|
|
||||||
</n-collapse>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {PlanetOutline, SunnyOutline} from "@vicons/ionicons5";
|
|
||||||
import {ref, onBeforeUnmount, onMounted} from "vue";
|
|
||||||
import {Hooks} from "@astral3d/engine";
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const info = ref({
|
|
||||||
parameters: [{GroupName: "", Parameters: [{name: "", value: ""}]}]
|
|
||||||
})
|
|
||||||
|
|
||||||
function objectSelected(object) {
|
|
||||||
if (!object) {
|
|
||||||
visible.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!object.userData.BIM) {
|
|
||||||
if (!object.parent || !object.parent.userData.BIM) {
|
|
||||||
visible.value = false;
|
|
||||||
} else {
|
|
||||||
visible.value = true;
|
|
||||||
info.value = object.parent.userData.BIM;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
visible.value = true;
|
|
||||||
info.value = object.userData.BIM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
Hooks.useAddSignal("objectSelected", objectSelected);
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
Hooks.useRemoveSignal("objectSelected", objectSelected);
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, watch } from "vue";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { AddObjectCommand, App, Hooks, Path, Utils, getDefaultPathOptions } from "@astral3d/engine";
|
||||||
|
import { t } from "@/language";
|
||||||
|
import type { IPathDrawingResult } from "@/store/modules/pathDrawing";
|
||||||
|
import { usePathDrawingStore } from "@/store/modules/pathDrawing";
|
||||||
|
|
||||||
|
const pathDrawingStore = usePathDrawingStore();
|
||||||
|
const isDrawing = computed(() => pathDrawingStore.isActive);
|
||||||
|
const submit = computed(() => pathDrawingStore.getSubmit);
|
||||||
|
let templateOptions: Record<string, any> | null = null;
|
||||||
|
let points: THREE.Vector3[] = [];
|
||||||
|
let origin: THREE.Vector3 | null = null;
|
||||||
|
let previewPath: Path | null = null;
|
||||||
|
let viewerRef: any = null;
|
||||||
|
let prevCursor = "";
|
||||||
|
const previewFlag = "__pathPreview";
|
||||||
|
|
||||||
|
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
||||||
|
const tempNdc = new THREE.Vector2();
|
||||||
|
const onDownPosition = new THREE.Vector2();
|
||||||
|
const onUpPosition = new THREE.Vector2();
|
||||||
|
|
||||||
|
let isPointerDown = false;
|
||||||
|
let lastClickTime = 0;
|
||||||
|
const signal = Hooks.useSignal();
|
||||||
|
|
||||||
|
function haltSelectionOnIntersect() {
|
||||||
|
if (!isDrawing.value) return;
|
||||||
|
signal.halt("intersectionsDetected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setViewer(viewer: any) {
|
||||||
|
viewerRef = viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneTemplate() {
|
||||||
|
const current = pathDrawingStore.getTemplate;
|
||||||
|
templateOptions = current ? JSON.parse(JSON.stringify(current)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupPreview() {
|
||||||
|
let removed = false;
|
||||||
|
|
||||||
|
if (previewPath) {
|
||||||
|
previewPath.parent?.remove(previewPath);
|
||||||
|
previewPath.dispose?.();
|
||||||
|
previewPath = null;
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orphans: THREE.Object3D[] = [];
|
||||||
|
App.scene.traverse(child => {
|
||||||
|
if (child.userData?.[previewFlag]) {
|
||||||
|
orphans.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orphans.length > 0) {
|
||||||
|
orphans.forEach(child => {
|
||||||
|
child.parent?.remove(child);
|
||||||
|
(child as any).dispose?.();
|
||||||
|
});
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed) {
|
||||||
|
Hooks.useDispatchSignal("sceneGraphChanged");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
points = [];
|
||||||
|
origin = null;
|
||||||
|
isPointerDown = false;
|
||||||
|
lastClickTime = 0;
|
||||||
|
cleanupPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOptions(worldPoints: THREE.Vector3[], baseOrigin: THREE.Vector3) {
|
||||||
|
const options = getDefaultPathOptions();
|
||||||
|
if (templateOptions) {
|
||||||
|
Utils.deepAssign(options, templateOptions);
|
||||||
|
}
|
||||||
|
options.position = baseOrigin.toArray();
|
||||||
|
options.points = worldPoints.map(point => ({
|
||||||
|
x: point.x - baseOrigin.x,
|
||||||
|
y: point.y - baseOrigin.y,
|
||||||
|
z: point.z - baseOrigin.z,
|
||||||
|
}));
|
||||||
|
if (!options.name) options.name = "Path";
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResult(worldPoints: THREE.Vector3[], baseOrigin: THREE.Vector3): IPathDrawingResult {
|
||||||
|
return {
|
||||||
|
worldPoints: worldPoints.map(point => ({ x: point.x, y: point.y, z: point.z })),
|
||||||
|
origin: {
|
||||||
|
x: baseOrigin.x,
|
||||||
|
y: baseOrigin.y,
|
||||||
|
z: baseOrigin.z,
|
||||||
|
},
|
||||||
|
options: buildOptions(worldPoints, baseOrigin),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
if (!templateOptions) return;
|
||||||
|
if (points.length === 0) return;
|
||||||
|
|
||||||
|
if (!origin) {
|
||||||
|
origin = points[0].clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = buildOptions(points, origin);
|
||||||
|
if (!previewPath) {
|
||||||
|
const path = new Path(options);
|
||||||
|
path.ignore = true;
|
||||||
|
path.userData[previewFlag] = true;
|
||||||
|
previewPath = path;
|
||||||
|
App.scene.add(path);
|
||||||
|
} else {
|
||||||
|
previewPath.updateOptions({
|
||||||
|
mode: options.mode,
|
||||||
|
closed: options.closed,
|
||||||
|
cornerRadius: options.cornerRadius,
|
||||||
|
cornerSplit: options.cornerSplit,
|
||||||
|
path: options.path,
|
||||||
|
tube: options.tube,
|
||||||
|
position: options.position,
|
||||||
|
points: options.points,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Hooks.useDispatchSignal("sceneGraphChanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPickPoint(event: PointerEvent) {
|
||||||
|
const viewer = viewerRef || (window as any).viewer;
|
||||||
|
if (!viewer) return null;
|
||||||
|
|
||||||
|
const mouse = Utils.getMousePosition(viewer.container, event.clientX, event.clientY);
|
||||||
|
const intersects = viewer.getIntersects(new THREE.Vector2(mouse[0], mouse[1]));
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
return intersects[0].point.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
tempNdc.set(mouse[0] * 2 - 1, -(mouse[1] * 2) + 1);
|
||||||
|
viewer.raycaster.setFromCamera(tempNdc, viewer.camera);
|
||||||
|
const hit = new THREE.Vector3();
|
||||||
|
return viewer.raycaster.ray.intersectPlane(groundPlane, hit) ? hit.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPoint(point: THREE.Vector3) {
|
||||||
|
points.push(point);
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishDrawing() {
|
||||||
|
if (!isDrawing.value) return;
|
||||||
|
if (points.length <= 2) {
|
||||||
|
cleanupPreview();
|
||||||
|
pathDrawingStore.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseOrigin = origin || points[0];
|
||||||
|
const result = buildResult(points, baseOrigin);
|
||||||
|
if (submit.value) {
|
||||||
|
try {
|
||||||
|
submit.value(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[PathDrawingOverlay] path drawing submit failed", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const path = new Path(result.options);
|
||||||
|
App.execute(new AddObjectCommand(path), `Add Path: ${result.options.name}`);
|
||||||
|
}
|
||||||
|
cleanupPreview();
|
||||||
|
pathDrawingStore.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelDrawing() {
|
||||||
|
if (!isDrawing.value) return;
|
||||||
|
cleanupPreview();
|
||||||
|
pathDrawingStore.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrawing() {
|
||||||
|
unbindEvents();
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
const viewer = viewerRef || (window as any).viewer;
|
||||||
|
if (viewer) {
|
||||||
|
viewer.container.style.cursor = prevCursor || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
const viewer = viewerRef || (window as any).viewer;
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
prevCursor = viewer.container.style.cursor || "";
|
||||||
|
viewer.container.style.cursor = "crosshair";
|
||||||
|
|
||||||
|
viewer.container.addEventListener("pointerdown", onPointerDown, true);
|
||||||
|
viewer.container.addEventListener("pointerup", onPointerUp, true);
|
||||||
|
viewer.container.addEventListener("dblclick", onDoubleClick, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindEvents() {
|
||||||
|
const viewer = viewerRef || (window as any).viewer;
|
||||||
|
if (!viewer) return;
|
||||||
|
viewer.container.removeEventListener("pointerdown", onPointerDown, true);
|
||||||
|
viewer.container.removeEventListener("pointerup", onPointerUp, true);
|
||||||
|
viewer.container.removeEventListener("dblclick", onDoubleClick, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerDown(event: PointerEvent) {
|
||||||
|
if (!isDrawing.value) return;
|
||||||
|
if (event.button !== 0) return;
|
||||||
|
const viewer = viewerRef || (window as any).viewer;
|
||||||
|
if (!viewer) return;
|
||||||
|
const array = Utils.getMousePosition(viewer.container, event.clientX, event.clientY);
|
||||||
|
onDownPosition.fromArray(array);
|
||||||
|
isPointerDown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp(event: PointerEvent) {
|
||||||
|
if (!isDrawing.value) return;
|
||||||
|
if (event.button !== 0) return;
|
||||||
|
if (!isPointerDown) return;
|
||||||
|
const viewer = viewerRef || (window as any).viewer;
|
||||||
|
if (!viewer) return;
|
||||||
|
const array = Utils.getMousePosition(viewer.container, event.clientX, event.clientY);
|
||||||
|
onUpPosition.fromArray(array);
|
||||||
|
isPointerDown = false;
|
||||||
|
|
||||||
|
if (onDownPosition.distanceTo(onUpPosition) !== 0) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastClickTime < 200) return;
|
||||||
|
lastClickTime = now;
|
||||||
|
|
||||||
|
const point = getPickPoint(event);
|
||||||
|
if (!point) return;
|
||||||
|
addPoint(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDoubleClick(event: MouseEvent) {
|
||||||
|
if (!isDrawing.value) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
finishDrawing();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isDrawing.value,
|
||||||
|
active => {
|
||||||
|
if (active) {
|
||||||
|
resetState();
|
||||||
|
cloneTemplate();
|
||||||
|
bindEvents();
|
||||||
|
} else {
|
||||||
|
stopDrawing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Hooks.useAddSignal("intersectionsDetected", haltSelectionOnIntersect, undefined, 999);
|
||||||
|
Hooks.useAddSignal("viewerInitCompleted", setViewer);
|
||||||
|
if ((window as any).viewer) {
|
||||||
|
setViewer((window as any).viewer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
Hooks.useRemoveSignal("intersectionsDetected", haltSelectionOnIntersect);
|
||||||
|
Hooks.useRemoveSignal("viewerInitCompleted", setViewer);
|
||||||
|
stopDrawing();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-card v-if="isDrawing" size="small" class="path-drawing-overlay">
|
||||||
|
<n-space size="small" align="center" @pointerdown.stop>
|
||||||
|
<n-text strong>
|
||||||
|
{{ t("layout.scene.path['Path drawing: Left-click to add a point, and double-click to end the drawing']") }}
|
||||||
|
</n-text>
|
||||||
|
<n-button size="small" type="primary" @click="finishDrawing">{{ t("other.Finish") }}</n-button>
|
||||||
|
<n-button size="small" @click="cancelDrawing">{{ t("other.Cancel") }}</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.path-drawing-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 50%;
|
||||||
|
width: max-content;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,24 +6,22 @@
|
|||||||
<ViewportInfo/>
|
<ViewportInfo/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RVT BIM 构件信息悬浮框 -->
|
|
||||||
<BIMProperties/>
|
|
||||||
|
|
||||||
<!-- IFC BIM 构件信息悬浮框 -->
|
<!-- IFC BIM 构件信息悬浮框 -->
|
||||||
<IFCProperties/>
|
<IFCProperties/>
|
||||||
|
|
||||||
|
<PathDrawingOverlay/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref, nextTick, onBeforeUnmount} from 'vue';
|
import {onMounted, ref, nextTick, onBeforeUnmount} from 'vue';
|
||||||
import {App,Viewer,Hooks} from "@astral3d/engine";
|
import {App,Viewer,Hooks,WaterPool} from "@astral3d/engine";
|
||||||
import Toolbar from "./Toolbar.vue";
|
import Toolbar from "./Toolbar.vue";
|
||||||
|
import PathDrawingOverlay from "./PathDrawingOverlay.vue";
|
||||||
import {useGlobalConfigStore} from "@/store/modules/globalConfig";
|
import {useGlobalConfigStore} from "@/store/modules/globalConfig";
|
||||||
import {usePluginStore} from "@/store/modules/plugin";
|
import {usePluginStore} from "@/store/modules/plugin";
|
||||||
import {installBuiltinPlugin} from "@/plugin";
|
import {installBuiltinPlugin} from "@/plugin";
|
||||||
import { clearBuffer } from "@/utils/wasm/optimize";
|
|
||||||
import ViewportInfo from "./ViewportInfo.vue";
|
import ViewportInfo from "./ViewportInfo.vue";
|
||||||
import BIMProperties from "./BIMProperties.vue";
|
|
||||||
import IFCProperties from "./IFCProperties.vue";
|
import IFCProperties from "./IFCProperties.vue";
|
||||||
|
|
||||||
const globalStore = useGlobalConfigStore();
|
const globalStore = useGlobalConfigStore();
|
||||||
@@ -31,6 +29,43 @@ const pluginStore = usePluginStore();
|
|||||||
|
|
||||||
const viewportRef = ref();
|
const viewportRef = ref();
|
||||||
|
|
||||||
|
function bootstrapWaterPoolWarmup(viewer: Viewer) {
|
||||||
|
const warmupPool = new WaterPool({
|
||||||
|
sky: viewer.scene.environment,
|
||||||
|
name: '__water_pool_warmup__',
|
||||||
|
type: 'cylinder',
|
||||||
|
light: [0.7, 1, -0.3],
|
||||||
|
diameter: 5,
|
||||||
|
height: 5,
|
||||||
|
wallMode: 'none',
|
||||||
|
wallOpacity: 0,
|
||||||
|
useSceneRefraction: 1,
|
||||||
|
surfaceTransmittance: 0.6,
|
||||||
|
normalStrength: 0.5,
|
||||||
|
refractionStrength: 0.035,
|
||||||
|
});
|
||||||
|
|
||||||
|
warmupPool.visible = false;
|
||||||
|
|
||||||
|
// Use scene.add/remove directly to avoid objectAdded/objectRemoved signals and scene tree refresh.
|
||||||
|
viewer.scene.add(warmupPool);
|
||||||
|
|
||||||
|
const removeWarmupPool = () => {
|
||||||
|
if (warmupPool.parent) {
|
||||||
|
warmupPool.parent.remove(warmupPool);
|
||||||
|
}
|
||||||
|
warmupPool.dispose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loaded = (warmupPool as any).loaded;
|
||||||
|
if (loaded && typeof loaded.then === 'function') {
|
||||||
|
loaded.finally(removeWarmupPool);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeWarmupPool();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
App.setConfig({
|
App.setConfig({
|
||||||
theme: globalStore.theme.replace("Theme", ""),
|
theme: globalStore.theme.replace("Theme", ""),
|
||||||
@@ -49,6 +84,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
bootstrapWaterPoolWarmup(window.viewer);
|
||||||
|
|
||||||
// 添加astral engine内置插件并监听插件注册
|
// 添加astral engine内置插件并监听插件注册
|
||||||
pluginStore.setPlugins(Array.from(window.viewer.modules.plugin.plugins.values()));
|
pluginStore.setPlugins(Array.from(window.viewer.modules.plugin.plugins.values()));
|
||||||
Hooks.useAddSignal("pluginInstall",pluginStore.addPlugin);
|
Hooks.useAddSignal("pluginInstall",pluginStore.addPlugin);
|
||||||
@@ -56,9 +93,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 注册astral editor内置插件
|
// 注册astral editor内置插件
|
||||||
installBuiltinPlugin(window.viewer);
|
installBuiltinPlugin(window.viewer);
|
||||||
|
|
||||||
// 清理wasm缓存
|
|
||||||
clearBuffer();
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</n-split> -->
|
</n-split> -->
|
||||||
|
|
||||||
<n-flex class="h-full w-full">
|
<n-flex class="h-full w-full">
|
||||||
<DataSetGroup class="w-50 min-w-300px max-w-500px" />
|
<DataSetGroup class="w-50 min-w-300px max-w-500px" @select="handleGroupSelect" />
|
||||||
|
|
||||||
<n-divider vertical class="!h-full" />
|
<n-divider vertical class="!h-full" />
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-input>
|
</n-input>
|
||||||
|
|
||||||
<n-button type="primary" @click="showDataSetModal = true">{{ t('home.Add data set') }}</n-button>
|
<n-button type="primary" @click="handleAddDataSet">{{ t('home.Add data set') }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
<n-data-table ref="tableRef" :columns="dataSetColumns" :data="dataSets" :pagination="pagination" />
|
<n-data-table :columns="dataSetColumns" :data="dataSets" :pagination="pagination" :loading="tableLoading" />
|
||||||
</div>
|
</div>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|
||||||
@@ -45,29 +45,32 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, h, onMounted, useTemplateRef } from 'vue';
|
import { ref, reactive, h, onMounted } from 'vue';
|
||||||
import { NButton, NPopconfirm } from 'naive-ui';
|
import { NButton, NPopconfirm } from 'naive-ui';
|
||||||
import type { DataTableInst } from 'naive-ui';
|
|
||||||
import { Search } from '@vicons/carbon';
|
import { Search } from '@vicons/carbon';
|
||||||
import { t } from "@/language";
|
import { t } from "@/language";
|
||||||
|
import { fetchDataSetDetail, fetchDataSetPage, fetchDeleteDataSet } from "@/http/api/dataSet";
|
||||||
import DataSetModal from "./DataSetModal.vue";
|
import DataSetModal from "./DataSetModal.vue";
|
||||||
import DataSetGroup from "./DataSetGroup.vue";
|
import DataSetGroup from "./DataSetGroup.vue";
|
||||||
|
|
||||||
const tableRef = useTemplateRef<DataTableInst>("tableRef");
|
const dataSets = ref<IDataSet.Item[]>([])
|
||||||
const dataSets = ref<IDataSet.Item[]>([
|
const tableLoading = ref(false);
|
||||||
{
|
|
||||||
id: '',
|
|
||||||
groupId:"1",
|
|
||||||
name: '323324234',
|
|
||||||
type: 'SQL'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
const showDataSetModal = ref(false)
|
const showDataSetModal = ref(false)
|
||||||
const currentDataSet = reactive<IDataSet.Item>({
|
const searchName = ref("");
|
||||||
|
const selectedGroupId = ref<IDataSet.IGroup["id"] | null>(null);
|
||||||
|
const defaultDataSet: IDataSet.Item = {
|
||||||
id: '',
|
id: '',
|
||||||
groupId:"1",
|
groupId: '',
|
||||||
name: '',
|
name: '',
|
||||||
type: 'SQL'
|
type: 'API',
|
||||||
|
method: 'GET',
|
||||||
|
api: '',
|
||||||
|
dataSourceId: '',
|
||||||
|
sql: '',
|
||||||
|
json: ''
|
||||||
|
};
|
||||||
|
const currentDataSet = reactive<IDataSet.Item>({
|
||||||
|
...defaultDataSet
|
||||||
})
|
})
|
||||||
const dataSetColumns = [
|
const dataSetColumns = [
|
||||||
{
|
{
|
||||||
@@ -119,23 +122,75 @@ const dataSetColumns = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
// 分页配置
|
// 分页配置
|
||||||
const pagination = { pageSize: 10 }
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
pageCount: 1,
|
||||||
|
"on-update:page": (page: number) => {
|
||||||
|
pagination.page = page;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function getList() { }
|
function resetCurrentDataSet(data?: Partial<IDataSet.Item>) {
|
||||||
|
Object.assign(currentDataSet, defaultDataSet, data || {});
|
||||||
function handleSearch(searchText: string) {
|
|
||||||
tableRef.value?.filter({
|
|
||||||
name: [searchText]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function editDataSet(item) {
|
async function getList() {
|
||||||
Object.assign(currentDataSet, item)
|
tableLoading.value = true;
|
||||||
|
const res = await fetchDataSetPage({
|
||||||
|
page: pagination.page,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
name: searchName.value || undefined,
|
||||||
|
groupId: selectedGroupId.value ?? undefined
|
||||||
|
});
|
||||||
|
tableLoading.value = false;
|
||||||
|
dataSets.value = res.data?.items || [];
|
||||||
|
pagination.pageCount = res.data?.pages || 1;
|
||||||
|
if (res.data?.current) {
|
||||||
|
pagination.page = res.data.current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch(searchText: string) {
|
||||||
|
searchName.value = searchText;
|
||||||
|
pagination.page = 1;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGroupSelect(group: IDataSet.IGroup | null) {
|
||||||
|
selectedGroupId.value = group?.id ?? null;
|
||||||
|
pagination.page = 1;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddDataSet() {
|
||||||
|
resetCurrentDataSet({
|
||||||
|
groupId: selectedGroupId.value ?? ""
|
||||||
|
});
|
||||||
|
showDataSetModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editDataSet(item) {
|
||||||
|
const res = await fetchDataSetDetail(item.id);
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const detail = (res.data || item) as any;
|
||||||
|
resetCurrentDataSet({
|
||||||
|
...detail,
|
||||||
|
dataSourceId: detail.dataSourceId || detail.dataSource ? String(detail.dataSourceId || detail.dataSource) : ""
|
||||||
|
});
|
||||||
showDataSetModal.value = true
|
showDataSetModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteDataSet(item) {
|
async function deleteDataSet(item) {
|
||||||
dataSets.value = dataSets.value.filter(ds => ds.id !== item.id)
|
const res = await fetchDeleteDataSet(item.id);
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.$message?.success(t("prompt.Success to delete"));
|
||||||
|
getList();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -34,49 +34,20 @@ import { ref, reactive,onMounted } from "vue";
|
|||||||
import type { DropdownOption, TreeOption, TreeOverrideNodeClickBehaviorReturn } from 'naive-ui';
|
import type { DropdownOption, TreeOption, TreeOverrideNodeClickBehaviorReturn } from 'naive-ui';
|
||||||
import { Search, Add } from '@vicons/carbon';
|
import { Search, Add } from '@vicons/carbon';
|
||||||
import { t } from "@/language";
|
import { t } from "@/language";
|
||||||
|
import { fetchDataSetGroupTree, fetchDeleteDataSetGroup } from "@/http/api/dataSetGroup";
|
||||||
import DataSetGroupModal from "./DataSetGroupModal.vue";
|
import DataSetGroupModal from "./DataSetGroupModal.vue";
|
||||||
|
|
||||||
const pattern = ref("");
|
const emits = defineEmits(["select"]);
|
||||||
const data: IDataSet.IGroup[] = [
|
|
||||||
{
|
|
||||||
name: '0',
|
|
||||||
id: '0',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: '0-0',
|
|
||||||
id: '0-0',
|
|
||||||
children: [
|
|
||||||
{ name: '0-0-0', id: '0-0-0' },
|
|
||||||
{ name: '0-0-1', id: '0-0-1' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '0-1',
|
|
||||||
id: '0-1',
|
|
||||||
children: [
|
|
||||||
{ name: '0-1-0', id: '0-1-0' },
|
|
||||||
{ name: '0-1-1', id: '0-1-1' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '1',
|
|
||||||
id: '1',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: '1-0',
|
|
||||||
id: '1-0'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '1-1',
|
|
||||||
id: '1-1'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
function getTreeData(){}
|
const pattern = ref("");
|
||||||
|
const data = ref<IDataSet.IGroup[]>([]);
|
||||||
|
const selectedGroup = ref<IDataSet.IGroup | null>(null);
|
||||||
|
const activeNode = ref<IDataSet.IGroup | null>(null);
|
||||||
|
|
||||||
|
async function getTreeData(){
|
||||||
|
const res = await fetchDataSetGroupTree();
|
||||||
|
data.value = res.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
// 子节点选中,含子节点则展开
|
// 子节点选中,含子节点则展开
|
||||||
function handleOverride({ option }): TreeOverrideNodeClickBehaviorReturn {
|
function handleOverride({ option }): TreeOverrideNodeClickBehaviorReturn {
|
||||||
@@ -96,7 +67,7 @@ const currentGroup = reactive<IDataSet.IGroup>({
|
|||||||
|
|
||||||
function handleAddGroup(){
|
function handleAddGroup(){
|
||||||
currentGroup.id = "";
|
currentGroup.id = "";
|
||||||
currentGroup.pid = "";
|
currentGroup.pid = selectedGroup.value?.id ?? "";
|
||||||
currentGroup.name = "";
|
currentGroup.name = "";
|
||||||
|
|
||||||
showGroupModal.value = true;
|
showGroupModal.value = true;
|
||||||
@@ -107,8 +78,7 @@ const showDropdown = ref(false);
|
|||||||
const options = ref<DropdownOption[]>([
|
const options = ref<DropdownOption[]>([
|
||||||
{
|
{
|
||||||
label: t("home.Delete"),
|
label: t("home.Delete"),
|
||||||
key: 'delete',
|
key: 'delete'
|
||||||
disabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("layout.sider.script.Edit"),
|
label: t("layout.sider.script.Edit"),
|
||||||
@@ -120,9 +90,15 @@ const y = ref(0)
|
|||||||
|
|
||||||
// 绑定节点属性以实现右键菜单
|
// 绑定节点属性以实现右键菜单
|
||||||
function handleNodeProps({ option }: { option: TreeOption }) {
|
function handleNodeProps({ option }: { option: TreeOption }) {
|
||||||
|
const group = option as IDataSet.IGroup;
|
||||||
return {
|
return {
|
||||||
onClick() { },
|
onClick() {
|
||||||
|
selectedGroup.value = group;
|
||||||
|
emits("select", group);
|
||||||
|
},
|
||||||
onContextmenu(e: MouseEvent): void {
|
onContextmenu(e: MouseEvent): void {
|
||||||
|
activeNode.value = group;
|
||||||
|
selectedGroup.value = group;
|
||||||
showDropdown.value = true;
|
showDropdown.value = true;
|
||||||
x.value = e.clientX;
|
x.value = e.clientX;
|
||||||
y.value = e.clientY;
|
y.value = e.clientY;
|
||||||
@@ -132,8 +108,34 @@ function handleNodeProps({ option }: { option: TreeOption }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDropdownSelect() {
|
async function handleDropdownSelect(key: string) {
|
||||||
showDropdown.value = false;
|
showDropdown.value = false;
|
||||||
|
if (!activeNode.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "edit") {
|
||||||
|
Object.assign(currentGroup, {
|
||||||
|
id: activeNode.value.id,
|
||||||
|
pid: activeNode.value.pid ?? "",
|
||||||
|
name: activeNode.value.name
|
||||||
|
});
|
||||||
|
showGroupModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "delete") {
|
||||||
|
const res = await fetchDeleteDataSetGroup(activeNode.value.id);
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.$message?.success(t("prompt.Success to delete"));
|
||||||
|
if (selectedGroup.value?.id === activeNode.value.id) {
|
||||||
|
selectedGroup.value = null;
|
||||||
|
emits("select", null);
|
||||||
|
}
|
||||||
|
getTreeData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDropdownClickoutside() {
|
function handleDropdownClickoutside() {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-modal :show="show" @mask-click="handleClose">
|
<n-modal :show="show" @mask-click="handleClose">
|
||||||
<n-card class="w-120 max-w-600px" :title="t('home.Data set group config')">
|
<n-card class="w-120 max-w-600px" :title="t('home.Data set group config')">
|
||||||
<n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto">
|
<n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto" :disabled="submitLoading">
|
||||||
<n-form-item :label="t('home.Parent group')">
|
<n-form-item :label="t('home.Parent group')">
|
||||||
<n-cascader v-model:value="model.pid" expand-trigger="hover" :options="treeData"
|
<n-cascader v-model:value="model.pid" expand-trigger="hover" :options="treeData"
|
||||||
check-strategy="child" show-path filterable clearable label-field="name" value-field="id" />
|
check-strategy="all" show-path filterable clearable label-field="name" value-field="id" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item :label="t('home.Group name')" path="name">
|
<n-form-item :label="t('home.Group name')" path="name">
|
||||||
<n-input v-model:value="model.name" />
|
<n-input v-model:value="model.name" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<div class="flex justify-end mt-4">
|
<div class="flex justify-end mt-4">
|
||||||
<n-button @click="handleClose" class="mr-2">{{ t("other.Cancel") }}</n-button>
|
<n-button :disabled="submitLoading" @click="handleClose" class="mr-2">{{ t("other.Cancel") }}</n-button>
|
||||||
<n-button type="primary" @click="saveDataSet">{{ t("other.Ok") }}</n-button>
|
<n-button type="primary" :loading="submitLoading" @click="saveDataSet">{{ t("other.Ok") }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
@@ -20,9 +20,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useTemplateRef,computed } from "vue";
|
import { useTemplateRef, ref } from "vue";
|
||||||
import type { FormInst } from 'naive-ui'
|
import type { FormInst } from 'naive-ui'
|
||||||
import { t } from "@/language";
|
import { t } from "@/language";
|
||||||
|
import { DataSetGroupPayload, fetchCreateDataSetGroup, fetchUpdateDataSetGroup } from "@/http/api/dataSetGroup";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
show: boolean,
|
show: boolean,
|
||||||
@@ -43,6 +44,7 @@ const formRef = useTemplateRef<FormInst>("formRef");
|
|||||||
const rules = {
|
const rules = {
|
||||||
name: { required: true, message: t("prompt.Please enter a grouping name for the dataset"), trigger: 'blur' }
|
name: { required: true, message: t("prompt.Please enter a grouping name for the dataset"), trigger: 'blur' }
|
||||||
};
|
};
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
emits("update:show", false);
|
emits("update:show", false);
|
||||||
@@ -51,15 +53,21 @@ function handleClose() {
|
|||||||
function saveDataSet(e: MouseEvent) {
|
function saveDataSet(e: MouseEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
formRef.value?.validate((errors) => {
|
formRef.value?.validate(async (errors) => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
if (!props.model.id) {
|
const payload: DataSetGroupPayload = {
|
||||||
// 新增
|
id: props.model.id || undefined,
|
||||||
|
pid: props.model.pid || "",
|
||||||
} else {
|
name: props.model.name?.trim() || ""
|
||||||
// 更新
|
};
|
||||||
|
|
||||||
|
submitLoading.value = true;
|
||||||
|
const res = props.model.id ? await fetchUpdateDataSetGroup(payload) : await fetchCreateDataSetGroup(payload);
|
||||||
|
submitLoading.value = false;
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
window.$message?.success(props.model.id ? t("prompt.Success to update") : t("prompt.Saved successfully!"));
|
||||||
|
|
||||||
// 刷新父页面表格
|
// 刷新父页面表格
|
||||||
emits("refresh");
|
emits("refresh");
|
||||||
|
|||||||
@@ -1,36 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-modal :show="show" @mask-click="handleClose">
|
<n-modal :show="show" @mask-click="handleClose">
|
||||||
<n-card class="w-200 max-w-1200px" :title="t('home.Data set config')">
|
<n-card class="w-200 max-w-1200px" :title="t('home.Data set config')">
|
||||||
<n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto">
|
<n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto" :disabled="submitLoading">
|
||||||
<n-grid :cols="24" :x-gap="24">
|
<n-grid :cols="24" :x-gap="24">
|
||||||
<n-form-item-gi :span="12" :label="t('home.Data set name')" path="name">
|
<n-form-item-gi :span="12" :label="t('home.Data set name')" path="name">
|
||||||
<n-input v-model:value="model.name" />
|
<n-input v-model:value="model.name" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="12" :label="t('home.Data set group')">
|
<n-form-item-gi :span="12" :label="t('home.Data set group')" path="groupId">
|
||||||
<n-cascader v-model:value="model.groupId" expand-trigger="hover" :options="groupOptions"
|
<n-cascader
|
||||||
check-strategy="child" show-path filterable clearable label-field="name" value-field="id" />
|
v-model:value="model.groupId"
|
||||||
|
expand-trigger="hover"
|
||||||
|
:options="groupOptions"
|
||||||
|
check-strategy="all"
|
||||||
|
show-path
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
label-field="name"
|
||||||
|
value-field="id"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="12" :label="t('home.Data set type')">
|
<n-form-item-gi :span="12" :label="t('home.Data set type')" path="type">
|
||||||
<n-select v-model:value="model.type" :options="setTypes" />
|
<n-select v-model:value="model.type" :options="setTypes" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
|
|
||||||
<template v-if="model.type === 'API'">
|
<template v-if="model.type === 'API'">
|
||||||
<n-form-item-gi :span="12" :label="t('home.Method')">
|
<n-form-item-gi :span="12" :label="t('home.Method')" path="method">
|
||||||
<n-select v-model:value="model.type" :options="[
|
<n-select
|
||||||
|
:key="'dataset-method-select'"
|
||||||
|
v-model:value="model.method"
|
||||||
|
:options="[
|
||||||
{ label: 'GET', value: 'GET' },
|
{ label: 'GET', value: 'GET' },
|
||||||
{ label: 'POST', value: 'POST' },
|
{ label: 'POST', value: 'POST' },
|
||||||
]" />
|
]"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="24" :label="t('home.API interface')">
|
<n-form-item-gi :span="24" :label="t('home.API interface')" path="api">
|
||||||
<n-input v-model:value="model.api" type="textarea" />
|
<n-input v-model:value="model.api" type="textarea" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="model.type === 'SQL'">
|
<template v-else-if="model.type === 'SQL'">
|
||||||
<n-form-item-gi :span="12" :label="t('home.Data sources')">
|
<n-form-item-gi :span="12" :label="t('home.Data sources')" path="dataSourceId">
|
||||||
<n-select v-model:value="model.dataSource" :options="dataSourceOptions" />
|
<n-select :key="'dataset-datasource-select'" v-model:value="model.dataSourceId" :options="dataSourceOptions" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="24" :label="`SQL(${t('home.Query only')})`">
|
<n-form-item-gi :span="24" :label="`SQL(${t('home.Query only')})`" path="sql">
|
||||||
<SQLEditor v-model:value="model.sql as string" />
|
<SQLEditor v-model:value="model.sql as string" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="24" label=" ">
|
<n-form-item-gi :span="24" label=" ">
|
||||||
@@ -44,15 +57,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="model.type === 'JSON'">
|
<template v-else-if="model.type === 'JSON'">
|
||||||
<n-form-item-gi :span="24" label="JSON">
|
<n-form-item-gi :span="24" label="JSON" path="json">
|
||||||
<JSONEditor v-model:value="model.json as string" />
|
<JSONEditor v-model:value="model.json as string" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<n-gi :span="24">
|
<n-gi :span="24">
|
||||||
<div class="flex justify-end mt-4">
|
<div class="flex justify-end mt-4">
|
||||||
<n-button @click="handleClose" class="mr-2">{{ t("other.Cancel") }}</n-button>
|
<n-button :disabled="submitLoading" @click="handleClose" class="mr-2">{{ t("other.Cancel") }}</n-button>
|
||||||
<n-button type="primary" @click="saveDataSet">{{ t("other.Ok") }}</n-button>
|
<n-button type="primary" :loading="submitLoading" @click="saveDataSet">{{ t("other.Ok") }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
@@ -62,60 +75,256 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useTemplateRef } from "vue";
|
import { ref, watch, computed, useTemplateRef } from "vue";
|
||||||
import type { FormInst } from 'naive-ui'
|
import type { FormInst } from "naive-ui";
|
||||||
import { t } from "@/language";
|
import { t } from "@/language";
|
||||||
import SQLEditor from "@/components/code/SQLEditor.vue";
|
import { DataSetPayload, fetchCreateDataSet, fetchUpdateDataSet } from "@/http/api/dataSet";
|
||||||
import JSONEditor from "@/components/code/JSONEditor.vue";
|
import { fetchDataSetGroupTree } from "@/http/api/dataSetGroup";
|
||||||
|
import { fetchDataSourceList } from "@/http/api/dataSource";
|
||||||
const props = withDefaults(defineProps<{
|
import SQLEditor from "@/components/code/SQLEditor.vue";
|
||||||
show: boolean,
|
import JSONEditor from "@/components/code/JSONEditor.vue";
|
||||||
model: IDataSet.Item
|
const props = withDefaults(
|
||||||
}>(), {
|
defineProps<{
|
||||||
|
show: boolean;
|
||||||
|
model?: IDataSet.Item;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
show: false,
|
show: false,
|
||||||
model: () => ({
|
model: () => ({
|
||||||
id: '',
|
id: "",
|
||||||
groupId: "",
|
groupId: "",
|
||||||
name: '',
|
name: "",
|
||||||
type: 'SQL',
|
type: "API",
|
||||||
})
|
method: "GET",
|
||||||
})
|
api: "",
|
||||||
const emits = defineEmits(["update:show", "refresh"]);
|
dataSourceId: "",
|
||||||
|
sql: "",
|
||||||
|
json: "",
|
||||||
|
createTime: "",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const emits = defineEmits(["update:show", "refresh"]);
|
||||||
|
|
||||||
const formRef = useTemplateRef<FormInst>("formRef");
|
const formRef = useTemplateRef<FormInst>("formRef");
|
||||||
const rules = {
|
function createRequiredRule(message: string, trigger: Array<"input" | "blur" | "change">) {
|
||||||
name: { required: true, message: t("prompt.Please enter a name for the dataset"), trigger: 'blur' }
|
return {
|
||||||
};
|
trigger,
|
||||||
const groupOptions = [];
|
validator(_: unknown, value: unknown) {
|
||||||
const setTypes = [
|
if (value === undefined || value === null || value === "") {
|
||||||
{ label: 'API', value: 'API' },
|
return new Error(message);
|
||||||
{ label: 'SQL', value: 'SQL' },
|
}
|
||||||
{ label: 'JSON', value: 'JSON' },
|
if (Array.isArray(value) && value.length === 0) {
|
||||||
];
|
return new Error(message);
|
||||||
const dataSourceOptions = [];
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function handleClose() {
|
const rules = computed(() => {
|
||||||
emits("update:show", false);
|
const baseRules: Record<string, any> = {
|
||||||
}
|
name: { required: true, message: t("prompt.Please enter a name for the dataset"), trigger: ["input", "blur"] },
|
||||||
|
groupId: createRequiredRule("请选择数据集分组", ["change", "blur"]),
|
||||||
|
type: { required: true, message: "请选择数据集类型", trigger: ["change", "blur"] },
|
||||||
|
};
|
||||||
|
const type = props.model?.type;
|
||||||
|
if (type === "API") {
|
||||||
|
baseRules.method = {
|
||||||
|
required: true,
|
||||||
|
message: "请选择请求方式",
|
||||||
|
trigger: ["change", "blur"],
|
||||||
|
};
|
||||||
|
baseRules.api = {
|
||||||
|
required: true,
|
||||||
|
message: "请输入接口地址",
|
||||||
|
trigger: ["input", "blur"],
|
||||||
|
};
|
||||||
|
} else if (type === "SQL") {
|
||||||
|
baseRules.dataSourceId = createRequiredRule("请选择数据源", ["change", "blur"]);
|
||||||
|
baseRules.sql = { required: true, message: "请输入 SQL 语句", trigger: ["blur"] };
|
||||||
|
} else if (type === "JSON") {
|
||||||
|
baseRules.json = { required: true, message: "请输入 JSON 数据", trigger: ["blur"] };
|
||||||
|
}
|
||||||
|
return baseRules;
|
||||||
|
});
|
||||||
|
const groupOptions = ref<IDataSet.IGroup[]>([]);
|
||||||
|
const setTypes = [
|
||||||
|
{ label: "API", value: "API" },
|
||||||
|
{ label: "SQL", value: "SQL" },
|
||||||
|
{ label: "JSON", value: "JSON" },
|
||||||
|
];
|
||||||
|
const dataSourceOptions = ref<{ label: string; value: IDataSource.Item["id"] }[]>([]);
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
const isEdit = computed(() => Boolean(props.model?.id));
|
||||||
|
|
||||||
function saveDataSet(e: MouseEvent) {
|
function normalizeDataSourceId(value: unknown): IDataSource.Item["id"] | undefined {
|
||||||
e.preventDefault()
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
formRef.value?.validate((errors) => {
|
function extractList<T>(value: unknown): T[] {
|
||||||
if (!errors) {
|
if (Array.isArray(value)) {
|
||||||
if (!props.model.id) {
|
return value as T[];
|
||||||
// 新增
|
}
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
const nested = (value as Record<string, unknown>).result;
|
||||||
|
if (Array.isArray(nested)) {
|
||||||
|
return nested as T[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOptions() {
|
||||||
|
const [groupRes, dataSourceRes] = await Promise.allSettled([fetchDataSetGroupTree(), fetchDataSourceList()]);
|
||||||
|
|
||||||
|
if (groupRes.status === "fulfilled") {
|
||||||
|
groupOptions.value = extractList<IDataSet.IGroup>(groupRes.value.data);
|
||||||
} else {
|
} else {
|
||||||
// 更新
|
groupOptions.value = [];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新父页面表格
|
if (dataSourceRes.status === "fulfilled") {
|
||||||
|
const list = extractList<IDataSource.Item>(dataSourceRes.value.data);
|
||||||
|
dataSourceOptions.value = list.map(item => ({
|
||||||
|
label: item.name,
|
||||||
|
value: String(item.id),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dataSourceOptions.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeJsonValue(value?: IDataSet.Item["json"]) {
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFieldsForType(type: string, prevType?: string) {
|
||||||
|
if (!props.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type !== "API") {
|
||||||
|
props.model.method = undefined;
|
||||||
|
props.model.api = "";
|
||||||
|
}
|
||||||
|
if (type === "API" && !props.model.method) {
|
||||||
|
props.model.method = "GET";
|
||||||
|
}
|
||||||
|
if (type !== "SQL") {
|
||||||
|
props.model.dataSourceId = undefined;
|
||||||
|
props.model.sql = "";
|
||||||
|
} else if (prevType && prevType !== "SQL") {
|
||||||
|
props.model.dataSourceId = undefined;
|
||||||
|
props.model.sql = "";
|
||||||
|
}
|
||||||
|
if (type !== "JSON") {
|
||||||
|
props.model.json = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSql(sql: string) {
|
||||||
|
const trimmed = sql.trim();
|
||||||
|
if (!/^(SELECT|WITH)\b/i.test(trimmed)) {
|
||||||
|
window.$message?.error("SQL 必须以 SELECT 或 WITH 开头");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/[;]|--|\/\*/.test(trimmed)) {
|
||||||
|
window.$message?.error("SQL 不允许包含危险语句标记");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayload(): DataSetPayload {
|
||||||
|
const payload: DataSetPayload = {
|
||||||
|
name: props.model?.name?.trim() || "",
|
||||||
|
groupId: props.model?.groupId ?? "",
|
||||||
|
type: props.model?.type || "API",
|
||||||
|
};
|
||||||
|
if (props.model?.id) {
|
||||||
|
payload.id = props.model.id;
|
||||||
|
}
|
||||||
|
if (payload.type === "API") {
|
||||||
|
payload.method = props.model?.method;
|
||||||
|
payload.api = props.model?.api?.trim();
|
||||||
|
} else if (payload.type === "SQL") {
|
||||||
|
payload.dataSourceId = normalizeDataSourceId(props.model?.dataSourceId);
|
||||||
|
payload.sql = props.model?.sql?.trim();
|
||||||
|
} else if (payload.type === "JSON") {
|
||||||
|
payload.json = normalizeJsonValue(props.model?.json);
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
show => {
|
||||||
|
if (!show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadOptions();
|
||||||
|
if (props.model) {
|
||||||
|
props.model.dataSourceId = normalizeDataSourceId(props.model.dataSourceId);
|
||||||
|
}
|
||||||
|
if (props.model?.type === "API" && !props.model.method) {
|
||||||
|
props.model.method = "GET";
|
||||||
|
}
|
||||||
|
if (props.model?.type === "JSON") {
|
||||||
|
props.model.json = normalizeJsonValue(props.model.json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.model?.type,
|
||||||
|
(next, prev) => {
|
||||||
|
if (!next || next === prev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetFieldsForType(next, prev);
|
||||||
|
formRef.value?.restoreValidation();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emits("update:show", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDataSet(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
formRef.value?.validate(async errors => {
|
||||||
|
if (errors || !props.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.model.type === "SQL" && !validateSql(props.model.sql || "")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLoading.value = true;
|
||||||
|
const payload = buildPayload();
|
||||||
|
const res = isEdit.value ? await fetchUpdateDataSet(payload) : await fetchCreateDataSet(payload);
|
||||||
|
submitLoading.value = false;
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.$message?.success(isEdit.value ? t("prompt.Success to update") : t("prompt.Saved successfully!"));
|
||||||
emits("refresh");
|
emits("refresh");
|
||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-input>
|
</n-input>
|
||||||
|
|
||||||
<n-button type="primary" @click="showDataSourceModal = true">{{ t('home.Add data source') }}</n-button>
|
<n-button type="primary" @click="handleAddDataSource">{{ t('home.Add data source') }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
<n-data-table ref="tableRef" :columns="dataSourceColumns" :data="dataSources" :pagination="pagination" striped />
|
<n-data-table ref="tableRef" :columns="dataSourceColumns" :data="dataSources" :pagination="pagination" :loading="tableLoading" striped />
|
||||||
|
|
||||||
<!-- 数据源模态框 -->
|
<!-- 数据源模态框 -->
|
||||||
<DataSourceModal v-model:show="showDataSourceModal" :model="currentDataSource" @refresh="getList" />
|
<DataSourceModal v-model:show="showDataSourceModal" :model="currentDataSource" @refresh="getList" />
|
||||||
@@ -21,18 +21,23 @@ import { NButton, NPopconfirm } from 'naive-ui';
|
|||||||
import type { DataTableInst } from 'naive-ui';
|
import type { DataTableInst } from 'naive-ui';
|
||||||
import { Search } from '@vicons/carbon';
|
import { Search } from '@vicons/carbon';
|
||||||
import { t } from "@/language";
|
import { t } from "@/language";
|
||||||
|
import { fetchDataSourceList, fetchDeleteDataSource } from "@/http/api/dataSource";
|
||||||
import DataSourceModal from "./DataSourceModal.vue";
|
import DataSourceModal from "./DataSourceModal.vue";
|
||||||
|
|
||||||
const tableRef = useTemplateRef<DataTableInst>("tableRef");
|
const tableRef = useTemplateRef<DataTableInst>("tableRef");
|
||||||
const dataSources = ref<IDataSource.Item[]>([])
|
const dataSources = ref<IDataSource.Item[]>([])
|
||||||
const showDataSourceModal = ref(false)
|
const showDataSourceModal = ref(false)
|
||||||
const currentDataSource = reactive({
|
const tableLoading = ref(false);
|
||||||
|
const defaultDataSource = {
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
type: 'MySQL',
|
type: 'MySQL',
|
||||||
connectionString: '',
|
connectionString: '',
|
||||||
username: "",
|
username: "",
|
||||||
password: ""
|
password: ""
|
||||||
|
}
|
||||||
|
const currentDataSource = reactive({
|
||||||
|
...defaultDataSource
|
||||||
})
|
})
|
||||||
const dataSourceColumns = [
|
const dataSourceColumns = [
|
||||||
{
|
{
|
||||||
@@ -106,7 +111,21 @@ const dataSourceColumns = [
|
|||||||
// 分页配置
|
// 分页配置
|
||||||
const pagination = { pageSize: 10 }
|
const pagination = { pageSize: 10 }
|
||||||
|
|
||||||
function getList() { }
|
async function getList() {
|
||||||
|
tableLoading.value = true;
|
||||||
|
const res = await fetchDataSourceList();
|
||||||
|
tableLoading.value = false;
|
||||||
|
dataSources.value = res.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCurrentDataSource(data?: Partial<IDataSource.Item>) {
|
||||||
|
Object.assign(currentDataSource, defaultDataSource, data || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddDataSource() {
|
||||||
|
resetCurrentDataSource();
|
||||||
|
showDataSourceModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
function handleSearch(searchText: string) {
|
function handleSearch(searchText: string) {
|
||||||
tableRef.value?.filter({
|
tableRef.value?.filter({
|
||||||
@@ -115,12 +134,17 @@ function handleSearch(searchText: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editDataSource(item) {
|
function editDataSource(item) {
|
||||||
Object.assign(currentDataSource, item)
|
resetCurrentDataSource(item)
|
||||||
showDataSourceModal.value = true
|
showDataSourceModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteDataSource(item) {
|
async function deleteDataSource(item) {
|
||||||
dataSources.value = dataSources.value.filter(ds => ds.id !== item.id)
|
const res = await fetchDeleteDataSource(item.id);
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.$message?.success(t("prompt.Success to delete"));
|
||||||
|
getList();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-modal :show="show" @mask-click="handleClose">
|
<n-modal :show="show" @mask-click="handleClose">
|
||||||
<n-card class="w-100 max-w-600px" :title="t('home.Data source config')">
|
<n-card class="w-100 max-w-600px" :title="t('home.Data source config')">
|
||||||
<n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto">
|
<n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto" :disabled="testLinkLoading || submitLoading">
|
||||||
<n-form-item :label="t('home.Data source name')" path="name">
|
<n-form-item :label="t('home.Data source name')" path="name">
|
||||||
<n-input v-model:value="model.name" :disabled="testLinkLoading" />
|
<n-input v-model:value="model.name" :disabled="testLinkLoading" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
<n-button type="primary" :loading="testLinkLoading" @click="handleTestLink">{{ t("home.Test the connection") }}</n-button>
|
<n-button type="primary" :loading="testLinkLoading" @click="handleTestLink">{{ t("home.Test the connection") }}</n-button>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<n-button @click="handleClose" class="mr-2">{{ t("other.Cancel") }}</n-button>
|
<n-button :disabled="testLinkLoading || submitLoading" @click="handleClose" class="mr-2">{{ t("other.Cancel") }}</n-button>
|
||||||
<n-button type="primary" @click="saveDataSource">{{ t("other.Ok") }}</n-button>
|
<n-button type="primary" :loading="submitLoading" @click="saveDataSource">{{ t("other.Ok") }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -33,9 +33,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, useTemplateRef } from "vue";
|
import { ref, computed, useTemplateRef } from "vue";
|
||||||
import type { FormInst } from 'naive-ui'
|
import type { FormInst } from 'naive-ui'
|
||||||
import { t } from "@/language";
|
import { t } from "@/language";
|
||||||
|
import { DataSourcePayload, fetchCreateDataSource, fetchTestDataSource, fetchUpdateDataSource } from "@/http/api/dataSource";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
show: boolean,
|
show: boolean,
|
||||||
@@ -65,6 +66,8 @@ const dbTypes = [
|
|||||||
{ label: 'Oracle', value: 'Oracle' }
|
{ label: 'Oracle', value: 'Oracle' }
|
||||||
]
|
]
|
||||||
const testLinkLoading = ref(false);
|
const testLinkLoading = ref(false);
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
const isEdit = computed(() => Boolean(props.model.id));
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
emits("update:show", false);
|
emits("update:show", false);
|
||||||
@@ -72,25 +75,52 @@ function handleClose() {
|
|||||||
|
|
||||||
function handleTestLink(e: MouseEvent) {
|
function handleTestLink(e: MouseEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
formRef.value?.validate((errors) => {
|
formRef.value?.validate(async (errors) => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
testLinkLoading.value = true;
|
testLinkLoading.value = true;
|
||||||
|
const res = await fetchTestDataSource(buildPayload(false));
|
||||||
|
testLinkLoading.value = false;
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.$message?.success(t("prompt.Operation successful"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPayload(includeId: boolean): DataSourcePayload {
|
||||||
|
const payload: DataSourcePayload = {
|
||||||
|
name: props.model.name?.trim() || "",
|
||||||
|
type: props.model.type,
|
||||||
|
connectionString: props.model.connectionString?.trim() || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.model.username) {
|
||||||
|
payload.username = props.model.username;
|
||||||
|
}
|
||||||
|
if (props.model.password) {
|
||||||
|
payload.password = props.model.password;
|
||||||
|
}
|
||||||
|
if (includeId && props.model.id) {
|
||||||
|
payload.id = props.model.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
function saveDataSource(e: MouseEvent) {
|
function saveDataSource(e: MouseEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
formRef.value?.validate((errors) => {
|
formRef.value?.validate(async (errors) => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
if (!props.model.id) {
|
submitLoading.value = true;
|
||||||
// 新增
|
const payload = buildPayload(true);
|
||||||
|
const res = isEdit.value ? await fetchUpdateDataSource(payload) : await fetchCreateDataSource(payload);
|
||||||
} else {
|
submitLoading.value = false;
|
||||||
// 更新
|
if (res.error) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
window.$message?.success(isEdit.value ? t("prompt.Success to update") : t("prompt.Saved successfully!"));
|
||||||
|
|
||||||
// 刷新父页面表格
|
// 刷新父页面表格
|
||||||
emits("refresh");
|
emits("refresh");
|
||||||
|
|||||||
@@ -19,26 +19,12 @@ const options = [
|
|||||||
key: 'details',
|
key: 'details',
|
||||||
icon: renderIcon(Information)
|
icon: renderIcon(Information)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: () => cpt("home.Rename").value,
|
|
||||||
key: 'rename',
|
|
||||||
icon: renderIcon(Edit)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: () => cpt("home.Delete").value,
|
label: () => cpt("home.Delete").value,
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
icon: renderIcon(Delete)
|
icon: renderIcon(Delete)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: () => cpt("home.Release").value,
|
|
||||||
key: 'release',
|
|
||||||
icon: renderIcon(SendAlt)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: () => cpt("layout.header.Export").value,
|
|
||||||
key: 'export',
|
|
||||||
icon: renderIcon(Export)
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
const detail = ref();
|
const detail = ref();
|
||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {nextTick, onMounted, provide, ref} from "vue";
|
|||||||
import {useRoute} from 'vue-router';
|
import {useRoute} from 'vue-router';
|
||||||
import {t} from "@/language";
|
import {t} from "@/language";
|
||||||
import {fetchGetOneScene} from "@/http/api/scenes";
|
import {fetchGetOneScene} from "@/http/api/scenes";
|
||||||
import {App,Viewer,Hooks,Package,defaultProjectInfo} from "@astral3d/engine";
|
import {App,Viewer,Hooks,defaultProjectInfo} from "@astral3d/engine";
|
||||||
import {usePreviewOperationStore} from "@/store/modules/previewOperation";
|
import {usePreviewOperationStore} from "@/store/modules/previewOperation";
|
||||||
import EsCubeLoading from "@/components/es/EsCubeLoading.vue";
|
import EsCubeLoading from "@/components/es/EsCubeLoading.vue";
|
||||||
import PreviewSceneTree from "@/views/preview/components/PreviewSceneTree.vue";
|
import PreviewSceneTree from "@/views/preview/components/PreviewSceneTree.vue";
|
||||||
@@ -97,8 +97,7 @@ function getScene(sceneInfo) {
|
|||||||
closable: false,
|
closable: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const p = new Package(window.viewer);
|
window.viewer.package.unpack({
|
||||||
p.unpack({
|
|
||||||
url: sceneInfo.zip,
|
url: sceneInfo.zip,
|
||||||
onSceneLoad: (sceneJson: ISceneJson) => {
|
onSceneLoad: (sceneJson: ISceneJson) => {
|
||||||
if (sceneJson.controls?.state) {
|
if (sceneJson.controls?.state) {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import MapLayer from "@/cesium/modules/mapLayer";
|
||||||
|
import CameraUtils from "@/cesium/modules/cameraUtils";
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { VNode } from "vue";
|
||||||
|
export default class CesiumApp {
|
||||||
|
dom: HTMLElement;
|
||||||
|
cesiumParentElement: HTMLElement;
|
||||||
|
viewer: Cesium.Viewer;
|
||||||
|
helper: Cesium.EventHelper;
|
||||||
|
module: {
|
||||||
|
mapLayer: MapLayer;
|
||||||
|
cameraUtils: CameraUtils;
|
||||||
|
};
|
||||||
|
_three: {
|
||||||
|
camera: THREE.PerspectiveCamera;
|
||||||
|
scene: THREE.Scene;
|
||||||
|
sceneHelpers: THREE.Scene;
|
||||||
|
showSceneHelpers: boolean;
|
||||||
|
renderer: THREE.WebGLRenderer;
|
||||||
|
};
|
||||||
|
constructor(dom: any);
|
||||||
|
getViewer(): Cesium.Viewer;
|
||||||
|
/**
|
||||||
|
* 场景事件监听
|
||||||
|
*/
|
||||||
|
eventListener(): void;
|
||||||
|
/**
|
||||||
|
* 鼠标按下处理
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
onMouseDown(event: any): void;
|
||||||
|
/**
|
||||||
|
* 鼠标抬起处理
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
onMouseUp(event: any): void;
|
||||||
|
/**
|
||||||
|
* three 场景点击事件
|
||||||
|
*/
|
||||||
|
handleClick(): void;
|
||||||
|
/**
|
||||||
|
* 初始化cesium 场景后的处理操作
|
||||||
|
*/
|
||||||
|
handleInitCesiumAfter(): void;
|
||||||
|
/**
|
||||||
|
* 添加中国地图遮罩
|
||||||
|
*/
|
||||||
|
addChinaMask(): void;
|
||||||
|
/**
|
||||||
|
* 获取与鼠标点击位置射线相交的对象数组
|
||||||
|
*/
|
||||||
|
getIntersects(point: any): THREE.Intersection<THREE.Object3D<THREE.Object3DEventMap>>[];
|
||||||
|
/**
|
||||||
|
* 添加VNode至 viewer._toolbar
|
||||||
|
*/
|
||||||
|
addVNodeToViewer(vNode: VNode): void;
|
||||||
|
/**
|
||||||
|
* 重置cesium场景
|
||||||
|
*/
|
||||||
|
reset(): void;
|
||||||
|
/**
|
||||||
|
* 销毁
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import * as Cesium from 'cesium';
|
||||||
|
/**
|
||||||
|
* @Date 2022-06-09
|
||||||
|
* @Author 二三
|
||||||
|
* @param {*} viewer Cesium.Viewer
|
||||||
|
* @Description: cesium相机类
|
||||||
|
*/
|
||||||
|
export default class CameraUtils {
|
||||||
|
viewer: Cesium.Viewer | null;
|
||||||
|
entity: Cesium.Entity | null;
|
||||||
|
constructor();
|
||||||
|
init(): void;
|
||||||
|
setViewer(viewer: any): void;
|
||||||
|
/**
|
||||||
|
* 修改鼠标操作方式
|
||||||
|
*/
|
||||||
|
changeMouseOperate(): void;
|
||||||
|
flyTo(lng: any, lat: any, distance: any, pitch?: number, heading?: number): void;
|
||||||
|
/**
|
||||||
|
* 销毁
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import * as Cesium from 'cesium';
|
||||||
|
/**
|
||||||
|
* @Date 2023-03-07
|
||||||
|
* @Author 二三
|
||||||
|
* @Description: cesium地图底图图层管理
|
||||||
|
*/
|
||||||
|
export default class MapLayer {
|
||||||
|
viewer: Cesium.Viewer | null;
|
||||||
|
layers: {
|
||||||
|
[s: string]: {
|
||||||
|
satellite?: Cesium.UrlTemplateImageryProvider | Cesium.WebMapTileServiceImageryProvider;
|
||||||
|
mark?: Cesium.UrlTemplateImageryProvider;
|
||||||
|
vector?: Cesium.UrlTemplateImageryProvider;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
constructor();
|
||||||
|
setViewer(viewer: any): void;
|
||||||
|
/**
|
||||||
|
* 获取默认底图
|
||||||
|
* @param layer 底图类型,默认卫星影像图 enum: satellite | vector
|
||||||
|
*/
|
||||||
|
getDefaultLayer(layer?: 'satellite' | 'vector'): Cesium.UrlTemplateImageryProvider | Cesium.WebMapTileServiceImageryProvider | undefined;
|
||||||
|
/**
|
||||||
|
* 获取默认底图对应得标记图
|
||||||
|
*/
|
||||||
|
getMarkMapByDefaultLayer(): Cesium.UrlTemplateImageryProvider | undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* 笛卡尔坐标转换经纬度坐标
|
||||||
|
* @param {*} car3_ps
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export declare function getLngLatByCartesian3(car3_ps: any): {
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
elevation: number;
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import * as THREE from "three";
|
||||||
|
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
|
||||||
|
import CesiumApp from "@/cesium/cesiumApp";
|
||||||
|
/**
|
||||||
|
* @Date 2023-02-06
|
||||||
|
* @Author 二三
|
||||||
|
* @Description: cesium视图出口
|
||||||
|
*/
|
||||||
|
export default class ViewPort {
|
||||||
|
app: CesiumApp;
|
||||||
|
cesiumParentElement: HTMLElement;
|
||||||
|
_three: {
|
||||||
|
camera: THREE.PerspectiveCamera;
|
||||||
|
scene: THREE.Scene;
|
||||||
|
sceneHelpers: THREE.Scene;
|
||||||
|
showSceneHelpers: boolean;
|
||||||
|
renderer: THREE.WebGLRenderer;
|
||||||
|
transformControls: TransformControls | null;
|
||||||
|
box: THREE.Box3;
|
||||||
|
};
|
||||||
|
threeSelectionBox: THREE.Box3Helper;
|
||||||
|
minWGS84: Array<number>;
|
||||||
|
maxWGS84: Array<number>;
|
||||||
|
animationFrameID: number | null;
|
||||||
|
constructor(dom: any);
|
||||||
|
init(): void;
|
||||||
|
/**
|
||||||
|
* 相关signals注册
|
||||||
|
*/
|
||||||
|
signalsRegister(isAdd?: boolean): void;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
addButtonToViewer(): void;
|
||||||
|
/**
|
||||||
|
* cesium 开始加载后的相关操作
|
||||||
|
*/
|
||||||
|
handleCesiumEvent(): void;
|
||||||
|
/**
|
||||||
|
* 初始化three变换控制器
|
||||||
|
*/
|
||||||
|
initThreeTransformControls(): void;
|
||||||
|
/**
|
||||||
|
* 计算场景中心位置等信息
|
||||||
|
*/
|
||||||
|
calcCenter(): void;
|
||||||
|
/**
|
||||||
|
* 处理three canvas 的 transformControls,以便于场景融合;
|
||||||
|
*/
|
||||||
|
handleThreeMouseMove(event: any): void;
|
||||||
|
/**
|
||||||
|
* 飞行至three场景
|
||||||
|
*/
|
||||||
|
flyToThree(): void;
|
||||||
|
/**
|
||||||
|
* cesium 渲染
|
||||||
|
*/
|
||||||
|
renderCesium(): void;
|
||||||
|
/**
|
||||||
|
* 两个画布相机融合
|
||||||
|
*/
|
||||||
|
fusionCanvas(): void;
|
||||||
|
/**
|
||||||
|
* three 渲染器的渲染
|
||||||
|
*/
|
||||||
|
renderThree(): void;
|
||||||
|
loop(): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export declare const CESIUM_DEFAULT_MAP: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
coordinateSystem: string;
|
||||||
|
}[];
|
||||||
|
export declare const CESIUM_DEFAULT_MAP_TYPE: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/** 错误信息的显示时间 */
|
||||||
|
export declare const ERROR_MSG_DURATION: number;
|
||||||
|
/** 默认的请求错误code */
|
||||||
|
export declare const DEFAULT_REQUEST_ERROR_CODE = "DEFAULT";
|
||||||
|
/** 默认的请求错误文本 */
|
||||||
|
export declare const DEFAULT_REQUEST_ERROR_MSG = "\u8BF7\u6C42\u9519\u8BEF~";
|
||||||
|
/** 请求超时的错误code(为固定值:ECONNABORTED) */
|
||||||
|
export declare const REQUEST_TIMEOUT_CODE = "ECONNABORTED";
|
||||||
|
/** 请求超时的错误文本 */
|
||||||
|
export declare const REQUEST_TIMEOUT_MSG = "\u8BF7\u6C42\u8D85\u65F6~";
|
||||||
|
/** 网络不可用的code */
|
||||||
|
export declare const NETWORK_ERROR_CODE = "NETWORK_ERROR";
|
||||||
|
/** 网络不可用的错误文本 */
|
||||||
|
export declare const NETWORK_ERROR_MSG = "\u7F51\u7EDC\u4E0D\u53EF\u7528~";
|
||||||
|
/** 请求不成功各种状态的错误 */
|
||||||
|
export declare const ERROR_STATUS: {
|
||||||
|
400: string;
|
||||||
|
401: string;
|
||||||
|
403: string;
|
||||||
|
404: string;
|
||||||
|
405: string;
|
||||||
|
408: string;
|
||||||
|
500: string;
|
||||||
|
501: string;
|
||||||
|
502: string;
|
||||||
|
503: string;
|
||||||
|
504: string;
|
||||||
|
505: string;
|
||||||
|
DEFAULT: string;
|
||||||
|
};
|
||||||
|
/** 不弹出错误信息的code */
|
||||||
|
export declare const NO_ERROR_MSG_CODE: (string | number)[];
|
||||||
|
/** token失效需要刷新token的code */
|
||||||
|
export declare const REFRESH_TOKEN_CODE: (string | number)[];
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Preview } from "@astral3d/engine";
|
||||||
|
export declare class AssetPreview {
|
||||||
|
#private;
|
||||||
|
previewer: Preview;
|
||||||
|
private container;
|
||||||
|
static getInstance(container?: HTMLElement): AssetPreview;
|
||||||
|
constructor(container?: HTMLElement);
|
||||||
|
updateContainer(container: HTMLElement): void;
|
||||||
|
/**
|
||||||
|
* 加载预览项
|
||||||
|
*/
|
||||||
|
load(fileOrUrl: string | File, type?: string): Promise<unknown>;
|
||||||
|
clear(): void;
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
declare namespace IDataSource {
|
declare namespace IDataSource {
|
||||||
|
type Id = string | number;
|
||||||
interface Item {
|
interface Item {
|
||||||
id: string;
|
id: Id;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
connectionString: string;
|
connectionString: string;
|
||||||
@@ -10,21 +11,24 @@ declare namespace IDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare namespace IDataSet {
|
declare namespace IDataSet {
|
||||||
|
type Id = string | number;
|
||||||
interface Item {
|
interface Item {
|
||||||
id: string;
|
id: Id;
|
||||||
groupId: string;
|
groupId: Id;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
method?: "GET" | "POST";
|
method?: "GET" | "POST";
|
||||||
api?: string;
|
api?: string;
|
||||||
dataSource?: string;
|
dataSourceId?: Id;
|
||||||
|
dataSource?: Id;
|
||||||
sql?: string;
|
sql?: string;
|
||||||
json?: string;
|
json?: string;
|
||||||
|
createTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGroup {
|
interface IGroup {
|
||||||
id: string;
|
id: Id;
|
||||||
pid?: string;
|
pid?: Id;
|
||||||
name: string;
|
name: string;
|
||||||
children?: IGroup[];
|
children?: IGroup[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import useBoolean from './useBoolean';
|
||||||
|
import useLoading from './useLoading';
|
||||||
|
export { useBoolean, useLoading };
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* boolean组合式函数
|
||||||
|
* @param initValue 初始值
|
||||||
|
*/
|
||||||
|
export default function useBoolean(initValue?: boolean): {
|
||||||
|
bool: import("vue").Ref<boolean, boolean>;
|
||||||
|
setBool: (value: boolean) => void;
|
||||||
|
setTrue: () => void;
|
||||||
|
setFalse: () => void;
|
||||||
|
toggle: () => void;
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default function useLoading(initValue?: boolean): {
|
||||||
|
loading: import("vue").Ref<boolean, boolean>;
|
||||||
|
startLoading: () => void;
|
||||||
|
endLoading: () => void;
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 开启 WebSocket 链接,全局只需执行一次
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
export declare function connectWebSocket(url: any): void;
|
||||||
|
/**
|
||||||
|
* 添加 WebSocket 消息监听
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
export declare function onWebSocket(callback: (data: object) => any): void;
|
||||||
|
/**
|
||||||
|
* 解除 WebSocket 消息监听
|
||||||
|
*
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
export declare function offWebSocket(callback: (data: object) => any): void;
|
||||||
|
export declare function useWebSocket(): {
|
||||||
|
data: string | null;
|
||||||
|
status: import("@vueuse/core").WebSocketStatus;
|
||||||
|
close: WebSocket["close"];
|
||||||
|
open: import("@vueuse/core").Fn;
|
||||||
|
send: (data: string | ArrayBuffer | Blob, useBuffer?: boolean) => boolean;
|
||||||
|
ws: WebSocket | undefined;
|
||||||
|
} | null;
|
||||||
|
export declare function send(message: string | ArrayBuffer | Blob): void;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* 获取资产分类树
|
||||||
|
*/
|
||||||
|
export declare function fetchAssetsCategoryTreeList(params?: {
|
||||||
|
type: string;
|
||||||
|
}): Promise<Service.RequestResult<T>>;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* 获取资产列表
|
||||||
|
*/
|
||||||
|
export declare function fetchGetAssetsList(params: Service.ListPageQueryParams): Promise<Service.RequestResult<T>>;
|
||||||
|
/**
|
||||||
|
* 新增资产
|
||||||
|
*/
|
||||||
|
export declare function fetchAddAsset(data: IAssets.Item): Promise<Service.RequestResult<T>>;
|
||||||
|
/**
|
||||||
|
* 更新资产
|
||||||
|
*/
|
||||||
|
export declare function fetchUpdateAsset(data: IAssets.Item): Promise<Service.RequestResult<T>>;
|
||||||
|
/**
|
||||||
|
* 移除资产
|
||||||
|
*/
|
||||||
|
export declare function fetchRemoveAsset(id: IAssets.Item['id']): Promise<Service.RequestResult<T>>;
|
||||||
|
/**
|
||||||
|
* 获取分类下的资产tags
|
||||||
|
*/
|
||||||
|
export declare function fetchGetAssetCategoryTags(type: IAssets.SupportType, category: string): Promise<Service.RequestResult<T>>;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 获取bim转换列表
|
||||||
|
*/
|
||||||
|
export declare function fetchGetBim2GltfList(params: any): Promise<Service.RequestResult<T>>;
|
||||||
|
/**
|
||||||
|
* 上传bim文件
|
||||||
|
*/
|
||||||
|
export declare function fetchUploadRvt(data: any): Promise<Service.RequestResult<T>>;
|
||||||
|
/**
|
||||||
|
* 添加数据并启动revit转换( 需传入接收结果的websocket uname)
|
||||||
|
*/
|
||||||
|
export declare function fetchAddBim2Gltf(data: any): Promise<Service.RequestResult<T>>;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* 获取cad列表
|
||||||
|
*/
|
||||||
|
export declare function fetchGetCadList(params: any): Promise<Service.RequestResult<T>>;
|
||||||
|
/**
|
||||||
|
* 添加数据并启动cad解析( 需传入接收结果的websocket uname)
|
||||||
|
*/
|
||||||
|
export declare function fetchAddDwg2dxf(data: any): Promise<Service.RequestResult<T>>;
|
||||||