Build Undo for a Furniture Layout Editor with Plain JavaScript
Moving a sofa is easy to represent as a coordinate change. Making that change safely reversible takes a little more care. Roomture, an AI interior design and room-planning app, lists undo among the improvements in its 2
Moving a sofa is easy to represent as a coordinate change. Making that change safely reversible takes a little more care.
Roomture, an AI interior design and room-planning app, lists undo among the improvements in its 2.0.1 release notes. Here is a standalone JavaScript exercise inspired by that interaction. It demonstrates a general editor pattern, not Roomtureβs internal implementation.
Keep document history separate from the interface
The document should contain room and furniture data. A selected item, an open menu and the current zoom level can live in separate interface state. Undoing a furniture move should not unexpectedly restore an old menu.
For a small plain-data document, snapshots are a straightforward starting point:
const initialPlan = {
revision: 0,
items: [{ id: "sofa-1", x: 20, y: 30 }]
};
let past = [];
let present = structuredClone(initialPlan);
let future = [];
let derivedChecks = null;
let generation = 0;
const HISTORY_LIMIT = 50;
function invalidateChecks() {
generation += 1;
derivedChecks = null;
}
function moveItem(id, x, y) {
const item = present.items.find(item => item.id === id);
if (!item || !Number.isFinite(x) || !Number.isFinite(y)) return false;
if (item.x === x && item.y === y) return false;
const next = structuredClone(present);
Object.assign(next.items.find(item => item.id === id), { x, y });
next.revision += 1;
past = [...past, structuredClone(present)].slice(-HISTORY_LIMIT);
present = next;
future = [];
invalidateChecks();
return true;
}
function undo() {
if (!past.length) return false;
future.push(structuredClone(present));
present = past.pop();
invalidateChecks();
return true;
}
function redo() {
if (!future.length) return false;
past.push(structuredClone(present));
present = future.pop();
invalidateChecks();
return true;
}
structuredClone() copies supported values deeply. This example intentionally uses plain data; do not put functions or DOM elements in the plan document.
Commit one drag as one edit
Pointer movement can generate many intermediate positions. Keep those in a temporary preview, then call moveItem once when the gesture finishes. The expected result is one undo step per completed drag.
The early return for unchanged coordinates also matters. Clicking an object without moving it should not consume history.
Clear the abandoned branch
Suppose a user moves a sofa, undoes the move, then chooses a different position. The old redo branch no longer represents the current editing path. Clearing future on a new edit makes that rule explicit.
The history limit bounds the number of stored snapshots. Larger documents may justify patches or commands, but a snapshot model is easy to inspect while building an initial editor.
Recompute checks after every state change
Overlap or clearance messages belong to the current layout. After a move, undo or redo, discard old results and calculate again. If checks run asynchronously, capture generation when starting and discard the result if the counter has changed before completion. An undoable document revision is not sufficient for that check because a revision number may be revisited.
Useful acceptance cases include move β undo β redo; undo β new move β no redo; an unknown furniture ID; and a canceled drag that creates no history entry.
Reversible edits give people room to explore furniture arrangements. You can see the product context in Roomtureβs AI room planner, while adapting this small example to your own editor.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.