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

@@ -19,8 +19,9 @@ function FloorCreator() {
const { toolMode } = useToolMode();
const { activeLayer } = useActiveLayer();
const { socket } = useSocketStore();
const { floorStore } = useSceneContext();
const { addFloor, getFloorPointById, getFloorByPoints } = floorStore();
const { floorStore, undoRedo2DStore } = useSceneContext();
const { addFloor, getFloorPointById } = floorStore();
const { push2D } = undoRedo2DStore();
const drag = useRef(false);
const isLeftMouseDown = useRef(false);
const { selectedVersionStore } = useVersionContext();
@@ -103,6 +104,21 @@ function FloorCreator() {
};
addFloor(floor);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Line-Create',
point: {
type: 'Floor',
lineData: floor,
timeStamp: new Date().toISOString(),
}
}
],
})
if (projectId) {
// API
@@ -142,6 +158,21 @@ function FloorCreator() {
};
addFloor(floor);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Line-Create',
point: {
type: 'Floor',
lineData: floor,
timeStamp: new Date().toISOString(),
}
}
],
})
if (projectId) {
// API
@@ -191,6 +222,21 @@ function FloorCreator() {
};
addFloor(floor);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Line-Create',
point: {
type: 'Floor',
lineData: floor,
timeStamp: new Date().toISOString(),
}
}
],
})
if (projectId) {
// API
@@ -243,7 +289,7 @@ function FloorCreator() {
canvasElement.removeEventListener("click", onMouseClick);
canvasElement.removeEventListener("contextmenu", onContext);
};
}, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addFloor, getFloorPointById, getFloorByPoints, floorDepth, isBeveled, bevelStrength, sideMaterial, topMaterial, snappedPosition, snappedPoint]);
}, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addFloor, getFloorPointById, floorDepth, isBeveled, bevelStrength, sideMaterial, topMaterial, snappedPosition, snappedPoint]);
return (
<>

View File

@@ -30,10 +30,11 @@ function Line({ points }: Readonly<LineProps>) {
const [isDeletable, setIsDeletable] = useState(false);
const { socket } = useSocketStore();
const { toolMode } = useToolMode();
const { wallStore, floorStore, zoneStore } = useSceneContext();
const { wallStore, floorStore, zoneStore, undoRedo2DStore } = useSceneContext();
const { push2D } = undoRedo2DStore();
const { removeWallByPoints, setPosition: setWallPosition, getWallsByPointId } = wallStore();
const { removeFloorByPoints, setPosition: setFloorPosition, getFloorsByPointId } = floorStore();
const { removeZoneByPoints, setPosition: setZonePosition, getZonesByPointId } = zoneStore();
const { removeFloorByPoints, setPosition: setFloorPosition, getFloorsByPointId, getFloorsByPoints } = floorStore();
const { removeZoneByPoints, setPosition: setZonePosition, getZonesByPointId, getZonesByPoints } = zoneStore();
const { userId, organization } = getUserData();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
@@ -117,10 +118,26 @@ function Line({ points }: Readonly<LineProps>) {
}
socket.emit('v1:model-Wall:delete', data);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Line-Delete',
point: {
type: 'Wall',
lineData: removedWall,
timeStamp: new Date().toISOString(),
}
}
]
});
}
setHoveredLine(null);
}
if (points[0].pointType === 'Floor' && points[1].pointType === 'Floor') {
const Floors = getFloorsByPoints(points);
const { removedFloors, updatedFloors } = removeFloorByPoints(points);
if (removedFloors.length > 0) {
removedFloors.forEach(floor => {
@@ -143,6 +160,22 @@ function Line({ points }: Readonly<LineProps>) {
socket.emit('v1:model-Floor:delete', data);
}
});
const removedFloorsData = removedFloors.map((floor) => ({
type: "Floor" as const,
lineData: floor,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Delete',
points: removedFloorsData
}
]
});
}
if (updatedFloors.length > 0) {
updatedFloors.forEach(floor => {
@@ -165,11 +198,29 @@ function Line({ points }: Readonly<LineProps>) {
socket.emit('v1:model-Floor:add', data);
}
});
const updatedFloorsData = updatedFloors.map((floor) => ({
type: "Floor" as const,
lineData: Floors.find(f => f.floorUuid === floor.floorUuid) || floor,
newData: floor,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Update',
points: updatedFloorsData
}
]
});
}
setHoveredLine(null);
}
if (points[0].pointType === 'Zone' && points[1].pointType === 'Zone') {
const Zones = getZonesByPoints(points);
const { removedZones, updatedZones } = removeZoneByPoints(points);
if (removedZones.length > 0) {
removedZones.forEach(zone => {
@@ -192,6 +243,22 @@ function Line({ points }: Readonly<LineProps>) {
socket.emit('v1:zone:delete', data);
}
});
const removedZonesData = removedZones.map((zone) => ({
type: "Zone" as const,
lineData: zone,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Delete',
points: removedZonesData
}
]
});
}
if (updatedZones.length > 0) {
updatedZones.forEach(zone => {
@@ -214,7 +281,26 @@ function Line({ points }: Readonly<LineProps>) {
socket.emit('v1:zone:add', data);
}
});
const updatedZonesData = updatedZones.map((zone) => ({
type: "Zone" as const,
lineData: Zones.find(z => z.zoneUuid === zone.zoneUuid) || zone,
newData: zone,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Update',
points: updatedZonesData
}
]
});
}
setHoveredLine(null);
}
handleCanvasCursors('default');
}

View File

@@ -32,13 +32,14 @@ function Point({ point }: { readonly point: Point }) {
const [dragOffset, setDragOffset] = useState<THREE.Vector3 | null>(null);
const { socket } = useSocketStore();
const { toolMode } = useToolMode();
const { aisleStore, wallStore, floorStore, zoneStore } = useSceneContext();
const { aisleStore, wallStore, floorStore, zoneStore, undoRedo2DStore } = useSceneContext();
const { push2D } = undoRedo2DStore();
const { setPosition: setAislePosition, removePoint: removeAislePoint, getAislesByPointId } = aisleStore();
const { setPosition: setWallPosition, removePoint: removeWallPoint, getWallsByPointId } = wallStore();
const { setPosition: setFloorPosition, removePoint: removeFloorPoint, getFloorsByPointId } = floorStore();
const { setPosition: setZonePosition, removePoint: removeZonePoint, getZonesByPointId } = zoneStore();
const { snapAislePoint, snapAisleAngle, snapWallPoint, snapWallAngle, snapFloorPoint, snapFloorAngle, snapZonePoint, snapZoneAngle } = usePointSnapping({ uuid: point.pointUuid, pointType: point.pointType, position: point.position });
const { hoveredPoint,hoveredLine, setHoveredPoint } = useBuilderStore();
const { hoveredPoint, hoveredLine, setHoveredPoint } = useBuilderStore();
const { selectedPoints } = useSelectedPoints();
const { userId, organization } = getUserData();
const { selectedVersionStore } = useVersionContext();
@@ -47,6 +48,13 @@ function Point({ point }: { readonly point: Point }) {
const boxScale: [number, number, number] = Constants.pointConfig.boxScale;
const colors = getColor(point);
const [initialPositions, setInitialPositions] = useState<{
aisles?: Aisle[],
walls?: Wall[],
floors?: Floor[],
zones?: Zone[]
}>({});
useEffect(() => {
handleCanvasCursors('default');
}, [toolMode])
@@ -152,6 +160,20 @@ function Point({ point }: { readonly point: Point }) {
const currentPosition = new THREE.Vector3(...point.position);
const offset = new THREE.Vector3().subVectors(currentPosition, hit);
setDragOffset(offset);
if (point.pointType === 'Aisle') {
const aisles = getAislesByPointId(point.pointUuid);
setInitialPositions({ aisles });
} else if (point.pointType === 'Wall') {
const walls = getWallsByPointId(point.pointUuid);
setInitialPositions({ walls });
} else if (point.pointType === 'Floor') {
const floors = getFloorsByPointId(point.pointUuid);
setInitialPositions({ floors });
} else if (point.pointType === 'Zone') {
const zones = getZonesByPointId(point.pointUuid);
setInitialPositions({ zones });
}
}
};
@@ -159,6 +181,7 @@ function Point({ point }: { readonly point: Point }) {
handleCanvasCursors('default');
setDragOffset(null);
if (toolMode !== 'move') return;
if (point.pointType === 'Aisle') {
const updatedAisles = getAislesByPointId(point.pointUuid);
if (updatedAisles.length > 0 && projectId) {
@@ -180,6 +203,23 @@ function Point({ point }: { readonly point: Point }) {
type: updatedAisle.type
})
})
if (initialPositions.aisles && initialPositions.aisles.length > 0) {
const updatedPoints = initialPositions.aisles.map((aisle) => ({
type: "Aisle" as const,
lineData: aisle,
newData: updatedAisles.find(a => a.aisleUuid === aisle.aisleUuid),
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [{
actionType: 'Lines-Update',
points: updatedPoints,
}]
});
}
}
} else if (point.pointType === 'Wall') {
const updatedWalls = getWallsByPointId(point.pointUuid);
@@ -203,6 +243,23 @@ function Point({ point }: { readonly point: Point }) {
socket.emit('v1:model-Wall:add', data);
});
}
if (initialPositions.walls && initialPositions.walls.length > 0) {
const updatedPoints = initialPositions.walls.map((wall) => ({
type: "Wall" as const,
lineData: wall,
newData: updatedWalls.find(w => w.wallUuid === wall.wallUuid),
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [{
actionType: 'Lines-Update',
points: updatedPoints,
}]
});
}
} else if (point.pointType === 'Floor') {
const updatedFloors = getFloorsByPointId(point.pointUuid);
if (updatedFloors && updatedFloors.length > 0 && projectId) {
@@ -225,6 +282,23 @@ function Point({ point }: { readonly point: Point }) {
socket.emit('v1:model-Floor:add', data);
});
}
if (initialPositions.floors && initialPositions.floors.length > 0) {
const updatedPoints = initialPositions.floors.map((floor) => ({
type: "Floor" as const,
lineData: floor,
newData: updatedFloors.find(f => f.floorUuid === floor.floorUuid),
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [{
actionType: 'Lines-Update',
points: updatedPoints,
}]
});
}
} else if (point.pointType === 'Zone') {
const updatedZones = getZonesByPointId(point.pointUuid);
if (updatedZones && updatedZones.length > 0 && projectId) {
@@ -247,13 +321,33 @@ function Point({ point }: { readonly point: Point }) {
socket.emit('v1:zone:add', data);
});
}
if (initialPositions.zones && initialPositions.zones.length > 0) {
const updatedPoints = initialPositions.zones.map((zone) => ({
type: "Zone" as const,
lineData: zone,
newData: updatedZones.find(z => z.zoneUuid === zone.zoneUuid),
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [{
actionType: 'Lines-Update',
points: updatedPoints,
}]
});
}
}
setInitialPositions({});
}
const handlePointClick = (point: Point) => {
if (toolMode === '2D-Delete') {
if (point.pointType === 'Aisle') {
const removedAisles = removeAislePoint(point.pointUuid);
setHoveredPoint(null);
if (removedAisles.length > 0) {
removedAisles.forEach(aisle => {
if (projectId) {
@@ -273,9 +367,25 @@ function Point({ point }: { readonly point: Point }) {
}
socket.emit('v1:model-aisle:delete', data);
}
});
setHoveredPoint(null);
const removedAislesData = removedAisles.map((aisle) => ({
type: "Aisle" as const,
lineData: aisle,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Delete',
points: removedAislesData
}
]
});
}
}
if (point.pointType === 'Wall') {
@@ -302,9 +412,26 @@ function Point({ point }: { readonly point: Point }) {
socket.emit('v1:model-Wall:delete', data);
}
});
const removedWallsData = removedWalls.map((wall) => ({
type: "Wall" as const,
lineData: wall,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Delete',
points: removedWallsData
}
]
});
}
}
if (point.pointType === 'Floor') {
const Floors = getFloorsByPointId(point.pointUuid);
const { removedFloors, updatedFloors } = removeFloorPoint(point.pointUuid);
setHoveredPoint(null);
if (removedFloors.length > 0) {
@@ -328,6 +455,22 @@ function Point({ point }: { readonly point: Point }) {
socket.emit('v1:model-Floor:delete', data);
}
});
const removedFloorsData = removedFloors.map((floor) => ({
type: "Floor" as const,
lineData: floor,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Delete',
points: removedFloorsData
}
]
});
}
if (updatedFloors.length > 0) {
updatedFloors.forEach(floor => {
@@ -350,9 +493,27 @@ function Point({ point }: { readonly point: Point }) {
socket.emit('v1:model-Floor:add', data);
}
});
const updatedFloorsData = updatedFloors.map((floor) => ({
type: "Floor" as const,
lineData: Floors.find(f => f.floorUuid === floor.floorUuid) || floor,
newData: floor,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Update',
points: updatedFloorsData
}
]
});
}
}
if (point.pointType === 'Zone') {
const Zones = getZonesByPointId(point.pointUuid);
const { removedZones, updatedZones } = removeZonePoint(point.pointUuid);
setHoveredPoint(null);
if (removedZones.length > 0) {
@@ -376,6 +537,22 @@ function Point({ point }: { readonly point: Point }) {
socket.emit('v1:zone:delete', data);
}
});
const removedZonesData = removedZones.map((zone) => ({
type: "Zone" as const,
lineData: zone,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Delete',
points: removedZonesData
}
]
});
}
if (updatedZones.length > 0) {
updatedZones.forEach(zone => {
@@ -398,6 +575,23 @@ function Point({ point }: { readonly point: Point }) {
socket.emit('v1:zone:add', data);
}
});
const updatedZonesData = updatedZones.map((zone) => ({
type: "Zone" as const,
lineData: Zones.find(z => z.zoneUuid === zone.zoneUuid) || zone,
newData: zone,
timeStamp: new Date().toISOString(),
}));
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Update',
points: updatedZonesData
}
]
});
}
}
handleCanvasCursors('default');
@@ -422,7 +616,6 @@ function Point({ point }: { readonly point: Point }) {
return null;
}
return (
<>
{!isSelected ?
@@ -453,7 +646,7 @@ function Point({ point }: { readonly point: Point }) {
onPointerOut={() => {
if (hoveredPoint) {
setHoveredPoint(null);
if(!hoveredLine){
if (!hoveredLine) {
handleCanvasCursors('default');
}
}

View File

@@ -22,8 +22,9 @@ function WallCreator() {
const { toolMode } = useToolMode();
const { activeLayer } = useActiveLayer();
const { socket } = useSocketStore();
const { wallStore } = useSceneContext();
const { wallStore, undoRedo2DStore } = useSceneContext();
const { addWall, getWallPointById, removeWall, getWallByPoints } = wallStore();
const { push2D } = undoRedo2DStore();
const drag = useRef(false);
const isLeftMouseDown = useRef(false);
const { selectedVersionStore } = useVersionContext();
@@ -91,6 +92,7 @@ function WallCreator() {
const closestPoint = new THREE.Vector3().lerpVectors(point1Vec, point2Vec, t);
removeWall(wall.wallUuid);
if (projectId) {
// API
@@ -142,6 +144,7 @@ function WallCreator() {
wallHeight: wallHeight,
decals: []
}
addWall(wall2);
// API
@@ -171,8 +174,36 @@ function WallCreator() {
wallHeight: wallHeight,
decals: []
}
addWall(wall3);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Create',
points: [
{
type: 'Wall',
lineData: wall3,
timeStamp: new Date().toISOString(),
}, {
type: 'Wall',
lineData: wall2,
timeStamp: new Date().toISOString(),
}
]
}, {
actionType: 'Line-Delete',
point: {
type: 'Wall',
lineData: wall,
timeStamp: new Date().toISOString(),
}
}
],
})
// API
// if (projectId) {
@@ -202,7 +233,8 @@ function WallCreator() {
wallThickness: wallThickness,
wallHeight: wallHeight,
decals: []
};
}
addWall(wall1);
// API
@@ -232,6 +264,7 @@ function WallCreator() {
wallHeight: wallHeight,
decals: []
}
addWall(wall2);
// API
@@ -261,8 +294,40 @@ function WallCreator() {
wallHeight: wallHeight,
decals: []
}
addWall(wall3);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Lines-Create',
points: [
{
type: 'Wall',
lineData: wall3,
timeStamp: new Date().toISOString(),
}, {
type: 'Wall',
lineData: wall1,
timeStamp: new Date().toISOString(),
}, {
type: 'Wall',
lineData: wall2,
timeStamp: new Date().toISOString(),
}
]
}, {
actionType: 'Line-Delete',
point: {
type: 'Wall',
lineData: wall,
timeStamp: new Date().toISOString(),
}
}
],
})
// API
// if (projectId) {
@@ -328,9 +393,24 @@ function WallCreator() {
wallThickness: wallThickness,
wallHeight: wallHeight,
decals: []
};
}
addWall(wall);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Line-Create',
point: {
type: 'Wall',
lineData: wall,
timeStamp: new Date().toISOString(),
}
}
],
})
// API
// if (projectId) {

View File

@@ -19,8 +19,9 @@ function ZoneCreator() {
const { toolMode } = useToolMode();
const { activeLayer } = useActiveLayer();
const { socket } = useSocketStore();
const { zoneStore } = useSceneContext();
const { zones, addZone, getZonePointById, getZoneByPoints } = zoneStore();
const { zoneStore, undoRedo2DStore } = useSceneContext();
const { addZone, getZonePointById } = zoneStore();
const { push2D } = undoRedo2DStore();
const drag = useRef(false);
const isLeftMouseDown = useRef(false);
const { selectedVersionStore } = useVersionContext();
@@ -102,6 +103,21 @@ function ZoneCreator() {
};
addZone(zone);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Line-Create',
point: {
type: 'Zone',
lineData: zone,
timeStamp: new Date().toISOString(),
}
}
],
})
if (projectId) {
// API
@@ -139,6 +155,21 @@ function ZoneCreator() {
};
addZone(zone);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Line-Create',
point: {
type: 'Zone',
lineData: zone,
timeStamp: new Date().toISOString(),
}
}
],
})
if (projectId) {
// API
@@ -186,6 +217,21 @@ function ZoneCreator() {
};
addZone(zone);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Line-Create',
point: {
type: 'Zone',
lineData: zone,
timeStamp: new Date().toISOString(),
}
}
],
})
if (projectId) {
// API
@@ -238,7 +284,7 @@ function ZoneCreator() {
canvasElement.removeEventListener("click", onMouseClick);
canvasElement.removeEventListener("contextmenu", onContext);
};
}, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addZone, getZonePointById, getZoneByPoints, zoneColor, zoneHeight, snappedPosition, snappedPoint]);
}, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addZone, getZonePointById, zoneColor, zoneHeight, snappedPosition, snappedPoint]);
return (
<>