feat: add initial undo/redo system
This commit is contained in:
parent
305341fdf0
commit
c4c203968d
@ -20,6 +20,7 @@
|
|||||||
"@threlte/extras": "^8.7.5",
|
"@threlte/extras": "^8.7.5",
|
||||||
"@threlte/flex": "^1.0.1",
|
"@threlte/flex": "^1.0.1",
|
||||||
"@types/three": "^0.159.0",
|
"@types/three": "^0.159.0",
|
||||||
|
"jsondiffpatch": "^0.6.0",
|
||||||
"three": "^0.159.0"
|
"three": "^0.159.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Node } from "$lib/types";
|
import type { Node } from "$lib/types";
|
||||||
|
import { getContext, onMount } from "svelte";
|
||||||
import NodeHeader from "./NodeHeader.svelte";
|
import NodeHeader from "./NodeHeader.svelte";
|
||||||
import NodeParameter from "./NodeParameter.svelte";
|
import NodeParameter from "./NodeParameter.svelte";
|
||||||
import { activeNodeId, selectedNodes } from "./graph/stores";
|
import { activeNodeId, selectedNodes } from "./graph/stores";
|
||||||
@ -7,9 +8,20 @@
|
|||||||
export let node: Node;
|
export let node: Node;
|
||||||
export let inView = true;
|
export let inView = true;
|
||||||
|
|
||||||
|
const updateNodePosition =
|
||||||
|
getContext<(n: Node) => void>("updateNodePosition");
|
||||||
|
|
||||||
const type = node?.tmp?.type;
|
const type = node?.tmp?.type;
|
||||||
|
|
||||||
const parameters = Object.entries(type?.inputs || {});
|
const parameters = Object.entries(type?.inputs || {});
|
||||||
|
|
||||||
|
let ref: HTMLDivElement;
|
||||||
|
|
||||||
|
$: if (node && ref) {
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.tmp.ref = ref;
|
||||||
|
updateNodePosition(node);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -18,7 +30,7 @@
|
|||||||
class:selected={!!$selectedNodes?.has(node.id)}
|
class:selected={!!$selectedNodes?.has(node.id)}
|
||||||
class:in-view={inView}
|
class:in-view={inView}
|
||||||
data-node-id={node.id}
|
data-node-id={node.id}
|
||||||
bind:this={node.tmp.ref}
|
bind:this={ref}
|
||||||
>
|
>
|
||||||
<NodeHeader {node} />
|
<NodeHeader {node} />
|
||||||
|
|
||||||
|
@ -56,10 +56,20 @@
|
|||||||
function updateNodePosition(node: NodeType) {
|
function updateNodePosition(node: NodeType) {
|
||||||
node.tmp = node.tmp || {};
|
node.tmp = node.tmp || {};
|
||||||
if (node?.tmp?.ref) {
|
if (node?.tmp?.ref) {
|
||||||
|
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) {
|
||||||
|
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`);
|
||||||
|
node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`);
|
||||||
|
if (node.tmp.x === node.position.x && node.tmp.y === node.position.y) {
|
||||||
|
delete node.tmp.x;
|
||||||
|
delete node.tmp.y;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
node.tmp.ref.style.setProperty("--nx", `${node.position.x * 10}px`);
|
node.tmp.ref.style.setProperty("--nx", `${node.position.x * 10}px`);
|
||||||
node.tmp.ref.style.setProperty("--ny", `${node.position.y * 10}px`);
|
node.tmp.ref.style.setProperty("--ny", `${node.position.y * 10}px`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
setContext("updateNodePosition", updateNodePosition);
|
||||||
|
|
||||||
const nodeHeightCache: Record<string, number> = {};
|
const nodeHeightCache: Record<string, number> = {};
|
||||||
function getNodeHeight(nodeTypeId: string) {
|
function getNodeHeight(nodeTypeId: string) {
|
||||||
@ -100,7 +110,7 @@
|
|||||||
if (edge[3] === index) {
|
if (edge[3] === index) {
|
||||||
node = edge[0];
|
node = edge[0];
|
||||||
index = edge[1];
|
index = edge[1];
|
||||||
position = getSocketPosition({ node, index });
|
position = getSocketPosition(node, index);
|
||||||
graph.removeEdge(edge);
|
graph.removeEdge(edge);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -120,7 +130,7 @@
|
|||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
index,
|
index,
|
||||||
position: getSocketPosition({ node, index }),
|
position: getSocketPosition(node, index),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
$possibleSocketIds = new Set(
|
$possibleSocketIds = new Set(
|
||||||
@ -142,23 +152,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSocketPosition(
|
function getSocketPosition(
|
||||||
socket: Omit<Socket, "position">,
|
node: NodeType,
|
||||||
|
index: string | number,
|
||||||
): [number, number] {
|
): [number, number] {
|
||||||
if (typeof socket.index === "number") {
|
if (typeof index === "number") {
|
||||||
return [
|
return [
|
||||||
socket.node.position.x + 5,
|
(node?.tmp?.x ?? node.position.x) + 5,
|
||||||
socket.node.position.y + 0.625 + 2.5 * socket.index,
|
(node?.tmp?.y ?? node.position.y) + 0.625 + 2.5 * index,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const _index = Object.keys(socket.node.tmp?.type?.inputs || {}).indexOf(
|
const _index = Object.keys(node.tmp?.type?.inputs || {}).indexOf(index);
|
||||||
socket.index,
|
|
||||||
);
|
|
||||||
return [
|
return [
|
||||||
socket.node.position.x,
|
node?.tmp?.x ?? node.position.x,
|
||||||
socket.node.position.y + 2.5 + 2.5 * _index,
|
(node?.tmp?.y ?? node.position.y) + 2.5 + 2.5 * _index,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setContext("getSocketPosition", getSocketPosition);
|
||||||
|
|
||||||
function setMouseFromEvent(event: MouseEvent) {
|
function setMouseFromEvent(event: MouseEvent) {
|
||||||
const x = event.clientX;
|
const x = event.clientX;
|
||||||
@ -230,16 +240,15 @@
|
|||||||
if ($selectedNodes?.size) {
|
if ($selectedNodes?.size) {
|
||||||
for (const nodeId of $selectedNodes) {
|
for (const nodeId of $selectedNodes) {
|
||||||
const n = graph.getNode(nodeId);
|
const n = graph.getNode(nodeId);
|
||||||
if (!n) continue;
|
if (!n?.tmp) continue;
|
||||||
n.position.x = (n?.tmp?.downX || 0) - vecX;
|
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
|
||||||
n.position.y = (n?.tmp?.downY || 0) - vecY;
|
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
|
||||||
updateNodePosition(n);
|
updateNodePosition(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
node.position.x = newX;
|
node.tmp.x = newX;
|
||||||
node.position.y = newY;
|
node.tmp.y = newY;
|
||||||
node.position = node.position;
|
|
||||||
|
|
||||||
updateNodePosition(node);
|
updateNodePosition(node);
|
||||||
|
|
||||||
@ -306,7 +315,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
if (event.key === "Delete") {
|
if (event.key === "Escape") {
|
||||||
|
$activeNodeId = -1;
|
||||||
|
$selectedNodes?.clear();
|
||||||
|
$selectedNodes = $selectedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "a" && event.ctrlKey) {
|
||||||
|
$selectedNodes = new Set($nodes.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "c" && event.ctrlKey) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "v" && event.ctrlKey) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "z" && event.ctrlKey) {
|
||||||
|
graph.history.undo();
|
||||||
|
for (const node of $nodes.values()) {
|
||||||
|
updateNodePosition(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "y" && event.ctrlKey) {
|
||||||
|
graph.history.redo();
|
||||||
|
for (const node of $nodes.values()) {
|
||||||
|
updateNodePosition(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.key === "Delete" ||
|
||||||
|
event.key === "Backspace" ||
|
||||||
|
event.key === "x"
|
||||||
|
) {
|
||||||
if ($activeNodeId !== -1) {
|
if ($activeNodeId !== -1) {
|
||||||
const node = graph.getNode($activeNodeId);
|
const node = graph.getNode($activeNodeId);
|
||||||
if (node) {
|
if (node) {
|
||||||
@ -349,38 +392,52 @@
|
|||||||
activeNode.tmp = activeNode.tmp || {};
|
activeNode.tmp = activeNode.tmp || {};
|
||||||
activeNode.tmp.isMoving = false;
|
activeNode.tmp.isMoving = false;
|
||||||
const snapLevel = getSnapLevel();
|
const snapLevel = getSnapLevel();
|
||||||
const fx = snapToGrid(activeNode.position.x, 5 / snapLevel);
|
activeNode.position.x = snapToGrid(
|
||||||
const fy = snapToGrid(activeNode.position.y, 5 / snapLevel);
|
activeNode?.tmp?.x ?? activeNode.position.x,
|
||||||
if ($selectedNodes) {
|
5 / snapLevel,
|
||||||
for (const nodeId of $selectedNodes) {
|
);
|
||||||
const node = graph.getNode(nodeId);
|
activeNode.position.y = snapToGrid(
|
||||||
|
activeNode?.tmp?.y ?? activeNode.position.y,
|
||||||
|
5 / snapLevel,
|
||||||
|
);
|
||||||
|
const nodes = [
|
||||||
|
...[...($selectedNodes?.values() || [])].map((id) => graph.getNode(id)),
|
||||||
|
] as NodeType[];
|
||||||
|
|
||||||
|
const vec = [
|
||||||
|
activeNode.position.x - (activeNode?.tmp.x || 0),
|
||||||
|
activeNode.position.y - (activeNode?.tmp.y || 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
if (!node) continue;
|
if (!node) continue;
|
||||||
node.tmp = node.tmp || {};
|
node.tmp = node.tmp || {};
|
||||||
node.tmp.snapX = node.position.x - (activeNode.position.x - fx);
|
const { x, y } = node.tmp;
|
||||||
node.tmp.snapY = node.position.y - (activeNode.position.y - fy);
|
if (x !== undefined && y !== undefined) {
|
||||||
|
node.position.x = x + vec[0];
|
||||||
|
node.position.y = y + vec[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
nodes.push(activeNode);
|
||||||
animate(500, (a: number) => {
|
animate(500, (a: number) => {
|
||||||
activeNode.position.x = lerp(activeNode.position.x, fx, a);
|
for (const node of nodes) {
|
||||||
activeNode.position.y = lerp(activeNode.position.y, fy, a);
|
if (
|
||||||
updateNodePosition(activeNode);
|
node?.tmp &&
|
||||||
|
node.tmp["x"] !== undefined &&
|
||||||
if ($selectedNodes) {
|
node.tmp["y"] !== undefined
|
||||||
for (const nodeId of $selectedNodes) {
|
) {
|
||||||
const node = graph.getNode(nodeId);
|
node.tmp.x = lerp(node.tmp.x, node.position.x, a);
|
||||||
if (!node) continue;
|
node.tmp.y = lerp(node.tmp.y, node.position.y, a);
|
||||||
node.position.x = lerp(node.position.x, node?.tmp?.snapX || 0, a);
|
|
||||||
node.position.y = lerp(node.position.y, node?.tmp?.snapY || 0, a);
|
|
||||||
updateNodePosition(node);
|
updateNodePosition(node);
|
||||||
}
|
if (node?.tmp?.isMoving) {
|
||||||
}
|
|
||||||
|
|
||||||
if (activeNode?.tmp?.isMoving) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$edges = $edges;
|
$edges = $edges;
|
||||||
});
|
});
|
||||||
|
graph.history.save();
|
||||||
} else if ($hoveredSocket && $activeSocket) {
|
} else if ($hoveredSocket && $activeSocket) {
|
||||||
if (
|
if (
|
||||||
typeof $hoveredSocket.index === "number" &&
|
typeof $hoveredSocket.index === "number" &&
|
||||||
@ -403,6 +460,7 @@
|
|||||||
$hoveredSocket.index,
|
$hoveredSocket.index,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
graph.history.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
mouseDown = null;
|
mouseDown = null;
|
||||||
|
@ -14,14 +14,16 @@
|
|||||||
|
|
||||||
const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView");
|
const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView");
|
||||||
|
|
||||||
|
const getSocketPosition =
|
||||||
|
getContext<(node: NodeType, index: string | number) => [number, number]>(
|
||||||
|
"getSocketPosition",
|
||||||
|
);
|
||||||
|
|
||||||
function getEdgePosition(edge: EdgeType) {
|
function getEdgePosition(edge: EdgeType) {
|
||||||
const index = Object.keys(edge[2].tmp?.type?.inputs || {}).indexOf(edge[3]);
|
const pos1 = getSocketPosition(edge[0], edge[1]);
|
||||||
return [
|
const pos2 = getSocketPosition(edge[2], edge[3]);
|
||||||
edge[0].position.x + 5,
|
|
||||||
edge[0].position.y + 0.625 + edge[1] * 2.5,
|
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
||||||
edge[2].position.x,
|
|
||||||
edge[2].position.y + 2.5 + index * 2.5,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node, Edge, Socket } from "./types";
|
import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node, Edge, Socket } from "./types";
|
||||||
|
import { HistoryManager } from "./history-manager";
|
||||||
|
|
||||||
const nodeTypes: NodeType[] = [
|
const nodeTypes: NodeType[] = [
|
||||||
{
|
{
|
||||||
@ -43,6 +44,8 @@ export class GraphManager {
|
|||||||
private _edges: Edge[] = [];
|
private _edges: Edge[] = [];
|
||||||
edges: Writable<Edge[]> = writable([]);
|
edges: Writable<Edge[]> = writable([]);
|
||||||
|
|
||||||
|
history: HistoryManager = new HistoryManager(this);
|
||||||
|
|
||||||
private constructor(private graph: Graph, private nodeRegistry: NodeRegistry = new NodeRegistry()) {
|
private constructor(private graph: Graph, private nodeRegistry: NodeRegistry = new NodeRegistry()) {
|
||||||
this.nodes.subscribe((nodes) => {
|
this.nodes.subscribe((nodes) => {
|
||||||
this._nodes = nodes;
|
this._nodes = nodes;
|
||||||
@ -50,38 +53,30 @@ export class GraphManager {
|
|||||||
this.edges.subscribe((edges) => {
|
this.edges.subscribe((edges) => {
|
||||||
this._edges = edges;
|
this._edges = edges;
|
||||||
});
|
});
|
||||||
|
|
||||||
globalThis["serialize"] = () => this.serialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize() {
|
serialize(): 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: { x: node.position.x, y: node.position.y },
|
||||||
type: node.type,
|
type: node.type,
|
||||||
props: node.props,
|
props: node.props,
|
||||||
}));
|
}));
|
||||||
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]);
|
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"];
|
||||||
return { nodes, edges };
|
return { nodes, edges };
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
private _init(graph: Graph) {
|
||||||
|
const nodes = new Map(graph.nodes.map(node => {
|
||||||
const nodes = new Map(this.graph.nodes.map(node => [node.id, node]));
|
|
||||||
|
|
||||||
for (const node of nodes.values()) {
|
|
||||||
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}`);
|
|
||||||
this.status.set("error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
node.tmp = node.tmp || {};
|
node.tmp = node.tmp || {};
|
||||||
node.tmp.type = nodeType;
|
node.tmp.type = nodeType;
|
||||||
}
|
}
|
||||||
|
return [node.id, node]
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.edges.set(graph.edges.map((edge) => {
|
||||||
this.edges.set(this.graph.edges.map((edge) => {
|
|
||||||
const from = nodes.get(edge[0]);
|
const from = nodes.get(edge[0]);
|
||||||
const to = nodes.get(edge[2]);
|
const to = nodes.get(edge[2]);
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
@ -101,7 +96,25 @@ export class GraphManager {
|
|||||||
|
|
||||||
this.nodes.set(nodes);
|
this.nodes.set(nodes);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
|
||||||
|
for (const node of this.graph.nodes) {
|
||||||
|
const nodeType = this.nodeRegistry.getNode(node.type);
|
||||||
|
if (!nodeType) {
|
||||||
|
console.error(`Node type not found: ${node.type}`);
|
||||||
|
this.status.set("error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.tmp.type = nodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._init(this.graph);
|
||||||
|
|
||||||
this.status.set("idle");
|
this.status.set("idle");
|
||||||
|
this.history.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -155,6 +168,7 @@ export class GraphManager {
|
|||||||
nodes.delete(node.id);
|
nodes.delete(node.id);
|
||||||
return nodes;
|
return nodes;
|
||||||
});
|
});
|
||||||
|
this.history.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string) {
|
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string) {
|
||||||
@ -181,6 +195,8 @@ export class GraphManager {
|
|||||||
this.edges.update((edges) => {
|
this.edges.update((edges) => {
|
||||||
return [...edges.filter(e => e[2].id !== to.id || e[3] !== toSocket), [from, fromSocket, to, toSocket]];
|
return [...edges.filter(e => e[2].id !== to.id || e[3] !== toSocket), [from, fromSocket, to, toSocket]];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.history.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentsOfNode(node: Node) {
|
getParentsOfNode(node: Node) {
|
||||||
@ -257,6 +273,7 @@ export class GraphManager {
|
|||||||
this.edges.update((edges) => {
|
this.edges.update((edges) => {
|
||||||
return edges.filter((e) => e[0].id !== id0 || e[1] !== sid0 || e[2].id !== id2 || e[3] !== sid2);
|
return edges.filter((e) => e[0].id !== id0 || e[1] !== sid0 || e[2].id !== id2 || e[3] !== sid2);
|
||||||
});
|
});
|
||||||
|
this.history.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
getEdgesToNode(node: Node) {
|
getEdgesToNode(node: Node) {
|
||||||
@ -328,7 +345,5 @@ export class GraphManager {
|
|||||||
return new GraphManager(graph);
|
return new GraphManager(graph);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
95
frontend/src/lib/history-manager.ts
Normal file
95
frontend/src/lib/history-manager.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import type { GraphManager } from "./graph-manager";
|
||||||
|
import { create, type Delta } from "jsondiffpatch";
|
||||||
|
import type { Graph } from "./types";
|
||||||
|
|
||||||
|
|
||||||
|
const diff = create({
|
||||||
|
objectHash: function (obj, index) {
|
||||||
|
if (obj === null) return obj;
|
||||||
|
if ("id" in obj) return obj.id;
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.join("-")
|
||||||
|
}
|
||||||
|
return obj?.id || obj._id || '$$index:' + index;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export class HistoryManager {
|
||||||
|
|
||||||
|
index: number = -1;
|
||||||
|
history: Delta[] = [];
|
||||||
|
private initialState: Graph | undefined;
|
||||||
|
private prevState: Graph | undefined;
|
||||||
|
private timeout: number | undefined;
|
||||||
|
|
||||||
|
private opts = {
|
||||||
|
debounce: 400,
|
||||||
|
maxHistory: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private manager: GraphManager, { maxHistory = 100, debounce = 100 } = {}) {
|
||||||
|
this.history = [];
|
||||||
|
this.index = -1;
|
||||||
|
this.opts.debounce = debounce;
|
||||||
|
this.opts.maxHistory = maxHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
if (!this.prevState) {
|
||||||
|
this.prevState = this.manager.serialize();
|
||||||
|
this.initialState = globalThis.structuredClone(this.prevState);
|
||||||
|
} else {
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
const newState = this.manager.serialize();
|
||||||
|
const delta = diff.diff(this.prevState, newState);
|
||||||
|
if (delta) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
if (this.index > 0) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redo() {
|
||||||
|
if (this.index < this.history.length - 1) {
|
||||||
|
const nextIndex = this.index + 1;
|
||||||
|
const delta = this.history[nextIndex];
|
||||||
|
const nextState = diff.patch(this.prevState, delta) as Graph;
|
||||||
|
this.manager._init(nextState);
|
||||||
|
this.index = nextIndex;
|
||||||
|
this.prevState = nextState;
|
||||||
|
} else {
|
||||||
|
console.log("Reached end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,8 +11,8 @@ export type Node = {
|
|||||||
type?: NodeType;
|
type?: NodeType;
|
||||||
downX?: number;
|
downX?: number;
|
||||||
downY?: number;
|
downY?: number;
|
||||||
snapX?: number;
|
x?: number;
|
||||||
snapY?: number;
|
y?: number;
|
||||||
ref?: HTMLElement;
|
ref?: HTMLElement;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
isMoving?: boolean;
|
isMoving?: boolean;
|
||||||
|
@ -35,6 +35,9 @@ importers:
|
|||||||
'@types/three':
|
'@types/three':
|
||||||
specifier: ^0.159.0
|
specifier: ^0.159.0
|
||||||
version: 0.159.0
|
version: 0.159.0
|
||||||
|
jsondiffpatch:
|
||||||
|
specifier: ^0.6.0
|
||||||
|
version: 0.6.0
|
||||||
three:
|
three:
|
||||||
specifier: ^0.159.0
|
specifier: ^0.159.0
|
||||||
version: 0.159.0
|
version: 0.159.0
|
||||||
@ -1146,6 +1149,10 @@ packages:
|
|||||||
/@types/cookie@0.6.0:
|
/@types/cookie@0.6.0:
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
|
||||||
|
/@types/diff-match-patch@1.0.36:
|
||||||
|
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/estree@1.0.5:
|
/@types/estree@1.0.5:
|
||||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||||
|
|
||||||
@ -1803,6 +1810,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==}
|
resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/diff-match-patch@1.0.5:
|
||||||
|
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dir-glob@3.0.1:
|
/dir-glob@3.0.1:
|
||||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -2638,6 +2649,16 @@ packages:
|
|||||||
/jsonc-parser@3.2.1:
|
/jsonc-parser@3.2.1:
|
||||||
resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
|
resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
|
||||||
|
|
||||||
|
/jsondiffpatch@0.6.0:
|
||||||
|
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
'@types/diff-match-patch': 1.0.36
|
||||||
|
chalk: 5.3.0
|
||||||
|
diff-match-patch: 1.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/jsonfile@6.1.0:
|
/jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user