Skip to content

Resolver Engine — Design Brief

Status: Design brief

A technical brief of the resolved architecture. Each Feature page owns its own open questions; a slice runs through the SDD chain on its own when it's ready to build.

Created 22/07/2026
Linear ticket none yet — created once this work starts

Scope

Three systems bundled into one delivery:

  • The resolve → measure → solve → animate → place pipeline and layout system
  • Price / background / group-flow
  • Keyframe animation

No automated schema migration exists, so this ships as a permanent, explicit two-pipeline fork gated on schemaVersion rather than a live transformer — see Schema-Version Gating for why and how.

What's built today

Only Text Foundation's core reshape and its padding fix are built and tested today, on feature/neo-1253-retail-studio-text-engine — the only pre-existing code this plan treats as a trusted foundation; see that page for what "BUILT" means concretely. pin is reserved in the schema but not yet implemented; price, background, group-flow, point/box collapse, and keyframes are all new work.

Why this exists

Every non-trivial element in a Retail Studio template today is a bespoke React component — a betta-price-box, a freshmart-save-box — written by us, per client, per shape. That is a production-house model: a client can't add an element without an engineer. The goal this brief serves is a client who onboards themselves and builds their own elements, which means an element has to be expressible in the template JSON alone. Pinning, flow, content-sized text, a real animation model, and one motion graph underneath them are exactly the primitives that need to exist for that to be true. Without them the only way to make a new element is React, and we don't want that for a long list of reasons.

A document that fully describes its own layout and motion is also something a model can read and write. That's a later conversation, but it's the same door.

See the discount lockup for what an element built this way looks like as JSON.

Summary

We build a render-time resolve → measure → solve → animate → place pipeline in the Retail Studio compositions renderer, and reshape the template schema once to support it.

Text is measured with canvas measureText (font-file-derived, so Firefox and Chromium agree); all boxes, line breaks, baselines, and cap-alignment are computed in our own JavaScript; each text run is emitted as an absolutely-positioned span. The browser is reduced to a glyph rasteriser and never lays text out itself.

On top of this foundation — each one a full Feature:

  • A dedicated padding-model correction and a unified point/box text model, collapsing today's two hardcoded text layer types into one (Text Foundation)
  • Relative-to-another-layer pins, superseding and retiring attachTo, plus flat group flow (Positioning)
  • Cue objects as a time-addressing primitive, populated only with auto-derived scene edges for now — animations built to reference them from the start, so the full retiming system doesn't require rewriting templates later (Cues)
  • Price as a mode of text (Price)
  • A spanning background primitive (Background)
  • A generalized keyframe/cue animation system (Keyframe Animation)

The validated schema only ever admits capabilities whose renderer exists, so the one breaking reshape happens up front and every later capability is an additive union variant that ships with its renderer.

Ships behind schemaVersion: 2

All of it ships behind schemaVersion: 2, gated permanently against the current, unmodified rendering pipeline preserved for schemaVersion: 1, with no automated migration between them this round — see Schema-Version Gating for why and how.

The pipeline, at a glance

flowchart LR
    A["Resolve<br/>content → styled runs<br/>relative typography → absolute<br/>cues → frame numbers"] --> C["Solve<br/>frame-invariant resting box per layer<br/>absolute / flow / pin / derived<br/>in topological order"]
    C <-. "measure(runs, constraint)" .-> B["Measure<br/>canvas measureText<br/>ink extents, line breaks"]
    C --> D["Animate<br/>per-frame transform + opacity<br/>walking the motion graph"]
    D --> E["Place<br/>emit positioned elements<br/>browser only paints glyphs"]

Measure isn't a stage that finishes before Solve starts: it's the one canvas-backed service Solve calls per layer, once that layer's width constraint is known — which for a stretch flow member or a targetFraction-sized layer means after the layer it depends on has solved.

See Foundations: Pipeline & Architecture for what each stage actually does and why the pipeline is shaped this way.

The hard cases

Every Feature's "done when" is written against one of these. They're the acceptance set for the whole brief, listed once here so no page has to re-describe them:

Hard case What it exercises Owned by
Multiline box text Wraps to its authored width, then shrinks to fit its authored height; renders layout-identically in Firefox and Chromium. Text Foundation
Discount lockup A prefix and a hero number in a flowBox row aligned by cap-height, the suffix as an inline run in the hero's own content, one spanning background over the assembly. Written out in full as an example. Positioning, Background
Price with every lever A price run exercising every formatter part at once: currency symbol, thousands grouping, decimals only when present, a free-text unit; a bound value change restyles correctly across currencies. Price
Inline price A price run sitting inside a sentence of ordinary text runs, sharing their line. Price
Spanning background A background whose box re-derives every frame as a member it spans moves. Background
Retimed keyframe track A keyframed track survives a voiceover-duration change with no keyframe hand-edited. Keyframe Animation

Two side benefits of analytical (non-DOM) text measurement

Computing text metrics ourselves instead of reading them back from live DOM layout also closes NEO-1408's motion-blur pooled-context gap for text specifically (NEO-1408 fixed the DOM-capture settle-race for image, shape, and component motion blur, but explicitly left text unfixed until this plan exists), and removes ~3 of a text layer's ~9 DOM wrapper elements (the in-primitive measurement wrapper). Neither is a blocker on this plan — both are just gaps it closes once it ships.

Non-goals (this round)

  • A dedicated, full authoring GUI (later, still deferred) — drag/manipulate support for pin, flow, background, and keyframes as first-class canvas tools stays out of scope
  • Migrating already-shipped templates beyond the one pattern-size fix this delivery makes mandatory (otherwise secondary, hand-done as needed)
  • An automated live schema-migration path from schemaVersion: 1 to 2 (see Schema-Version Gating — deliberately deferred, not an oversight)
  • Latin-script, left-to-right retail copy only — the self-computed greedy line-breaker is not designed against CJK, RTL, or complex-script shaping, matching the real scope of the bespoke components it replaces (Betta, JB Hi-Fi, Freshmart, all Latin/LTR)

Cheap to keep the door open for other scripts, expensive to build them now

Real CJK support needs character-level break candidates with kinsoku (line-start/end) exclusion rules instead of word-boundary breaks, and real RTL support needs bidi-aware directionality on top of that (glyph shaping itself is already handled correctly by canvas — see Foundations) — both are genuine, non-trivial work, correctly left out of this round. What's cheap to do now: implement the line-breaker's "find candidate break points in this string" step as its own isolated function, not inlined into the measure/wrap loop. A future complex-script provider then plugs in at that one seam instead of requiring the surrounding pipeline to be redesigned.

In scope, not the same as the authoring GUI: TemplateBuilder authoring

TemplateBuilder needs enough updating to let someone actually author a schemaVersion: 2 document by hand, since no other authoring surface exists yet and hand-authored JSON alone doesn't scale to real production use. This is narrower than the full authoring GUI (no new drag/manipulate canvas tools for pin/flow/background/keyframes), but wider than "does nothing" — at minimum, TemplateBuilder must not misrender or corrupt a v2 document it doesn't fully understand, and should expose whatever of the new schema is cheap to surface via existing form-driven editing (the same pattern ManifestBindingsEditor.tsx already uses for schema-driven forms). Concrete scope isn't fully specified here; flagged as required, not optional, and needs its own real estimate before implementation starts.

Features

Each one is a complete blueprint: schema shape, pipeline behaviour, frontend impact, its own risks and open questions, and what "done" looks like — read one page for everything about that part.

  • Text Foundation — canvas measurement, line-breaking, point/box consolidation, the padding-is-paint-only fix
  • Positioning — layout modes (absolute/flow/pin), flowBox, pin, attachTo retirement
  • Cues — the time-addressing primitive and the full cue/retiming system
  • Price — price as a content-source mode of text
  • Background — the spanning background layer type
  • Keyframe Animation — the generalized per-property keyframe timeline
  • Brand Style Tokens — named colour and font roles on the brand, referenced by documents, overridable per template

See Foundations, the motion graph, and coordinate system & sizing for what's shared across all of them.