comfyanonymous
12 months ago
1 changed files with 150 additions and 0 deletions
@ -0,0 +1,150 @@
|
||||
import { app } from "../../scripts/app.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; |
||||
} |
||||
} |
||||
|
||||
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) => { |
||||
if (e.ctrlKey || e.metaKey) { |
||||
if (e.key === "y") { |
||||
const prevState = redo.pop(); |
||||
if (prevState) { |
||||
undo.push(activeState); |
||||
isOurLoad = true; |
||||
await app.loadGraphData(prevState); |
||||
activeState = prevState; |
||||
} |
||||
return true; |
||||
} else if (e.key === "z") { |
||||
const prevState = undo.pop(); |
||||
if (prevState) { |
||||
redo.push(activeState); |
||||
isOurLoad = true; |
||||
await app.loadGraphData(prevState); |
||||
activeState = prevState; |
||||
} |
||||
return true; |
||||
} |
||||
} |
||||
}; |
||||
|
||||
const bindInput = (activeEl) => { |
||||
if (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; |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
|
||||
window.addEventListener( |
||||
"keydown", |
||||
(e) => { |
||||
requestAnimationFrame(async () => { |
||||
const activeEl = document.activeElement; |
||||
if (activeEl?.tagName === "INPUT" || activeEl?.type === "textarea") { |
||||
// Ignore events on inputs, they have their native history
|
||||
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 |
||||
); |
||||
|
||||
// Handle clicking DOM elements (e.g. widgets)
|
||||
window.addEventListener("mouseup", () => { |
||||
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; |
||||
}; |
Loading…
Reference in new issue