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:
@@ -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) => {
|
||||
|
||||
78
app/src/store/builder/useUndoRedo2DStore.ts
Normal file
78
app/src/store/builder/useUndoRedo2DStore.ts
Normal 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>;
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user