feat(timeline): P2 并行编排——scene 多轨编辑器 + 白模重叠预览
剧情 Timeline P2 前端 + 共享内核(与 SGame 源真同步): - ir_core/IR_SCHEMA/样张:scene v0.3 + scene 校验 + 导出 gate(D3),与 SGame 仓逐字一致 - timeline.js:appendScene 按 authored start 铺多轨 clip(自然重叠预览),move from 同 actor 跨轨续连(D4); drawStage 改逐 actor 查对话→多人气泡同时计时;导出 _clipDur 纯函数;show() 加 startId 参;常量加 CAMERA_DUR - scene_edit.js(新):演出段编辑模态——拖 clip 改 start(吸附 0.1s)、拖右缘改 dur、增删 clip/轨道、 选中属性条精确编辑、客户端轻量 lint(镜像 validate.py)、▶ 预览此段(复用播放核) - graph.js:scene 节点(KIND_CN/summary/nodeInner 列轨道)+双击进编辑模态 - form.js:右栏 renderScene 精确数值编辑(轨道/clip 的 start/dur/kind/目标)+打开编辑器按钮 - app.py export:捕获 CompileError 并入 report(scene 被拦时不再 500) - test_scene.js:离线 10 断言全过(重叠确凿/晚 1.5s 起步/from 续连);gitignore 忽略本地 _localdemo.db 待浏览器目测拖拽编辑落 IR + 白模重叠演出。
This commit is contained in:
@ -31,6 +31,10 @@
|
||||
if (n.kind === "anim") return ["动画 · " + nameOf(ir, names, n.actor), n.ani || ""];
|
||||
if (n.kind === "reward") return ["奖励结算", ""];
|
||||
if (n.kind === "out_ref") return ["引用子序列", "→ " + (n.ref || "")];
|
||||
if (n.kind === "scene") {
|
||||
const tks = n.tracks || [], nclip = tks.reduce((a, t) => a + ((t.clips || []).length), 0);
|
||||
return ["演出段 (" + tks.length + "轨)", nclip + " clip" + (n.duration ? " · " + n.duration + "s" : "")];
|
||||
}
|
||||
if (n.kind === "ending") return ["★ 结局", n.summary || ""];
|
||||
return [n.kind, ""];
|
||||
}
|
||||
@ -61,7 +65,7 @@
|
||||
}
|
||||
|
||||
// ---------- 节点 HTML ----------
|
||||
const KIND_CN = { narration: "旁白", dialogue: "对话", choice: "选择", choice_once: "一次性选择", random: "随机", fight: "战斗", move: "走位", anim: "动画", reward: "奖励", out_ref: "引用", ending: "结局" };
|
||||
const KIND_CN = { narration: "旁白", dialogue: "对话", choice: "选择", choice_once: "一次性选择", random: "随机", fight: "战斗", move: "走位", anim: "动画", reward: "奖励", out_ref: "引用", scene: "演出段", ending: "结局" };
|
||||
function nodeInner(ir, node) {
|
||||
const names = roleNames(ir), end = isEnding(node);
|
||||
const kind = end ? "ending" : node.kind;
|
||||
@ -85,6 +89,16 @@
|
||||
}
|
||||
return label + chOpts;
|
||||
}
|
||||
// 演出段 scene:列出各轨(演员/镜头)+ clip 数 + 双击编辑提示
|
||||
if (!end && node.kind === "scene") {
|
||||
const tks = node.tracks || [];
|
||||
const lines = tks.slice(0, 4).map(tk => {
|
||||
const who = tk.role === "camera" ? "🎬 镜头" : nameOf(ir, names, tk.actor || "P1");
|
||||
return '<div class="screl">' + esc(who) + ' · ' + ((tk.clips || []).length) + ' clip</div>';
|
||||
}).join("");
|
||||
const more = tks.length > 4 ? '<div class="screl more">…+' + (tks.length - 4) + ' 轨</div>' : "";
|
||||
return label + '<div class="nbody scenebody">' + (lines || '<div class="screl more">(空演出段)</div>') + more + '<div class="schint">双击编辑时间线</div></div>';
|
||||
}
|
||||
// 线性 / 单出口:角色(可选)+ 文本
|
||||
const sm = summary(ir, names, node);
|
||||
const actor = node.speaker || node.actor;
|
||||
@ -188,6 +202,14 @@
|
||||
editor.on("connectionCreated", info => { if (building) return; handleConnect(info); });
|
||||
editor.on("connectionRemoved", info => { if (building) return; handleDisconnect(info); });
|
||||
editor.on("nodeRemoved", id => { if (building) return; const ir = dfId2ir[id]; if (ir && cb.onRemove) cb.onRemove(ir); });
|
||||
// 双击 scene 节点 → 打开时间线编辑模态(演出段编排入口)
|
||||
document.getElementById(containerId).addEventListener("dblclick", e => {
|
||||
const nodeEl = e.target.closest && e.target.closest(".drawflow-node");
|
||||
if (!nodeEl) return;
|
||||
const irId = dfId2ir[nodeEl.id.replace("node-", "")];
|
||||
const n = irId && findNode(irId);
|
||||
if (n && n.kind === "scene" && cb.onEditScene) { e.stopPropagation(); e.preventDefault(); cb.onEditScene(irId); }
|
||||
});
|
||||
// 键盘 Del:删画布选中节点 → 交给 app 做「缝合删除」;焦点在输入框时放行文本编辑;选中连线时交给 Drawflow
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||
|
||||
Reference in New Issue
Block a user