节点编辑器改用 Drawflow 拖拽连线版
- 中间画布换成 Drawflow:拖动节点摆位、从出口圆点拉线到目标=建跳转 - 出口端口动态映射 IR:线性next/choice选项/random分支/fight胜败 - 连线/拖动实时写回 IR;节点坐标持久化到 ir._layout(编译忽略) - 右栏表单保留并双向联动;改跳转目标触发画布重渲 - 工具栏:自动整理、加后继;防误删(右栏输入时 Del 不删节点) - 移除旧 tree.js
This commit is contained in:
@ -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, "&").replace(/</g, "<").replace(/>/g, ">"); }
|
||||
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(); }
|
||||
|
||||
Reference in New Issue
Block a user