command model과 undo/redo
편집기는 실수를 되돌릴 수 있어야 합니다. move, resize, delete, group 같은 작업은 undo/redo history에 남습니다.
이때 command model을 사용하면 편집 동작을 명확히 기록할 수 있습니다.
command는 before/after를 안다
move command는 어떤 node가 어디에서 어디로 이동했는지 기록합니다.
const command = {
type: "move",
nodeIds: ["rect-1"],
before: [...],
after: [...]
};
undo는 before를 적용하고, redo는 after를 적용합니다.
type EditorCommand =
| { type: "moveNodes"; nodeIds: NodeId[]; before: NodePatch[]; after: NodePatch[] }
| { type: "resizeNode"; nodeId: NodeId; before: NodePatch; after: NodePatch }
| { type: "updateNode"; nodeId: NodeId; before: NodePatch; after: NodePatch }
| { type: "reorderChild"; parentId: NodeId; childId: NodeId; from: number; to: number };
interface HistoryState {
undoStack: EditorCommand[];
redoStack: EditorCommand[];
}
function applyCommand(document, command, direction = "redo") {
const patch = direction === "redo" ? command.after : command.before;
if (command.type === "moveNodes") {
return patch.reduce((nextDocument, item) => {
return updateNode(nextDocument, item.id, item.patch);
}, document);
}
if (command.type === "updateNode" || command.type === "resizeNode") {
return updateNode(document, command.nodeId, patch);
}
if (command.type === "reorderChild") {
const index = direction === "redo" ? command.to : command.from;
return reorderChild(document, command.parentId, command.childId, index);
}
return document;
}
drag 중 모든 move를 history에 넣지 않는다
pointermove는 매우 자주 발생합니다. 모든 중간 상태를 history에 넣으면 undo가 쓸 수 없게 됩니다.
pointerdown: begin
pointermove: live preview
pointerup: commit one command
사용자는 한 번 드래그한 동작을 undo 한 번으로 되돌리기를 기대합니다.
command는 renderer와 분리된다
command는 scene model을 바꿉니다. renderer buffer를 직접 수정하는 명령이 아닙니다.
command apply
-> scene model changes
-> renderer sees new state
이 구조가 있어야 WebGL/WebGPU renderer를 바꿔도 history는 그대로 유지됩니다.
오늘의 핵심
undo/redo는 편집 동작을 model change로 기록하는 시스템입니다.
command
before/after
apply/revert
history stack
renderer cache는 history의 대상이 아닙니다.