fix: make history work as expected
This commit is contained in:
parent
2629008447
commit
2df3035855
@ -6,7 +6,7 @@
|
||||
import { onMount, setContext } from "svelte";
|
||||
import Camera from "../Camera.svelte";
|
||||
import GraphView from "./GraphView.svelte";
|
||||
import type { Node as NodeType } from "$lib/types";
|
||||
import type { Node, Node as NodeType } from "$lib/types";
|
||||
import FloatingEdge from "../edges/FloatingEdge.svelte";
|
||||
import type { Socket } from "$lib/types";
|
||||
import {
|
||||
@ -38,6 +38,10 @@
|
||||
const cameraDown = [0, 0];
|
||||
let cameraPosition: [number, number, number] = [0, 0, 4];
|
||||
let addMenuPosition: [number, number] | null = null;
|
||||
let clipboard: null | {
|
||||
nodes: Node[];
|
||||
edges: [number, number, number, string][];
|
||||
} = null;
|
||||
|
||||
$: if (cameraPosition && loaded) {
|
||||
localStorage.setItem("cameraPosition", JSON.stringify(cameraPosition));
|
||||
@ -438,23 +442,60 @@
|
||||
}
|
||||
}
|
||||
|
||||
function copyNodes() {
|
||||
if ($activeNodeId === -1 && !$selectedNodes?.size) return;
|
||||
let _nodes = [$activeNodeId, ...($selectedNodes?.values() || [])]
|
||||
.map((id) => graph.getNode(id))
|
||||
.filter(Boolean) as Node[];
|
||||
|
||||
const _edges = graph.getEdgesBetweenNodes(_nodes);
|
||||
|
||||
_nodes = _nodes.map((_node) => {
|
||||
const node = globalThis.structuredClone({
|
||||
..._node,
|
||||
tmp: {
|
||||
downX: mousePosition[0] - _node.position[0],
|
||||
downY: mousePosition[1] - _node.position[1],
|
||||
},
|
||||
});
|
||||
return node;
|
||||
});
|
||||
|
||||
clipboard = {
|
||||
nodes: _nodes,
|
||||
edges: _edges,
|
||||
};
|
||||
}
|
||||
|
||||
function pasteNodes() {
|
||||
if (!clipboard) return;
|
||||
|
||||
const _nodes = clipboard.nodes
|
||||
.map((node) => {
|
||||
node.tmp = node.tmp || {};
|
||||
node.position[0] = mousePosition[0] - (node?.tmp?.downX || 0);
|
||||
node.position[1] = mousePosition[1] - (node?.tmp?.downY || 0);
|
||||
return node;
|
||||
})
|
||||
.filter(Boolean) as Node[];
|
||||
|
||||
const newNodes = graph.createGraph(_nodes, clipboard.edges);
|
||||
$selectedNodes = new Set(newNodes.map((n) => n.id));
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
const bodyIsFocused =
|
||||
document.activeElement === document.body ||
|
||||
document?.activeElement?.id === "graph";
|
||||
|
||||
if (event.key === "l") {
|
||||
if (event.ctrlKey) {
|
||||
const activeNode = graph.getNode($activeNodeId);
|
||||
if (activeNode) {
|
||||
const nodes = graph.getLinkedNodes(activeNode);
|
||||
$selectedNodes = new Set(nodes.map((n) => n.id));
|
||||
}
|
||||
} else {
|
||||
const activeNode = graph.getNode($activeNodeId);
|
||||
console.log(activeNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
$activeNodeId = -1;
|
||||
@ -497,20 +538,22 @@
|
||||
}
|
||||
|
||||
if (event.key === "c" && event.ctrlKey) {
|
||||
copyNodes();
|
||||
}
|
||||
|
||||
if (event.key === "v" && event.ctrlKey) {
|
||||
pasteNodes();
|
||||
}
|
||||
|
||||
if (event.key === "z" && event.ctrlKey) {
|
||||
graph.history.undo();
|
||||
graph.undo();
|
||||
for (const node of $nodes.values()) {
|
||||
updateNodePosition(node);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "y" && event.ctrlKey) {
|
||||
graph.history.redo();
|
||||
graph.redo();
|
||||
for (const node of $nodes.values()) {
|
||||
updateNodePosition(node);
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import { HistoryManager } from "./history-manager";
|
||||
import * as templates from "./graphs";
|
||||
import EventEmitter from "./helpers/EventEmitter";
|
||||
import throttle from "./helpers/throttle";
|
||||
import { createLogger } from "./helpers";
|
||||
|
||||
const logger = createLogger("graph-manager");
|
||||
|
||||
export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
|
||||
@ -22,7 +25,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
|
||||
inputSockets: Writable<Set<string>> = writable(new Set());
|
||||
|
||||
history: HistoryManager = new HistoryManager(this);
|
||||
history: HistoryManager = new HistoryManager();
|
||||
|
||||
constructor(private nodeRegistry: NodeRegistry, private runtime: RuntimeExecutor) {
|
||||
super();
|
||||
@ -41,12 +44,13 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
}
|
||||
|
||||
serialize(): Graph {
|
||||
logger.log("serializing graph")
|
||||
const nodes = Array.from(this._nodes.values()).map(node => ({
|
||||
id: node.id,
|
||||
position: node.position,
|
||||
position: [...node.position],
|
||||
type: node.type,
|
||||
props: node.props,
|
||||
}));
|
||||
})) as Node[];
|
||||
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"];
|
||||
return { id: this.graph.id, nodes, edges };
|
||||
}
|
||||
@ -57,7 +61,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
const start = performance.now();
|
||||
const result = this.runtime.execute(this.serialize());
|
||||
const end = performance.now();
|
||||
console.log(`Execution took ${end - start}ms -> ${result}`);
|
||||
logger.log(`Execution took ${end - start}ms -> ${result}`);
|
||||
}
|
||||
|
||||
getNodeTypes() {
|
||||
@ -80,6 +84,25 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
}
|
||||
|
||||
|
||||
getEdgesBetweenNodes(nodes: Node[]): [number, number, number, string][] {
|
||||
|
||||
const edges = [];
|
||||
for (const node of nodes) {
|
||||
const children = node.tmp?.children || [];
|
||||
for (const child of children) {
|
||||
if (nodes.includes(child)) {
|
||||
const edge = this._edges.find(e => e[0].id === node.id && e[2].id === child.id);
|
||||
if (edge) {
|
||||
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [number, number, number, string]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
|
||||
private _init(graph: Graph) {
|
||||
const nodes = new Map(graph.nodes.map(node => {
|
||||
const nodeType = this.nodeRegistry.getNode(node.type);
|
||||
@ -116,36 +139,28 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
async load(graph: Graph) {
|
||||
this.loaded = false;
|
||||
this.graph = graph;
|
||||
const a = performance.now();
|
||||
this.status.set("loading");
|
||||
this.id.set(graph.id);
|
||||
|
||||
const b = performance.now();
|
||||
for (const node of this.graph.nodes) {
|
||||
const nodeType = this.nodeRegistry.getNode(node.type);
|
||||
if (!nodeType) {
|
||||
console.error(`Node type not found: ${node.type}`);
|
||||
logger.error(`Node type not found: ${node.type}`);
|
||||
this.status.set("error");
|
||||
return;
|
||||
}
|
||||
node.tmp = node.tmp || {};
|
||||
node.tmp.type = nodeType;
|
||||
}
|
||||
const c = performance.now();
|
||||
|
||||
this.history.reset();
|
||||
this._init(this.graph);
|
||||
|
||||
const d = performance.now();
|
||||
|
||||
this.save();
|
||||
|
||||
const e = performance.now();
|
||||
|
||||
this.status.set("idle");
|
||||
|
||||
this.loaded = true;
|
||||
const f = performance.now();
|
||||
console.log(`Loading took ${f - a}ms; a-b: ${b - a}ms; b-c: ${c - b}ms; c-d: ${d - c}ms; d-e: ${e - d}ms; e-f: ${f - e}ms`);
|
||||
}
|
||||
|
||||
|
||||
@ -224,11 +239,60 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
this.save();
|
||||
}
|
||||
|
||||
private createNodeId() {
|
||||
return Math.max(...this.getAllNodes().map(n => n.id), 0) + 1;
|
||||
createNodeId() {
|
||||
const max = Math.max(...this._nodes.keys());
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
createNode({ type, position }: { type: string, position: [number, number] }) {
|
||||
createGraph(nodes: Node[], edges: [number, number, number, string][]) {
|
||||
|
||||
// map old ids to new ids
|
||||
const idMap = new Map<number, number>();
|
||||
|
||||
const startId = this.createNodeId();
|
||||
|
||||
nodes = nodes.map((node, i) => {
|
||||
const id = startId + i;
|
||||
idMap.set(node.id, id);
|
||||
const type = this.nodeRegistry.getNode(node.type);
|
||||
if (!type) {
|
||||
throw new Error(`Node type not found: ${node.type}`);
|
||||
}
|
||||
return { ...node, id, tmp: { type } };
|
||||
});
|
||||
|
||||
const _edges = edges.map(edge => {
|
||||
const from = nodes.find(n => n.id === idMap.get(edge[0]));
|
||||
const to = nodes.find(n => n.id === idMap.get(edge[2]));
|
||||
|
||||
if (!from || !to) {
|
||||
throw new Error("Edge references non-existing node");
|
||||
}
|
||||
|
||||
to.tmp = to.tmp || {};
|
||||
to.tmp.parents = to.tmp.parents || [];
|
||||
to.tmp.parents.push(from);
|
||||
|
||||
from.tmp = from.tmp || {};
|
||||
from.tmp.children = from.tmp.children || [];
|
||||
from.tmp.children.push(to);
|
||||
|
||||
return [from, edge[1], to, edge[3]] as Edge;
|
||||
});
|
||||
|
||||
for (const node of nodes) {
|
||||
this._nodes.set(node.id, node);
|
||||
}
|
||||
|
||||
this._edges.push(..._edges);
|
||||
|
||||
this.nodes.set(this._nodes);
|
||||
this.edges.set(this._edges);
|
||||
this.save();
|
||||
return nodes;
|
||||
}
|
||||
|
||||
createNode({ type, position, props = {} }: { type: Node["type"], position: Node["position"], props: Node["props"] }) {
|
||||
|
||||
const nodeType = this.nodeRegistry.getNode(type);
|
||||
if (!nodeType) {
|
||||
@ -236,7 +300,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
return;
|
||||
}
|
||||
|
||||
const node: Node = { id: this.createNodeId(), type, position, tmp: { type: nodeType } };
|
||||
const node: Node = { id: this.createNodeId(), type, position, tmp: { type: nodeType }, props };
|
||||
|
||||
this.nodes.update((nodes) => {
|
||||
nodes.set(node.id, node);
|
||||
@ -294,6 +358,24 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
const nextState = this.history.undo();
|
||||
if (nextState) {
|
||||
this._init(nextState);
|
||||
this.emit("save", this.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
redo() {
|
||||
const nextState = this.history.redo();
|
||||
if (nextState) {
|
||||
this._init(nextState);
|
||||
this.emit("save", this.serialize());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
startUndoGroup() {
|
||||
this.currentUndoGroup = 1;
|
||||
}
|
||||
@ -305,8 +387,10 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
||||
|
||||
save() {
|
||||
if (this.currentUndoGroup) return;
|
||||
this.emit("save", this.serialize());
|
||||
this.history.save();
|
||||
const state = this.serialize();
|
||||
this.history.save(state);
|
||||
this.emit("save", state);
|
||||
logger.log("saving graph");
|
||||
}
|
||||
|
||||
getParentsOfNode(node: Node) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
|
||||
import throttle from './throttle';
|
||||
|
||||
const debug = { amountEmitters: 0, amountCallbacks: 0, emitters: [] };
|
||||
|
||||
|
||||
type EventMap = Record<string, unknown>;
|
||||
@ -13,8 +12,6 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
|
||||
index = 0;
|
||||
public eventMap: T = {} as T;
|
||||
constructor() {
|
||||
this.index = debug.amountEmitters;
|
||||
debug.amountEmitters++;
|
||||
}
|
||||
|
||||
private cbs: { [key: string]: ((data?: unknown) => unknown)[] } = {};
|
||||
@ -42,24 +39,9 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
|
||||
});
|
||||
this.cbs = cbs;
|
||||
|
||||
debug.emitters[this.index] = {
|
||||
name: this.constructor.name,
|
||||
cbs: Object.fromEntries(
|
||||
Object.keys(this.cbs).map((key) => [key, this.cbs[key].length]),
|
||||
),
|
||||
};
|
||||
debug.amountCallbacks++;
|
||||
|
||||
// console.log('New EventEmitter ', this.constructor.name);
|
||||
return () => {
|
||||
debug.amountCallbacks--;
|
||||
cbs[event]?.splice(cbs[event].indexOf(cb), 1);
|
||||
debug.emitters[this.index] = {
|
||||
name: this.constructor.name,
|
||||
cbs: Object.fromEntries(
|
||||
Object.keys(this.cbs).map((key) => [key, this.cbs[key].length]),
|
||||
),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -77,14 +59,11 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
|
||||
}
|
||||
|
||||
public destroyEventEmitter() {
|
||||
debug.amountEmitters--;
|
||||
Object.keys(this.cbs).forEach((key) => {
|
||||
debug.amountCallbacks -= this.cbs[key].length;
|
||||
delete this.cbs[key];
|
||||
});
|
||||
Object.keys(this.cbsOnce).forEach((key) => delete this.cbsOnce[key]);
|
||||
this.cbs = {};
|
||||
this.cbsOnce = {};
|
||||
delete debug.emitters[this.index];
|
||||
}
|
||||
}
|
||||
|
@ -70,3 +70,27 @@ export const debounce = (fn: Function, ms = 300) => {
|
||||
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||
};
|
||||
};
|
||||
|
||||
export const clone: <T>(v: T) => T = "structedClone" in globalThis ? globalThis.structuredClone : (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
export const createLogger = (() => {
|
||||
let maxLength = 5;
|
||||
return (scope: string) => {
|
||||
maxLength = Math.max(maxLength, scope.length);
|
||||
let muted = false;
|
||||
return {
|
||||
log: (...args: any[]) => !muted && console.log(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
|
||||
info: (...args: any[]) => !muted && console.info(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
|
||||
warn: (...args: any[]) => !muted && console.warn(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
|
||||
error: (...args: any[]) => console.error(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #f88", ...args),
|
||||
mute() {
|
||||
muted = true;
|
||||
},
|
||||
unmute() {
|
||||
muted = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { GraphManager } from "./graph-manager";
|
||||
import { create, type Delta } from "jsondiffpatch";
|
||||
import type { Graph } from "./types";
|
||||
import { createLogger, clone } from "./helpers";
|
||||
|
||||
|
||||
const diff = create({
|
||||
@ -14,40 +14,38 @@ const diff = create({
|
||||
}
|
||||
})
|
||||
|
||||
const log = createLogger("history")
|
||||
|
||||
export class HistoryManager {
|
||||
|
||||
index: number = -1;
|
||||
history: Delta[] = [];
|
||||
private initialState: Graph | undefined;
|
||||
private prevState: Graph | undefined;
|
||||
private timeout: number | undefined;
|
||||
private state: Graph | undefined;
|
||||
|
||||
private opts = {
|
||||
debounce: 400,
|
||||
maxHistory: 100,
|
||||
}
|
||||
|
||||
|
||||
constructor(private manager: GraphManager, { maxHistory = 100, debounce = 100 } = {}) {
|
||||
constructor({ maxHistory = 100, debounce = 100 } = {}) {
|
||||
this.history = [];
|
||||
this.index = -1;
|
||||
this.opts.debounce = debounce;
|
||||
this.opts.maxHistory = maxHistory;
|
||||
globalThis["_history"] = this;
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.prevState) {
|
||||
this.prevState = this.manager.serialize();
|
||||
this.initialState = globalThis.structuredClone(this.prevState);
|
||||
save(state: Graph) {
|
||||
if (!this.state) {
|
||||
this.state = clone(state);
|
||||
this.initialState = this.state;
|
||||
log.log("initial state saved")
|
||||
} else {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
const newState = this.manager.serialize();
|
||||
const delta = diff.diff(this.prevState, newState);
|
||||
const newState = state;
|
||||
const delta = diff.diff(this.state, newState);
|
||||
if (delta) {
|
||||
log.log("saving state")
|
||||
// Add the delta to history
|
||||
if (this.index < this.history.length - 1) {
|
||||
// Clear the history after the current index if new changes are made
|
||||
@ -61,35 +59,43 @@ export class HistoryManager {
|
||||
if (this.history.length > this.opts.maxHistory) {
|
||||
this.history.shift();
|
||||
}
|
||||
this.state = newState;
|
||||
} else {
|
||||
log.log("no changes")
|
||||
}
|
||||
this.prevState = newState;
|
||||
}, this.opts.debounce) as unknown as number;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.history = [];
|
||||
this.index = -1;
|
||||
this.state = undefined;
|
||||
this.initialState = undefined;
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.index > 0) {
|
||||
if (this.index === -1 && this.initialState) {
|
||||
log.log("reached start, loading initial state")
|
||||
return clone(this.initialState);
|
||||
} else {
|
||||
const delta = this.history[this.index];
|
||||
const prevState = diff.unpatch(this.prevState, delta) as Graph;
|
||||
this.manager._init(prevState);
|
||||
this.index--;
|
||||
this.prevState = prevState;
|
||||
} else if (this.index === 0 && this.initialState) {
|
||||
this.manager._init(globalThis.structuredClone(this.initialState));
|
||||
console.log("Reached start", this.index, this.history.length)
|
||||
const prevState = diff.unpatch(this.state, delta) as Graph;
|
||||
this.state = prevState;
|
||||
this.index = Math.max(-1, this.index - 1);
|
||||
return clone(prevState);
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.index < this.history.length - 1) {
|
||||
const nextIndex = this.index + 1;
|
||||
if (this.index <= this.history.length - 1) {
|
||||
const nextIndex = Math.min(this.history.length - 1, this.index + 1);
|
||||
const delta = this.history[nextIndex];
|
||||
const nextState = diff.patch(this.prevState, delta) as Graph;
|
||||
this.manager._init(nextState);
|
||||
const nextState = diff.patch(this.state, delta) as Graph;
|
||||
this.index = nextIndex;
|
||||
this.prevState = nextState;
|
||||
this.state = nextState;
|
||||
return clone(nextState);
|
||||
} else {
|
||||
console.log("Reached end")
|
||||
log.log("reached end")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@
|
||||
<br />
|
||||
<button
|
||||
on:click={() =>
|
||||
graphManager.load(graphManager.createTemplate("grid", 10, 10))}
|
||||
graphManager.load(graphManager.createTemplate("grid", 5, 5))}
|
||||
>load grid</button
|
||||
>
|
||||
<br />
|
||||
|
Loading…
Reference in New Issue
Block a user