feat(pointview): 新增「场景/点位」页签——正交俯视真实场景底图 + 点位精确叠加
第三个页签(与海选审核/演出配置平级),只读查看每个点位集里各点的真实
位置/朝向,配 move.to/camera.focus 时对照用,不必回 Unity 翻 json。
- pointview.js: 独立白模点位查看器(按 kind 上色/朝向箭头/悬停坐标/侧栏清单);
有底图则把正交俯视真实场景图当画布底图、点位按 shot.bounds 线性投上去
(像素级对齐家具),带显隐开关;无底图回退黑底白模。
- app.py: /api/pointsets 给有底图的点位集附 shot{url,bounds};新增
/sceneshot/{name}.png 路由(防目录穿越)。
- Dockerfile/compose: 加 STORY_SCENESHOTS_DIR(/sceneshots) env + 挂载点与注释。
底图由 SGame 仓新增 Editor 工具「剧情场景俯视抓拍」产出
({name}.png + {name}.shot.json,map-local 覆盖范围)。
This commit is contained in:
@ -9,7 +9,7 @@
|
||||
status: null,
|
||||
selectedNode: null,
|
||||
dirty: false,
|
||||
mode: "review", // review=海选审核 / perform=演出配置
|
||||
mode: "review", // review=海选审核 / perform=演出配置 / points=场景点位
|
||||
by: localStorage.getItem("story_by") || "匿名",
|
||||
};
|
||||
window.App = App;
|
||||
@ -196,20 +196,50 @@
|
||||
// ---------- 试走 ----------
|
||||
$("btn-playtest").onclick = () => Playtest.open(App.ir, App.dict);
|
||||
|
||||
// ---------- 模式切换:海选审核 / 演出配置 ----------
|
||||
// ---------- 模式切换:海选审核 / 演出配置 / 场景点位 ----------
|
||||
function setMode(m) {
|
||||
App.mode = m;
|
||||
$("mode-review").classList.toggle("active", m === "review");
|
||||
$("mode-perform").classList.toggle("active", m === "perform");
|
||||
$("mode-points").classList.toggle("active", m === "points");
|
||||
$("wrap").classList.toggle("hidden", m !== "review");
|
||||
$("perform-wrap").classList.toggle("hidden", m !== "perform");
|
||||
$("points-wrap").classList.toggle("hidden", m !== "points");
|
||||
$("review-toolbar").style.display = m === "review" ? "" : "none";
|
||||
document.body.classList.toggle("perform-mode", m === "perform"); // 切背景色调
|
||||
document.body.classList.toggle("points-mode", m === "points");
|
||||
Timeline.stop();
|
||||
if (m === "points") PointView.clear();
|
||||
if (m === "perform") performLoadList();
|
||||
if (m === "points") pointsLoadList();
|
||||
}
|
||||
$("mode-review").onclick = () => setMode("review");
|
||||
$("mode-perform").onclick = () => setMode("perform");
|
||||
$("mode-points").onclick = () => setMode("points");
|
||||
|
||||
// ---------- 场景点位页:点位集列表 + 只读白模查看器 ----------
|
||||
let pointsCurrent = null;
|
||||
function pointsLoadList() {
|
||||
const names = Object.keys(App.pointsets || {}).sort();
|
||||
const host = $("points-set-list"); host.innerHTML = "";
|
||||
if (!names.length) { host.innerHTML = '<div class="empty" style="padding:14px">未找到任何点位集(StreamingAssets/Story/PointSets/*.points.json)。</div>'; return; }
|
||||
names.forEach(name => {
|
||||
const ps = App.pointsets[name] || {};
|
||||
const cnt = (ps.anchors || ps.points || []).length;
|
||||
const d = document.createElement("div");
|
||||
d.className = "ev" + (name === pointsCurrent ? " sel" : "");
|
||||
d.innerHTML = '<div class="t">' + esc(name) + '</div><div class="g">'
|
||||
+ (ps.mapId ? '地图 ' + esc(ps.mapId) + ' · ' : '') + cnt + ' 个点</div>';
|
||||
d.onclick = () => pointsSelect(name);
|
||||
host.appendChild(d);
|
||||
});
|
||||
}
|
||||
function pointsSelect(name) {
|
||||
pointsCurrent = name;
|
||||
pointsLoadList();
|
||||
$("points-empty").style.display = "none";
|
||||
PointView.show($("points-view"), name, App.pointsets[name] || {});
|
||||
}
|
||||
|
||||
// ---------- 演出配置页:已确认事件列表 + 内嵌白模预览 ----------
|
||||
let performCurrent = null;
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
<div class="mode-switch">
|
||||
<button id="mode-review" class="mode-btn active">海选审核</button>
|
||||
<button id="mode-perform" class="mode-btn">演出配置</button>
|
||||
<button id="mode-points" class="mode-btn">场景/点位</button>
|
||||
</div>
|
||||
<div class="toolbar" id="review-toolbar">
|
||||
<button id="btn-save" class="primary" disabled>保存</button>
|
||||
@ -88,6 +89,18 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 场景/点位页(只读:看每个点位集里各点的真实位置/朝向,配 move.to 时对照用)-->
|
||||
<div id="points-wrap" class="hidden">
|
||||
<aside id="points-list-pane">
|
||||
<div class="perform-listhead">场景点位集</div>
|
||||
<div id="points-set-list"></div>
|
||||
</aside>
|
||||
<main id="points-main">
|
||||
<div id="points-empty" class="empty-center">从左侧选择一个点位集,查看其中各点的位置与朝向</div>
|
||||
<div id="points-view"></div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 校验结果遮罩 -->
|
||||
<div id="validate-modal" class="overlay hidden">
|
||||
<div class="modal">
|
||||
@ -136,6 +149,7 @@
|
||||
<script src="playtest.js"></script>
|
||||
<script src="timeline.js"></script>
|
||||
<script src="scene_edit.js"></script>
|
||||
<script src="pointview.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
269
web/static/pointview.js
Normal file
269
web/static/pointview.js
Normal file
@ -0,0 +1,269 @@
|
||||
// 场景/点位查看器(只读):把一个点位集里所有命名点画到 2D 俯视白模上,
|
||||
// 标名字、按 kind 上色、画朝向箭头(Unity Y 欧拉角),悬停/选中显示 map-local 坐标。
|
||||
// 用途:在网页里配 move.to / camera.focus 时,对照"这个点到底在场景哪儿、朝哪边"。
|
||||
// 坐标口径与 timeline.js 一致:俯视取 (x, z),+z 向上;rot=Unity eulerAngles.y(0=朝+Z)。
|
||||
// 暴露 window.PointView = { show(host, name, pointset), clear() }。
|
||||
|
||||
(function () {
|
||||
const KIND = {
|
||||
player: { col: "#f0d890", label: "玩家起点" },
|
||||
role: { col: "#7ec8e3", label: "NPC 站位" },
|
||||
point: { col: "#b8a878", label: "走位/镜头点" },
|
||||
"": { col: "#9a8f7e", label: "未分类" },
|
||||
};
|
||||
function kindOf(k) { return KIND[k] || KIND[""]; }
|
||||
function esc(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); }
|
||||
|
||||
let els = {}, cv, ctx, anchors = [], bounds = null, selected = null, hovered = null, ro = null;
|
||||
let shot = null, bgImg = null, bgReady = false, showBg = true; // 正交俯视底图
|
||||
|
||||
const TEMPLATE =
|
||||
'<div class="pv-head"></div>' +
|
||||
'<div class="pv-body">' +
|
||||
' <div class="pv-stagewrap"><canvas class="pv-stage"></canvas><div class="pv-tip hidden"></div></div>' +
|
||||
' <div class="pv-side">' +
|
||||
' <div class="pv-legend"></div>' +
|
||||
' <div class="pv-listhead">点位清单(点击定位)</div>' +
|
||||
' <div class="pv-list"></div>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
|
||||
function show(host, name, ps) {
|
||||
clear();
|
||||
ps = ps || {};
|
||||
anchors = (ps.anchors || []).slice();
|
||||
selected = null; hovered = null;
|
||||
shot = (ps.shot && Array.isArray(ps.shot.bounds) && ps.shot.bounds.length === 4) ? ps.shot : null;
|
||||
bgImg = null; bgReady = false; showBg = true;
|
||||
|
||||
host.innerHTML = TEMPLATE;
|
||||
els = {
|
||||
host,
|
||||
head: host.querySelector(".pv-head"),
|
||||
stagewrap: host.querySelector(".pv-stagewrap"),
|
||||
stage: host.querySelector(".pv-stage"),
|
||||
tip: host.querySelector(".pv-tip"),
|
||||
legend: host.querySelector(".pv-legend"),
|
||||
list: host.querySelector(".pv-list"),
|
||||
};
|
||||
cv = els.stage; ctx = cv.getContext("2d");
|
||||
|
||||
const n = anchors.length;
|
||||
els.head.innerHTML = "点位集 <b>" + esc(name) + "</b>" +
|
||||
(ps.mapId ? "(地图 " + esc(ps.mapId) + ")" : "") + " · " + n + " 个点" +
|
||||
(shot ? ' · <span class="pv-bgtag"><input type="checkbox" id="pv-bgtoggle" checked>正交俯视底图</span>'
|
||||
: ' · <span class="pv-nobg">无底图(在 Unity「剧情场景俯视抓拍」生成)</span>');
|
||||
|
||||
if (!n) {
|
||||
els.stagewrap.innerHTML = '<div class="pv-noanchor">该点位集没有坐标数据(可能是旧文件只有点名)。<br>请在 Unity 用「SGame/剧情点位集取点」补采坐标。</div>';
|
||||
els.legend.innerHTML = ""; els.list.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
computeBounds();
|
||||
buildLegend();
|
||||
buildList();
|
||||
|
||||
// 底图:异步加载,ready 后重绘;勾选框切显隐
|
||||
if (shot) {
|
||||
bgImg = new Image();
|
||||
bgImg.onload = () => { bgReady = true; draw(); };
|
||||
bgImg.onerror = () => { bgReady = false; };
|
||||
bgImg.src = shot.url;
|
||||
const cb = host.querySelector("#pv-bgtoggle");
|
||||
if (cb) cb.onchange = () => { showBg = cb.checked; draw(); };
|
||||
}
|
||||
|
||||
// 悬停拾取最近点
|
||||
cv.onmousemove = e => {
|
||||
const r = cv.getBoundingClientRect();
|
||||
const mx = (e.clientX - r.left) * (cv.width / r.width);
|
||||
const my = (e.clientY - r.top) * (cv.height / r.height);
|
||||
const hit = pickNearest(mx, my, 14);
|
||||
if (hit !== hovered) { hovered = hit; draw(); }
|
||||
if (hit) showTip(e.clientX - r.left, e.clientY - r.top, hit); else hideTip();
|
||||
};
|
||||
cv.onmouseleave = () => { if (hovered) { hovered = null; draw(); } hideTip(); };
|
||||
cv.onclick = () => { if (hovered) selectPoint(hovered.name); };
|
||||
|
||||
// 自适应尺寸:跟随容器宽高重绘
|
||||
ro = new ResizeObserver(() => resizeAndDraw());
|
||||
ro.observe(els.stagewrap);
|
||||
resizeAndDraw();
|
||||
}
|
||||
|
||||
function clear() {
|
||||
if (ro) { ro.disconnect(); ro = null; }
|
||||
if (els.host) els.host.innerHTML = "";
|
||||
els = {}; cv = ctx = null; anchors = []; bounds = null; selected = hovered = null;
|
||||
}
|
||||
|
||||
function computeBounds() {
|
||||
// 有底图:用底图覆盖的 map-local 范围当画布范围,点位投影才与底图像素级对齐。
|
||||
if (shot) {
|
||||
const b = shot.bounds;
|
||||
bounds = { minX: b[0], maxX: b[1], minZ: b[2], maxZ: b[3] };
|
||||
return;
|
||||
}
|
||||
// 无底图:按点位自身范围自适应(点位常远离世界原点,强含原点会把它们挤到角落)。
|
||||
// 原点十字仍由 draw() 在其落入视野时绘出。单点/共线时补一点跨度避免除零。
|
||||
const xs = anchors.map(a => a.pos[0]), zs = anchors.map(a => a.pos[2]);
|
||||
let minX = Math.min(...xs), maxX = Math.max(...xs), minZ = Math.min(...zs), maxZ = Math.max(...zs);
|
||||
if (maxX - minX < 1) { minX -= 1; maxX += 1; }
|
||||
if (maxZ - minZ < 1) { minZ -= 1; maxZ += 1; }
|
||||
bounds = { minX, maxX, minZ, maxZ };
|
||||
}
|
||||
|
||||
function resizeAndDraw() {
|
||||
if (!cv) return;
|
||||
const w = Math.max(320, els.stagewrap.clientWidth), h = Math.max(240, els.stagewrap.clientHeight);
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr);
|
||||
cv.style.width = w + "px"; cv.style.height = h + "px";
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
draw();
|
||||
}
|
||||
|
||||
// 世界(x,z) → 画布像素。等比缩放,+z 朝上。
|
||||
function proj(x, z) {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = cv.width / dpr, h = cv.height / dpr, pad = 48;
|
||||
const dx = (bounds.maxX - bounds.minX) || 1, dz = (bounds.maxZ - bounds.minZ) || 1;
|
||||
const sc = Math.min((w - pad * 2) / dx, (h - pad * 2) / dz);
|
||||
const cx = (bounds.minX + bounds.maxX) / 2, cz = (bounds.minZ + bounds.maxZ) / 2;
|
||||
return { x: w / 2 + (x - cx) * sc, y: h / 2 - (z - cz) * sc, sc };
|
||||
}
|
||||
|
||||
function draw() {
|
||||
if (!ctx) return;
|
||||
const dpr = window.devicePixelRatio || 1, w = cv.width / dpr, h = cv.height / dpr;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillStyle = "#0f1714"; ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// 底图:画进 bounds 投影出的矩形(其纵横比 = bounds 纵横比 = 图纵横比,故像素级贴合)
|
||||
const bgOn = shot && bgReady && showBg;
|
||||
if (bgOn) {
|
||||
const tl = proj(bounds.minX, bounds.maxZ), br = proj(bounds.maxX, bounds.minZ);
|
||||
ctx.drawImage(bgImg, tl.x, tl.y, br.x - tl.x, br.y - tl.y);
|
||||
}
|
||||
|
||||
if (!bgOn) drawGrid(w, h); // 有底图时网格让位给真实场景
|
||||
|
||||
// 世界原点十字(空间参照)
|
||||
const o = proj(0, 0);
|
||||
if (o.x >= 0 && o.x <= w && o.y >= 0 && o.y <= h) {
|
||||
ctx.strokeStyle = "rgba(120,180,150,.5)"; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.moveTo(o.x - 9, o.y); ctx.lineTo(o.x + 9, o.y); ctx.moveTo(o.x, o.y - 9); ctx.lineTo(o.x, o.y + 9); ctx.stroke();
|
||||
ctx.fillStyle = "rgba(120,180,150,.6)"; ctx.font = "10px sans-serif"; ctx.textAlign = "left"; ctx.fillText("原点(0,0)", o.x + 6, o.y - 6);
|
||||
}
|
||||
// 指北:+Z 朝上
|
||||
ctx.fillStyle = "rgba(150,200,175,.65)"; ctx.font = "11px sans-serif"; ctx.textAlign = "center";
|
||||
ctx.fillText("+Z ↑", w - 34, 16); ctx.fillText("+X →", w - 30, 30);
|
||||
|
||||
anchors.forEach(a => drawAnchor(a));
|
||||
}
|
||||
|
||||
function drawGrid(w, h) {
|
||||
// 取整世界单位画网格(自适应步长,避免过密)
|
||||
const range = Math.max(bounds.maxX - bounds.minX, bounds.maxZ - bounds.minZ) || 1;
|
||||
let step = 1; while (range / step > 14) step *= (step === 1 ? 2 : (step === 2 ? 2.5 : 2));
|
||||
ctx.strokeStyle = "rgba(120,150,135,.10)"; ctx.lineWidth = 1; ctx.font = "9px sans-serif"; ctx.textAlign = "center";
|
||||
const x0 = Math.ceil(bounds.minX / step) * step, x1 = bounds.maxX;
|
||||
for (let x = x0; x <= x1 + 1e-6; x += step) {
|
||||
const p = proj(x, bounds.minZ); ctx.beginPath(); ctx.moveTo(p.x, 0); ctx.lineTo(p.x, h); ctx.stroke();
|
||||
}
|
||||
const z0 = Math.ceil(bounds.minZ / step) * step, z1 = bounds.maxZ;
|
||||
for (let z = z0; z <= z1 + 1e-6; z += step) {
|
||||
const p = proj(bounds.minX, z); ctx.beginPath(); ctx.moveTo(0, p.y); ctx.lineTo(w, p.y); ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function drawAnchor(a) {
|
||||
const p = proj(a.pos[0], a.pos[2]), col = kindOf(a.kind).col;
|
||||
const isSel = selected === a.name, isHov = hovered && hovered.name === a.name;
|
||||
const r = isSel ? 9 : (isHov ? 8 : 6);
|
||||
|
||||
// 朝向箭头:Unity Y 欧拉角 θ,forward=(sinθ, cosθ) in (x,z);屏幕 +z 向上故 dy 取负
|
||||
const th = (a.rot || 0) * Math.PI / 180, L = 22;
|
||||
const ex = p.x + Math.sin(th) * L, ey = p.y - Math.cos(th) * L;
|
||||
ctx.strokeStyle = col; ctx.lineWidth = isSel || isHov ? 2 : 1.4; ctx.globalAlpha = isSel || isHov ? 1 : .75;
|
||||
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(ex, ey); ctx.stroke();
|
||||
// 箭头头
|
||||
const ah = 5, base = Math.atan2(ey - p.y, ex - p.x);
|
||||
ctx.beginPath(); ctx.moveTo(ex, ey);
|
||||
ctx.lineTo(ex - ah * Math.cos(base - .4), ey - ah * Math.sin(base - .4));
|
||||
ctx.lineTo(ex - ah * Math.cos(base + .4), ey - ah * Math.sin(base + .4));
|
||||
ctx.closePath(); ctx.fillStyle = col; ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// 点
|
||||
if (isSel || isHov) { ctx.shadowColor = col; ctx.shadowBlur = 10; }
|
||||
ctx.fillStyle = col; ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = "#000"; ctx.lineWidth = 1; ctx.stroke();
|
||||
|
||||
// 名字(深色描边,叠在真实底图上也清晰)
|
||||
ctx.textAlign = "center"; ctx.lineJoin = "round";
|
||||
ctx.font = (isSel ? "bold " : "") + "11px sans-serif";
|
||||
ctx.strokeStyle = "rgba(0,0,0,.85)"; ctx.lineWidth = 3; ctx.strokeText(a.name, p.x, p.y - r - 5);
|
||||
ctx.fillStyle = isSel ? "#fff" : "#eaf2ec"; ctx.fillText(a.name, p.x, p.y - r - 5);
|
||||
if (a.npc) {
|
||||
ctx.font = "9px sans-serif";
|
||||
ctx.strokeStyle = "rgba(0,0,0,.85)"; ctx.lineWidth = 3; ctx.strokeText(a.npc, p.x, p.y + r + 11);
|
||||
ctx.fillStyle = "rgba(210,225,215,.95)"; ctx.fillText(a.npc, p.x, p.y + r + 11);
|
||||
}
|
||||
}
|
||||
|
||||
function pickNearest(mx, my, tol) {
|
||||
const dpr = window.devicePixelRatio || 1; mx /= dpr; my /= dpr;
|
||||
let best = null, bd = tol * tol;
|
||||
anchors.forEach(a => {
|
||||
const p = proj(a.pos[0], a.pos[2]), d = (p.x - mx) * (p.x - mx) + (p.y - my) * (p.y - my);
|
||||
if (d < bd) { bd = d; best = a; }
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
function showTip(x, y, a) {
|
||||
const t = els.tip;
|
||||
t.innerHTML = '<b>' + esc(a.name) + '</b> · ' + esc(kindOf(a.kind).label) +
|
||||
(a.npc ? '(' + esc(a.npc) + ')' : '') +
|
||||
'<br>pos [' + a.pos.map(v => (+v).toFixed(2)).join(", ") + '] 朝向 ' + (a.rot || 0) + '°';
|
||||
t.classList.remove("hidden");
|
||||
const tw = t.offsetWidth, th = t.offsetHeight, W = els.stagewrap.clientWidth, H = els.stagewrap.clientHeight;
|
||||
t.style.left = Math.max(4, Math.min(W - tw - 4, x + 14)) + "px";
|
||||
t.style.top = Math.max(4, Math.min(H - th - 4, y + 14)) + "px";
|
||||
}
|
||||
function hideTip() { if (els.tip) els.tip.classList.add("hidden"); }
|
||||
|
||||
function buildLegend() {
|
||||
const used = {}; anchors.forEach(a => { const k = (a.kind in KIND) ? a.kind : ""; used[k] = true; });
|
||||
els.legend.innerHTML = Object.keys(used).map(k =>
|
||||
'<span class="pv-leg"><i style="background:' + kindOf(k).col + '"></i>' + esc(kindOf(k).label) + '</span>'
|
||||
).join("") + '<span class="pv-leg pv-leg-hint">箭头=朝向</span>';
|
||||
}
|
||||
|
||||
function buildList() {
|
||||
const order = { player: 0, role: 1, point: 2, "": 3 };
|
||||
const sorted = anchors.slice().sort((a, b) => (order[a.kind] ?? 3) - (order[b.kind] ?? 3) || a.name.localeCompare(b.name));
|
||||
els.list.innerHTML = "";
|
||||
sorted.forEach(a => {
|
||||
const d = document.createElement("div");
|
||||
d.className = "pv-item" + (selected === a.name ? " sel" : "");
|
||||
d.dataset.name = a.name;
|
||||
d.innerHTML = '<i class="pv-dot" style="background:' + kindOf(a.kind).col + '"></i>' +
|
||||
'<span class="pv-nm">' + esc(a.name) + '</span>' +
|
||||
'<span class="pv-pos">[' + a.pos.map(v => (+v).toFixed(1)).join(", ") + ']' + (a.npc ? " · " + esc(a.npc) : "") + '</span>';
|
||||
d.onclick = () => selectPoint(a.name);
|
||||
els.list.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
function selectPoint(name) {
|
||||
selected = (selected === name) ? null : name;
|
||||
els.list.querySelectorAll(".pv-item").forEach(el => el.classList.toggle("sel", el.dataset.name === selected));
|
||||
draw();
|
||||
}
|
||||
|
||||
window.PointView = { show, clear };
|
||||
})();
|
||||
@ -255,6 +255,55 @@ body.perform-mode .mode-switch { border-color:#2f7a60; }
|
||||
#perform-main { flex:1; min-width:0; display:flex; flex-direction:column; padding:14px 16px; min-height:0; }
|
||||
#perform-main .empty-center { position:static; inset:auto; min-height:240px; }
|
||||
|
||||
/* ---- 场景/点位页:靛蓝色调,区分于海选(棕)/演出(青绿) ---- */
|
||||
body.points-mode { background:#0d0f17; }
|
||||
body.points-mode header { background:#11142a; border-bottom-color:#2c3160; }
|
||||
body.points-mode .mode-btn.active { background:#2e3470; border-color:#4a52a0; color:#c5cbf0; }
|
||||
body.points-mode .mode-switch { border-color:#4a52a0; }
|
||||
#points-wrap { display:flex; flex:1; min-height:0; background:#0d0f17; }
|
||||
#wrap.hidden, #perform-wrap.hidden, #points-wrap.hidden { display:none; }
|
||||
#points-list-pane { width:250px; background:#12152a; border-right:1px solid #262c52;
|
||||
display:flex; flex-direction:column; flex:none; }
|
||||
#points-list-pane .perform-listhead { background:#11142a; color:#aab0e0; border-bottom-color:#262c52; }
|
||||
#points-set-list { overflow:auto; flex:1; }
|
||||
#points-set-list .ev { border-bottom-color:#1c2040; }
|
||||
#points-set-list .ev:hover { background:#1a1f3e; }
|
||||
#points-set-list .ev.sel { background:#222853; border-left-color:#6b73c8; }
|
||||
#points-main { flex:1; min-width:0; display:flex; flex-direction:column; padding:14px 16px; min-height:0; position:relative; }
|
||||
#points-main .empty-center { position:static; inset:auto; min-height:240px; }
|
||||
#points-view { flex:1; min-height:0; display:flex; flex-direction:column; }
|
||||
#points-view:empty { display:none; }
|
||||
|
||||
.pv-head { flex:none; font-size:13px; color:#aab0e0; margin-bottom:8px; }
|
||||
.pv-head b { color:#e6e9ff; }
|
||||
.pv-bgtag { color:#9aa0d0; } .pv-bgtag input { vertical-align:middle; margin-right:3px; }
|
||||
.pv-nobg { color:#7077a8; font-size:12px; }
|
||||
.pv-body { flex:1; min-height:0; display:flex; gap:12px; }
|
||||
.pv-stagewrap { position:relative; flex:1; min-width:0; min-height:0; background:#0f1714;
|
||||
border:1px solid #262c52; border-radius:6px; overflow:hidden; }
|
||||
.pv-stage { display:block; }
|
||||
.pv-noanchor { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center;
|
||||
justify-content:center; text-align:center; color:#8088c0; font-size:13px; line-height:1.7; padding:20px; }
|
||||
.pv-tip { position:absolute; z-index:5; background:rgba(16,18,34,.95); border:1px solid #4a52a0;
|
||||
border-radius:5px; padding:6px 9px; font-size:11.5px; color:#dfe2ff; pointer-events:none;
|
||||
white-space:nowrap; line-height:1.5; box-shadow:0 2px 10px rgba(0,0,0,.5); }
|
||||
.pv-tip.hidden { display:none; }
|
||||
.pv-tip b { color:#fff; }
|
||||
|
||||
.pv-side { width:230px; flex:none; display:flex; flex-direction:column; min-height:0; }
|
||||
.pv-legend { flex:none; display:flex; flex-wrap:wrap; gap:8px 12px; padding:4px 2px 10px; font-size:11px; color:#9aa0d0; }
|
||||
.pv-leg { display:inline-flex; align-items:center; gap:5px; }
|
||||
.pv-leg i { width:11px; height:11px; border-radius:50%; display:inline-block; border:1px solid #000; }
|
||||
.pv-leg-hint { color:#7077a8; }
|
||||
.pv-listhead { flex:none; font-size:11px; color:#8088c0; letter-spacing:.5px; padding:6px 2px; border-top:1px solid #262c52; }
|
||||
.pv-list { flex:1; overflow:auto; }
|
||||
.pv-item { display:flex; align-items:center; gap:7px; padding:5px 7px; border-radius:4px; cursor:pointer; font-size:12px; }
|
||||
.pv-item:hover { background:#1a1f3e; }
|
||||
.pv-item.sel { background:#262c5c; }
|
||||
.pv-dot { width:9px; height:9px; border-radius:50%; flex:none; border:1px solid #000; }
|
||||
.pv-nm { color:#dfe2ff; flex:none; }
|
||||
.pv-pos { color:#7e85b8; font-size:10.5px; margin-left:auto; text-align:right; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
|
||||
/* ---- 演出预览:上=舞台面板(自适应放大) / 下=时间轴面板(可拖高度) ---- */
|
||||
.tl-stagepanel { flex:1 1 auto; min-height:150px; display:flex; flex-direction:column; }
|
||||
.tl-mapinfo { flex:none; font-size:12px; color:#9a8f7e; margin-bottom:6px; }
|
||||
|
||||
Reference in New Issue
Block a user