feat: Implement undo and redo functionality for 2D scene controls

- Added useRedoHandler to manage redo actions, including socket communication for wall, floor, zone, and aisle updates.
- Added useUndoHandler to manage undo actions, reversing the effects of previous actions with corresponding socket updates.
- Created UndoRedo2DControls component to handle keyboard shortcuts for undo (Ctrl+Z) and redo (Ctrl+Y).
- Established a Zustand store (useUndoRedo2DStore) to maintain undo and redo stacks, with methods for pushing, popping, and peeking actions.
This commit is contained in:
2025-07-29 17:20:34 +05:30
parent 253b3db2ed
commit fcd924eb31
15 changed files with 1701 additions and 301 deletions

View File

@@ -28,7 +28,7 @@ interface FloorStore {
getFloorById: (uuid: string) => Floor | undefined;
getFloorsByPointId: (uuid: string) => Floor[] | [];
getFloorByPoints: (points: Point[]) => Floor | undefined;
getFloorsByPoints: (points: [Point, Point]) => Floor[] | [];
getFloorPointById: (uuid: string) => Point | undefined;
getConnectedPoints: (uuid: string) => Point[];
}
@@ -74,10 +74,13 @@ export const createFloorStore = () => {
const updatedFloors: Floor[] = [];
set(state => {
const newFloors: Floor[] = [];
for (const floor of state.floors) {
const pointIndex = floor.points.findIndex(p => p.pointUuid === pointUuid);
if (pointIndex === -1) {
updatedFloors.push(JSON.parse(JSON.stringify(floor)));
newFloors.push(floor);
continue;
}
@@ -87,11 +90,13 @@ export const createFloorStore = () => {
removedFloors.push(JSON.parse(JSON.stringify(floor)));
continue;
}
floor.points = remainingPoints;
updatedFloors.push(JSON.parse(JSON.stringify(floor)));
const updatedFloor = { ...floor, points: remainingPoints };
updatedFloors.push(JSON.parse(JSON.stringify(updatedFloor)));
newFloors.push(updatedFloor);
}
state.floors = updatedFloors;
state.floors = newFloors;
});
return { removedFloors, updatedFloors };
@@ -102,6 +107,7 @@ export const createFloorStore = () => {
const updatedFloors: Floor[] = [];
set(state => {
const newFloors: Floor[] = [];
for (const floor of state.floors) {
const indices = floor.points.map((p, i) => ({ uuid: p.pointUuid, index: i }));
@@ -110,7 +116,7 @@ export const createFloorStore = () => {
const idxB = indices.find(i => i.uuid === pointB.pointUuid)?.index ?? -1;
if (idxA === -1 || idxB === -1) {
updatedFloors.push(JSON.parse(JSON.stringify(floor)));
newFloors.push(floor);
continue;
}
@@ -120,7 +126,7 @@ export const createFloorStore = () => {
(idxB === 0 && idxA === floor.points.length - 1);
if (!areAdjacent) {
updatedFloors.push(JSON.parse(JSON.stringify(floor)));
newFloors.push(floor);
continue;
}
@@ -129,14 +135,15 @@ export const createFloorStore = () => {
);
if (remainingPoints.length > 2) {
floor.points = remainingPoints;
updatedFloors.push(JSON.parse(JSON.stringify(floor)));
const updatedFloor = { ...floor, points: remainingPoints };
updatedFloors.push(JSON.parse(JSON.stringify(updatedFloor)));
newFloors.push(updatedFloor);
} else {
removedFloors.push(JSON.parse(JSON.stringify(floor)));
}
}
state.floors = updatedFloors;
state.floors = newFloors;
});
return { removedFloors, updatedFloors };
@@ -253,12 +260,32 @@ export const createFloorStore = () => {
});
},
getFloorByPoints: (points) => {
return get().floors.find(floor => {
const floorPointIds = new Set(floor.points.map(p => p.pointUuid));
const givenPointIds = new Set(points.map(p => p.pointUuid));
return floorPointIds.size === givenPointIds.size && [...floorPointIds].every(id => givenPointIds.has(id));
});
getFloorsByPoints: ([pointA, pointB]) => {
const Floors: Floor[] = [];
for (const floor of get().floors) {
const indices = floor.points.map((p, i) => ({ uuid: p.pointUuid, index: i }));
const idxA = indices.find(i => i.uuid === pointA.pointUuid)?.index ?? -1;
const idxB = indices.find(i => i.uuid === pointB.pointUuid)?.index ?? -1;
if (idxA === -1 || idxB === -1) {
continue;
}
const areAdjacent =
Math.abs(idxA - idxB) === 1 ||
(idxA === 0 && idxB === floor.points.length - 1) ||
(idxB === 0 && idxA === floor.points.length - 1);
if (!areAdjacent) {
continue;
}
Floors.push(JSON.parse(JSON.stringify(floor)));
}
return Floors;
},
getFloorPointById: (pointUuid) => {

View File

@@ -0,0 +1,78 @@
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { undoRedoConfig } from '../../types/world/worldConstants';
type UndoRedo2DStore = {
undoStack: UndoRedo2DTypes[];
redoStack: UndoRedo2DTypes[];
push2D: (entry: UndoRedo2DTypes) => void;
undo2D: () => UndoRedo2DTypes | undefined;
redo2D: () => UndoRedo2DTypes | undefined;
clearUndoRedo2D: () => void;
peekUndo2D: () => UndoRedo2DTypes | undefined;
peekRedo2D: () => UndoRedo2DTypes | undefined;
};
export const createUndoRedo2DStore = () => {
return create<UndoRedo2DStore>()(
immer((set, get) => ({
undoStack: [],
redoStack: [],
push2D: (entry) => {
set((state) => {
state.undoStack.push(entry);
if (state.undoStack.length > undoRedoConfig.undoRedoCount) {
state.undoStack.shift();
}
state.redoStack = [];
});
},
undo2D: () => {
let lastAction: UndoRedo2DTypes | undefined;
set((state) => {
lastAction = state.undoStack.pop();
if (lastAction) {
state.redoStack.unshift(lastAction);
}
});
return lastAction;
},
redo2D: () => {
let redoAction: UndoRedo2DTypes | undefined;
set((state) => {
redoAction = state.redoStack.shift();
if (redoAction) {
state.undoStack.push(redoAction);
}
});
return redoAction;
},
clearUndoRedo2D: () => {
set((state) => {
state.undoStack = [];
state.redoStack = [];
});
},
peekUndo2D: () => {
const stack = get().undoStack;
return stack.length > 0 ? stack[stack.length - 1] : undefined;
},
peekRedo2D: () => {
const stack = get().redoStack;
return stack.length > 0 ? stack[0] : undefined;
},
}))
)
}
export type UndoRedo2DStoreType = ReturnType<typeof createUndoRedo2DStore>;

View File

@@ -21,7 +21,7 @@ interface ZoneStore {
getZoneById: (uuid: string) => Zone | undefined;
getZonesByPointId: (uuid: string) => Zone[] | [];
getZoneByPoints: (points: Point[]) => Zone | undefined;
getZonesByPoints: (points: Point[]) => Zone[] | [];
getZonePointById: (uuid: string) => Point | undefined;
getConnectedPoints: (uuid: string) => Point[];
}
@@ -76,10 +76,13 @@ export const createZoneStore = () => {
const updatedZones: Zone[] = [];
set(state => {
const newZones: Zone[] = [];
for (const zone of state.zones) {
const pointIndex = zone.points.findIndex(p => p.pointUuid === pointUuid);
if (pointIndex === -1) {
updatedZones.push(JSON.parse(JSON.stringify(zone)));
newZones.push(zone);
continue;
}
@@ -89,11 +92,13 @@ export const createZoneStore = () => {
removedZones.push(JSON.parse(JSON.stringify(zone)));
continue;
}
zone.points = remainingPoints;
updatedZones.push(JSON.parse(JSON.stringify(zone)));
const updatedZone = { ...zone, points: remainingPoints };
updatedZones.push(JSON.parse(JSON.stringify(updatedZone)));
newZones.push(updatedZone);
}
state.zones = updatedZones;
state.zones = newZones;
});
return { removedZones, updatedZones };
@@ -104,6 +109,7 @@ export const createZoneStore = () => {
const updatedZones: Zone[] = [];
set(state => {
const newZones: Zone[] = [];
for (const zone of state.zones) {
const indices = zone.points.map((p, i) => ({ uuid: p.pointUuid, index: i }));
@@ -112,7 +118,7 @@ export const createZoneStore = () => {
const idxB = indices.find(i => i.uuid === pointB.pointUuid)?.index ?? -1;
if (idxA === -1 || idxB === -1) {
updatedZones.push(JSON.parse(JSON.stringify(zone)));
newZones.push(zone);
continue;
}
@@ -122,7 +128,7 @@ export const createZoneStore = () => {
(idxB === 0 && idxA === zone.points.length - 1);
if (!areAdjacent) {
updatedZones.push(JSON.parse(JSON.stringify(zone)));
newZones.push(zone);
continue;
}
@@ -131,14 +137,15 @@ export const createZoneStore = () => {
);
if (remainingPoints.length > 2) {
zone.points = remainingPoints;
updatedZones.push(JSON.parse(JSON.stringify(zone)));
const updatedZone = { ...zone, points: remainingPoints };
updatedZones.push(JSON.parse(JSON.stringify(updatedZone)));
newZones.push(updatedZone);
} else {
removedZones.push(JSON.parse(JSON.stringify(zone)));
}
}
state.zones = updatedZones;
state.zones = newZones;
});
return { removedZones, updatedZones };
@@ -180,12 +187,32 @@ export const createZoneStore = () => {
});
},
getZoneByPoints: (points) => {
return get().zones.find(zone => {
const zonePointIds = new Set(zone.points.map(p => p.pointUuid));
const givenPointIds = new Set(points.map(p => p.pointUuid));
return zonePointIds.size === givenPointIds.size && [...zonePointIds].every(id => givenPointIds.has(id));
});
getZonesByPoints: ([pointA, pointB]) => {
const Zones: Zone[] = [];
for (const zone of get().zones) {
const indices = zone.points.map((p, i) => ({ uuid: p.pointUuid, index: i }));
const idxA = indices.find(i => i.uuid === pointA.pointUuid)?.index ?? -1;
const idxB = indices.find(i => i.uuid === pointB.pointUuid)?.index ?? -1;
if (idxA === -1 || idxB === -1) {
continue;
}
const areAdjacent =
Math.abs(idxA - idxB) === 1 ||
(idxA === 0 && idxB === zone.points.length - 1) ||
(idxB === 0 && idxA === zone.points.length - 1);
if (!areAdjacent) {
continue;
}
Zones.push(JSON.parse(JSON.stringify(zone)));
}
return Zones;
},
getZonePointById: (pointUuid) => {