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 { onMount, setContext } from "svelte";
|
||||||
import Camera from "../Camera.svelte";
|
import Camera from "../Camera.svelte";
|
||||||
import GraphView from "./GraphView.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 FloatingEdge from "../edges/FloatingEdge.svelte";
|
||||||
import type { Socket } from "$lib/types";
|
import type { Socket } from "$lib/types";
|
||||||
import {
|
import {
|
||||||
@ -38,6 +38,10 @@
|
|||||||
const cameraDown = [0, 0];
|
const cameraDown = [0, 0];
|
||||||
let cameraPosition: [number, number, number] = [0, 0, 4];
|
let cameraPosition: [number, number, number] = [0, 0, 4];
|
||||||
let addMenuPosition: [number, number] | null = null;
|
let addMenuPosition: [number, number] | null = null;
|
||||||
|
let clipboard: null | {
|
||||||
|
nodes: Node[];
|
||||||
|
edges: [number, number, number, string][];
|
||||||
|
} = null;
|
||||||
|
|
||||||
$: if (cameraPosition && loaded) {
|
$: if (cameraPosition && loaded) {
|
||||||
localStorage.setItem("cameraPosition", JSON.stringify(cameraPosition));
|
localStorage.setItem("cameraPosition", JSON.stringify(cameraPosition));
|
||||||
@ -438,22 +442,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
const bodyIsFocused =
|
const bodyIsFocused =
|
||||||
document.activeElement === document.body ||
|
document.activeElement === document.body ||
|
||||||
document?.activeElement?.id === "graph";
|
document?.activeElement?.id === "graph";
|
||||||
|
|
||||||
if (event.key === "l") {
|
if (event.key === "l") {
|
||||||
if (event.ctrlKey) {
|
const activeNode = graph.getNode($activeNodeId);
|
||||||
const activeNode = graph.getNode($activeNodeId);
|
if (activeNode) {
|
||||||
if (activeNode) {
|
const nodes = graph.getLinkedNodes(activeNode);
|
||||||
const nodes = graph.getLinkedNodes(activeNode);
|
$selectedNodes = new Set(nodes.map((n) => n.id));
|
||||||
$selectedNodes = new Set(nodes.map((n) => n.id));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const activeNode = graph.getNode($activeNodeId);
|
|
||||||
console.log(activeNode);
|
|
||||||
}
|
}
|
||||||
|
console.log(activeNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
@ -497,20 +538,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "c" && event.ctrlKey) {
|
if (event.key === "c" && event.ctrlKey) {
|
||||||
|
copyNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "v" && event.ctrlKey) {
|
if (event.key === "v" && event.ctrlKey) {
|
||||||
|
pasteNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "z" && event.ctrlKey) {
|
if (event.key === "z" && event.ctrlKey) {
|
||||||
graph.history.undo();
|
graph.undo();
|
||||||
for (const node of $nodes.values()) {
|
for (const node of $nodes.values()) {
|
||||||
updateNodePosition(node);
|
updateNodePosition(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "y" && event.ctrlKey) {
|
if (event.key === "y" && event.ctrlKey) {
|
||||||
graph.history.redo();
|
graph.redo();
|
||||||
for (const node of $nodes.values()) {
|
for (const node of $nodes.values()) {
|
||||||
updateNodePosition(node);
|
updateNodePosition(node);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ import { HistoryManager } from "./history-manager";
|
|||||||
import * as templates from "./graphs";
|
import * as templates from "./graphs";
|
||||||
import EventEmitter from "./helpers/EventEmitter";
|
import EventEmitter from "./helpers/EventEmitter";
|
||||||
import throttle from "./helpers/throttle";
|
import throttle from "./helpers/throttle";
|
||||||
|
import { createLogger } from "./helpers";
|
||||||
|
|
||||||
|
const logger = createLogger("graph-manager");
|
||||||
|
|
||||||
export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
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());
|
inputSockets: Writable<Set<string>> = writable(new Set());
|
||||||
|
|
||||||
history: HistoryManager = new HistoryManager(this);
|
history: HistoryManager = new HistoryManager();
|
||||||
|
|
||||||
constructor(private nodeRegistry: NodeRegistry, private runtime: RuntimeExecutor) {
|
constructor(private nodeRegistry: NodeRegistry, private runtime: RuntimeExecutor) {
|
||||||
super();
|
super();
|
||||||
@ -41,12 +44,13 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serialize(): Graph {
|
serialize(): Graph {
|
||||||
|
logger.log("serializing graph")
|
||||||
const nodes = Array.from(this._nodes.values()).map(node => ({
|
const nodes = Array.from(this._nodes.values()).map(node => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
position: node.position,
|
position: [...node.position],
|
||||||
type: node.type,
|
type: node.type,
|
||||||
props: node.props,
|
props: node.props,
|
||||||
}));
|
})) as Node[];
|
||||||
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"];
|
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 };
|
return { id: this.graph.id, nodes, edges };
|
||||||
}
|
}
|
||||||
@ -57,7 +61,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
|||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
const result = this.runtime.execute(this.serialize());
|
const result = this.runtime.execute(this.serialize());
|
||||||
const end = performance.now();
|
const end = performance.now();
|
||||||
console.log(`Execution took ${end - start}ms -> ${result}`);
|
logger.log(`Execution took ${end - start}ms -> ${result}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeTypes() {
|
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) {
|
private _init(graph: Graph) {
|
||||||
const nodes = new Map(graph.nodes.map(node => {
|
const nodes = new Map(graph.nodes.map(node => {
|
||||||
const nodeType = this.nodeRegistry.getNode(node.type);
|
const nodeType = this.nodeRegistry.getNode(node.type);
|
||||||
@ -116,36 +139,28 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
|||||||
async load(graph: Graph) {
|
async load(graph: Graph) {
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.graph = graph;
|
this.graph = graph;
|
||||||
const a = performance.now();
|
|
||||||
this.status.set("loading");
|
this.status.set("loading");
|
||||||
this.id.set(graph.id);
|
this.id.set(graph.id);
|
||||||
|
|
||||||
const b = performance.now();
|
|
||||||
for (const node of this.graph.nodes) {
|
for (const node of this.graph.nodes) {
|
||||||
const nodeType = this.nodeRegistry.getNode(node.type);
|
const nodeType = this.nodeRegistry.getNode(node.type);
|
||||||
if (!nodeType) {
|
if (!nodeType) {
|
||||||
console.error(`Node type not found: ${node.type}`);
|
logger.error(`Node type not found: ${node.type}`);
|
||||||
this.status.set("error");
|
this.status.set("error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
node.tmp = node.tmp || {};
|
node.tmp = node.tmp || {};
|
||||||
node.tmp.type = nodeType;
|
node.tmp.type = nodeType;
|
||||||
}
|
}
|
||||||
const c = performance.now();
|
|
||||||
|
|
||||||
|
this.history.reset();
|
||||||
this._init(this.graph);
|
this._init(this.graph);
|
||||||
|
|
||||||
const d = performance.now();
|
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
const e = performance.now();
|
|
||||||
|
|
||||||
this.status.set("idle");
|
this.status.set("idle");
|
||||||
|
|
||||||
this.loaded = true;
|
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();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
private createNodeId() {
|
createNodeId() {
|
||||||
return Math.max(...this.getAllNodes().map(n => n.id), 0) + 1;
|
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);
|
const nodeType = this.nodeRegistry.getNode(type);
|
||||||
if (!nodeType) {
|
if (!nodeType) {
|
||||||
@ -236,7 +300,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
|||||||
return;
|
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) => {
|
this.nodes.update((nodes) => {
|
||||||
nodes.set(node.id, node);
|
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() {
|
startUndoGroup() {
|
||||||
this.currentUndoGroup = 1;
|
this.currentUndoGroup = 1;
|
||||||
}
|
}
|
||||||
@ -305,8 +387,10 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
|||||||
|
|
||||||
save() {
|
save() {
|
||||||
if (this.currentUndoGroup) return;
|
if (this.currentUndoGroup) return;
|
||||||
this.emit("save", this.serialize());
|
const state = this.serialize();
|
||||||
this.history.save();
|
this.history.save(state);
|
||||||
|
this.emit("save", state);
|
||||||
|
logger.log("saving graph");
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentsOfNode(node: Node) {
|
getParentsOfNode(node: Node) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import throttle from './throttle';
|
import throttle from './throttle';
|
||||||
|
|
||||||
const debug = { amountEmitters: 0, amountCallbacks: 0, emitters: [] };
|
|
||||||
|
|
||||||
|
|
||||||
type EventMap = Record<string, unknown>;
|
type EventMap = Record<string, unknown>;
|
||||||
@ -13,8 +12,6 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
|
|||||||
index = 0;
|
index = 0;
|
||||||
public eventMap: T = {} as T;
|
public eventMap: T = {} as T;
|
||||||
constructor() {
|
constructor() {
|
||||||
this.index = debug.amountEmitters;
|
|
||||||
debug.amountEmitters++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private cbs: { [key: string]: ((data?: unknown) => unknown)[] } = {};
|
private cbs: { [key: string]: ((data?: unknown) => unknown)[] } = {};
|
||||||
@ -42,24 +39,9 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
|
|||||||
});
|
});
|
||||||
this.cbs = cbs;
|
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);
|
// console.log('New EventEmitter ', this.constructor.name);
|
||||||
return () => {
|
return () => {
|
||||||
debug.amountCallbacks--;
|
|
||||||
cbs[event]?.splice(cbs[event].indexOf(cb), 1);
|
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() {
|
public destroyEventEmitter() {
|
||||||
debug.amountEmitters--;
|
|
||||||
Object.keys(this.cbs).forEach((key) => {
|
Object.keys(this.cbs).forEach((key) => {
|
||||||
debug.amountCallbacks -= this.cbs[key].length;
|
|
||||||
delete this.cbs[key];
|
delete this.cbs[key];
|
||||||
});
|
});
|
||||||
Object.keys(this.cbsOnce).forEach((key) => delete this.cbsOnce[key]);
|
Object.keys(this.cbsOnce).forEach((key) => delete this.cbsOnce[key]);
|
||||||
this.cbs = {};
|
this.cbs = {};
|
||||||
this.cbsOnce = {};
|
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);
|
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 { create, type Delta } from "jsondiffpatch";
|
||||||
import type { Graph } from "./types";
|
import type { Graph } from "./types";
|
||||||
|
import { createLogger, clone } from "./helpers";
|
||||||
|
|
||||||
|
|
||||||
const diff = create({
|
const diff = create({
|
||||||
@ -14,82 +14,88 @@ const diff = create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const log = createLogger("history")
|
||||||
|
|
||||||
export class HistoryManager {
|
export class HistoryManager {
|
||||||
|
|
||||||
index: number = -1;
|
index: number = -1;
|
||||||
history: Delta[] = [];
|
history: Delta[] = [];
|
||||||
private initialState: Graph | undefined;
|
private initialState: Graph | undefined;
|
||||||
private prevState: Graph | undefined;
|
private state: Graph | undefined;
|
||||||
private timeout: number | undefined;
|
|
||||||
|
|
||||||
private opts = {
|
private opts = {
|
||||||
debounce: 400,
|
debounce: 400,
|
||||||
maxHistory: 100,
|
maxHistory: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor({ maxHistory = 100, debounce = 100 } = {}) {
|
||||||
constructor(private manager: GraphManager, { maxHistory = 100, debounce = 100 } = {}) {
|
|
||||||
this.history = [];
|
this.history = [];
|
||||||
this.index = -1;
|
this.index = -1;
|
||||||
this.opts.debounce = debounce;
|
this.opts.debounce = debounce;
|
||||||
this.opts.maxHistory = maxHistory;
|
this.opts.maxHistory = maxHistory;
|
||||||
|
globalThis["_history"] = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save(state: Graph) {
|
||||||
if (!this.prevState) {
|
if (!this.state) {
|
||||||
this.prevState = this.manager.serialize();
|
this.state = clone(state);
|
||||||
this.initialState = globalThis.structuredClone(this.prevState);
|
this.initialState = this.state;
|
||||||
|
log.log("initial state saved")
|
||||||
} else {
|
} else {
|
||||||
if (this.timeout) {
|
const newState = state;
|
||||||
clearTimeout(this.timeout);
|
const delta = diff.diff(this.state, newState);
|
||||||
}
|
if (delta) {
|
||||||
|
log.log("saving state")
|
||||||
this.timeout = setTimeout(() => {
|
// Add the delta to history
|
||||||
const newState = this.manager.serialize();
|
if (this.index < this.history.length - 1) {
|
||||||
const delta = diff.diff(this.prevState, newState);
|
// Clear the history after the current index if new changes are made
|
||||||
if (delta) {
|
this.history.splice(this.index + 1);
|
||||||
// Add the delta to history
|
|
||||||
if (this.index < this.history.length - 1) {
|
|
||||||
// Clear the history after the current index if new changes are made
|
|
||||||
this.history.splice(this.index + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.history.push(delta);
|
|
||||||
this.index++;
|
|
||||||
|
|
||||||
// Limit the size of the history
|
|
||||||
if (this.history.length > this.opts.maxHistory) {
|
|
||||||
this.history.shift();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.prevState = newState;
|
|
||||||
}, this.opts.debounce) as unknown as number;
|
this.history.push(delta);
|
||||||
|
this.index++;
|
||||||
|
|
||||||
|
// Limit the size of the history
|
||||||
|
if (this.history.length > this.opts.maxHistory) {
|
||||||
|
this.history.shift();
|
||||||
|
}
|
||||||
|
this.state = newState;
|
||||||
|
} else {
|
||||||
|
log.log("no changes")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.history = [];
|
||||||
|
this.index = -1;
|
||||||
|
this.state = undefined;
|
||||||
|
this.initialState = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
undo() {
|
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 delta = this.history[this.index];
|
||||||
const prevState = diff.unpatch(this.prevState, delta) as Graph;
|
const prevState = diff.unpatch(this.state, delta) as Graph;
|
||||||
this.manager._init(prevState);
|
this.state = prevState;
|
||||||
this.index--;
|
this.index = Math.max(-1, this.index - 1);
|
||||||
this.prevState = prevState;
|
return clone(prevState);
|
||||||
} else if (this.index === 0 && this.initialState) {
|
|
||||||
this.manager._init(globalThis.structuredClone(this.initialState));
|
|
||||||
console.log("Reached start", this.index, this.history.length)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redo() {
|
redo() {
|
||||||
if (this.index < this.history.length - 1) {
|
if (this.index <= this.history.length - 1) {
|
||||||
const nextIndex = this.index + 1;
|
const nextIndex = Math.min(this.history.length - 1, this.index + 1);
|
||||||
const delta = this.history[nextIndex];
|
const delta = this.history[nextIndex];
|
||||||
const nextState = diff.patch(this.prevState, delta) as Graph;
|
const nextState = diff.patch(this.state, delta) as Graph;
|
||||||
this.manager._init(nextState);
|
|
||||||
this.index = nextIndex;
|
this.index = nextIndex;
|
||||||
this.prevState = nextState;
|
this.state = nextState;
|
||||||
|
return clone(nextState);
|
||||||
} else {
|
} else {
|
||||||
console.log("Reached end")
|
log.log("reached end")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<button
|
<button
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
graphManager.load(graphManager.createTemplate("grid", 10, 10))}
|
graphManager.load(graphManager.createTemplate("grid", 5, 5))}
|
||||||
>load grid</button
|
>load grid</button
|
||||||
>
|
>
|
||||||
<br />
|
<br />
|
||||||
|
Loading…
Reference in New Issue
Block a user