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: 1to2(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,attachToretirement - 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.