PixelWeaver v0.5.0 /server.src.pixelweaver.undo_history
On this page

Undo and redo that actually revert authoritative pixel and role buffers by restoring the before and after snapshots recorded on each covered command entry.

#server.src.pixelweaver.undo_history

#server.src.pixelweaver.undo_history

Undo/redo revert for authoritative state.

Defect 3: undo/redo popped/pushed the command history and redo stack but never reverted the authoritative pixel/role buffers -- pixel_data was byte-identical across an undo.

Mechanism: when a covered pixel/role command is applied (on either the WebSocket or MCP path) it records a small _undo snapshot on the command entry capturing the affected layer's target-frame buffer BEFORE and AFTER the mutation. Undo restores the BEFORE image; redo restores the AFTER image. Because the whole record (base64-encoded buffers) rides the command entry, it survives the JSON round-trip between the collab server and the MCP process, so both undo paths revert identically without re-executing the command (which would be fragile across the differing client/MCP command schemas).

The record is intentionally targeted (one layer buffer, not the whole project) to bound the size of the persisted history. Structural commands (layer/frame/ resize) and uncovered command types carry no _undo record, so undo pops them from history without a buffer revert -- reverting those is future work.

#make_pixel_undo_record

python
def make_pixel_undo_record(canvas_name: str, kind: str, layer_id: str, frame_index: int, before: bytes | None, after: bytes | None) -> dict[str, Any]

Build a JSON-serializable before/after buffer snapshot record.

kind is "rgba" (pixel_data) or "role" (role_data). before/after are the target-frame layer buffers (None = the layer had no buffer in that plane, which undo/redo restores by deleting the key).

#snapshot_canvas_buffers

python
def snapshot_canvas_buffers(canvas) -> dict[str, Any]

Capture a canvas's full dimension + buffer state for a dimension undo.

Dimension ops (rotate/scale/crop/resize) rewrite every frame's buffers and the canvas size, so a targeted per-layer record cannot revert them. This stores the whole state by necessity (bounded to one canvas).

#make_dimension_undo_record

python
def make_dimension_undo_record(canvas_name: str, before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]

Build a full before/after dimension snapshot record for one canvas.

before/after come from :func:snapshot_canvas_buffers.

#serialize_canvas

python
def serialize_canvas(canvas) -> dict[str, Any]

Capture a WHOLE canvas (metadata + layer tree + every frame buffer).

Unlike :func:snapshot_canvas_buffers (dims + buffers only) this is a full, self-contained description from which :func:_deserialize_canvas can rebuild an identical CanvasState -- used by the project-level canvases_added record so a redo re-creates the exact canvases an undo removed.

#_deserialize_canvas

python
def _deserialize_canvas(content: dict[str, Any])

Rebuild a CanvasState from a :func:serialize_canvas snapshot.

#make_canvases_added_record

python
def make_canvases_added_record(canvases: list[dict[str, Any]]) -> dict[str, Any]

Build a project-level canvases_added record for a multi-canvas add.

canvases is a list of serialize_canvas snapshots (in creation order). Undo removes each named canvas from the project; redo re-adds them with the exact captured content. The first project-scoped structural undo record -- every other record family is canvas-scoped.

#apply_undo_record

python
def apply_undo_record(project: ProjectState, record: dict[str, Any], *, redo: bool) -> bool

Restore the BEFORE (undo) or AFTER (redo) state from a record.

Two record families share this entry point: - pixel/role buffer snapshots (kind == "rgba"/"role"), the original targeted before/after image revert; - structural records (an op key), which revert/re-apply a layer or frame structure change produced by structural_ops.

Returns True if state was restored, False if the target canvas/frame no longer resolves (a best-effort no-op rather than a crash).

#_apply_structural_record

python
def _apply_structural_record(project: ProjectState, record: dict[str, Any], *, redo: bool) -> bool

Revert (undo) or re-apply (redo) a structural op record.

#_apply_compound

python
def _apply_compound(project: ProjectState, record: dict[str, Any], *, redo: bool) -> bool

Revert (undo) or re-apply (redo) a compound record.

A compound wraps an ordered list of sub-records (each a normal record type) applied together as one undoable step. Redo re-applies them in forward order; undo reverts them in REVERSE order (so the state unwinds exactly as it was built). Each sub-record is dispatched through apply_undo_record, so any record family (meta_snapshot, pixel/role, dimension, ...) can nest. This is the reusable multi-part record: future combined ops get undo for free.

#_apply_canvases_added

python
def _apply_canvases_added(project: ProjectState, record: dict[str, Any], *, redo: bool) -> bool

Revert (undo) or re-apply (redo) a multi-canvas add.

Undo removes each named canvas from the project; redo rebuilds each canvas from its captured content and re-inserts it. Project-scoped (the record has no single canvas target), so it is dispatched before the canvas lookup.

#_apply_dimension_record

python
def _apply_dimension_record(project: ProjectState, record: dict[str, Any], *, redo: bool) -> bool

Revert (undo) or re-apply (redo) a whole-canvas dimension snapshot.

Restores the canvas dimensions and rebuilds every frame's pixel/role buffers from the captured base64 images. Frames are matched by id; a frame whose id is absent from the snapshot is left untouched (best-effort).

#_apply_meta_snapshot

python
def _apply_meta_snapshot(project: ProjectState, record: dict[str, Any], *, redo: bool) -> bool

Revert (undo) or re-apply (redo) a project/canvas metadata snapshot.

Restores each captured attribute by raw setattr (the value was already validated when it was first written, so no re-validation is needed). The sentinel target "__project__" addresses project-level attributes; any other target is a canvas name. A missing canvas target is a best-effort no-op for that change rather than a crash.

Search