Spaces:
Building
on
A10G
Building
on
A10G
import { app } from "../../scripts/app.js"; | |
import { api } from "../../scripts/api.js" | |
const MAX_HISTORY = 50; | |
let undo = []; | |
let redo = []; | |
let activeState = null; | |
let isOurLoad = false; | |
function checkState() { | |
const currentState = app.graph.serialize(); | |
if (!graphEqual(activeState, currentState)) { | |
undo.push(activeState); | |
if (undo.length > MAX_HISTORY) { | |
undo.shift(); | |
} | |
activeState = clone(currentState); | |
redo.length = 0; | |
api.dispatchEvent(new CustomEvent("graphChanged", { detail: activeState })); | |
} | |
} | |
const loadGraphData = app.loadGraphData; | |
app.loadGraphData = async function () { | |
const v = await loadGraphData.apply(this, arguments); | |
if (isOurLoad) { | |
isOurLoad = false; | |
} else { | |
checkState(); | |
} | |
return v; | |
}; | |
function clone(obj) { | |
try { | |
if (typeof structuredClone !== "undefined") { | |
return structuredClone(obj); | |
} | |
} catch (error) { | |
// structuredClone is stricter than using JSON.parse/stringify so fallback to that | |
} | |
return JSON.parse(JSON.stringify(obj)); | |
} | |
function graphEqual(a, b, root = true) { | |
if (a === b) return true; | |
if (typeof a == "object" && a && typeof b == "object" && b) { | |
const keys = Object.getOwnPropertyNames(a); | |
if (keys.length != Object.getOwnPropertyNames(b).length) { | |
return false; | |
} | |
for (const key of keys) { | |
let av = a[key]; | |
let bv = b[key]; | |
if (root && key === "nodes") { | |
// Nodes need to be sorted as the order changes when selecting nodes | |
av = [...av].sort((a, b) => a.id - b.id); | |
bv = [...bv].sort((a, b) => a.id - b.id); | |
} | |
if (!graphEqual(av, bv, false)) { | |
return false; | |
} | |
} | |
return true; | |
} | |
return false; | |
} | |
const undoRedo = async (e) => { | |
const updateState = async (source, target) => { | |
const prevState = source.pop(); | |
if (prevState) { | |
target.push(activeState); | |
isOurLoad = true; | |
await app.loadGraphData(prevState, false); | |
activeState = prevState; | |
} | |
} | |
if (e.ctrlKey || e.metaKey) { | |
if (e.key === "y") { | |
updateState(redo, undo); | |
return true; | |
} else if (e.key === "z") { | |
updateState(undo, redo); | |
return true; | |
} | |
} | |
}; | |
const bindInput = (activeEl) => { | |
if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") { | |
for (const evt of ["change", "input", "blur"]) { | |
if (`on${evt}` in activeEl) { | |
const listener = () => { | |
checkState(); | |
activeEl.removeEventListener(evt, listener); | |
}; | |
activeEl.addEventListener(evt, listener); | |
return true; | |
} | |
} | |
} | |
}; | |
let keyIgnored = false; | |
window.addEventListener( | |
"keydown", | |
(e) => { | |
requestAnimationFrame(async () => { | |
let activeEl; | |
// If we are auto queue in change mode then we do want to trigger on inputs | |
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") { | |
activeEl = document.activeElement; | |
if (activeEl?.tagName === "INPUT" || activeEl?.type === "textarea") { | |
// Ignore events on inputs, they have their native history | |
return; | |
} | |
} | |
keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta"; | |
if (keyIgnored) return; | |
// Check if this is a ctrl+z ctrl+y | |
if (await undoRedo(e)) return; | |
// If our active element is some type of input then handle changes after they're done | |
if (bindInput(activeEl)) return; | |
checkState(); | |
}); | |
}, | |
true | |
); | |
window.addEventListener("keyup", (e) => { | |
if (keyIgnored) { | |
keyIgnored = false; | |
checkState(); | |
} | |
}); | |
// Handle clicking DOM elements (e.g. widgets) | |
window.addEventListener("mouseup", () => { | |
checkState(); | |
}); | |
// Handle prompt queue event for dynamic widget changes | |
api.addEventListener("promptQueued", () => { | |
checkState(); | |
}); | |
// Handle litegraph clicks | |
const processMouseUp = LGraphCanvas.prototype.processMouseUp; | |
LGraphCanvas.prototype.processMouseUp = function (e) { | |
const v = processMouseUp.apply(this, arguments); | |
checkState(); | |
return v; | |
}; | |
const processMouseDown = LGraphCanvas.prototype.processMouseDown; | |
LGraphCanvas.prototype.processMouseDown = function (e) { | |
const v = processMouseDown.apply(this, arguments); | |
checkState(); | |
return v; | |
}; | |
// Handle litegraph context menu for COMBO widgets | |
const close = LiteGraph.ContextMenu.prototype.close; | |
LiteGraph.ContextMenu.prototype.close = function(e) { | |
const v = close.apply(this, arguments); | |
checkState(); | |
return v; | |
} |