feat(web): 演出配置 Timeline 加「场景底图」选择器(venue 特写)

- 后端 /api/sceneshots:列 SceneShots 全部俯视底图(venue 特写) name->{url,bounds}
- timeline.js:底图优先级 ir.stage.backdrop(venue) > 点位集默认 shot;
  顶栏加底图下拉 renderMapInfo + applyBackdrop(换底+改投影范围+重画+回调)
- app.js:拉 /api/sceneshots;performSelect 传入;saveBackdrop 写 ir.stage.backdrop 并 PUT
- venue 特写与点位集同 map-local → 换底图后锚点自动落对位(无头实拍擂台验证)
- ir.stage.backdrop 是编辑器元数据:validate 不读、compile 不碰
@
This commit is contained in:
2026-06-15 12:01:14 +08:00
parent 65424a4dfb
commit 676df30c67
4 changed files with 96 additions and 7 deletions

View File

@ -305,6 +305,9 @@
// ===================== 状态 & 挂载 =====================
let S, model, playT = 0, playing = false, rafId = 0, lastTs = 0, stageCv, stageCtx, els = {}, pending = null, selNode = null, PX = 80, fitMode = false, lastDecisionId = null;
let shotBg = null, shotReady = false, shotBounds = null; // 真实场景正交俯视底图
// 场景底图选择器SHOTS=所有可选 venue 特写({name:{url,bounds}})curBackdrop=当前选中底图名(""=点位集默认)
// onPick=切换底图回调(app.js 写 ir.stage.backdrop 并持久化)baseBounds=无底图时回退的投影范围。
let SHOTS = {}, curPsName = "", curPs = {}, curBackdrop = "", onPick = null, baseBounds = null;
const TEMPLATE =
'<div class="tl-stagepanel">' +
@ -322,12 +325,21 @@
'<div class="tl-resizer" title="拖动调整时间轴高度"></div>' +
'<div class="tl-timelinepanel"><div class="tl-tracks"></div></div>';
function show(host, IR, DICT, POINTSETS, startId) {
function show(host, IR, DICT, POINTSETS, opts) {
stopPlay();
opts = opts || {};
const startId = opts.startId;
SHOTS = opts.sceneshots || {};
onPick = opts.onPickBackdrop || null;
const psName = (IR.stage || {}).point_set || IR.id;
const ps = (POINTSETS || {})[psName] || {};
curPsName = psName; curPs = ps;
curBackdrop = (IR.stage || {}).backdrop || "";
S = prepare(IR, ps.anchors || []); model = S;
setupShot(ps.shot); // 有真实场景俯视底图 → 覆盖投影范围并异步加载drawStage 当舞台底
baseBounds = Object.assign({}, S.bounds); // 无底图时回退用
// 底图优先级:事件指定的 venue 特写(ir.stage.backdrop) > 点位集默认底图
const shot = (curBackdrop && SHOTS[curBackdrop]) ? SHOTS[curBackdrop] : ps.shot;
setupShot(shot); // 有真实场景俯视底图 → 覆盖投影范围并异步加载drawStage 当舞台底
host.innerHTML = TEMPLATE;
els = {
@ -345,9 +357,7 @@
timelinepanel: host.querySelector(".tl-timelinepanel"),
playhead: null,
};
els.mapinfo.textContent = "点位集:" + psName + (ps.mapId ? "(地图 " + ps.mapId + "" : "") +
(S.synthetic ? " · ⚠ 未取到真实坐标,按示意布局自动铺开(走位仍可预览)"
: (shotBounds ? " · 真实坐标 · 已叠场景俯视底图" : " · 真实坐标"));
renderMapInfo();
stageCv = els.stage; stageCtx = stageCv.getContext("2d");
els.play.onclick = () => playing ? stopPlay() : play();
@ -422,6 +432,51 @@
shotBg.src = shot.url;
}
// 顶部信息栏 + 场景底图选择器。底图列表来自 /api/sceneshotsvenue 特写等)。
function renderMapInfo() {
const mi = els.mapinfo; if (!mi) return;
mi.innerHTML = "";
const span = document.createElement("span");
span.textContent = "点位集:" + curPsName + (curPs.mapId ? "(地图 " + curPs.mapId + "" : "") +
(S.synthetic ? " · ⚠ 未取到真实坐标,按示意布局自动铺开(走位仍可预览)"
: (shotBounds ? " · 真实坐标 · 已叠场景底图" : " · 真实坐标"));
mi.appendChild(span);
const names = Object.keys(SHOTS).sort();
if (names.length) {
const lab = document.createElement("label");
lab.className = "tl-bd-label"; lab.textContent = "场景底图:";
const sel = document.createElement("select");
sel.className = "tl-bd-select";
const def = document.createElement("option");
def.value = ""; def.textContent = curPs.shot ? "(点位集默认)" : "(无)";
sel.appendChild(def);
names.forEach(n => {
const o = document.createElement("option");
o.value = n; o.textContent = n;
if (n === curBackdrop) o.selected = true;
sel.appendChild(o);
});
sel.onchange = () => applyBackdrop(sel.value);
mi.appendChild(lab); mi.appendChild(sel);
}
}
// 切换 Timeline 底图:换底 + 改投影范围 + 重画 + 回调持久化(写 ir.stage.backdrop)。
// 锚点(点位集坐标)不变,因 venue 特写与点位集同为 map-local事件点会落在特写图正确位置上。
function applyBackdrop(name) {
curBackdrop = name;
const shot = (name && SHOTS[name]) ? SHOTS[name] : curPs.shot;
if (shot && Array.isArray(shot.bounds) && shot.bounds.length === 4) {
setupShot(shot);
} else { // 选「无/默认」且无点位集底图 → 回退基础范围
shotBg = null; shotReady = false; shotBounds = null;
if (baseBounds) S.bounds = Object.assign({}, baseBounds);
}
renderMapInfo();
renderFrame();
if (onPick) onPick(name); // app.js: ir.stage.backdrop = name 并 PUT 持久化
}
function restart() { startFrom(firstNode(S.IR)); }
function refreshTimeline() { orderRows(S); buildTracks(); }