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:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user