← Creative tools

A panoramic canvas is also a document system

The connection between six-face painting, animation identity, undo ownership, and late stylus samples in PanoPainter.

From the development archive

Retrospective technical note. No OmiComm post covers the original canvas architecture, so the date uses the scoped 8 August development-documentation record. September and October findings are identified as later checkpoints.

The visible purpose of PanoPainter is simple: give an artist a way to work inside a panoramic image instead of treating a 360-degree world as a distorted flat rectangle. The less visible part is a document system that has to preserve the artist’s work through layers, frames, history, import, export, and device input.

The first scoped architecture record used for this retrospective is the August codebase panorama. The repository itself has older commits going back to 2017, but an old commit date is not evidence that this particular explanation existed then. The technical story here follows the documented canvas architecture and later issue-level investigations.

Current Windows PanoPainter app showing its painting tools around a view into the Prague panorama.
Later application capture: Windows Direct3D 12, PanoPainter 0.2.3.1742, 29 October 2026. The panorama is “Prague” by Sergey Orekhov. This development-build screenshot is separate from the older architecture record and is not a store-release claim.

Six faces, one editable world

The documented canvas and layer representation uses six square faces. The panoramic export is derived from that representation. The codebase map records an equirectangular target of four face widths by two face heights, producing the intended 2:1 image when the source faces are square.

This distinction separates editing storage from delivery format. The artist experiences a view into the panorama. Export presents the result in a form another panoramic viewer can consume. Neither fact removes the need to define face dimensions, layer content, and frame ownership consistently inside the document.

It also explains why a resize is not merely changing two numbers. A layer frame can own several render targets. Replacing some successfully while another allocation fails can leave a mixed document unless the operation is staged and published as one transaction. A recoverable refusal should preserve the previous usable document rather than expose a partially resized one.

History must target identity, not position

Animation makes an apparently ordinary undo problem more subtle. The frame at position one is not necessarily the same frame after a reorder. A clear or import action that remembers only an ordinal can later restore pixels to the wrong neighbour.

The September review recorded that exact class of finding. A production stroke action already carried a stable frame identity, while older clear/import actions retained positional targets. The desired correction was to resolve the original frame by identity and refuse safely if it no longer existed. The audited tracker still held related verification work open in October, so the finding should not be described as universally resolved merely because a patch or test commit exists.

The general rule is useful beyond painting. A command refers to the thing the user edited, not whatever happens to occupy the same slot later. Reordering is a presentation operation. It must not silently redefine the target of a pending inverse operation.

Replaying an edit is not a fresh user edit

Undo and redo also have their own ownership. The review traced a panorama-import redo path that could re-enter the fresh-edit history route while consuming the redo stack. The important architectural distinction is between applying a command and recording a new command. Reapplication must not destroy the history container that is currently executing it.

This is why the acceptance record asks for complete cycles with distinguishable pixels, exact undo/redo counts, missing-source failure, and stable memory accounting. A success message or a screenshot after one import cannot establish that the inverse operations preserve the same document.

GPU completion adds another boundary. A CPU render task finishing does not automatically mean a readback has completed. The asynchronous-transfer milestone addresses history, saves, exports, thumbnails, and recording together because they all consume GPU-produced data. Its open acceptance should remain visible rather than being compressed into a claim that every document workflow is finished.

An input sample can change after arrival

Apple Pencil input provides a smaller version of the same identity problem. UIKit can initially deliver estimated touch properties and later revise them. Apple’s documentation describes using estimationUpdateIndex to correlate that revision with the original sample.

The September force-revision change preserved that identity through the input queue. A late callback updates the matching active-stroke sample once and rerenders the preview without adding another point, including the corresponding dual-brush contribution. Cancellation and committed publication close that correction window.

This is more precise than smoothing all pressure values after the fact. The correction belongs to a particular sample in a particular active stream. It must not attach to an unrelated stroke that happens to reuse a position or arrive later. The source review also keeps physical stylus qualification separate from automated and simulator evidence.

These systems meet at the same user expectation: a brush mark should remain the mark the artist intended. Projection supplies the immersive canvas, but identity, transactions, and ownership are what let the artist trust it while continuing to work.

References & further reading

Primary papers and documentation related to the techniques discussed here. Research references explain the background; the article describes implementation and validation at its dated checkpoint.

  1. Apple — Updating previously estimated touch properties
  2. Apple — Correlating revised touches with estimationUpdateIndex

Have a thought or a question?
I'd love to hear it. ↗