节点编辑器改用 Drawflow 拖拽连线版

- 中间画布换成 Drawflow:拖动节点摆位、从出口圆点拉线到目标=建跳转
- 出口端口动态映射 IR:线性next/choice选项/random分支/fight胜败
- 连线/拖动实时写回 IR;节点坐标持久化到 ir._layout(编译忽略)
- 右栏表单保留并双向联动;改跳转目标触发画布重渲
- 工具栏:自动整理、加后继;防误删(右栏输入时 Del 不删节点)
- 移除旧 tree.js
This commit is contained in:
bia
2026-06-08 17:27:45 +08:00
parent cd6cab33f1
commit 4a681dfe91
8 changed files with 305 additions and 196 deletions

View File

@ -79,7 +79,7 @@
App.current = group; App.ir = JSON.parse(JSON.stringify(d.ir));
App.status = d.status; App.selectedNode = null; App.dirty = false;
$("graph-empty").style.display = "none";
["btn-save", "btn-validate", "btn-playtest", "btn-confirm", "btn-discard", "btn-addnode"].forEach(b => $(b).disabled = false);
["btn-save", "btn-validate", "btn-playtest", "btn-confirm", "btn-discard", "btn-addnode", "btn-autolayout", "btn-addsucc"].forEach(b => $(b).disabled = false);
renderAll(true);
renderList();
updateDirty();
@ -88,35 +88,33 @@
const ctx = () => ({
dict: App.dict,
pointNames: (App.pointsets[(App.ir.stage || {}).point_set || App.ir.id] || {}).points || [],
onChange: structural => { App.dirty = true; updateDirty(); drawTree(); if (structural) { FormUI.renderMeta(App.ir, ctx()); FormUI.renderNode(App.ir, App.selectedNode, ctx()); } },
onChange: structural => {
App.dirty = true; updateDirty();
if (structural) { GraphUI.render(App.ir, App.selectedNode); FormUI.renderMeta(App.ir, ctx()); FormUI.renderNode(App.ir, App.selectedNode, ctx()); }
else { GraphUI.updateLabel(App.ir, App.selectedNode); }
},
selectNode: id => { App.selectedNode = id; },
});
function drawTree() {
if (App.ir) renderTree(App.ir, {
selected: App.selectedNode, onSelect: selectNode,
onAddNext: addSuccessor, onDelete: deleteNode,
});
}
function selectNode(id) { App.selectedNode = id; drawTree(); FormUI.renderNode(App.ir, id, ctx()); }
function selectNode(id) { App.selectedNode = id; GraphUI.select(id); FormUI.renderNode(App.ir, id, ctx()); }
// 节点快捷按钮:加后继 / 删除
// 工具栏「加后继」:给当前选中节点建一个后继并自动接线(无选中则忽略)
function addSuccessor(id) {
const r = FormUI.addSuccessor(App.ir, id);
if (!r) return;
if (!r.linked) alert("该战斗节点的「胜→win」「败→lose」出口都已占用\n新节点已创建但未自动接线——请在右栏把胜或败指向它id: " + r.id + ")。");
App.dirty = true; selectNode(r.id); FormUI.renderMeta(App.ir, ctx()); updateDirty();
App.dirty = true; App.selectedNode = r.id;
GraphUI.render(App.ir, r.id); FormUI.renderMeta(App.ir, ctx()); FormUI.renderNode(App.ir, r.id, ctx()); updateDirty();
}
function deleteNode(id) {
const isEnding = (App.ir.endings || []).some(e => e.id === id);
if (!confirm("删除" + (isEnding ? "结局" : "节点") + " " + id + "?指向它的跳转需手动修复(校验会提示)。")) return;
if (isEnding) App.ir.endings = (App.ir.endings || []).filter(e => e.id !== id);
else App.ir.nodes = (App.ir.nodes || []).filter(n => n.id !== id);
// 纯数据删除节点/结局(供右栏删除按钮与画布 Del 键共用)
function removeFromIr(id) {
App.ir.nodes = (App.ir.nodes || []).filter(n => n.id !== id);
App.ir.endings = (App.ir.endings || []).filter(e => e.id !== id);
if (App.ir._layout) delete App.ir._layout[id];
if (App.selectedNode === id) App.selectedNode = null;
App.dirty = true; drawTree(); FormUI.renderMeta(App.ir, ctx());
FormUI.renderNode(App.ir, App.selectedNode, ctx()); updateDirty();
App.dirty = true; updateDirty();
}
function renderAll() { drawTree(); FormUI.renderMeta(App.ir, ctx()); FormUI.renderNode(App.ir, App.selectedNode, ctx()); }
function renderAll() { GraphUI.render(App.ir, App.selectedNode); FormUI.renderMeta(App.ir, ctx()); FormUI.renderNode(App.ir, App.selectedNode, ctx()); }
function updateDirty() {
$("btn-save").textContent = App.dirty ? "保存 *" : "保存";
@ -127,7 +125,8 @@
$("btn-addnode").onclick = () => {
if (!App.ir) return;
const id = FormUI.newNode(App.ir);
App.dirty = true; selectNode(id); FormUI.renderMeta(App.ir, ctx()); updateDirty();
App.dirty = true; App.selectedNode = id;
GraphUI.render(App.ir, id); FormUI.renderMeta(App.ir, ctx()); FormUI.renderNode(App.ir, id, ctx()); updateDirty();
};
// ---------- 保存 ----------
@ -242,9 +241,27 @@
// ---------- 工具 ----------
function esc(s) { return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
window.addEventListener("resize", drawTree);
// ---------- 画布工具栏 ----------
$("btn-autolayout").onclick = () => {
if (!App.ir) return;
App.ir._layout = null; // 清坐标 → render 时按自动布局重排
GraphUI.render(App.ir, App.selectedNode);
App.dirty = true; updateDirty();
};
$("btn-addsucc").onclick = () => {
if (!App.ir || !App.selectedNode) { alert("先在画布上点选一个节点,再点「加后继」。"); return; }
addSuccessor(App.selectedNode);
};
// ---------- 启动 ----------
GraphUI.init("drawflow", {
onSelect: id => selectNode(id),
onMove: (id, x, y) => { if (!App.ir) return; (App.ir._layout = App.ir._layout || {})[id] = { x: x, y: y }; App.dirty = true; updateDirty(); },
onConnect: () => { App.dirty = true; updateDirty(); FormUI.renderNode(App.ir, App.selectedNode, ctx()); },
onDisconnect: () => { App.dirty = true; updateDirty(); FormUI.renderNode(App.ir, App.selectedNode, ctx()); },
onRemove: id => { removeFromIr(id); FormUI.renderNode(App.ir, App.selectedNode, ctx()); },
});
(async function () {
try { const r = await fetch("/api/events?status=all"); if (r.status === 401) { showLogin(); return; } hideLogin(); init(); }
catch (e) { showLogin(); }