Skip to content

Commit 50f6f82

Browse files
Manuel-PolpgallinoMegaRedHand
authored
Undo redo (#81)
Co-authored-by: pgallino <[email protected]> Co-authored-by: Pedro Gallino <[email protected]> Co-authored-by: Tomás Grüner <[email protected]>
1 parent 1c0711a commit 50f6f82

File tree

19 files changed

+617
-78
lines changed

19 files changed

+617
-78
lines changed

src/assets/left-curve-arrow.svg

Lines changed: 5 additions & 0 deletions
Loading

src/assets/right-curve-arrow.svg

Lines changed: 3 additions & 0 deletions
Loading

src/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ViewGraph } from "./types/graphs/viewgraph";
44
import {
55
loadFromLocalStorage,
66
saveToLocalStorage,
7+
urManager,
78
} from "./types/viewportManager";
89
import { Layer } from "./types/devices/device";
910
import { IpAddress, IpAddressGenerator } from "./packets/ip";
@@ -40,6 +41,7 @@ export class GlobalContext {
4041
this.setNetwork(datagraph, layer);
4142
this.setupAutoSave();
4243
saveToLocalStorage(this);
44+
urManager.reset();
4345
}
4446

4547
getViewport() {

src/graphics/renderables/device_info.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { Device } from "../../types/devices";
22
import { DeviceType } from "../../types/devices/device";
3+
import { CreateDevice } from "../../types/devices/utils";
34
import { RoutingTableEntry } from "../../types/graphs/datagraph";
45
import { ViewGraph } from "../../types/graphs/viewgraph";
56
import { sendPacket } from "../../types/packet";
7+
import { RemoveDeviceMove } from "../../types/undo-redo";
8+
import { urManager } from "../../types/viewportManager";
69
import {
710
createDropdown,
811
createToggleTable,
@@ -38,7 +41,22 @@ export class DeviceInfo extends StyledInfo {
3841
),
3942
createRightBarButton(
4043
"Delete device",
41-
() => this.device.delete(),
44+
() => {
45+
const deviceData: CreateDevice = {
46+
id: this.device.id,
47+
type: this.device.getType(),
48+
x: this.device.x,
49+
y: this.device.y,
50+
ip: this.device.ip.toString(),
51+
mask: this.device.ipMask.toString(),
52+
};
53+
const move = new RemoveDeviceMove(
54+
deviceData,
55+
this.device.getConnections(),
56+
);
57+
this.device.delete();
58+
urManager.push(move);
59+
},
4260
"right-bar-delete-button",
4361
),
4462
);

src/index.ejs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<div class="canvas-container">
1717
<canvas id="canvas"></canvas>
1818
<button id="pause-button" class="pause-button" title="Pause"></button>
19+
<i id="undo-button" class="undo-button" title="Undo"></i>
20+
<i id="redo-button" class="redo-button" title="Redo"></i>
1921
<select id="layer-select" class="dropdown-menu">
2022
<option value="application">App Layer</option>
2123
<option value="transport">Transport Layer</option>

src/index.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
loadFromFile,
66
loadFromLocalStorage,
77
saveToFile,
8+
urManager,
89
} from "./types/viewportManager";
910
import { DataGraph } from "./types/graphs/datagraph";
1011
import { Packet } from "./types/packet";
@@ -21,6 +22,8 @@ import RouterSvg from "./assets/router.svg";
2122
import ComputerSvg from "./assets/pc.svg";
2223
import PlaySvg from "./assets/play-icon.svg";
2324
import PauseSvg from "./assets/pause-icon.svg";
25+
import UndoSvg from "./assets/left-curve-arrow.svg";
26+
import RedoSvg from "./assets/right-curve-arrow.svg";
2427

2528
// IIFE to avoid errors
2629
(async () => {
@@ -118,6 +121,69 @@ import PauseSvg from "./assets/pause-icon.svg";
118121
saveButton.onclick = () => saveToFile(ctx);
119122
loadButton.onclick = () => loadFromFile(ctx);
120123

124+
// Undo button’s logic
125+
const undoButton = document.getElementById(
126+
"undo-button",
127+
) as HTMLButtonElement;
128+
129+
const undoIcon = document.createElement("img");
130+
undoIcon.src = UndoSvg;
131+
undoIcon.alt = "Undo Icon";
132+
undoButton.appendChild(undoIcon);
133+
134+
console.log(undoIcon.style.filter);
135+
urManager.suscribe(() => {
136+
undoButton.disabled = !urManager.canUndo();
137+
undoIcon.style.opacity = urManager.canUndo() ? "1" : "0.5"; // Full opacity for active, reduced for inactive
138+
});
139+
140+
const triggerUndo = () => {
141+
if (urManager.canUndo()) {
142+
urManager.undo(ctx.getViewGraph());
143+
}
144+
};
145+
146+
undoButton.onclick = triggerUndo;
147+
148+
// Redo button’s logic
149+
const redoButton = document.getElementById(
150+
"redo-button",
151+
) as HTMLButtonElement;
152+
const redoIcon = document.createElement("img");
153+
redoIcon.src = RedoSvg;
154+
redoIcon.alt = "Redo Icon";
155+
redoButton.appendChild(redoIcon);
156+
157+
urManager.suscribe(() => {
158+
redoButton.disabled = !urManager.canRedo();
159+
redoIcon.style.opacity = urManager.canRedo() ? "1" : "0.5"; // Full opacity for active, reduced for inactive
160+
});
161+
162+
const triggerRedo = () => {
163+
if (urManager.canRedo()) {
164+
urManager.redo(ctx.getViewGraph());
165+
}
166+
};
167+
168+
redoButton.onclick = triggerRedo;
169+
170+
// Add keyboard shortcuts for Undo (Ctrl+Z) and Redo (Ctrl+Y)
171+
document.addEventListener("keydown", (event) => {
172+
if (event.ctrlKey) {
173+
switch (event.key) {
174+
case "z": // Ctrl+Z for Undo
175+
event.preventDefault(); // Prevent default browser action (like undo in text inputs)
176+
triggerUndo();
177+
break;
178+
case "y": // Ctrl+Y for Redo
179+
event.preventDefault(); // Prevent default browser action
180+
triggerRedo();
181+
break;
182+
}
183+
}
184+
});
185+
186+
// Pause button’s logic
121187
const pauseButton = document.getElementById("pause-button");
122188
let paused = false;
123189

src/styles/canvas.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@
7676
justify-content: center; /* Horizontally centers content */
7777
transition: background-color 0.3s ease; /* Smooth color transition */
7878
position: absolute; /* Permite colocarlo sobre el canvas */
79+
}
80+
81+
.pause-button {
7982
top: 10px; /* Separación desde el borde superior */
8083
left: 15px; /* Separación desde el borde derecho */
8184
}
@@ -85,7 +88,32 @@
8588
background-color: #006400; /* Darker forest green on hover */
8689
}
8790

91+
.undo-button,
92+
.redo-button {
93+
cursor: pointer; /* Changes cursor to pointer on hover */
94+
width: 45px; /* Fixed width for the circular button */
95+
height: 45px; /* Fixed height for the circular button */
96+
border-radius: 50%; /* Makes the button circular */
97+
display: flex; /* Uses flexbox for centering */
98+
align-items: center; /* Vertically centers content */
99+
justify-content: center; /* Horizontally centers content */
100+
transition: background-color 0.3s ease; /* Smooth color transition */
101+
position: absolute; /* Permite colocarlo sobre el canvas */
102+
}
103+
104+
.undo-button {
105+
top: 10px;
106+
left: 70px;
107+
}
108+
109+
.redo-button {
110+
top: 10px;
111+
left: 110px;
112+
}
113+
88114
/* Icon inside the pause button */
115+
.undo-button img,
116+
.redo-button img,
89117
.pause-button img {
90118
width: 60%; /* Scales the icon to fit the button */
91119
height: 60%; /* Scales the icon to fit the button */

src/types/devices/device.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import {
1111
deselectElement,
1212
refreshElement,
1313
selectElement,
14+
urManager,
1415
} from "./../viewportManager";
1516
import { RightBar } from "../../graphics/right_bar";
1617
import { Colors, ZIndexLevels } from "../../utils";
1718
import { Position } from "../common";
1819
import { DeviceInfo } from "../../graphics/renderables/device_info";
1920
import { IpAddress } from "../../packets/ip";
2021
import { DeviceId } from "../graphs/datagraph";
22+
import { DragDeviceMove, EdgeData, AddEdgeMove } from "../undo-redo";
2123

2224
export const DEVICE_SIZE = 20;
2325

@@ -42,6 +44,7 @@ export abstract class Device extends Sprite {
4244

4345
static dragTarget: Device | null = null;
4446
static connectionTarget: Device | null = null;
47+
static startPosition: Position | null = null;
4548

4649
ip: IpAddress;
4750
ipMask: IpAddress;
@@ -117,6 +120,8 @@ export abstract class Device extends Sprite {
117120
// Clear connections
118121
this.connections.clear();
119122
deselectElement();
123+
console.log(`Device ${this.id} deleted`);
124+
this.destroy();
120125
}
121126

122127
onPointerDown(event: FederatedPointerEvent): void {
@@ -125,6 +130,9 @@ export abstract class Device extends Sprite {
125130
selectElement(this);
126131
}
127132
Device.dragTarget = this;
133+
134+
// Guardar posición inicial
135+
Device.startPosition = { x: this.x, y: this.y };
128136
event.stopPropagation();
129137

130138
// Listen to global pointermove and pointerup events
@@ -141,6 +149,15 @@ export abstract class Device extends Sprite {
141149
const adyacentDevice = this.viewgraph.getDevice(adyacentId);
142150
this.addConnection(edgeId, adyacentId);
143151
adyacentDevice.addConnection(edgeId, this.id);
152+
153+
// Register move
154+
const moveData: EdgeData = {
155+
edgeId,
156+
connectedNodes: { n1: this.id, n2: adyacentId },
157+
};
158+
const move = new AddEdgeMove(moveData);
159+
urManager.push(move);
160+
144161
return true;
145162
}
146163
return false;
@@ -225,7 +242,6 @@ export abstract class Device extends Sprite {
225242
}
226243

227244
function onPointerMove(event: FederatedPointerEvent): void {
228-
console.log("Entered onPointerMove");
229245
if (Device.dragTarget) {
230246
Device.dragTarget.parent.toLocal(
231247
event.global,
@@ -239,8 +255,36 @@ function onPointerMove(event: FederatedPointerEvent): void {
239255
}
240256

241257
function onPointerUp(): void {
242-
console.log("Entered onPointerUp");
243-
if (Device.dragTarget) {
258+
if (Device.dragTarget && Device.startPosition) {
259+
const endPosition: Position = {
260+
x: Device.dragTarget.x,
261+
y: Device.dragTarget.y,
262+
};
263+
console.log("Finalizing move for device:", {
264+
id: Device.dragTarget.id,
265+
startPosition: Device.startPosition,
266+
endPosition,
267+
});
268+
269+
if (
270+
Device.startPosition.x === endPosition.x &&
271+
Device.startPosition.y === endPosition.y
272+
) {
273+
console.log(
274+
`No movement detected for device ID ${Device.dragTarget.id}. Move not registered.`,
275+
);
276+
} else {
277+
const move = new DragDeviceMove(
278+
Device.dragTarget.id,
279+
Device.startPosition,
280+
endPosition,
281+
);
282+
urManager.push(move);
283+
}
284+
285+
// Resetear variables estáticas
286+
Device.startPosition = null;
287+
244288
// Remove global pointermove and pointerup events
245289
Device.dragTarget.parent.off("pointermove", onPointerMove);
246290
Device.dragTarget.parent.off("pointerup", onPointerUp);

src/types/devices/utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ export function createDevice(
1717
deviceInfo: CreateDevice,
1818
viewgraph: ViewGraph,
1919
): Device {
20-
const { x, y } = deviceInfo;
21-
const position = { x, y };
20+
const position: { x: number; y: number } = deviceInfo;
2221
const ip = IpAddress.parse(deviceInfo.ip);
2322
const mask = IpAddress.parse(deviceInfo.mask);
2423

src/types/edge.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Graphics, Point } from "pixi.js";
22
import { EdgeId, ViewGraph } from "./graphs/viewgraph";
33
import { Device } from "./devices/index"; // Import the Device class
4-
import { deselectElement, selectElement } from "./viewportManager";
4+
import { deselectElement, selectElement, urManager } from "./viewportManager";
55
import { RightBar, StyledInfo } from "../graphics/right_bar";
66
import { Colors, ZIndexLevels } from "../utils";
77
import { Packet } from "./packet";
8+
import { RemoveEdgeMove } from "./undo-redo";
89
import { DeviceId } from "./graphs/datagraph";
910

1011
export interface EdgeEdges {
@@ -117,7 +118,16 @@ export class Edge extends Graphics {
117118
// Adds the delete button using addButton
118119
this.rightbar.addButton(
119120
"Delete Edge",
120-
() => this.delete(),
121+
() => {
122+
// obtener info
123+
const move = new RemoveEdgeMove({
124+
edgeId: this.id,
125+
connectedNodes: this.connectedNodes,
126+
});
127+
this.delete();
128+
urManager.push(move);
129+
// registrar movimiento
130+
},
121131
"right-bar-delete-button",
122132
);
123133
}
@@ -127,8 +137,9 @@ export class Edge extends Graphics {
127137
// Remove the edge from the viewgraph and datagraph
128138
deselectElement();
129139
this.viewgraph.removeEdge(this.id);
140+
this.destroy();
130141

131-
console.log(`Edge with ID ${this.id} deleted.`);
142+
console.log(`Edge ${this.id} deleted.`);
132143
}
133144

134145
public updatePosition(device1: Device, device2: Device) {
@@ -152,22 +163,4 @@ export class Edge extends Graphics {
152163

153164
this.drawEdge(newStartPos, newEndPos, Colors.Lightblue);
154165
}
155-
156-
public remove() {
157-
const { n1, n2 } = this.connectedNodes;
158-
const device1 = this.viewgraph.getDevice(n1);
159-
const device2 = this.viewgraph.getDevice(n2);
160-
161-
// Remove the connection from each connected device
162-
if (device1) device1.connections.delete(this.id);
163-
if (device2) device2.connections.delete(this.id);
164-
165-
// Remove the edge from the viewport
166-
this.viewgraph.getViewport().removeChild(this);
167-
this.destroy();
168-
169-
console.log(
170-
`Edge with ID ${this.id} removed between devices ${n1} and ${n2}.`,
171-
);
172-
}
173166
}

0 commit comments

Comments
 (0)