@
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:
@ -3,6 +3,7 @@
|
||||
const App = {
|
||||
dict: { conditions: {}, grants: {} },
|
||||
pointsets: {}, // name -> {mapId, points:[]}
|
||||
sceneshots: {}, // name -> {url,bounds,w,h}(演出配置「场景底图」可选项)
|
||||
events: [],
|
||||
current: null, // 当前 group
|
||||
ir: null, // 工作副本
|
||||
@ -42,6 +43,7 @@
|
||||
try {
|
||||
App.dict = await (await api("/api/dictionary")).json();
|
||||
App.pointsets = await (await api("/api/pointsets")).json();
|
||||
try { App.sceneshots = await (await api("/api/sceneshots")).json(); } catch (_) { App.sceneshots = {}; }
|
||||
} catch (e) { return; }
|
||||
await loadList();
|
||||
}
|
||||
@ -252,12 +254,27 @@
|
||||
host.appendChild(d);
|
||||
});
|
||||
}
|
||||
let performIr = null; // 演出配置当前事件 IR(底图选择写这里并持久化)
|
||||
async function performSelect(group) {
|
||||
let d;
|
||||
try { d = await (await api("/api/events/" + encodeURIComponent(group))).json(); } catch (e) { return; }
|
||||
performCurrent = group;
|
||||
performIr = d.ir;
|
||||
performLoadList();
|
||||
Timeline.show($("perform-main"), d.ir, App.dict, App.pointsets);
|
||||
Timeline.show($("perform-main"), d.ir, App.dict, App.pointsets, {
|
||||
sceneshots: App.sceneshots,
|
||||
onPickBackdrop: name => saveBackdrop(group, name),
|
||||
});
|
||||
}
|
||||
// 演出配置选底图 → 写 ir.stage.backdrop(编辑器元数据,validate/compile 都不读)并 PUT 持久化。
|
||||
async function saveBackdrop(group, name) {
|
||||
if (!performIr || group !== performCurrent) return;
|
||||
performIr.stage = performIr.stage || {};
|
||||
if (name) performIr.stage.backdrop = name; else delete performIr.stage.backdrop;
|
||||
try {
|
||||
await api("/api/events/" + encodeURIComponent(group), { method: "PUT", body: JSON.stringify({ ir: performIr, by: App.by }) });
|
||||
toast(name ? ("底图已设为 " + name) : "已恢复默认底图");
|
||||
} catch (e) { toast("底图保存失败"); }
|
||||
}
|
||||
|
||||
// ---------- 导入 ----------
|
||||
|
||||
@ -323,7 +323,9 @@ body.points-mode .mode-switch { border-color:#4a52a0; }
|
||||
|
||||
/* ---- 演出预览:上=舞台面板(自适应放大) / 下=时间轴面板(可拖高度) ---- */
|
||||
.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; }
|
||||
.tl-mapinfo { flex:none; font-size:12px; color:#9a8f7e; margin-bottom:6px; display:flex; align-items:center; flex-wrap:wrap; gap:4px; }
|
||||
.tl-bd-label { margin-left:10px; color:#7fae9c; }
|
||||
.tl-bd-select { background:#1d1a13; color:#d8cda0; border:1px solid #4a4534; border-radius:4px; padding:1px 4px; font-size:12px; max-width:200px; }
|
||||
.tl-stagewrap { position:relative; flex:1; min-height:0; background:#15130d; border:1px solid #3a322a;
|
||||
border-radius:6px; overflow:hidden; }
|
||||
.tl-stage { width:100%; height:100%; object-fit:contain; display:block; }
|
||||
|
||||
@ -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/sceneshots(venue 特写等)。
|
||||
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(); }
|
||||
|
||||
Reference in New Issue
Block a user