Character Reference Sheet — Redesign (design spec)

Date: 2026-06-28 Branch: feat/character-sheet-redesign Status: approved design, twice audited against the codebase → ready for implementation plan

Problem

The Character Studio “Sheet” page (shared reference-sheet-tab.tsx) generates a composited turnaround/reference sheet, but:

  1. You can’t view a sheet full-screen — result + “Existing sheets” grid have only Download / Set-thumbnail.
  2. You can’t copy the sheet URL.
  3. The sheet “types” are bare, unexplained, uncosted chips — no hint of what each produces, that some need to generate angles first, or how many credits it costs.
  4. The clean head + body turnaround format most useful for AI video reference (front/profiles/back-of-head + front/side/back body — see attached screenshots) isn’t a clearly-offered option.

Current state (verified — reuse, don’t rebuild)

Goals

Character-first (the tab is shared by character/object/location; the redesign is gated to character — see §2 guard):

  1. Full-screen viewing of sheets with ←/→ navigation.
  2. Copy-URL on sheets.
  3. Named, explained, live-costed presets: Studio · Main, Studio · Extended, plus à-la-carte board add-ons.
  4. Two-step generation: ① Prepare angles (generate only what’s missing; angles persist as reusable assets) → ② Compose sheet, with transparent cost.

Design

1. Full-screen + Copy-URL

2. Preset model — entries-based, character-only

New packages/shared/src/reference-sheet/presets.ts — the SoT for the preset cards:

type SheetPresetId = "studio-main" | "studio-extended"
interface SheetPreset {
  id: SheetPresetId
  label: string            // "Studio · Main"
  description: string
  type: SheetType          // CARRIER for the route's required `type` enum (Main→"turnaround", Extended→"full-reference")
  baseSections: SheetSection[]  // curated via `entries`
  skin: SheetSkin          // "studio"
  aspect: SheetAspect      // "landscape"
}
const presetEntries = (vs: string[]): SheetEntry[] => vs.map((variant) => ({ kind: "preset", variant }))
const PRESET_LABELS: Record<SheetPresetId, string>

Every variant exists in headAngles/bodyAngles except head back → the single catalog edit below. (Layout note: Main head = 4 panels renders as a single row (matches the screenshots); Extended head = 8 wraps 5+3 in the landscape grid — acceptable for the “more coverage” preset.)

À-la-carte boards — optional toggles appended to the base preset’s sections (no entries → board’s default slice):

Toggle Section kind Board Default panels
Expressions expression-board expressions 4
Poses pose-board poses 4
Wardrobe wardrobe-board wardrobe 3
Detail (eyes/hands/hair) detail-board detail 3
Palette palette 0 (structural, free)

🔴 Panel-count cap (required). Studio·Extended (15) + à-la-carte can exceed MAX_PANELS_PER_SHEET = 24 (Extended + 3 panel-boards = 26; + all 4 = 29) → planSheetPanels throws, and because the live cost calls it, the throw surfaces in the cost handler, not just a route 400. So:

Object/Location guard (required). head-turnaround/body-turnaround boards don’t exist for object/location (resolveBoard throws). The preset UI renders only when adapter.entityKind === "character"; object/location keep the existing SHEET_TYPES chip UI unchanged. For character, the presets + à-la-carte boards are a superset of the old character types (turnaround / variation-board≈Expressions / detail≈Detail / full-reference≈Extended+boards), so the chip UI is replaced there. (Deliberate simplification: character loses the rarely-used “expression-board alone, no turnaround” option; acceptable per the “simplify” goal.)

Single catalog edit: append "back" to CHARACTER_ASSET_VARIANTS.headAngles (entity-prompts.ts) at the end (index 7) — keeps the default-4 slice [front, 3/4 left, left profile, right profile] unchanged (no test churn), enables the catalog AND satisfies the route’s .includes gate; the head prompt already has back.

3. Two-step flow + live cost

New packages/shared/src/reference-sheet/cost.ts:

function estimateSheetCost(
  entityKind, sections, flavour, buckets, name, perPanelCost, assemblyCost,
): { present: number; missing: PanelGenRequest[]; prepareCost: number; assemblyCost: number; total: number; overflow: boolean }
// present = planSheetGeneration(...).presentUrls.length; missing = .missing
// prepareCost = missing.length * perPanelCost; total = prepareCost + assemblyCost
// wrap planSheetGeneration in try/catch → overflow:true (MAX_PANELS) instead of throwing

UI (recomputed live as the user toggles preset/boards and as panels get prepared):

┌ Studio · Extended ──────────────────────────────┐
│ Full turnaround — adds above/below + 3/4 angles. │
│ Reuses 9 existing angles · 6 missing             │
│ ① Prepare 6 angles  ~6 cr     ② Compose  4 cr    │
└─────────────────────────────────────────────────┘

4. Cost gating + edition boundary

5. Preset labeling (persisted, no worker change)

6. Style controls (secondary)

In the character preset UI, a compact “Style” row keeps skin (studio default; cinematic/blueprint/illustrated) + aspect + the Title-metadata / Panel-labels toggles. Presets set defaults (studio/landscape); user can override. (Object/location keep their full existing chip UI.)

Files touched

Shared (packages/shared/src/reference-sheet/) — rebuild dist after (auto via frontend pre* hooks; a mid-session shared edit needs a manual npm -w @nodaro/shared run build).

Backend

Frontend (components/editor/)

No new credit SKUs.

Testing

Out of scope (now)

Resolved