Contents

M14: Immersive editors for World, Cast and Sets

Status: design, 2026-10-05 (task #222); phase 1, the set explorer, and phase 2, the character studio, built 2026-10-05 (see "Phase 1: what was built" and "Phase 2: what was built"); phase 3, editing a set in 3D, built (see "Phase 3: what was built"); phase 4, previs, built 2026-10-06 (see "Phase 4: what was built"); the core of phase 5, the style lab and a style made from scratch, built 2026-10-06 (task #224, see "Phase 5, its core: what was built"). It is a critical look at how a showrunner sees and changes what was built, and a phased plan for editors that show it live.

Read with docs/adr/0004-3d-stage-2d-characters.md, docs/adr/0011-ux-surfaces.md, docs/design/m7-redirection.md ("Stage"), docs/design/m10-3d-characters.md, docs/design/m12-web-service.md ("The same picture as the desktop") and docs/design/m13-shared-shows.md.

The question

The owner, 2026-10-05:

On the world page .. it has sections for SHOW, THEME, STYLES, and BIBLE … where and how is a user to "visually" experience what was built? … I'm just wondering if each should be tied into their own immersive editors that allow users to explore the 2d/3d spaces of world, cast, sets in more detail.

The short answer:

  • Today the show can be seen in two ways only. The timeline plays an episode live. Anything else is a still that the daemon renders on request: 10 to 15 s each on this desktop, and about 13 s for a model sheet on the service. World, Cast and Sets are lists of names. Each name opens a form of fields with one such still on top. Nothing can be turned, walked round, looked through or moved by hand.
  • Yes: each should open into an editor, and that costs less than it sounds. The engine already runs inside Studio, on the desktop and in the browser: the timeline plays episodes live with it. Every builder an editor needs is there, and most are fast enough (measured below). What is missing is the viewport, selection and gizmos around them, plus a few small protocol additions.
  • The order, by value to a showrunner. Sets come first, because every shot is taken in a set. Then Cast, because today nobody can tune a character without the showrunner. Then editing a set in 3D, previs of a scene, World's look book and style lab, voice auditions, and the bible as a story map.
  • This is not a 3D modelling tool. A show is parameters, not meshes. M14 adds no modelling, sculpting, rig building, keyframe posing, shader graphs or VR. The editors change the same parameters the showrunner changes, by direct manipulation, and show the result live.

What exists

World, Cast and Sets today

World: a list of documents by kind, each a name, an id and a badge

  • One component draws all three sections. RwShowBrowser (engine/addons/reelwright/ui/rw_show_browser.gd) lists the show's documents by kind (SECTION_KINDS):

    • World: the show's settings (ShowManifest), the theme, the styles and the bible;
    • Cast: the characters and their patterns;
    • Sets: sets, props, effects, graphics and their patterns.

    Each row is a name, an id and badges for status, locks and drift.

  • A document opens as a form. RwDocumentView (rw_document_view.gd) shows a header, then one preview (KINDS): a model sheet, a line-up, set views, a prop or effect sheet, or a style swatch. The theme, the bible and the show have no preview. Below it, each part of the document is a card of labelled values.

  • The form is read-only. Locks are the only thing a person can set there. Cast has New character and Sets has New set, both drawn from a pattern with no model. Everything else goes through the showrunner, which needs a key or a Claude plan: a hair color, a mark's place, a camera's lens. Someone with no key cannot move a mark.

  • A card says what a set is in numbers. A mark on the bridge reads "position (m) 0.8, −1.4 · facing (°) 180 · posture sit · seat pilot_seat". The engineering console's shape is SVG path data: "profile · value M 0 0 L 0.7 0 L 0.7 0.85 …". Nobody can judge from that whether the helm mark is in front of its console.

    The bridge's marks and elements as text

  • Pictures arrive late. A preview is an engine job (core/src/studio/previews.ts). The daemon runs one at a time and caches the result. The first look at the bridge was a spinner, then five pictures from its camera setups, 13.8 s later. The pictures show no marks, no characters and nothing to say where each camera stands.

    The bridge's set views: five stills from its setups, no marks, no cast, no plan

  • A style is shown on someone else's content. The style swatch (RwStyleSwatchTask) draws a fixed sample from library/: a character and a room that belong to no show. A showrunner choosing a style never sees it on Otto in the bridge.

    Saturday Morning's swatch: the library's sample, not the show's cast or sets

  • The model sheet is good. It shows the turnaround, 21 expressions, the mouth chart, the colors and the line-up (RwModelSheetTask), and it is the approval picture for canon (ADR-0007). It is still one still: no turning between its angles, no gesture, no line said.

    Otto's model sheet: the one picture of a character

  • The stage is the one spatial editor. The timeline's stage (RwTimelineStage, RwStageModel, RwStagePlan, docs/design/m7-redirection.md) draws the scene's set from above at the playhead. A person can drag someone onto a mark, turn them, and orbit the shot's camera; each drag is an edit_episode edit, applied as made. It is top-down only, so it shows no height (the Chief hidden behind his console, pipes overhead). It cannot look through another setup, and it exists only inside an episode.

    The timeline's stage beside the picture: the bridge from above at frame 100

What the engine already draws live inside Studio

Studio plays an episode live. episode.playback writes a frames job, and RwLivePlayer builds its Timeline with RwTimelinePlayer into a SubViewport in Studio's own process. The desktop draws with Forward+. The browser draws with Compatibility: WebGL 2 on one thread, with the web preset's thread_support=false. The two match the server's stills to within about two levels (tools/web/shots.ts). An editor can reach every builder it needs the same way:

Building block What an editor gets Measured
RwSetBuilder.build(design, style, lighting) a set in its style under one lighting preset bridge (16 elements) 82–195 ms; engine room (21) 1.28–1.38 s
RwSetBuilder.relight another lighting preset on a built set 1–7 ms
RwComposition, RwShotView.aim, RwSet.view_from, RwPost a camera on it, with wild walls and the style's post pass, as a render draws it first frame after a build 2.1 s (shader pipelines), then 11–34 ms
RwCardFactory.create and apply(state, camera, lighting) with an RwActorState a character in any posture, gesture, expression, mouth shape, look, held prop or float 2D card: made in 20–80 ms, posed at a new angle in 76–100 ms (worst 257 ms). 3D figure: made in 8–9 ms, posed in 0.3 ms
RwModelSheetTask (_figure_lighting, _add_figure_view, _add_figure) the model sheet's studio light and its views of a card or a figure —
RwProp.build, RwPropGripState a prop standing, or held in a hand as an episode holds it —
RwStyle.from_spec, RwStyle.FIELD_USES a style resolved, and for each field whether the engine, the characters or only the core reads it 0.4 ms
RwStagePlan each piece's footprint as a box (center, half extents, yaw, bottom, top): picking without collision shapes —
RwSetBuilder.geometry_hash proof that two builds made the same geometry —

These were measured on 2026-10-05 on the Linux desktop (RX 6700 XT, Forward+), in the process as an editor would build them, with scratch probes that are not committed. The machine was at a load of about 70 on 24 cores, so every time is high. In the same session, previews through the daemon took 10.8 s (style swatch), 14.6 s (Otto's model sheet) and 13.8 s (the bridge's five set views).

Two of these numbers shape the design:

  • Set edits rebuild one element. A whole rebuild of the engine room takes over a second, too slow to repeat on each edit.
  • Cards are re-posed on a budget while the camera moves. A 2D card costs about 100 ms to draw at a new angle, because a card is drawn again as the view turns round it (ADR-0004). A 3D figure poses every frame.

How edits flow today

  • The person's edits are applied as made. They go through tools.call (Studio.toolsCall, core/src/studio/studio.ts): the same tool chat calls, made by the person (author user) and applied at once. Ctrl+Z undoes them from anywhere in Studio (RwStudio.undo). The timeline, the stage and New character and New set all edit this way.
  • The edit tools already exist. revise_theme, revise_style, revise_character, revise_set, revise_prop, revise_pattern and write_bible_entry (core/src/studio/tools/revise.ts) take JSON Pointer edits. They refuse locked pointers, and refuse an invalid result with the edit that caused it. A change to the active style flags locked characters. Every field an editor shows can be edited this way, so World, Cast and Sets need no new edit tools.
  • The showrunner's changes are proposals. The timeline draws them as ghosts. RwProposalView reviews them field by field: changes.get gives each field's pointer, before and after.
  • Changes are shared and held to roles. Each applied change is committed to the show's git, one commit per change. In a shared show it reaches everyone as a change.event with a note. Roles are held by method class (M13): a viewer reads, a reviewer comments, an editor or owner writes.

The protocol Studio uses

From schemas/v1/session.schema.json (x-methods), by access class:

  • read: session.hello, session.place, changes.list, changes.get, docs.list, docs.get, previews.get, renders.list, episode.timeline, episode.playback, takes.list, screenplays.list|get|read|import|export, episodes.status, graph.impact, graph.status, memory.list, comments.list.
  • comment: comments.create|reply|edit|delete|resolve|reopen.
  • write: chat.send, chat.cancel, changes.approve|reject|undo|redo|lock|unlock, renders.start|cancel, episode.voice, takes.upload|add|choose|delete|edit, assist.write|cancel, memory.edit|configure|remember.
  • tool: tools.call, where a read tool reads and any other tool writes.
  • Events: doc.event, change.event, preview.event, chat.event, render.event, memory.event, comment.event, session.people.

Critical analysis

World

  • What a showrunner can do.
    • Read the theme (premise, genres, tone, setting, world rules, language, catalog) and every style field, as cards.
    • See one style swatch per style, drawn on the library's sample.
    • Read the bible's lore, relationships, gags, tone rules, arcs and threads as lists of fields.
    • Read the show's settings, and open People… on the web.
    • Lock things.
  • What they cannot do.
    • See a style on their own cast and sets, or two styles side by side on the same frame.
    • Change a style dial and see what it does.
    • See the bible as a whole: who relates to whom, which arcs moved in which episode, which threads are still open, which gags are worn out.
    • Get from a bible entry's evidence to the shot it cites.
    • See which places connect to which (entrances[].leadsTo).
    • See what the theme's catalog has actually produced.
  • Where the form fails.
    • A StyleSpec has 109 dials (RwStyle.FIELD_USES): line weight, bands, softness, rim, grain, boil, steps, smears. Their effect is visual and depends on content. As numbers in cards, nobody can judge them.
    • The bible is a graph: relationships join characters, arcs run across episodes, evidence points at shots. A form flattens it into seven lists.
    • The show's settings are not part of the world. They are going to Show settings (#217).
  • What pro tools do.
    • Figma's style systems: tokens (color, type, spacing) feed components, a page shows every component in every mode, and modes switch side by side. The look book is that page.
    • Look development (Blender's LookDev, Unreal's material previews): the material is judged on real assets under real light, with before and after.
    • Story tools: World Anvil and Campfire link wiki entries both ways and draw relationship webs. Plottr and Aeon Timeline put arcs and threads on a grid against episodes. Writers' rooms use beat boards.
  • Verdict.
    • Build the look book and the style lab. They are cheap, because the engine draws the cast and sets live, and they serve the restyle decision (M8) and the new-show wizard's style choice (#217).
    • Build the story map in 2D. It is plain Control drawing: cheap on the web, and its value grows with every episode.
    • Theme: only a catalog band in the look book, showing what each entry produced and what is missing.
    • No world map. The theme holds no geography; the place graph from entrances is all the data supports.
    • No new home for the show's settings. That is #217's.

Cast

  • What a showrunner can do.
    • See the model sheet (turnaround at five angles, 21 expressions, the mouth chart A–H and X, colors, line-up) and pattern line-ups.
    • Make a character from a pattern, and lock fields.
    • Hear a voice only by voicing an episode.
    • See a prop in a hand only on the prop sheet, held by whoever the sheet picks.
  • What they cannot do.
    • Turn the character freely, or watch a gesture, a walk, a blink or a line said.
    • Tune an expression ("her happy is too big") or a mouth shape. The fields exist (expressions[] with brow, eye, squint, pupil and mouth curve; mouths[] with open, width, round, teeth, tongue), but nothing in Studio edits them.
    • Try a different jacket or sleeve length, or see the options the part library draws.
    • Audition voices, compare a card with the same character as a 3D figure, or check them against canon at a glance.
  • Where the form fails.
    • A character is a drawing described in values: Otto has 153 of them (proportions, enums, colors, palette slots, persona, voice), and most of them change the drawing. The form shows every one with its lock, and none of the drawing.
    • The one picture takes about 15 s and goes stale on any edit.
  • What pro tools do.
    • Toon Boom Harmony: model sheets on a light table, rigs with drawing substitutions for mouths and hands, and a library of templates.
    • Character creators (MetaHuman Creator, The Sims' Create a Sim): sliders and presets with a live turntable, and try-on strips of options.
    • Adobe Character Animator: live lip sync from audio, and triggers for gestures.
    • Cascadeur: physics-assisted keyframe posing. It does not fit here: performance comes from cues and the gesture library, and the timeline's inspector already edits those.
  • Verdict: high value.
    • A character studio with a live turntable, expressions, mouths, postures and gestures, wardrobe try-on, palette, props in hand and a canon overlay.
    • A voice audition.
    • Not worth building: gesture authoring, keyframe posing, rig and part authoring, and hand shapes, which the 3D figure does not have yet (docs/design/m10-3d-characters.md).

Sets

  • What a showrunner can do.
    • See one still from each camera setup, under one lighting preset.
    • Read elements, marks, entrances, walkways, setups and lighting as text.
    • In an episode, use the timeline's top-down stage to reblock and orbit the shot's camera.
    • Build a set from a pattern.
  • What they cannot do.
    • Walk or orbit round a set, or see marks, walkways and entrances in place.
    • See where each camera stands and what its frustum covers.
    • Look through a setup with the cast on their marks, or switch the lighting preset.
    • Move a mark, a console or a camera, change a lens or turn the key light.
    • See a scene at the playhead from another angle (the "eject" of Unreal's Sequencer).
    • Place a prop where it stands.
  • Where the form fails.
    • A set is geometry, and nobody judges geometry as positionM triples and SVG path data.
    • Coverage, the set's whole purpose, is invisible: which setup sees the helm, and whether the Chief is hidden on eng_crouch. The form gives five stills and no plan.
  • What pro tools do.
    • Unreal's level editor: a perspective viewport with right-mouse plus WASD to fly and Alt to orbit. F frames the selection and G hides editor-only drawing. Snapping gizmos move and turn things. An outliner lists everything and a details panel edits it. Piloting looks through a camera actor and moves it. Sequencer can eject from a shot's camera.
    • Blender: middle-mouse orbit; G and R with axis constraints and typed values; a sidebar for numbers; Ctrl+Alt+Numpad 0 sets the camera from the view.
    • Previs tools (FrameForge, ShotPro, Shot Designer): a floor plan of cameras and marks beside the camera's view, real lens readouts (focal length, height, tilt), and a shot list from the cameras.
  • Verdict: the most valuable editor.
    • A set explorer: orbit or fly, overlays, look-through, lighting presets, outliner and inspector.
    • Then gizmos for marks, elements, setups, entrances, walkways and the key light.
    • Then the scene at the playhead in 3D.
    • Not worth building:
      • mesh or recipe modelling, since kit pieces are recipes and a recipe editor is a separate tool;
      • physically based lighting or baking, since the style lights in cel bands;
      • a first-person walk beyond a fly at eye height, since a point of view is a setup at eye height;
      • VR.

Across all three

  • Stills stand in for the real thing. Every picture is a render made to order, cached, and stale on any edit. The engine that drew it is already in the Studio window, unused outside the timeline. ADR-0011 meant Studio to "render previews in-process with the same builders the renderer uses". The editors make that true.
  • Nothing can be selected in a picture. Things cannot be pointed at, only named. So the chat cannot be told "this" about anything but a document or a shot.
  • A person cannot edit without the showrunner. Outside the timeline, the only hand edits are New character and New set. A Studio without a key is a viewer of its own show.
  • Forms are for exact numbers. A form is right once a thing is chosen: the lens is 35 mm, the facing is 180°. The editors keep it as the inspector, and the whole document stays one tab away ("Fields").
  • What stays. Previews keep their job as approval artifacts: a model sheet for canon, set views and storyboards on a proposal, stills in the MCP for Claude Code. The editors are the working view, not the record.

The web edition's limits

Limit What it means for an editor Answer
Compatibility renderer (WebGL 2) No compute shaders; a different light path, fixed in rw_color.gdshaderinc for playback The same builders and shaders as playback; parity proved the way tools/web/shots.ts proves playback
One thread (thread_support=false) Every build blocks input and drawing Build one element, card or thumbnail per frame; nothing over about 60 ms in one go; a progress veil for a set's first build
Card meshes rebuilt every drawing (RwCharacterRig.rebuilds_meshes) A 2D card is dearer to re-pose in the browser Re-pose cards at most one per frame while the camera moves, after a 15° change; exact at rest
Shader compilation at first use A pause the first time each material draws (2.1 s on desktop Vulkan) Warm the set's and characters' materials offscreen as the editor opens, once a session
Memory (a 32-bit heap, the browser's GPU) Card viewports grow with screen size (up to 4096 px); panoramas are large Cap card viewports at 1024 px in editors; at most 8 live characters; free everything on leaving
Network Every commit crosses the service Edits show locally at once; the commit catches up; a refusal rolls back
Keys the browser keeps (Ctrl+W, Ctrl+T, Ctrl+N, F5); Alt+drag taken by some Linux window managers Some pro-tool keys cannot be used No browser-reserved keys; every Alt gesture also on the middle or right mouse; no pointer lock (its Esc fights ours)
The service's 2 CPUs and lavapipe A server still costs about 13 s of shared CPU Editors draw on the person's GPU; previews become optional, which lightens the service

The last row is the strongest argument for building this on the web too. Today every look at a set or character on reelwright.diy costs the service a CPU render. A live editor costs it a JSON file.

The editors

Principles

  1. One engine, drawn live. Editors build with the same runtime builders as renders, in Studio's process, from documents the core resolved (ADR-0002: decide upstream, draw downstream). Looking through a setup in the explorer draws the same frame as the setup's still on the same renderer. A test holds that.
  2. Edits are spec edits. A gizmo drop becomes revise_* pointer edits through tools.call, made by the person, applied as made and undone with Ctrl+Z. The editor never writes engine state that the document does not hold. In scene mode, edits are edit_episode operations.
  3. Local first, the daemon decides. A drag shows its result in the local build at once. The commit is sent on drop. The answer either confirms it, which changes nothing on screen, or refuses it in the core's words and rolls back.
  4. Viewport, outliner, inspector. Every editor has the same frame, with collapsible and resizable side panes (the owner's standard for Studio panels). The whole document is one tab away (Fields: today's RwDocumentView, with its locks and, from M13 phase 3, its comments).
  5. The same everywhere. Plain Controls (ADR-0011), so the same code runs on the desktop, in the browser and in the Godot editor plugin's main screen. The only differences are file reads (RwFiles) and the quality profile.
  6. No clock in the runtime. Spins, live time and throttling belong to ui/. The runtime only ever receives frames and angles; the hidden-input grep stays clean.

The shared foundation

New, under engine/addons/reelwright/ui/view3d/ and ui/editors/:

  • RwView3D (Control). A SubViewport holding one RwComposition view and the style's post pass, shown in a TextureRect. It draws only when something changed: Studio already runs low-processor mode. A RwViewQuality profile sets the render scale while moving and at rest, the card viewport cap, live-character and thumbnail budgets, and MSAA. The profile adapts to measured frame time, not only to the platform.
  • RwOrbitCamera (RefCounted, pure). Orbit about a pivot, pan, dolly toward the pointer, fly (right mouse with W/A/S/D/Q/E), frame a box, plus top, front and side views. It also pilots: drives a setup's position, look-at and lens. Input in, a camera pose out, so it is testable without a window.
  • RwViewOverlay (Control over the view). Draws marks, walkways, entrances, setups with frustums, lights, labels, selection and hover. It projects with the view's camera and uses the stage view's visual language: rings, wedges, a frustum in the accent color, the axis dashed in amber. It is never part of the frame. A thing behind a piece of the set is drawn faint (a ray test against the pieces' boxes).
  • RwViewPick. Ray tests against RwStagePlan's piece boxes, marks, entrances, setups' glyphs, characters' screen bounds (RwCardFactory.screen_bounds) and props. No collision shapes are built.
  • RwGizmo. Floor move with grid snap (5 cm; Alt for none), a yaw ring (15°; Shift for 1°), a height handle, a camera's look-at handle, the lens on the wheel while a camera is hovered, and walkway points. It reports a drag's intent: what would change, as edits, and in words while dragging, as the stage view does. It never edits anything itself.
  • RwSceneBundle. Loads a scenes.get job (see Protocol) as RwLivePlayer.load_files loads a playback job: the documents, and in a browser their assets over /api/file, cached by hash.
  • RwDocBuffer. One document as JSON, with the person's local edits not yet confirmed. It applies pointer edits locally, commits them through tools.call, reconciles on doc.event and change.event (re-read with docs.get), rolls back a refusal, and asks for an incremental rebuild of what changed.
  • RwEditorShell. Outliner, view and inspector, with the tabs (Explore, Studio, Look book, Lab, Map, and Fields), the toolbar, a maximize button and a history popover. Pane widths and folds are remembered in RwUiSettings on the desktop and in the browser's storage on the web.
  • RwFieldEditor (widget). A typed editor for one pointer: a number with its unit and range from the schema, a vector, an enum (with live thumbnails where it draws something), or a color or palette slot. It commits through RwDocBuffer: a number when it rests or on Enter, as the timeline's inspector does, and a slider on release. Inspectors are built from these, so every field an editor shows can be changed by hand, with no key.
  • RwFieldForm (widget). The form every inspector shares: a row of editors with a heading for each object and list item (folded when it holds many rows, or nothing to change), Remove on an item or optional field, Add for a list and a missing field, and with lock toggles in a Fields tab. The set explorer's inspector lays out its selection with it; every document's Fields (RwDocumentView) lays out its whole document from docs.form, the core's reading of its JSON schema: the widget, range, unit and choices of every field, and a valid template for each Add. So every document kind is edited field by field: theme, bible, patterns, props, effects, graphics, sound recipes, characters, sets, outlines and styles.

The host is RwShowBrowser.open_document. It opens a set in the explorer, a character in the studio, a style in the look book, and the bible in the map. Everything else keeps RwDocumentView. RwStudio.show_section and open_document do not change, and RwStudio.chat_context asks the open editor for its context. That keeps the change to engine/app/rw_studio.gd, where most work in flight lands, to a few lines.

How edits flow

  • Direct. One gesture makes one change: a drop, a release, or Enter. "Moved the helm mark 0.3 m toward the porthole in the set explorer." goes as the rationale. Studio's undo stack picks it up from change.event as it does the timeline's edits, so Ctrl+Z and Ctrl+Shift+Z work across Studio. Several things moved together make one change with several edits.
  • Refused. The core's words appear on the view, and the local edit rolls back. This covers a locked pointer, an invalid set (a camera inside geometry, a doorless wall, a mark in a wall), a role that may not write, and a stale edit. Locked things show a lock in the outliner and on their gizmo, and the gizmo refuses before anything is sent ("Locked: canon"). The daemon refuses anyway.
  • Proposed. A showrunner's proposal touching the open document draws as a ghost in the view: outlined, translucent, a camera's moved frustum dashed. A banner says what it is, with Review. \ flips between the state now and the proposal. Approving and rejecting stay in the proposal view and on the chat's cards. The proposed state comes from scenes.get with change, so a proposal over several documents (a restyle, a set and an episode together) draws whole.
  • History. A popover lists this document's changes (changes.list, filtered by documents): who, when, what, and the proposal view on a click. Restoring a version is Studio's History (Ctrl+H; docs/design/m3-showrunner.md "Versions"), not M14's.
  • Roles. A viewer navigates, looks through cameras and plays. A reviewer does the same and comments (pins, phase 8). An editor or the owner does everything. Gizmos and inspector editors are absent for viewers and reviewers, and their tooltips say why in the words Studio already uses ("You can view this show: ask its owner to make you an editor.").
  • Two people on one set (M13). Another person's change arrives as doc.event and change.event with its note ("Ada moved the helm mark."). The buffer re-reads it and keeps the person's own unconfirmed edit on top.
    • The risk: edits address array items by index (/marks/2/positionM). Ada deleting mark 1 would send Bo's edit to the wrong mark.
    • The fix: an edit carries was, the value the editor saw. The core refuses it with edit.stale when the field no longer holds that value. Editors guard an item by its id, so the move is refused, and the editor finds the mark by id and sends it once more.

Every document by hand, and the Sounds section

The product audit (October 2026, findings 2, 3 and 9) found the canon read-only, nothing below the show deletable, and props, effects, sounds and patterns made only in chat.

  • Fields edits. Each field commits through the document's revise tool as the person's own change, shown at once, in History and undone with Ctrl+Z. A name (a character's short name too, an episode's or outline's title) goes through rename_document, so scripts' cues and every reference follow; the rename dialog's "Called in scripts" follows the name as it is typed while it was only the name. A whole number (a seed, a count) is shown and kept whole. Locked fields are shown and not edited, saying why, and their lock unlocks them. A viewer or a reviewer looks and changes nothing. show.json is changed in Settings and an episode on its timeline; their Fields say so.
  • Rename, Duplicate, Delete. In the Fields header (an editor's Fields tab too: its header keeps its width, so it gets no menu of its own) and each list row's ⋯ menu (or a right-click). Delete asks first when anything uses the document, listing exactly what and where (find_uses), and offers another of its kind to take its place, or to take it out of the lists that name it.
  • New for every kind. Sets' "More" makes a prop, an effect or a graphic from a template (a plain box, a puff of smoke, a painted sign), or a copy of one of the show's set, prop, effect or graphics patterns; Cast's makes a copy of a character pattern; World's "Bible entry" adds a place, a person, a relationship, a running gag, a story thread, an arc, a tone rule or a fact, making the bible with the first. Each is named by the person and opened to fill in.
  • Sounds (the rail, Ctrl+9, #/<show>/sounds and #/<show>/sounds/<id>). Sound effects and music, recipes and recorded files, with their length and how much uses them; search; each plays from the WAV sounds.preview names; New sound from a recipe (chime, beep, whoosh, thud, horn, tune: library/templates/sounds/) with create_sound; Import a file (WAV, Ogg, MP3, FLAC) through takes.upload and sounds.import; a recipe opens in the form (revise_sound); rename, duplicate and delete as any document. A recorded sound is named as the file it came in as ("Fog_Horn.wav" is "Fog Horn"), the name kept in the FLAC itself, and renamed there. The same in the installed Studio and in a browser.

The storyboard, and the cast's faces

The audit (findings 25 and 28) found the storyboard one read-only contact sheet in a dialog, and the cast list without faces.

  • Storyboard (Episodes, an episode's "Storyboard", #/<show>/storyboard/<episode>; RwStoryboardView). A card for every shot of the cut, in order under each scene's heading: the shot's still (its middle frame, previews kind panels: one image per shot at the storyboard's size, captioned "sc01 · sh020 · MCU", so each card finds its own), the label, the lines that start in the shot and the business (from episode.timeline through RwTimelineModel, as the timeline reads them; a line or business under the opening title goes with the first shot). A card opens the timeline with its playhead on that shot's still (RwStudio.open_timeline takes a frame). "Save as PDF" asks the core for the storyboard as a document (storyboards.export, six panels a page) and saves it where the person says on the desktop, or downloads it in a browser. Back returns to the episodes.
  • Faces (Cast). Each character's row shows their face in place of the kind's icon: a bust drawn here as the character studio and the model sheet draw theirs (RwFaceCache over RwCharacterPainter, from the character's scene, scenes.get), front on and neutral, one at a time. A face is kept for the life of the process by the character and style it was drawn from, so the list drawn again shows it at once; a change to the character, a style or the theme draws it again the next time it is shown.

The showrunner sees what you see

This builds on the page-aware work in flight (#221). In it, ChatContext gains page (the StudioPage path), title, part, selected and view, and chat.focus and studio.navigate arrive. Each editor fills them in:

  • page: the document, then the part, then the item: sets/set.marigold_bridge/marks/helm, cast/char.otto/expressions/happy, world/style.saturday_morning/lab.
  • selected: a few words each: "mark helm (sits, faces the porthole)", "element eng_console".
  • view: what is in view, short. For example: "Looking through setup wide (24 mm). In view: helm console, pilot seat, porthole, the Chief on eng_crouch (hidden by the console)." It is computed from the frustum against the pieces, marks and characters.
  • camera (new): {setup?, positionM, lookAtM, lensMm}. The showrunner can then act on "from here": add a setup from this view, check whether the helm is visible from this angle, or light the set from this side. core/src/showrunner/focus.ts says it in one line.
  • Links back. studio.navigate pages address items, so the showrunner's reply can link "Look through the new low angle", and the explorer opens looking through it. A proposal's card gains "Open in the set explorer".
  • The picture itself (phase 8). "Show the showrunner what I see" sends the view's frame with the turn. It is opt-in per message, about 1,500 tokens, and needs image input in the agent, which today sends images only over MCP (core/src/mcp/server.ts).

Sets: the set explorer

Opens from Sets › a set (the Explore tab), and from the timeline's stage for a scene (phase 4).

  • Layout.
    • Outliner: elements as a tree by parent, grouped by role (structure, furniture, dressing, practical, door, window, foreground); marks; entrances; walkways; camera setups; lighting presets; backdrops.
    • View: the set in its style.
    • Inspector: the selection's fields as typed editors. A setup's lens offers the common primes (18, 24, 35, 50, 85, 135 mm); a mark's posture and seat come from the set.
    • Toolbar:
      • the view: perspective, top (the plan, orthographic), front or side;
      • look through: a setup list, also on keys 1 to 9;
      • the lighting preset (a relight takes 1–7 ms);
      • overlays (marks, walkways, entrances, setups, frustums, lights, labels, safe area);
      • walls: up, cutaway (the walls between the camera and the room hidden, as wild walls are by RwSet.view_from) or down;
      • cast on marks;
      • live time (practical flicker, backdrop drift, screens: RwSet.evaluate(frame)).
  • Looking. Orbit, pan, dolly and fly. F frames the selection and Home frames the set. In pilot mode, the view is the setup's exact framing at the show's aspect, with the frame's mask and safe areas, and a readout of lens, height and tilt (FrameForge's). Wheel and fly in pilot change the setup; the change commits on release. Esc ejects to the free camera.
  • Cast on marks. "Blocking dolls": choose who stands on which mark, in the mark's posture, seated on its seat. They are drawn by the real builders: cards re-posed on the budget below, figures every frame. This shows what the set's cameras see of people before any episode exists. It is a view only and writes nothing.
  • Editing (phase 3).
    • Marks: move on the floor, turn the facing knob, posture and seat in the inspector.
    • Elements: move on the floor, turn, raise one that sits on a parent, and size a primitive's sizeM. A kit piece's own params are edited in the inspector. Children move with their parent. Duplicate (Ctrl+D) and delete (Delete) also work.
    • Setups: move the camera, drag the look-at, lens on the wheel, roll, and establishing. "Save this view as a setup" works like Blender's camera from view.
    • Entrances and walkways: move entrances; walkways' points can be dragged, added with a double-click on a segment, and removed.
    • Lighting: the key light as a sun arrow dragged round the set (key.directionM); key, fill, ambient and rim colors as swatches, with palette slots offered first; each practical's intensity on its element.
    • Each edit is revise_set pointer edits, and each set edit rebuilds only the element it changed.
  • Props. A set has no props of its own. Its dressing is elements, built from the set's own pieces recipes and primitives, and those the explorer edits. A PropInstance stands in a scene (place_prop), so props are placed in scene mode.
  • What it proves. Looking through a setup draws the frame RwSetViewsTask draws for that setup, byte for byte on the same renderer: the same RwSetBuilder.build, RwComposition, RwLens aim, view_from and post seed.

Scene mode: previs (phase 4)

"Open in 3D" on the timeline's stage (or Shift+V) opens the explorer on the episode at the playhead.

  • The picture. The playback job that episode.playback already writes is played by RwLivePlayer, and the explorer's camera takes over the active view after each evaluate. It needs one small runtime accessor for the active view.
  • Pilot and eject. Pilot shows the shot's own camera. Eject is a free camera, with the shot's camera drawn as a frustum.
  • Edits are the episode's, not the set's. The stage view's RwStageModel drops, in 3D:
    • a character onto a mark or by a piece (set_mark, set_move);
    • the facing knob (set_look, turn: "body");
    • the shot's camera dragged round its subject (set_camera yawOffsetDeg);
    • a prop from the show's shelf onto an element or a mark (place_prop).
  • "Use this view for this shot". This saves the view as a new setup on the set and puts the shot on it, as one change. edit_episode's set_camera gains newSetup, which writes the set and the episode in the same change set, so a single Ctrl+Z undoes both.
  • Space plays the episode from the playhead in the explorer, with its sound, as on the timeline.

Cast: the character studio

Opens from Cast › a character (the Studio tab).

  • The stage. The character alone under the model sheet's studio light, drawn with the sheet's own views: a card's rig as RwModelSheetTask draws it, or a figure in its 3D view. So the studio's front, 3/4, profile and back are the sheet's turnaround, and a test holds them equal. The backdrop is the sheet's paper, plain, or a set: stand them on a mark under its lighting.
  • Turn. Drag to turn. Keys 1 to 5 jump to the sheet's angles, and Space spins once over 4 s. A strip of the five angles stays live.
  • Expressions. The 21 emotions as live thumbnails, drawn one per frame and redrawn after an edit. The selected emotion is shown large, with an intensity slider that previews only.
    • Sliders for the character's own adjustment (browRaise, browAngle, browAsymmetry, eyeOpen, squint, pupilSize, mouthCurve …) move the face live while dragging. On release they commit revise_character at /expressions/<i>/<field>, or append the entry when the character has none for that emotion.
  • Mouths. The chart A–H and X. Sliders for open, width, round, and toggles for teeth and tongue. "Say it" plays a typed word's shapes, using the audition's lip sync (phase 6).
  • Postures, gestures, looks and walks. Stand, sit, crouch, lean and lie. Gestures come from the rig's library (RwGestureLibrary), held ones marked. A look follows a target dot dragged in the view, and the walk cycle loops. These show the performance; they change nothing. Gesture authoring came after M14: the pose maker (docs/design/m6-performance.md Parts 3 and 4) poses a gesture or a pose of the show's own by dragging handles on this stage.
  • Wardrobe. The costume's layers: slot, cut, fit, sleeves, legs and colors.
    • Try-on strip: for the selected field, every value the part library draws for it (toon.v1.parts.json fields[].draws, and the cuts that fit the slot), each a live thumbnail of this character wearing it. A click commits the edit.
    • Palette slots: swatches, checked against the style's color rules by the core's validation when the change is made (core/src/validate/slots.ts), which says why a color is off.
    • Variations…: vary with four options at the chosen pointers, shown as a variation group to pick from in place. This needs tools.call to keep proposals: see Protocol.
  • Props in hand. Any prop of the show in the left hand, the right or both, in one of its grips, held as an episode holds it (RwPropGripState, RwProp). A scale that looks wrong is fixed on the spot with revise_prop /holding/heldScale.
  • Canon. For a locked or approved character, \ lays the approved model sheet's drawing at this angle over the live one, like a light table. It is the drift report seen rather than read.
  • Card or figure. The same character side by side on biped_toon.v1 and biped_3d.v1. "Draw them in 3D" commits /rig/template.
  • Line-up. The character beside the cast at true height, live.
  • Voice (phase 6). The binding (provider, voice from voices.list, speed, pitch) and the casting note.
    • Audition: one of their own lines, or a typed one, with an emotion. It plays with lip sync on the live face. A and B compare two voices.
    • Use this voice commits /voice/bindings/0.

World: look book, style lab, story map

  • Look book (a style's first tab, phase 5). One style, or two compared, on the show's own things, all live:

    • The cast in a line-up.
    • Each set through its establishing setup.
    • Hero frames: each episode at its stillFrame (from episodes.status), from its playback job.
    • Props and effects.
    • The palette as tokens, with where each slot is used.
    • A title card (RwTitleCard).
    • Catalog band: the theme's catalog entries, each with what its pattern produced, and the missing ones marked, with "Make it" (fill_needs).
    • Comparing two styles: a drag splits the view between them, like a before-and-after wipe.
    • "Make this the show's style" is restyle_show. It stays a proposal even when the person asks for it, because it re-derives palettes and can move locked canon: it is reviewed with its before and after (M8).
  • Style lab (phase 5). The StyleSpec's dials in the schema's own groups: timing, line, shading, color, texture, motion, camera, post and design. Each dial is tagged from RwStyle.FIELD_USES:

    • live: the 70 engine fields and 13 character fields, redrawn at once;
    • on recompile: the 26 core fields, such as timing.preferredFps, timing.lipSyncStep and the camera language, shown through previews.get still on request;
    • reserved: none today; a field nothing reads yet would be hidden unless asked for.

    The lab works on a draft:

    • Local until saved. Changes stay local, the look book and the lab's frame follow them, and \ or the split compares before and after.
    • "Apply to Saturday Morning" commits revise_style. On the active style it is a proposal with its impact and locked canon, the rule the core already applies.
    • "Save as a new style" commits create_style.
    • The showrunner's proposals: "Make it more painterly" arrives as a proposal, which the lab draws as B against A.
  • Story map (the bible's first tab, phase 7). Four lenses, all 2D Control drawing:

    • People: characters as portraits (live heads) joined by their relationships, colored by kind. Clicking an edge shows its summary, dynamic and status. Dragging one character onto another starts a new relationship (write_bible_entry).
    • Arcs and threads: a grid of arcs, threads and gags against episodes. Cells show beats done, threads introduced and resolved, gags used (from memory's gag tracking). Dragging a beat into an episode's column sets its episode.
    • Places: sets joined by entrances[].leadsTo, with the lore about each.
    • Evidence: every entry's episode, scene and shot opens the timeline there (frame from episode.timeline).

    Memory (M10) stays its own section. The map links to its entries and reads their evidence.

Keys and mouse

These keys sit inside the editors and clash with nothing global (Ctrl+1 to 7, Ctrl+K, Ctrl+B, Ctrl+L, Ctrl+Z). They keep clear of what a browser keeps. On a Mac, Ctrl is Cmd and Alt is Option (RwShortcuts.shown). They join the shortcut sheet as "3D view", "Set explorer" and "Character studio" groups.

Keys and mouse What they do
Middle drag, or Alt+left drag; two fingers on a trackpad Orbit about the pivot
Shift+middle drag, or Alt+middle drag; Shift+two fingers Pan
Wheel (toward the pointer), or Alt+right drag; pinch Dolly
Hold right, then W A S D, Q E (down, up); wheel while held sets the speed; Shift for fast Fly
Click; Shift- or Ctrl-click Select; add to the selection or take away
F, Home Frame the selection, the whole set
T, then T again The plan from above; back to perspective
1 to 9; 0 or Esc Look through setup N (pilot); back to the free camera
O, C, L / Shift+L Overlays on or off; walls up, cutaway, down; next or previous lighting preset
Arrows (Shift: ×10) Nudge the selection 5 cm on the floor, relative to the view
, . (Shift: 1°) Turn the selection 15°
Ctrl+D, Delete Duplicate, delete (elements, marks, walkway points, setups)
Tab, Enter The next thing in the outliner; edit the selection in the inspector
\ Before and after: a proposal, the style draft, canon
Space Scene mode: play from the playhead. Studio: spin, or play the gesture or line
Esc In order: cancel a drag, eject from a camera, clear the selection, back to the list
Studio: drag, 1 to 5, Left/Right, Up/Down, B Turn; the sheet's angles; turn 45°; the expression before or after; blink

Performance budgets

Desktop (Forward+) Web (Compatibility, one thread)
A frame while moving (orbit, drag) 16 ms 33 ms
Render scale moving, at rest 1.0, 1.0 (up to 1440p) 0.5–0.75, 1.0 (up to 1080p)
Open a set (bundle, build, first frame) 1 s after the warm-up 2.5 s, behind a progress veil
One element edited (local rebuild) 30 ms 60 ms
Cards re-posed while moving every frame, up to 4; then one a frame one a frame, after a 15° change
Card viewport cap 2048 px 1024 px
Live characters in a view 12 8 (the rest as discs)
Thumbnails (expressions, try-on) 4 a frame 1 a frame
Memory for an open editor 1 GB 400 MB, freed on leaving
Drop to confirmed 300 ms 600 ms (the view never waits for it)

Each budget gets a probe in the editors' tours. A 360° orbit logs the 50th and 95th percentile frame times, and an edit logs its rebuild time. On the web the probe runs in headless Chromium on SwiftShader. That is slower than a person's GPU, so it guards against regressions rather than measuring real speed.

Protocol and schema changes

Change Where Why
scenes.get (read) {kind: "set"|"character"|"cast"|"style"|"pattern", target, style?, lighting?, change?} → {job, documents, diagnostics} session.schema.json, core/src/studio/scenes.ts, capability scenes One round trip gives a RenderJob of exactly what the engine reads: the set, its graphics, the style, the characters, props for the hands, and pattern samples not yet written. change gives a proposal's state, and style another style. It is written under .reelwright/scenes/<job id>/ (the newest few kept), and its assets reach a browser as playback's do.
tools.call keep: true ToolsCallParams, Studio.toolsCall Leaves proposals proposed. Today every proposal is approved as made: a vary through it applies its first option, which rejects the rest, then fails approving the second (change.not_proposed); a restyle would apply unseen.
Edit.was tools.schema.json Edit, core/src/studio/tools/edits.ts Refuses a stale edit with edit.stale when two people edit one document.
ChatContext.camera session.schema.json, core/src/showrunner/focus.ts "From here" means something.
StudioPage items (<section>/<doc>/<part>/<item>) #221's grammar, core/src/showrunner/page.ts Links to a setup, a mark or an expression.
edit_episode set_camera.newSetup core/src/director/edit.ts, tools.schema.json "Use this view for this shot" as one change.
voices.list (read), voices.audition (write) {character, text, emotion?, binding?} → {audio, visemes} core/src/studio/audition.ts over VoiceService, Kokoro, rhubarbLipSync Hear a voice and see it said before choosing it. Takes are cached by key like line takes, and nothing in the show changes.
changes.list document filter ChangesListParams Each editor's history without listing everything.
Comment anchor document gains pointer? and atM? comment.schema.json Pins on a mark, a setup or an expression (phase 8, after M13 phase 3).
ChatSendParams.attachments and image input in the agent session.schema.json, core/src/showrunner/agent.ts "Show the showrunner what I see" (phase 8).

Every schema change runs pnpm codegen and tools/check-all.sh. Every tool description change runs node tools/site/docs.ts.

Engine runtime changes are kept small and hold no clock:

  • RwSetBuilder.rebuild_element (with its children);
  • a max_height_px on RwCharacterCard for editors;
  • RwModelSheetTask.figure_lighting made public;
  • an active-view accessor on RwTimelinePlayer.

Phases

Each phase ships on its own, in both editions. Each is gated by tools/check-all.sh, then core and engine tests (each guard mutation-checked, as the repo does), then a live proof against the real daemon on the demo show, then the same in a browser. The order is by value to a showrunner.

Phase 1: the set explorer, looking (and editing by fields)

  • What a showrunner gets. Every set opens live in 3D in about a second. They can orbit, fly and look through each setup, switch lighting presets, see marks, walkways, entrances, cameras and frustums in place, and stand the cast on marks. The inspector edits any field of the selection by hand, without the showrunner: the first hand edits of a set.
  • Engine.
    • New: ui/view3d/rw_view3d.gd, rw_orbit_camera.gd, rw_view_overlay.gd, rw_view_pick.gd, rw_view_quality.gd; ui/editors/rw_editor_shell.gd, rw_scene_bundle.gd, rw_doc_buffer.gd; ui/widgets/rw_field_editor.gd; ui/editors/sets/rw_set_explorer.gd, rw_set_outliner.gd, rw_set_inspector.gd.
    • Changed: ui/rw_show_browser.gd (the host), ui/rw_document_view.gd (embeddable as Fields, without its own preview), ui/rw_shortcuts.gd, ui/rw_project_state.gd (the editor's place: tab, camera, selection), app/rw_studio.gd (chat_context asks the editor).
  • Core. core/src/studio/scenes.ts, the scenes.get method in studio.ts, api.ts and session/daemon.ts, ChatContext.camera, and the camera line in focus.ts.
  • Tests.
    • Engine:
      • tests/ui/rw_orbit_camera_test.gd: orbit, pan, dolly and frame are exact; a fly stays above the floor.
      • rw_view_pick_test.gd: a click picks the helm console, a mark and a setup on the bridge.
      • rw_view_overlay_test.gd: projection, faint behind a wall.
      • rw_set_explorer_test.gd: fake daemon, recorded scenes.get; an inspector edit sends the expected revise_set and a refusal rolls back.
      • tests/render/rw_look_through_test.gd: looking through each bridge setup equals RwSetViewsTask's image byte for byte.
    • Core: core/test/scenes.test.ts: the bundle embeds exactly the set's graphics and the active style; change gives the proposed set; a viewer may read it.
  • Live proof.
    • tools/explorer-watch.sh (engine/tests/support/rw_explorer_watch.gd) against the real daemon on a copy of the demo: open the bridge; screenshot a free view, the plan and each setup; orbit 360° and log the frame times; change a mark's facing in the inspector and check the file; Ctrl+Z and check the bytes are back.
    • In a browser, tools/web/shots.ts opens the explorer and looks through wide; the live frame against the setup's still must agree within 2 levels on average, with a clean console.
    • rw_studio_tour.gd captures the explorer at 1600×900 and 1280×720.
  • Conflicts. It lands after #216 part A, #220 and #221 (see below), and builds on their RwDocumentView, RwStudioRoute and ChatContext.

Phase 1: what was built

Built 2026-10-05, in both editions.

  • Core. scenes.get (core/src/studio/scenes.ts, kind set; style, lighting and change as above) writes a set_views job under .reelwright/scenes/, the newest six kept, embedding the set, the graphics its elements show, the style and the show's cast. It is a read, so viewers and reviewers may ask; every daemon offers it as the scenes capability. ChatContext.camera and the brief's camera line were already there from #221.
  • Engine. As planned, under ui/view3d/ and ui/editors/, with ui/widgets/rw_field_editor.gd; RwComposition.resize, RwJobDocuments.design_of, walkways and mark heights in RwStagePlan, and RwFiles.fetch_assets (taken out of RwLivePlayer, which uses it too). RwStudioRoute.parse keeps the parts after a document's id (deeper), major drops them, so looking through a setup, selecting and moving the camera replace the history's entry; going elsewhere in the open set (Back, a link) does not build it again.
  • Proofs. rw_look_through_test.gd holds every bridge setup byte for byte against RwSetViewsTask on the same job. tools/explorer-watch.sh (the desktop Studio against the real daemon) and tools/web/explorer-check.ts (the web export in headless Chromium against reelwright host) open the bridge, orbit, look through two setups, toggle the overlays, switch to a second lighting preset (each adds a red alert to its copy of the bridge first: the demo's sets have one preset each), turn the helm mark in the inspector, find it in the file and History, and undo it back to the same bytes.
  • Measured (2026-10-05; the machine's load swung from 3 to 24 on 24 cores, and the times with it):
    • Desktop, Forward+ on the RX 6700 XT in Xvfb, a 360° orbit of the bridge in Studio with the chat open: 12.3 ms a frame at the median and 13.9 at the 95th at 1600×900 when the machine was quiet, 16 to 21 ms and 20 to 40 ms when it was busy (the moving scale then stepped down to 0.75 by itself); 15.6 and 19.9 ms at 1280×720. Two 2D cards on the marks raise the 95th to 60 to 97 ms: a card drawn at a new angle cost 17 to 60 ms here, so the desktop, too, re-poses one card a frame, after a 15° turn, not every card every frame.
    • Web, headless Chromium at 1440×900, the rail and the chat folded: on this machine's GPU (ANGLE on Vulkan) the bridge opened 4.5 s after its link was followed, the page and the engine loading included, and a 360° orbit ran at 16.7 ms a frame at the median and 17.0 at the 95th, the display's own rate, so the moving scale rose to 0.75 (542×540). On SwiftShader, as CI has it, it opened in 5.7 to 7.1 s and orbited at 92 to 126 ms a frame at the median and 141 at the 95th, the moving scale at its floor; Studio redrawing its own controls alone, the 3D view still, took 85 to 89 ms a frame there, so on SwiftShader the page's own redraw, not the set, is most of the frame, and the numbers guard against regressions rather than measure a person's speed.
    • The key light's shadows were half the frame on a software renderer (96 ms against 48 at 720 px, Compatibility on llvmpipe), so a browser draws a move without them; at rest the frame is exact.
  • Changed from the plan.
    • A free camera narrower than the show's frame keeps the frame's height of view and crops the sides: fitted across, as setups are, a tall pane gaped 115° up and down.
    • The explorer starts free where the establishing setup stands, rather than framing the set.
    • Until the person sets them, the side panes fold for room: in Studio at 1280×720 with the chat open, the outliner folds and the view keeps about 350 px; vectors in the inspector are two numbers wide, so it stays about 300 px.
    • One selection at a time; History filters changes.list in Studio (the document filter is phase 3's).
  • Left for later. Walls up, cutaway and down; live time (practicals flickering, backdrops drifting); Shift- and Ctrl-click selection; the explorer's place (camera, selection, panes) kept across sessions in RwProjectState and RwUiSettings (the route keeps the setup and the selection for now); a browser parity check of the live frame against the setup's still; a progress veil spread over frames for big sets; rw_studio_tour.gd shots (the watch takes the explorer's at any size with --size).

Phase 2: the character studio

  • What a showrunner gets. Every character live: turned, emoting, mouthing, sitting, gesturing, walking, holding a prop, beside the cast, against canon. Expressions, mouths, wardrobe and palette are tuned by hand, and variations picked in place.
  • Engine. ui/editors/cast/rw_character_studio.gd, rw_turntable.gd, rw_expression_panel.gd, rw_mouth_panel.gd, rw_pose_panel.gd, rw_wardrobe_panel.gd, rw_palette_panel.gd, rw_props_in_hand.gd, and a time-sliced rw_thumbnail_queue.gd. RwModelSheetTask.figure_lighting is made public.
  • Core. tools.call keep; scenes.get kinds character and cast (props for the hands, the cast for the line-up).
  • Tests.
    • The studio at each of the sheet's angles equals the model sheet's turnaround panel.
    • An expression slider's release sends /expressions/<i>/<field>, or appends the entry.
    • The local preview equals the rebuild after commit.
    • No frame is over its budget while 21 thumbnails draw.
    • Try-on offers only values the part library draws for that field.
    • vary with keep shows four options; picking one approves it and rejects the rest.
    • Canon overlay alignment.
  • Live proof. tools/cast-watch.sh: open Otto, turn him, tune "happy" browRaise, check the file, Ctrl+Z, and pick from four sleeve variations. In a browser: the turntable's frame times, and a card's re-pose cost logged.

Phase 2: what was built

Built 2026-10-05, in both editions.

  • Core.
    • scenes.get kind character (core/src/studio/scenes.ts): a model_sheet job at the model sheet's size (1600 wide), embedding the character, the style, the rest of the cast for its line-up and the show's props for its hands; canon names the approved model sheet (.reelwright/approved/) when there is one. change gives a proposal's character, which is how each variation is drawn.
    • tools.call keep leaves the person's own change sets proposed: a try-on, a variation group. A variation group is now always left to pick from, keep or not: applying a person's vary had approved its first option, which rejected the rest, and then failed on the second (change.not_proposed).
  • Engine. Under ui/editors/cast/: RwCharacterStudio (on RwEditorShell, its tabs Studio and Fields), RwTurntable, RwCanonView, the panels (RwExpressionPanel, RwMouthPanel, RwPosePanel, RwWardrobePanel, RwPalettePanel, RwPropsInHand), RwThumbnailQueue and RwCharacterPainter.
    • RwModelSheetTask builds any of its panels alone (build, pose_figures, panel_rect_at); figure_lighting and style_for_sheet are public.
    • RwSceneProps.held_transform is how an episode holds a prop, and the studio holds one the same way.
    • RwFieldEditor gains a slider (previewed while dragged, saved on release) and a color.
    • RwViewQuality gains the thumbnail budget and clock_usec, the host's clock: Studio gives it, since the engine's own code reads no clock (tools/engine-check.sh).
    • RwSessionClient.hello_as names a person as the web service does: the watch looks at the show as a viewer with it.
  • How it draws.
    • The turntable is the episode's card (or 3D figure) in a world of its own, a camera 20 m off on a long lens straight at it, as the sheet's figure view has it. A card is lit white from where the sheet's light falls on its drawing; a figure and the props have the sheet's studio light.
    • Turning draws 15° steps, at most one a frame, and keeps each step it draws for that pose (a flipbook). Turned round again, it draws nothing; at rest it draws the exact angle. Anything that changes the drawing (a face, an outfit, a prop) starts it afresh.
    • Thumbnails (the 21 expressions, the mouth chart, the five angles, the try-on strip and the variations) are SubViewports that keep their drawing. One rig per strip moves from picture to picture, so a strip costs one rig and a face drawn per picture. They draw 4 a frame within 8 ms on the desktop and one a frame in a browser.
    • The Sheet view is the sheet's own turnaround, built on a viewport the sheet's size and shown cropped. It equals the model sheet's turnaround panel pixel for pixel. Drawn alone in a viewport the panel's size, six edge pixels of the silhouette lines came out otherwise.
  • Proofs.
    • engine/tests/ui/rw_character_studio_test.gd (18), rw_thumbnail_queue_test.gd (6), engine/tests/render/rw_canon_view_test.gd (2: the Sheet view equals the sheet's turnaround with a max delta of 0; canon matches as approved and shows a cap since), and engine/tests/app/rw_studio_cast_test.gd (5: the route, Back, a page path, Ctrl+Z, a picked variation to undo).
    • Core: core/test/scenes.test.ts (3 more) and core/test/tools-keep.test.ts (3).
    • Each guard was mutation-checked: 21 mutations, from a slider that appends instead of setting to a held prop off its grip, each failed a test.
    • tools/cast-watch.sh (35 checks) runs the desktop Studio against the real daemon:
      • opens Otto, drags him round, spins him twice and keys him to the 3/4 and the side;
      • sets happy's brow raise and sad's smile by their sliders, each found in the file and in History;
      • tries a cap on (kept back, the file unchanged), keeps it, picks one of four costume variations (the other three rejected) and undoes the pick with Ctrl+Z, the file back byte for byte;
      • puts the kettle in his right hand, lays his approved sheet over the turnaround ("Moved since it was approved: 10%", the cap), and opens him as a viewer, whose controls say why they are off and whose edit the daemon refuses too.
    • tools/web/cast-check.ts runs the same in the web export against reelwright host, with Vi as an invited viewer.
  • Measured (2026-10-05, the machine at a load of 27 to 48 on 24 cores, so every time is high):
    • Desktop, Forward+ on the RX 6700 XT in Xvfb, Studio at 1600×900 with the chat open (the stage 467×797), over two runs:
      • a first spin: p50 16.8 to 26.5 ms, p95 91 to 112 ms; each of its 19 new steps (5 were kept from the drag before it) took p50 45 to 54 ms to draw, p95 98 to 109 ms;
      • the second spin drew nothing: p50 10.8 to 17.9 ms, p95 26 to 34 ms;
      • a drag: p50 21 to 26 ms, p95 76 to 93 ms.
    • At 1280×720 (the stage 353×603, the outliner folded): the first spin p50 17.5 ms and p95 55.8 ms (steps at p50 30.6 ms), the second p50 15.3 ms and p95 21.9 ms.
    • Thumbnails. The 21 expressions drew one a frame, in 35 frames. A face took p50 15 to 31 ms and p95 37 to 91 ms, over the 8 ms slice, so no frame ever drew a second one.
    • Web on the GPU, headless Chromium at 1440×900 (ANGLE on Vulkan on the RX 6700 XT, the rail and the chat folded, the stage 512×510). Otto opened 8.5 to 9.8 s after his link was followed, the page and the engine loading included. The first spin ran at p50 16.7 ms (the display's own rate) and p95 97 to 129 ms; its 24 steps took p50 36 to 57 ms each. The second spin: p50 16.7 ms, p95 16.7 to 17.8 ms, nothing drawn.
    • Web on SwiftShader, as CI has it: open in 13.3 s. The first spin: p50 132 ms, p95 142 ms, its steps at p50 59 ms and p95 145 ms. The second: p50 115 ms and p95 142 ms with nothing drawn, so there, as phase 1 found, the page's own redraw is most of the frame.
    • Canon over the turnaround said "Moved since it was approved: 10%" after the cap. The cap's brim widens the profile, so the sheet packs every angle a little differently.
  • Changed from the plan.
    • The turntable draws the episode's card, not the sheet's 2D rig, so props are held there as an episode holds them. The sheet's exact drawing is the Sheet view.
    • Canon is laid over the Sheet view's turnaround, with a share of pixels that moved, and the locked fields are listed in the outliner's Canon box and marked on each control.
    • A try-on is a proposal kept back (keep), kept or taken off, as Variations are.
    • A cut is offered by the slot it fits (the studio's own table, SLOT_CUTS), then by what the part library draws. A field the library does not list offers every value the schema allows.
    • A color is saved when its picker closes; the drawing follows then, not while picking.
  • Left for later.
    • A look target dragged in the view, and "Say it".
    • The card and the 3D figure side by side, with "Draw them in 3D".
    • The line-up beside the cast, and a set as the backdrop.
    • A face costs more than the 8 ms slice on a loaded desktop: thumbnails draw one a frame there. Making the head cheaper to draw again is the fix.
    • Varying pointers beyond the four Variations… offers (costume, palette, hairstyle, height).

Phase 3: editing a set in 3D

  • What a showrunner gets. Move marks, furniture, cameras, entrances and walkway points with gizmos, light the set by dragging the key, save a view as a setup, and see the showrunner's proposed set as a ghost.
  • Engine.
    • ui/view3d/rw_gizmo.gd, ui/editors/rw_ghost_layer.gd, and the explorer's edit modes.
    • runtime/stage/rw_set_builder.gd: rebuild_element.
    • RwCharacterCard.max_height_px.
  • Core. Edit.was in tools.schema.json and edits.ts; the changes.list document filter.
  • Tests.
    • For every element of both demo sets, geometry_hash after rebuild_element equals a whole build's.
    • Each gizmo drop sends the expected edits.
    • A stale index is refused by the core and sent once more, by id, by the editor.
    • A locked pointer is refused before sending, and a viewer sees no gizmo.
    • A ghost draws the proposal's set.
  • Live proof.
    • Drag the helm mark 30 cm; the set view of otto_front renders again and matches the look-through. Move wide's camera, then Ctrl+Z both.
    • On a real host, two people move marks in one set: one stale edit is refused and retried (tools/web/collab-check.ts style).

Phase 3: what was built

Built 2026-10-05 and 06, in both editions.

  • Core.
    • Edit.was (core/src/studio/tools/edits.ts): what a field held as the editor saw it, as JSON text, null for nothing there, compared as values (Godot's 180.0 is 180). When a field holds something else, the whole call is refused with edit.stale, saying what is there now, and nothing is applied. An edit whose value is its was only checks: an editor guards an item by its id that way, and the change neither names the check in its title nor lists it. was is Studio's: the strict flavor of the tools leaves it out (studioOnly in core/src/schema/bundle.ts), so the showrunner's strict budget stays at 22 of 24 optional parameters.
    • changes.list takes document. History asks for the set's changes alone, and the explorer for the proposals waiting on it.
  • Engine runtime. RwSetBuilder.place_element (a move: the element under its parent, its children with it, the practicals placed again) and rebuild_element (its geometry and graphic built again, its children kept, the wild walls and screens listed in element order, the interior's shell repainted when the structure gives it another color). For every element of both demo sets, and after a move, a resize, a moved practical and a repainted wall, the geometry hash, the wild walls, the screens and the practicals equal a whole build of the edited set (engine/tests/stage/rw_set_rebuild_test.gd).
  • Engine UI.
    • ui/view3d/rw_gizmo.gd (RwGizmo, pure): what a move, a lift, a turn, an aim or the key light's drag changes, as edits with their was and their item, and in words. What is held last snaps (a 5 cm grid for moves and lifts, 15° for turns or 1° with Shift, 15° round and up for the key light) and everything else moves by as much; Ctrl snaps nothing; a number typed is the move in meters along x (or z) or the turn in degrees, exactly. An element moves in its parent's turned frame, a child held with its parent moves once, several things turn about the one held, and a camera moves with its aim and turns about itself.
    • The explorer: handles drawn by RwViewOverlay (a turn knob on a ring, a lift arrow, a camera's look-at diamond, a walkway's points, the key light's sun; a lock where a lock holds); a drag on a thing moves it, on a handle does what the handle does; G and R follow the pointer, X and Z hold an axis, a number is typed, Enter or a click keeps it and Esc or the right button drops it; the arrows nudge, , and . turn; the wheel over a camera steps its lens through the common primes; a double-click on a selected walkway adds a point and Delete takes away what is selected. Shift- and Ctrl-click, and the outliner, choose several. Looking through a setup with nothing selected, G and R move and turn that camera by the pointer across the screen, the view with it.
    • Each preview is the document with the gesture's edits on top, shown through the same path a commit takes (_show_doc): moved elements are placed, changed ones built again, a lighting change relit, and only a change to the stage, palette, pieces or backdrops (or elements added, removed or reordered) builds the whole set. The frame keeps its size while a mark or a camera's glyph is dragged (RwView3D.set_timing), and draws at the moving scale while a piece or a light moves.
    • RwDocBuffer sends each edit guarded: a check that its item is still the one by that id, then the edit with its was. On edit.stale it reads the document again and finds each item by its id: where only its place moved, the edit goes once more there; where its field changed, the edit is refused in words naming the change that stands, when the daemon said whose ("Ada changed Marigold - Bridge. Yours was not applied, so theirs stands.").
    • "Save view as setup" asks for an id (the next free view_N offered) and what it is for, appends the setup where the camera stands (rounded to 1 cm, its lens to 0.1 mm), and looks through it.
    • ui/editors/rw_ghost_layer.gd (RwGhostLayer): a showrunner's proposal on the set, drawn from scenes.get with change: what it would move or add outlined dashed with a translucent fill and a dashed line from where it stands, a camera's frustum dashed, what it takes away in the danger color, the key light's new way. A banner says what it is and what it changes, with Before / after (\), Review, Reject and Accept in place. A person's own change, which passes through proposed on its way to applied, is never drawn.
    • ui/editors/sets/rw_set_walls.gd (RwSetWalls): walls up, cutaway (each wall whose outer face is toward the camera, and the doors and windows standing in it) or down (and the interior's shell), on C and in the toolbar. Looking through a setup draws them up. Picks and the overlay see through what is left out.
    • Kept between sessions: the explorer's place in each set (the free camera's pose, the setup looked through, the selection, the walls) in RwProjectState sets, and each editor's panes (widths and folds) in RwUiSettings.editor_panes, which a browser keeps with the rest of the layout. rw_studio.gd changed by ten lines.
    • chat_place lists everything selected, a few words each.
  • Changed from the plan.
    • Snapping is held off with Ctrl, not Alt: Alt+left drag orbits.
    • A move snaps the thing held to the world's grid, not the move's length: positions in the file come out round (0.3, not 0.3127).
    • Every row of the inspector fits its 300 px. A mark's "Who stands here" widened to the longest name and a setup's lens presets were words, so the pane grew when they were selected, and the view shifted under the drag that selected them (a 28.7 cm drag landed at 35 cm); now the choice keeps its width and the presets are an icon.
    • A number field no longer rounds to its arrows' step: a seat 0.23 m up showed 0.25 and was saved as 0.25 with any change to its neighbors. The arrows step by the hint; the value keeps every digit to a tenth of a millimeter.
    • Only the showrunner's proposals are ghosts; another person's edits arrive applied.
    • RwCharacterCard.max_height_px was not needed: the cast stays on the phase 1 budget.
  • Proofs.
    • Core: core/test/edit-was.test.ts (7), mutation-checked (7 of 7 killed).
    • Engine: engine/tests/stage/rw_set_rebuild_test.gd (6, 7 mutations killed), engine/tests/ui/rw_gizmo_test.gd (9), rw_set_editing_test.gd (19), the explorer's own (14) and Studio's (rw_studio_explorer_test.gd, 5), rw_project_state_test.gd, rw_ui_settings_test.gd and rw_shortcut_sheet_test.gd. 49 mutations of the gizmo, the buffer, the explorer, the walls, the ghost, the fields and what Studio keeps each failed a test.
    • tools/explorer-watch.sh (37 checks) against the real daemon: a mark dragged 28.7 cm in the plan lands at x 0.3; the helm console dragged 50 cm is placed every frame and never built; wide turned by its knob lands on a 15° step; the view saved as low_angle and looked through; Ctrl+Z four times puts the file back byte for byte; the walls cut away and come down from outside; then a second client on the same daemon asks for a change as the showrunner (drawn as a ghost, flipped, accepted in place), moves the galley mark while the explorer has it in hand (refused, theirs stands) and removes door_in before it (the next move goes where galley went). Since 2026-10-06 (42 checks) it stops the daemon twice while Studio starts it again: the bridge opened while it is down opens once it is back, and the bridge open reads the helm moved in the file meanwhile, building nothing again.
    • tools/web/explorer-check.ts does the same in the web export against reelwright host, with Bo, an invited editor, on his own socket as the second person (31 checks): his first race is refused in his name ("Bo changed Marigold - Bridge. Yours was not applied, so theirs stands."), his second sends Ada's move where galley went. Since 2026-10-06 (40 checks) it bounces Ada's socket: the page opened at the bridge's address while it is down opens the bridge live once it is back, and Bo's move made while it was down is read again without building the set again.
  • Measured (2026-10-06):
    • Desktop, Forward+ on the RX 6700 XT in Xvfb, Studio at 1600×900 with the chat open, the machine at a load of about 10: a mark dragged in the plan p50 9.4 ms, p95 29.5 ms (the frame keeps its size, 430×720); the helm console dragged p50 16.2 ms, p95 32.9 ms at the moving scale (322×540), placed nine times at most 3.1 ms each, the set never built; wide's knob p50 10.6 ms, p95 36.1 ms. At a load near 30 the same drags ran p50 33 to 43 ms and p95 134 to 139 ms, the placing at most 18 ms.
    • Web on the GPU, headless Chromium at 1440×900: both drags at the display's own rate, p50 and p95 16.7 ms, the seat placed at most 3.4 ms a frame; the bridge opened 5.6 s after its link was followed.
    • Building one element again (rebuild_element, on the GPU box): the bridge's 1 to 18 ms; the engine room's 2 to 139 ms, and 436 ms for its core.
  • Left for later.
    • The set view of a setup rendered again after an edit and compared with the look-through: the builds are proven equal by their geometry hash, and the look-through equality test holds for an unedited set.
    • Duplicate (Ctrl+D), sizing a primitive by a handle, and each practical's intensity in the view (the inspector has them all).
    • Lens on the wheel commits one change a step; History does not yet group a run of edits.
    • Rebuilding the engine room's core costs about 436 ms (its recipe's ink creases, to check).

Phase 4: the scene in 3D (previs)

  • What a showrunner gets. The episode at the playhead from any angle, played live; blocking and props placed in 3D; "Use this view for this shot".
  • Engine. The explorer's scene mode, a button on ui/timeline/rw_timeline_stage.gd, and runtime/player/rw_timeline_player.gd (the active view's accessor). RwStageModel is reused as it is.
  • Core. edit_episode set_camera.newSetup in core/src/director/edit.ts.
  • Tests.
    • A 3D drop makes the same edit_episode edit as the same 2D drop.
    • Pilot at a frame equals the timeline's frame.
    • newSetup writes the set and the episode in one change, undone at once.
  • Live proof. M7's stage watch-it repeated in 3D: the Chief walked to center mid-scene, a facing turned, the camera orbited, and a new low angle used for sh020 and rendered.

Phase 4: what was built

Built 2026-10-06, in both editions.

  • Core.
    • edit_episode: set_camera takes newSetup ({id, positionM, lookAtM, lensMm, rollDeg?, description?}), and set_mark and set_move take newMark ({id, positionM: [x, z], facingDeg, posture?, description?}). setsWithNewPlaces (core/src/director/edit.ts) writes the setup or the mark into the set the scene is played on. The edited episode is compiled on that set (CompileOptions.sets, and loadEpisode given sets). The tool proposes one change with both files, so one changes.undo takes both back. A new setup puts the shot on {type: "setup"} and drops its move unless one is given: the frame is the view.
    • Refusals:
      • episode.bad_edit for an id the set already uses (a mark or an entrance for a mark), a newSetup beside a size, setup, subjects, insert or turn, and a newMark whose id is not the mark the edit goes to;
      • change.locked for a set locked where the place would go;
      • episode.edit_breaks for a move that breaks the cut (recut covers the scene again on the new mark).
    • The page path timeline/<episode>/previs keeps its part, and the brief says the person is looking at the episode in 3D at the playhead, where the shot's camera can be piloted and anyone blocked.
  • Engine runtime (no clock):
    • RwTimelinePlayer.aim: a host's camera, called once the shot's camera has aimed the view that shows the frame's shot. The stage is readied for it and that view is shown alone, with no transition, fade or super.
    • active_view() and resize().
    • RwSceneStage.held_cards: cards left as last drawn while a view moves.
  • Engine UI.
    • ui/editors/scene/rw_scene_previs.gd (RwScenePrevis, on RwEditorShell) and rw_previs_overlay.gd (RwPrevisOverlay, on RwViewOverlay).
    • RwLivePlayer gains aim, resize, redraw, opened and closed.
    • RwView3D.open_live shows a live player's frame through the camera of the view showing the frame's shot, sized by the view, and keeps the last frame, still, while the episode is read again.
    • In the timeline, previs takes the picture's place under the tracks: the 3D button in the picture's and the stage's corner, or Shift+V. The route is timeline/<episode>/previs, and …/previs/free for a free camera. RwStudioRoute.major keeps previs, so going into 3D is a new entry and its camera replaces it.
  • How it draws.
    • Previs plays the timeline's own RwLivePlayer, so the playhead, play, its sound and the shot boundaries are the timeline's, and the episode is built once.
    • Through the shot, nothing is changed: the frame is the one the timeline shows.
    • Piloting or free, the player's aim puts the camera there before the stage is readied, once a frame, playing or not.
    • While the camera moves and the episode stands still, at most one card a frame is drawn again (the one turned furthest round, past 15°); at rest every card is exact.
    • The camera taken over is aimed at the pose it was taken at, exactly, until it moves. A used view takes its rounded pose (1 mm, 0.1 mm of lens) before anything is sent. So the compiled shot is the piloted frame to the bit.
  • Changed from the plan.
    • Previs is its own editor on the shared foundation, over the timeline's tracks, not a mode of the set explorer (2,800 lines), and it does not open in Sets.
    • The 3D button is in the picture's and the stage's corner rather than in the header's Picture / Stage / Both switch: a fourth button there cut the episode's title on a laptop.
    • Blocking is the stage's drop (RwStageModel.reblock), as planned. A drop on open floor makes a new mark, spot_N, on the 5 cm grid (Ctrl: none), facing the way walked. The facing knob, the camera dragged round its subject and props from the shelf are not in 3D yet.
    • "Use this view" drops the shot's move unless one is given, and names the setup <shot>_view.
    • The route names previs and its free camera, not the shot or the frame.
  • Found by the live runs.
    • Two readings of the episode close together (an edit applied, then Ctrl+Z) left it closed: the second opening gave up while the first, outdated, was under way. It now waits its turn. The timeline's own picture had the same fault.
    • In a browser, a link straight to 3D asked for it before the episode was read, and it stayed empty. 3D now opens once the episode is read, and the camera the address names is taken then.
    • Opened again after an edit, the player shows frame 0 before the timeline puts it back at the playhead. That frame took the camera taken over away; frames shown while it opens are now passed by.
    • A Vector3 holds 32 bits: 0.656 was sent as 0.65600001811981. The numbers saved are now doubles.
    • With the 3D view focused, Left and Right moved the focus instead of the playhead. Previs now steps the timeline with them.
    • The view went black while the episode compiled again. Its last frame now stays, under a veil.
    • The view's words (a refusal in red) were lost over a bright frame. They now sit on a card.
    • The timeline kept every set's plan it had read for good, so its stage showed a mark moved in the set explorer where it was. A change or a doc.event naming a set now drops it.
  • Proofs.
    • Engine:
      • engine/tests/ui/rw_scene_previs_test.gd (15);
      • engine/tests/app/rw_studio_previs_test.gd (4);
      • new cases in rw_live_player_test.gd (2), engine/tests/player/rw_timeline_player_test.gd (2) and rw_studio_history_test.gd (1), and the shortcut sheet's.
      • 32 mutations of the player's hook, the held cards, the live player, the view, previs, the timeline's rereads and Studio's route each failed a test. Two survived a first pass: the double reread test now holds the replies in the order that breaks it.
    • Core: core/test/edit-new-places.test.ts (12) and page-brief.test.ts (1 more), 21 mutations, each killed.
    • Through the shot, previs draws the timeline's frame byte for byte, after an orbit and after the plan from above too.
    • tools/previs-watch.sh (20 checks) runs the desktop Studio against the real daemon:
      • Shift+V opens the Lighthouse at sh020 in 3D;
      • Esc ejects (the address replaced) and a 360° orbit is timed;
      • P and a drag take sh020's camera over, the wheel dollies it and Shift+wheel changes its lens, and the pilot is timed;
      • U writes sh020_view into the bridge and puts sh020 on it, one change;
      • the frame compiled again through sh020's own camera equals the piloted one, max delta 0;
      • the daemon's still of that frame (720 lines, scaled to 540) agrees, a mean of 2.17 levels apart;
      • Ctrl+Z puts both files back byte for byte;
      • Otto, dragged at frame 380 to open floor, gains spot_1 and a walk to it, the timeline reads it back, and played on, she walks there.
    • tools/web/previs-check.ts (24 checks) does the same in the web export against reelwright host, from a link to …/previs, on the GPU and on SwiftShader. The frame through sh020's own camera after U is the piloted one, max delta 0. Both are read from the engine's canvas as drawn: on SwiftShader the rounded pose is on screen for two frames before the veil, too brief for a screenshot. Vi, a viewer, pilots to look, and her U is refused in Studio's words.
  • Measured (2026-10-06, the machine at a load of 69 to 162 on 24 cores from other runs, so every time is high):
    • Desktop, Forward+ on the RX 6700 XT in Xvfb, Studio at 1600×900 with the chat open (the view about 440 px wide on screen):
      • a 360° orbit: p50 44.2 ms, p95 135.2 ms, drawn 544×472;
      • a 20° pilot: p50 36.9 ms, p95 77.1 ms, drawn 718×404;
      • the mouse's orbit drag p50 42.7 ms, p95 125.9 ms; its pilot drag p50 43.1 ms, p95 135.5 ms;
      • a run at a load of 84 to 123: the orbit p50 94.2 ms, p95 141.1 ms, the pilot p50 60.8 ms, p95 127.1 ms. The times follow the machine's load more than the view.
    • Web on the GPU, headless Chromium at 1440×900 (ANGLE on Vulkan, the rail and the chat folded):
      • 3D opened 38 s and 70 s after the link was followed (two runs): the page, the engine, and the service compiling the episode;
      • the orbit's halves p50 23.5 to 39.1 ms, p95 130 to 137.5 ms, drawn 670×338 and 536×270;
      • the pilot p50 17.9 and 20.4 ms, p95 79.1 and 78.9 ms, drawn 480×270.
    • Web on SwiftShader (no GPU, as on CI): the same checks hold. 3D opened 122 s after the link. The engine timed the orbit's halves at p50 133.7 and 133.1 ms (p95 143.2 and 144.7 ms) and the pilot at p50 133.7 ms (p95 142.7 ms), but the page drew a frame only every 0.55 to 1.57 s (p50, three runs): enough to prove previs, not to work in.
  • Left for later, then built 2026-10-07 (below) but for the last:
    • The facing knob (set_look) and the shot's camera dragged round its subject (set_camera yawOffsetDeg) in 3D, as the stage has them; props from the show's shelf (place_prop).
    • Walls cut away in previs, as the explorer has them.
    • The pilot's lens and pose kept when the playhead leaves the shot (the next shot is seen through its own camera).
    • A set's own props (SetInstance.props, the open question of the risks): still dressing as elements.

Phase 4's leftovers: what was built

Built 2026-10-07, in both editions, with adding on the timeline (the product audit's finding 7, docs/audits/product-audit-2026-10.md).

  • The facing knob. The one selected in previs has a knob on the floor, out from their feet the way they face (RwPrevisOverlay.knob_of). Dragged round, they turn to face whoever or whatever is nearest that way (turn_to: RwStageModel.look_target, then face, the stage's own set_look), the line drawn to it; nothing that way is refused. A press on someone's body drags them, not a knob behind them.
  • The shot's camera round its subject. In a free view the shot's camera, drawn where it stands, drags round what it frames at its own reach (orbit_to: RwStageModel.orbit, the framing's yawOffsetDeg), the place it would stand drawn as it goes.
  • Walls. In a free view, walls up, cut away where they stand between the camera and the room, or down (C, and the Walls choice beside the overlays), as the set explorer decides them (RwSetWalls). They are applied after the live player has drawn the frame, playing or not. Through the shot or piloting they are always up, as the shot is filmed; closing previs puts every one back, so the timeline's picture has them all.
  • The shelf. The outliner's Shelf lists the show's props (library.list). One dragged onto a piece of the set or a mark stands there from the start of the scene (drop_prop, place_prop); dropped anywhere else it is refused.
  • The pilot across a cut. Piloting carries on into the next shot with its pose and lens, for U to give that shot the view; P looks through its own camera.
  • Words on a card. What a 3D view says at its foot sits on a card (RwUiKit.view_message), in previs and the set explorer: a refusal in red was lost over a bright frame.
  • Proofs. rw_scene_previs_test.gd (the knob, the camera dragged, the walls, the shelf, the card, the pilot across a cut, a viewer turning no one): 9 mutations of them, each killed (two only once the camera test checked where the dragged camera is drawn and that a piloted view has none to drag).

Adding on the timeline (the product audit's finding 7)

Built 2026-10-07, in both editions. The timeline could change and delete but add nothing.

  • Ways in. A and a letter at the playhead (S sound, M music, F effect, G gesture, E expression, L look, P posture, I lighting, C camera move, N shot, D line, T title card, and since then W a lower third, docs/design/lower-thirds.md); a right-click on a track or a lane (the playhead moves there) for what that row can hold; the "+" in the tracks' corner for all of it, with "For someone else"; and the library (the box beside it), the show's sounds, music, effects, props and the cast's gestures (library.list, the rig's gesture library), to filter by typing, drag onto a row where it should start, or add at the playhead with Enter. The header had no room left on a laptop, so both sit in the corner.
  • What is sent. One edit_episode change of the person's own (Ctrl+Z), rationale "Added at the playhead on the timeline." (or "Dragged from the library onto the timeline."). A cue takes an id the scene has not used, so the block it makes is found again, and is timed with the latest line or business started by the playhead (RwTimelineModel.anchor_at, moved here from the stage), as the stage's own edits are; what it is defaults to what fits there (the last sound, a nod for who speaks, the set's other lighting, someone else to look at) and the inspector opens on it once the episode is read again. A camera move starts at the playhead (startOffsetSec) and lasts the shot; a shot that moves already is selected instead. N is a cut there. T adds the title card or the credits, now blocks on the Shots track with their length and the credits' lines in the inspector. Music opens Music and room tone….
  • A line. D asks who says it, the words and the feeling. add_line (new) puts it after the line or business under the playhead once that ends, so what followed waits for it, and writes it into the screenplay in the same change, under the name its speaker has there, after the dialogue of the line before it, moving every line below down (core/src/director/new-lines.ts). Words the screenplay would read otherwise (a parenthetical) are not written there, and said. It plays silent, timed from an estimate, until voiced.
  • Proofs. engine/tests/ui/rw_timeline_add_test.gd (12); core/test/add-line.test.ts (8), core/test/show-library.test.ts (1); 27 engine mutations and 19 core ones, each killed. tools/timeline-add-watch.sh adds a sound (dragged from the library), a gesture, a camera move, a lighting change and a line at points through the Lighthouse against the real daemon: each in the file, read back where it was added and open in the inspector; played, the picture the same just before each and changed from it, the clank heard from its time; Ctrl+Z five times puts the episode and the screenplay back byte for byte. tools/web/timeline-add-check.ts does the same in the web export against reelwright host, and Vi, a viewer, adds nothing.
  • Left for later. A prop dropped on a lane is picked up from where the scene has it, or held; handing it over or using it is not chosen on the timeline yet. New sounds and effects are made in their own sections, not from the timeline.

Phase 5: look book and style lab

  • What a showrunner gets. A style on their own cast, sets and episodes, two styles split side by side, and dials that act live, without any render.
  • Engine. ui/editors/world/rw_look_book.gd, rw_style_lab.gd, rw_style_dials.gd, and a split compositor over two RwView3Ds.
  • Core. scenes.get kinds style and pattern (catalog samples), and restyle_show through tools.call keep.
  • Tests.
    • Every StyleSpec field has a dial or is reserved; the test keeps it in step with FIELD_USES, which a test already keeps in step with the schema.
    • The draft sends nothing until Apply.
    • Apply on the active style is a proposal with locked canon in its impact.
    • The split's two halves come from the two styles.
  • Live proof.
    • Thicken Saturday Morning's line live and Apply; the look book's frame matches a still rendered after.
    • Compare Saturday Morning with Bright Modern on the Lighthouse's hero frame in a browser.

Phase 5, its core: what was built (task #224)

Built 2026-10-05 and 06, in both editions, ahead of phases 3 and 4. The owner: "right now creating a show gives you two styles to pick from. That's absolutely stupid. Part of the show is working with the studio to build and create the style from scratch."

  • A style is made, not picked.
    • New show asks how the look starts: described in words (the default), from pictures, from a blank, or from one of Reelwright's styles as a starting point only.
    • A blank show now holds one style of its own, style.<show>, a draft named after the show. Reelwright's styles are no longer copied in.
    • The show is made at once. Once it is open, the style lab opens on its style: pictures are added (takes.upload, then styles.reference), and words or pictures bring four directions to pick from.
    • init --blank takes --style-from words|pictures|blank|preset, --look and --reference; the service takes styleFrom and styleWords.
    • World's New style makes one from scratch in a show already made: a draft from the blank look, through create_style, opened in the lab.
  • Core (core/src/style/):
    • blankStyle reads the library's new Blank look; styleFromPreset copies a built-in style under the show's id.
    • styleDirections: ten looks Reelwright knows (Bold ink, Soft watercolor, Flat graphic, Noir, Storybook, Retro tape, Neon night, Pastel cozy, Comic halftone, Soft clay), ranked by the words and the theme, distinct, steered by color words and the pictures' hue.
    • readReferences and styleFromReferences: a k-means palette in OKLab, the color rules and the grade from the pictures.
    • blendStyles, and modelDirections for the show's model when one is set up.
    • Tools: propose_styles (two to four directions as one variation group) and blend_styles; create_style and revise_style as before, with schema validation and a warning for a typeface neither bundled nor the style's own.
    • Typefaces of the show's own (StyleSpec 1.1, product audit #19): styles.font freezes a TrueType, OpenType or WOFF file in the show's assets with the license the person brings it in under (OFL-1.1, Apache-2.0 or LicenseRef-owned), and adds it to typography.fonts as their own change; display, body and signage may then name it by its font.<slug> id. revise_style raises a style that has them to 1.1. The engine draws each from its file (its bold from weight 600 when it has one), and a render, the web and a runner carry the files with the job.
    • scenes.get kind style: a stills job of one look-book frame, the show's cast (up to three) in one of its sets, or stand-ins (the swatch's sitter and two drawn people in the swatch corner, its screen showing the show's title in the display and signage faces).
    • Page paths world/styles/<id>/<part>, a brief of the part in view, suggestions that fit it.
  • Engine (ui/editors/world/): RwStyleLab on RwEditorShell, RwStyleLook, RwStyleSplit, RwStyleSpecimen, RwStyleDials, RwStyleInspector and RwStyleTryOns. RwGraphicFonts bundles Noto Sans, Serif, Serif Display and Sans Mono beside Inter.
  • How it draws. Each frame is the look job's Timeline played by the episode's own player at its still frame, in a SubViewport of its own, with the style swapped. A dial dragged builds it again no more often than every 0.12 s, and no sooner than twice what the last build took (up to 0.5 s), so the frames between stay light on a slow machine; one the post pass alone draws (the grade, paper, grain, halftone, vignette, weave, fringes, a CRT) sets RwPost again and draws with nothing built. The before is a second frame: the style as it was when the lab opened, as a direction was picked, or as it is under a try-on.
  • What it costs (2026-10-06, tools/style-watch.sh and tools/web/style-check.ts, under a load of 15 to 33 on 24 cores). A build of the frame is 85 to 120 ms on the desktop (Forward+ on the RX 6700 XT), 123 to 138 ms in Chromium on the same GPU and up to 800 ms on SwiftShader. Dragging the line's weight on the desktop: 40 frames at p50 8 to 12 ms and p95 138 to 193 ms, four or five of them builds. In the browser on the GPU: 123 frames at p50 16.7 ms and p95 183 ms, seven drawn while it moved; on SwiftShader p50 33 ms and p95 650 ms, eleven drawn. A grade dial: p50 53 to 62 ms with nothing built. Four directions are drawn 4 to 5 s after Make the show on the GPU in a browser (12 s on SwiftShader), the show's making included.
  • Found by the live runs. In a browser a pick's doc.event came before the approval's answer, and RwDocBuffer.refresh returned at once while that read was under way, so the lab compared with the style as it was. A refresh now waits for a read after the one under way, for every editor on a document buffer.
  • Changed from the plan.
    • Dials apply as made, like every other editor in Studio, rather than a local draft with an Apply button: each release is one revise_style in History, undone with Ctrl+Z, and the split compares with the style as it was when the lab opened (Compare with now resets it). What is looked at before it is kept are kept-back proposals: try-ons, directions, blends.
    • The lab's frame is a stills job played live, not two RwView3Ds over a set.
    • On the active style an edit is applied as the person's own, as the core already does for them; "Make it the show's style" is restyle_show kept back and opened as a proposal.
  • Left for later. The look book (each set through its establishing setup, the cast's line-up, hero frames from episodes, the catalog band), two styles split on an episode's frame, the frame under each of a set's lighting presets, "Save as a new style", and scenes.get kind pattern.

Phase 6: voice audition

  • What a showrunner gets. Hear any character say any line in any voice, with their mouth, before casting the voice.
  • Engine. ui/editors/cast/rw_voice_panel.gd: the audio and visemes driven on the live rig.
  • Core. core/src/studio/audition.ts, plus voices.list and voices.audition.
  • Tests.
    • Auditions are cached by key.
    • The visemes drive the mouth in step with the audio.
    • A viewer is refused.
    • On the web, the audio arrives over /api/file.
  • Live proof. Otto says "Chief! Long-range contact!" in two Kokoro voices; choose one; the Lighthouse is voiced again with it.

Phase 7: the story map

  • What a showrunner gets. The bible as a map of people, arcs, threads, gags and places, every entry one click from the shot it cites.
  • Engine. ui/editors/world/rw_story_map.gd, rw_relationship_graph.gd, rw_arc_grid.gd, rw_place_graph.gd.
  • Core. None beyond write_bible_entry. Evidence frames come from episode.timeline.
  • Tests.
    • The graph is built from the bible.
    • Dragging one character onto another proposes the right entry.
    • Evidence opens the timeline at the shot's frame.
    • Layout is stable for the same bible.
  • Live proof. On the demo bible, add a relationship by dragging; open a gag's evidence and land on its shot.

Phase 8: working together in 3D

  • What a showrunner gets. Comments pinned to a mark, a setup, an element or an expression; others' selections drawn in their color; "Show the showrunner what I see".
  • Engine. Pins in RwViewOverlay and in the outliner. session.place carries part and item.
  • Core. The comment anchor's pointer and atM; ChatSendParams.attachments; image input in core/src/showrunner/agent.ts.
  • Tests and live proof. A reviewer pins a comment on eng_crouch in a browser, and the owner answers it from the installed Studio. A picture sent with a turn reaches the model, checked with a recorded fixture.

Conflicts with work in flight

  • M13 phase 3, Studio sharing (#216). Part A (m13-studio-people) adds the Share dialog, the bell, presence and follow, and comments. Part B (m13-desktop) adds sign-in, shows on Reelwright and the merge view.
    • Shared files: rw_document_view.gd, rw_show_browser.gd, rw_studio.gd and rw_session_status.gd.
    • Resolution:
      • phase 1 starts after part A merges;
      • the editors embed RwDocumentView as Fields rather than replace it, so its comments and People… carry over;
      • 3D pins wait for phase 8 on the comment UI;
      • following someone into an editor opens the same page, not their camera.
  • The showrunner knows where you are (#221). Its branch changes ChatContext (page, title, part, selected, view), adds StudioPage, chat.focus and studio.navigate, and adds core/src/showrunner/page.ts.
    • Shared files: the same schema and RwStudio.chat_context.
    • Resolution: M14 lands after it and extends it: camera, item paths, and page.ts resolving items. M14 does not add a second context.
    • Ask now: keep chat_context delegating to the open view, so editors plug in without editing Studio.
  • Show settings and the new-show wizard (#217). The show's settings leave World for settings, and the route grammar in flight already has marigold/settings.
    • Resolution: M14 never touches the World list's ShowManifest group and leaves it to #217.
    • Overlap: the wizard's style choice and the look book's "Make this the show's style" should share the style cards and call the same restyle_show.
  • Back and Forward in the browser (#220). RwStudioRoute names places as <show>/<section>/<id>, with exactly three parts for a document.
    • Needed: a fourth and fifth part, /<tab or part>/<item>, for example marigold/sets/set.marigold_bridge/setups/wide.
    • History rules: camera moves, selection and pilot always replace, never push, so Back never steps through an orbit. Changing tab or opening another document pushes.
    • Ask now: RwStudioRoute.parse should keep unknown trailing parts rather than reject them, so M14 can add them without breaking saved links.
  • Studio UX review (#207). It walks every screen, including the stage. There are no shared files beyond the tours. M14's screens join its 1280×720 tour.
  • Remote MCP (#219). MCP clients get a studio_context. The editors' camera and view reach Claude Code through it, and nothing else changes there.

Risks and open questions

  • Card cost. About 100 ms to re-pose a 2D card at a new angle (on a loaded machine) is the tightest number here.
    • Mitigation: the budget above, and keeping a card's last drawing while the camera moves. A card is a cutout (ADR-0004), so a cutout turning is honest.
    • If it is not enough: cache each card's drawing at 15° steps per pose while the editor is open.
  • The engine room's 1.3 s build.
    • Mitigation: rebuild_element fixes edits. The first open still pays the whole build: on the web it is spread over frames by element, behind the veil.
    • To check: whether the cost is the kit recipes' ink creases.
  • Two views of truth. The local build can drift from what the core holds: scenes.get documents differ from docs.get text. Reconciliation always re-reads from the daemon after a doc.event, and the look-through equality test pins the builders. Validation stays in the core, at the edge.
  • History noise. Gizmo work makes many small commits, one change each. The history popover groups a person's run of edits to one document ("12 edits to the Bridge in 2 minutes"), and Ctrl+Z still goes one at a time.
    • Open question: should the service coalesce them in git? Not in M14.
  • Set-level props. The engine room was once dressed with a drone so it stood in every episode. A set has no way to stand a PropInstance in it for every scene.
    • Open question: add SetInstance.props (placed like a scene's) or keep dressing as elements? Decide in phase 4.
  • Voice audition cost on the service. Kokoro on the service is CPU work (the demo's six lines took 46 s the first time, downloads included). Auditions are short and cached, but a quota per person may be needed.
  • Accessibility. Every editor stays usable by keyboard: outliner focus, arrows to nudge, the inspector for exact values. Every control has an accessible name, as Studio's do. Spins and live time stop when the person turns motion down.

Not in M14

  • Modelling, sculpting, and a kit-recipe editor. (The piece maker, a kit-recipe editor, came after M14 with the library of kits: docs/design/world-kits.md.)
  • Rig and part-library authoring, and hand shapes.
  • Keyframe posing and gesture authoring. (The pose maker came after M14: docs/design/m6-performance.md Part 4.)
  • Shader graphs and physically based lighting.
  • VR, and a world map.
  • Live co-editing of one drag by two people.
  • Restoring old versions (Studio's History, docs/design/m3-showrunner.md "Versions").