Repository navigation
Design cloud readiness
Status: assessed, nothing committed to. The application is desktop-only and this document does not change that. It exists because the question "could this also be a cloud/SaaS product" has a much more specific answer than it looks, and that answer will otherwise be re-derived from scratch by whoever asks next.
Cloud has never been considered in this repository, and that is worth stating
plainly rather than discovering. The word appears three times in the whole
tree and two are dismissals: ROADMAP.md:742 [?] Cloud libraries, :743
[?] Team asset sharing, and character-reference-gap-analysis.md:374, which
lists "Works offline (no cloud dependency)" as a strength. So there is no
prior decision to reconcile with and nothing to undo.
Nothing below Lightbox.App needs restructuring; the significant change is
that the application's brain lives in a view model.
That is a better position than it sounds. The parts that are usually hardest to
move — the document model, the pixel path, determinism — are already clean. The
part that is hard is concentrated in one file, and it is
docs/DESIGN-mainviewmodel-decomposition.md.
Every item here was verified rather than inferred.
-
Lightbox.Corehas zeroPackageReferenceand zeroProjectReference. It is display-free by construction rather than by convention, and a grep for Skia or Avalonia across it returns one hit, a comment inGeometry/TileGrid.cssaying that is why the type lives there. 706 tests in 838 ms. -
Lightbox.Rasteris CPU Skia with noGRContextanywhere insrc/, and already shipsSkiaSharp.NativeAssets.Linux. Its 457 tests run green headless in a Linux container with no display and no GPU. Server-side rendering is not a port; it is a deployment. -
Lightbox.Importtouches the filesystem nowhere at all —byte[]in, objects out. It is the shape the rest of the codebase should copy. - Invariant 1 makes the document a replayable command log. A frame is a list of strokes and the pixels are derived, so a document is 45.9 KB for 20 strokes. That is the data model a sync engine wants, arrived at for unrelated reasons.
-
Invariants 2 and 7, plus
RuntimeDeterminismTests, give bit-identical renders across runtimes. This is the single most valuable cloud property in the codebase and almost no painting application has it: a client and a server rendering the same record agree exactly, so pixels never have to cross the wire. Every plausible architecture below leans on it. -
An RPC boundary already exists.
IpcProtocol/IpcServer/PipeBridgespeak one JSON line per request and one per response,oppluspayload, a fresh connection per call and no session state. Pipe to HTTP is a transport swap rather than a redesign. -
.lbprojis a folder of plain JSON, chosen deliberately over an archive (Projects/ProjectIo.cs:7), which maps onto object storage without a format change. -
IVersionHistoryStoreandVersionHistoryManageralready sit in Core, are covered byVersioningTests, and are referenced by nothing insrc/— the only implementer is a test mock. A storage-agnostic version-history seam is sitting there waiting for a backend.
1 — The session is a view model. ViewModels/MainViewModel.cs is 10,098
lines and is simultaneously the document API, the tool state machine, the render
scheduler and the binding surface, with no interface between those four roles.
Mitigating, and it changes the estimate considerably: the whole file contains
three Avalonia touchpoints, and its base type is CommunityToolkit
ObservableObject, which is UI-framework-agnostic. It is movable. It is not
small. See DESIGN-mainviewmodel-decomposition.md.
2 — Six process-global registries in Lightbox.Raster. BrushTipRegistry,
ClipRegionRegistry, PaletteRegistry, SymbolRegistry, TextureRegistry and
ReferenceStripRegistry are static dictionaries keyed by asset id with no
document, session or tenant dimension and no eviction —
TextureRegistry.cs:105 says so outright: "Tests only — the app registers and
never unregisters." The severity differs by registry and the difference
matters:
- Clip regions are genuinely content-hashed —
MainViewModel.cs:4047builds the id from a SHA-256 of the serialized region — so equal ids mean equal content and sharing them is harmless. Invariant 3 holds. -
PaletteRegistryandSymbolRegistryoverwrite on register, by design, because a swatch is meant to be edited and the new value must win. Under one user that is the feature. Under two tenants in one process, a shared id repaints somebody else's art.
Also Media/FluidLattice.cs:274 is a [ThreadStatic] cache — fine pinned to a
UI thread, a lattice retained per pool thread on a server.
3 — Ids are not globally unique. Documents/Ids.cs mints
prefix_unixMillisHex_counterHex from a process-local counter that starts at
zero, so swatch_<ts>_1 is the first swatch of every process. This is already
a mild hazard when opening another artist's document; it is not safe as a
multi-tenant key.
4 — Around ten user-scoped stores are static and anchored to one directory.
AiSettings, ExportPresetStore, TimingPresetStore, SymbolLibrary,
TipStore, BrushPresets, ShortcutMap, AppSettings, WorkspaceStore and
AutosaveService all resolve under %APPDATA%/Lightbox, most of them off
ApiKeyProvider.SettingsPath. Static means one user per process. The
encouraging half: every one already has a PathOverride test seam, and
Docking/WorkspaceStore.cs:184 already did it per-instance and wrote down
why — "writing to a global path anyway is how one test's saved workspace
became the next test's starting layout." That comment is the template for the
whole migration.
5 — Core reads the disk directly. DocJson.Save and DocJson.Load take a
path, and no Stream overload exists anywhere in Core; ProjectIo has roughly
thirty filesystem call sites. Cheap to fix, because DocJson.Serialize and
Deserialize are already string-pure — the seam is additive.
6 — Undo is lambdas. DocumentEditor keeps a SnapshotStep holding a whole
Doc clone and a DeltaStep holding a pair of Action<Doc> delegates. A
closure has no data form, so the stroke commit — deliberately a delta, because
snapshotting per pen lift caused a visible pause — is precisely the step that
cannot be written down, sent, or made a collaborative operation. This is already
ROADMAP.md:696 and is justified on local grounds alone: a 64-deep stack is
2.8 MB for a 45.9 KB document.
7 — Latency. The charter budgets a pointer event during a 4K stroke at 20 ms and a whole stroke plus commit at 400 ms. No network hop fits inside the first number. Determinism is what rescues this — keep rendering local and sync the record — and it is why the browser-editor shape below is the expensive one.
8 — Some features are desktop-bound and do not travel. Pillar 5 writes into
the artist's own Unity/Godot/Unreal project directory; the planned version
control shells out to a locally installed p4/cm/git and explicitly must
not own the workspace root; FileReveal, the StorageProvider pickers and
ProjectWatcher's FileSystemWatcher all assume a local disk.
9 — The security posture inverts. Services/IpcServer.cs:10 states the
current model: "The pipe is per-user by OS default — nothing is exposed on the
network." Separately, and worth fixing regardless: ProjectIo.ResolveInProject
performs a proper root-containment check because a manifest is plain JSON a
person or an agent can edit, but Project.PathOf (Projects/Project.cs:118),
LoadDocument and Save join root and manifest path unchecked. A hostile
project.json escapes the root. Locally that needs a hand-edited file; the
moment a manifest is tenant-supplied it is a vulnerability.
10 — Credentials are plaintext. %APPDATA%/Lightbox/ai.json holds the API
key verbatim — no DPAPI, no keychain.
| Shape | Fit | The work it needs |
|---|---|---|
| Headless render / export / AI service beside the desktop app | Good, essentially today | Obstacles 3 and 2 — or sidestep both with one process per job, which is a legitimate answer at low volume |
| Local-first with cloud sync — desktop stays the editor, the server holds projects, versions, comments, review, shared libraries | Good, moderate | 6 as the keystone, then 5, 4, 3, 2, plus a sync and conflict model that does not exist yet |
| Browser-based editor | Poor | 1, plus an entire new front end, against 7 and against per-tenant memory — one 8K frame is 380 MB against a 512 MB cache |
The third is a second product rather than a port, and should be costed as one.
Where this lands: do not build cloud yet. Build obstacles 2 through 6. Every one of them pays for itself on the desktop — 6 removes a 64× memory multiplier, 4 removes a class of test-isolation bug the codebase has already been bitten by, 3 removes an id collision when opening someone else's document, 2 removes a cross-document overwrite that exists today with two tabs open. Doing them leaves the codebase one transport swap away from the second shape, without having bet on it.
Settled — AI billing: both, and the user's key wins. Bring-your-own-key
stays exactly as it is, and server-held keys become the fallback for hosted
users. The reason is that AiConnection.Value already resolves stored value,
then environment variable, then default — the precedence chain has the right
shape to extend with one more tier, and it preserves the local-Ollama and
custom-MCP-agent stories that a server-keys-only model would delete outright.
Settled — assess all three shapes, commit to none. This document is the assessment; the choice is deferred deliberately.
Open — does "every feature is reachable" survive plan tiers? The rule at
CLAUDE.md:41 and ROADMAP.md:909 forbids gating capability behind a value in
a manifest. A SaaS plan tier gates capability behind a value in an account,
which is the same move wearing a different noun. Three ways out: hold the rule
and monetise hosting, collaboration and AI credits rather than features; make it
a desktop-only invariant and amend the charter explicitly rather than by quiet
exception; or keep everything reachable and meter the expensive operations. Note
before choosing that the unbuilt FeatureDefaults registry (ROADMAP.md:952)
is the mechanism all three answers would use, and is currently designed pointing
the opposite way.
- The Collaboration subsection is entirely unbuilt, and it is exactly the set of things a server would serve: comments, review mode, version comparison, change history, asset locking, cloud libraries, team asset sharing.
-
ROADMAP.md:1014names "Real-time collaboration (Figma has set expectations; animation tools lag)" as a competitive gap, with no follow-up item anywhere. -
The existing multi-user answer is deliberately not cloud. Version control
is planned as file locks through a local Perforce/UVCS/git client, on the
reasoning at
ROADMAP.md:542that "locking is the feature, not history". That answer is not invalidated by anything here, and for a studio on a LAN it remains the cheaper one. -
DESIGN-ai-payload.md:72rejects GraphQL partly because "there is no schema of theirs to query and no server of ours in the path" — the nearest the repository comes to contemplating a server, and it dismisses one. - One number in that document is conditional on staying desktop, and should not
be carried across unexamined: "768 KB on a 20 Mbit link is about 0.3 s
against 30–120 s of generation" (
:59). That holds because the only thing crossing the wire is a model request. Put the canvas on a server and the same bytes land inside a 20 ms budget instead of a 30-second one, and every conclusion in that document needs re-deriving.
- Getting started
- Documents and projects
- Tools and strokes
- Brushes
- Colour
- Layers, selections and guides
- The timeline
- Onion skin, references and the camera
- Symbols
- Saving and recovery
- Exporting to a game engine
- AI assistance
- Keyboard, performance and what is planned
- Bones and rigging
- Effects — fire, smoke and water
- A simplified 3D space to draw in: planes, parallax first, orbit second
- Brush performance beyond 4k: what the GPU can and cannot buy us
- The add-on boundary
- Making the AI trustworthy
- What an AI request actually costs
- Bones and spline deformers for 2D — design
- Brush tips — a generated library, not a live calculation
- Cloud and SaaS: what the architecture already fits, and what it does not
- Colour mixing — paint that picks up what it is laid on
- DESIGN: Effects
- DESIGN: Fluid effects — fire, smoke and water as drawn line and fill
- Fluid and viscous media — notes for the next brush-engine pass
- Compositing on the GPU
- Decomposing the two big files: which pieces come out, and with which tool
- One memory limit for pictures
- .NET 10: what is safe, what is unverified, and what it narrows
- One picture cache: the paused canvas on tiles
- The paint path: one mark, made once, under the pen
- Pen dynamics: tilt and speed
- A pencil is graphite on the paper's tooth
- Performance: the ratchet and the map
- Playback's first loop
- Pressure curves — a shape, not an exponent
- Importing Procreate brushes
- A project as a game's asset set
- One hierarchy: folders scope everything
- Puppet pins — design and plan
- The raster checkpoint: making a painting cheap to reopen
- Scoped resources — palettes, references and everything else shared
- Sensitive topics, and the three-track flow
- Design: pose-assisted frame-by-frame animation (rigid bones + AI gap filling)
- Stroke groups: why a merge should never turn strokes into pixels
- The project window: what you do between drawings
- Reading the subject before drawing on it
- The ceiling of a swept soft mark (B349)
- Q171 — a symbol owns a layer stack
- Pillar 3 — reusable assets, as live symbols
- What a template is
- Text, and where fonts sit
- The tiled compositor, and getting it off the CPU
- Design tokens
- Transform tool design (M14)
- Why a brush stamps twice, and when it does not
- Migrating to the designed UI
- Undo stores only what changed
- Vector tooling: making the lines you already drew editable
- The smudge/blur family, and aligning brush settings with Photoshop
Generated from the repository. Edit the files under docs/, not these pages.