Skip to main content

blockworx/choreography/
mod.rs

1//! The choreographer's substrate: given a document and one commit, a
2//! [`Timeline`] depicting how that change could have been made. The contract
3//! is `docs/choreographer-playbook.md` (C1–C8); the load-bearing rule is C3 —
4//! a timeline is lossless, and [`Timeline::recovered`] rebuilding a commit
5//! whose fold matches the real one is the oracle that keeps a rule from being
6//! decorative.
7//!
8//! A derived layer beside `presentation`: presentation answers "where is the
9//! wire now", this module answers "how did it get there", and only this one
10//! has a clock — a caller-supplied `dt`, never a wall clock, which is what
11//! makes a golden timeline a deterministic artifact. Nothing here is
12//! authored, logged, or persisted, and the module draws nothing: it produces
13//! [`Frame`]s, and painting one belongs to the UI half.
14
15use core::time::Duration;
16use std::sync::Arc;
17
18use ahash::{HashMap, HashSet};
19
20use blockworx_doc::block_model::{
21    AreaUpdate, Block, BlockInit, BlockUpdate, Icon, ImageUpdate, Label, LabelUpdate, Pin,
22    PinUpdate, RouteLabelUpdate, RouteUpdate, TextUpdate,
23};
24use blockworx_doc::commit::{Commit, CommitBuilder};
25use blockworx_doc::document::{DocIndex, Document, IndexedDocument, TitleBlockUpdate};
26use blockworx_doc::geometry::{
27    FracVal, GridPoint, GridRect, GridSize, PinSlot, ScreenRect, ScreenSize,
28};
29use blockworx_doc::id::{
30    AreaId, BlockId, EntityRef, ImageId, PinId, RouteId, RouteLabelId, TextId,
31};
32use blockworx_doc::opcode::{Crud, OpCodes};
33use blockworx_doc::values::{LabelSide, PinDir, PinSide, Role};
34
35use crate::edit::naming::{InterfaceLock, LabelVisibility, TagVisibility};
36use crate::grid::{GRID_SIZE, pin_slot_row};
37use crate::path::Scope;
38use crate::presentation::Presentation;
39use crate::progress::Progress;
40use crate::tools::names::ToolName;
41
42/// How long an entity takes to arrive.
43const APPEAR: Duration = Duration::from_millis(600);
44/// How long a shape takes to travel.
45const MORPH: Duration = Duration::from_millis(500);
46/// How much later each level of a delete cascade begins ([`Cascade`]).
47const STAGGER: Duration = Duration::from_millis(120);
48
49/// One commit, depicted: concurrent [`Track`]s under one clock, the
50/// camera's advisory plan, and the one L1 [`Pantomime`]. The whole of what
51/// the UI half plays.
52///
53/// The pantomime sits beside the tracks rather than among them because it
54/// carries no op: [`Timeline::recovered`] rebuilds the commit from the
55/// tracks, and a track whose op were optional would weaken C3 from "every
56/// op lands in exactly one track" to "most of them do".
57pub struct Timeline {
58    label: String,
59    duration: Duration,
60    camera: Option<CameraPlan>,
61    tracks: Vec<Track>,
62    pantomime: Option<Pantomime>,
63}
64
65/// L1: what the *hand* did, as far as the commit's label and the L0
66/// subjects can say — the tool a viewer should see ringed, the affordance
67/// the gesture took hold of, and where a ghost cursor goes while it
68/// happens. Data only; painting a ghost is the UI half's.
69///
70/// One per timeline, appended by [`synthesize`] and never authored by a
71/// rule: a rule sees one op, and a gesture is the whole commit.
72pub struct Pantomime {
73    /// The tool to ring, where the label's verb and the subject name one.
74    /// `None` where no tool performs the verb — the hand still travels,
75    /// it simply rings nothing.
76    pub tool: Option<ToolName>,
77    pub affordance: Affordance,
78    /// The ghost cursor's path, as [`TrackValue::Point`] keyframes on the
79    /// same clock as the tracks.
80    pub cursor: Vec<Keyframe>,
81}
82
83/// What a gesture takes hold of, named for the UI half to ring. Derived
84/// from the primary subject and the shape of what the commit did to it;
85/// never resolved to pixels here (C5).
86#[derive(Clone, Copy, PartialEq, Eq, Debug)]
87pub enum Affordance {
88    /// Empty canvas: what arrives is drawn where nothing was.
89    Canvas,
90    /// The subject itself — its footprint, its anchor, or its length.
91    Body(EntityRef),
92    /// A corner of the subject's footprint: the grip a resize is made by.
93    Handle(EntityRef),
94    /// The stub on its owner's boundary that a pin is seen and grabbed by
95    /// (7·2), rather than the port body drawn one scope in.
96    Stub(EntityRef),
97    /// One of the subject's written lines.
98    Writing(EntityRef),
99}
100
101/// Where a played-back commit should look: the scope it happened in and the
102/// world region that contains everything it touched. Advisory — never part
103/// of the C3 oracle.
104#[derive(Clone, Copy, PartialEq, Eq, Debug)]
105pub struct CameraPlan {
106    pub scope: Scope,
107    pub region: GridRect,
108}
109
110/// One op's depiction. The op itself rides along verbatim — that is what
111/// makes [`Timeline::recovered`] total, so the oracle guards coverage and
112/// order while the keyframes stay free to depict.
113pub struct Track {
114    pub subject: EntityRef,
115    pub kind: TrackKind,
116    op: OpCodes,
117    pub keys: Vec<Keyframe>,
118}
119
120#[derive(Clone, Copy, PartialEq, Eq, Debug)]
121pub enum TrackKind {
122    Appear,
123    Vanish,
124    Morph,
125    Emphasis,
126    Overlay,
127    Pantomime,
128}
129
130impl TrackKind {
131    /// How a two-key track approaches its arrival, decided once here so no
132    /// two rules can disagree: a depiction that moves eases into what it
133    /// lands on, and one that steps has no approach to shape.
134    fn arrival(self) -> Easing {
135        match self {
136            TrackKind::Appear | TrackKind::Vanish | TrackKind::Morph => Easing::EaseOut,
137            TrackKind::Emphasis | TrackKind::Overlay | TrackKind::Pantomime => Easing::Linear,
138        }
139    }
140}
141
142/// A moment on a track: when, what value the depiction has reached, and how
143/// the approach from the previous key is shaped.
144#[derive(Clone, PartialEq, Debug)]
145pub struct Keyframe {
146    pub at: Duration,
147    pub value: TrackValue,
148    pub easing: Easing,
149}
150
151/// What a keyframe holds. Each variant is a shape the UI half must paint
152/// differently: a grid footprint, a bare anchor, the document's one
153/// unsnapped (world-pixel) artwork box, a solved wire polyline, and the two
154/// authored pairs — a place along a wire and a place along a shape's side —
155/// that only the painter can resolve to a point.
156///
157/// `Settled` is the value-free one: the stand-in of a rule that has not
158/// landed yet (the op is carried, so C3 holds, but nothing is depicted —
159/// step 7·6 removes the last of those), and the answer of the few landed
160/// arms whose subject has no geometry at all.
161#[derive(Clone, PartialEq, Debug)]
162pub enum TrackValue {
163    Rect(GridRect),
164    Point(GridPoint),
165    Artwork(ScreenRect),
166    /// `Arc`: a keyframe is cloned once per played frame, and a polyline
167    /// is the one value here that is not two numbers wide.
168    Path(Arc<[GridPoint]>),
169    Along {
170        route: RouteId,
171        at: FracVal,
172    },
173    /// A placed label: the pair its shape holds, for the same reason
174    /// [`TrackValue::Along`] is a pair — the projection that resolves an
175    /// offset on a side to a point speaks the UI toolkit's geometry, so
176    /// the painter resolves it (C5).
177    Label {
178        slot: LabelSlot,
179        side: LabelSide,
180        offset: FracVal,
181    },
182    /// One written string. A keyframe pair is a **crossfade** and holds
183    /// both strings, so a rename still says what the register became (C3).
184    Text {
185        of: Written,
186        /// `Arc`: a keyframe is cloned once per played frame.
187        text: Arc<str>,
188    },
189    /// One discrete register's value. A keyframe pair **steps** at its
190    /// second key: two enum values have no ground between them for a
191    /// depiction to cross, so nothing is drawn in between.
192    Flag {
193        at: Site,
194        state: FlagState,
195    },
196    Settled,
197}
198
199/// Which of a shape's placeable labels a [`TrackValue::Label`] carries: a
200/// block has both, an area only its title.
201#[derive(Clone, Copy, PartialEq, Eq, Debug)]
202pub enum LabelSlot {
203    Title,
204    TypeLabel,
205}
206
207/// Where a written value shows, as far as the document can say: on a shape
208/// it places, at a bare anchor, along a wire, or in the drawing's own
209/// title block. The exact spot is the painter's — placing a label needs a
210/// text width, and measuring text is the UI toolkit's job (C5) — so this
211/// names the place to *look at*, and is what the camera plans from.
212#[derive(Clone, Copy, PartialEq, Eq, Debug)]
213pub enum Site {
214    Shape(GridRect),
215    Anchor(GridPoint),
216    /// A wire's own registers show wherever its labels do — an arc length
217    /// along the polyline, the same seam as [`TrackValue::Along`].
218    Wire,
219    /// The title block in the sheet's corner, which is chrome pinned to
220    /// the viewport rather than anything on the canvas: the document's
221    /// own name has no world position at all, so it frames nothing.
222    Sheet,
223}
224
225impl Site {
226    fn bounds(self) -> Option<GridRect> {
227        match self {
228            Site::Shape(rect) => Some(rect),
229            Site::Anchor(at) => Some(spot(at)),
230            Site::Wire | Site::Sheet => None,
231        }
232    }
233}
234
235/// Which written line, and where it reads: without the first the painter
236/// could not tell which of a block's two strings is being rewritten, and
237/// without the second the camera could not frame it.
238#[derive(Clone, Copy, PartialEq, Eq, Debug)]
239pub struct Written {
240    pub at: Site,
241    pub line: TextLine,
242}
243
244/// The written lines an entity carries: a shape has two labels, a pin has
245/// three lines, a text box and a wire have one each, and the drawing
246/// itself has the name its title block states.
247#[derive(Clone, Copy, PartialEq, Eq, Debug)]
248pub enum TextLine {
249    Label(LabelSlot),
250    Pin(PinLine),
251    /// A text box, which is nothing but its content.
252    Content,
253    WireName,
254    DocumentName,
255}
256
257/// A pin's three written lines: what it is called, what it carries, and
258/// where it is on the board.
259#[derive(Clone, Copy, PartialEq, Eq, Debug)]
260pub enum PinLine {
261    Name,
262    Type,
263    Tag,
264}
265
266impl PinLine {
267    fn of(self, pin: &Pin) -> &str {
268        match self {
269            PinLine::Name => pin.name.as_ref(),
270            PinLine::Type => pin.type_name.as_ref(),
271            PinLine::Tag => pin.tag.as_ref(),
272        }
273    }
274}
275
276/// A discrete register and the value it holds: a colour role, an interface
277/// lock, a signal direction, a tag's visibility, and whether a shape draws
278/// one of its two placed labels.
279#[derive(Clone, Copy, PartialEq, Eq, Debug)]
280pub enum FlagState {
281    Accent(Role),
282    Lock(InterfaceLock),
283    Direction(PinDir),
284    Tag(TagVisibility),
285    Label(LabelSlot, LabelVisibility),
286}
287
288impl TrackValue {
289    /// The grid region this value covers, for the camera's advisory plan.
290    fn bounds(&self) -> Option<GridRect> {
291        match self {
292            TrackValue::Rect(r) => Some(*r),
293            TrackValue::Point(p) => Some(spot(*p)),
294            TrackValue::Artwork(r) => Some(grid_bounds(*r)),
295            TrackValue::Path(points) => points.iter().map(|p| spot(*p)).reduce(union),
296            TrackValue::Text { of, .. } => of.at.bounds(),
297            TrackValue::Flag { at, .. } => at.bounds(),
298            // An arc length along a solved polyline, and an offset along a
299            // shape's side: only the painter can place either, so they
300            // frame nothing.
301            TrackValue::Along { .. } | TrackValue::Label { .. } | TrackValue::Settled => None,
302        }
303    }
304}
305
306#[derive(Clone, Copy, PartialEq, Eq, Debug)]
307pub enum Easing {
308    Linear,
309    EaseOut,
310}
311
312impl Easing {
313    fn apply(self, p: Progress) -> Progress {
314        let t = f32::from(p);
315        match self {
316            Easing::Linear => p,
317            Easing::EaseOut => Progress::new(1.0 - (1.0 - t) * (1.0 - t)),
318        }
319    }
320}
321
322/// Where playback stands. Advanced by the caller's `dt`; the module never
323/// reads a clock (C4).
324#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
325pub struct Playhead {
326    pub elapsed: Duration,
327}
328
329impl Playhead {
330    pub fn advance(&mut self, dt: Duration) {
331        self.elapsed = self.elapsed.saturating_add(dt);
332    }
333
334    pub fn done(self, timeline: &Timeline) -> bool {
335        self.elapsed >= timeline.duration
336    }
337}
338
339/// One playback instant, ready to paint: every track's subject, kind,
340/// interpolated value, and progress, plus where the ghost hand is. The UI
341/// half's entire input.
342pub struct Frame {
343    pub tracks: Vec<FrameTrack>,
344    pub ghost: Option<Ghost>,
345}
346
347/// The [`Pantomime`] at one instant: what to ring, what to name, and where
348/// the hand is.
349pub struct Ghost {
350    pub tool: Option<ToolName>,
351    pub affordance: Affordance,
352    /// Grid cells, fractional — the hand moves between the positions the
353    /// document knows, as every other interpolated value does.
354    pub at: [f32; 2],
355}
356
357pub struct FrameTrack {
358    pub subject: EntityRef,
359    pub kind: TrackKind,
360    pub value: FrameValue,
361    pub progress: Progress,
362}
363
364/// An interpolated value: fractional, because animation happens between the
365/// grid positions the document knows. `Rect`, `Point` and `Path` are in
366/// grid cells; `Artwork` is in world pixels, as the document stores it.
367#[derive(Clone, PartialEq, Debug)]
368pub enum FrameValue {
369    Rect {
370        min: [f32; 2],
371        max: [f32; 2],
372    },
373    Point {
374        at: [f32; 2],
375    },
376    Artwork {
377        min: [f32; 2],
378        max: [f32; 2],
379    },
380    Path(Vec<[f32; 2]>),
381    Along {
382        route: RouteId,
383        at: f32,
384    },
385    Label {
386        slot: LabelSlot,
387        side: LabelSide,
388        offset: f32,
389    },
390    /// A crossfade in progress: both strings, and how far it has run.
391    Text {
392        of: Written,
393        from: Arc<str>,
394        to: Arc<str>,
395        mix: Progress,
396    },
397    /// A step: at any instant exactly one discrete value is true.
398    Flag {
399        at: Site,
400        state: FlagState,
401    },
402    Settled,
403}
404
405impl Timeline {
406    pub fn duration(&self) -> Duration {
407        self.duration
408    }
409
410    pub fn label(&self) -> &str {
411        &self.label
412    }
413
414    pub fn camera(&self) -> Option<CameraPlan> {
415        self.camera
416    }
417
418    pub fn tracks(&self) -> &[Track] {
419        &self.tracks
420    }
421
422    pub fn pantomime(&self) -> Option<&Pantomime> {
423        self.pantomime.as_ref()
424    }
425
426    /// 7·6's whole-inventory probe: the tracks that depict nothing but the
427    /// op they carry. A payload op is the one op with nothing to depict by
428    /// design — its bytes are invisible, and what the user sees is the
429    /// image referencing them (7·2) — so it is the one subject excused.
430    #[cfg(test)]
431    pub(crate) fn undepicted(&self) -> Vec<&Track> {
432        self.tracks
433            .iter()
434            .filter(|track| !matches!(track.subject, EntityRef::Asset(_)))
435            .filter(|track| {
436                track
437                    .keys
438                    .iter()
439                    .all(|key| key.value == TrackValue::Settled)
440            })
441            .collect()
442    }
443
444    /// The commit this timeline depicts, rebuilt from its tracks — the C3
445    /// oracle's left hand. Op order is track order, which is the commit's
446    /// own op order, so the write sequence survives the round trip.
447    pub fn recovered(&self) -> Option<Commit> {
448        let mut builder = CommitBuilder::new(self.label.clone());
449        for track in &self.tracks {
450            builder.push(track.op.clone());
451        }
452        builder.seal()
453    }
454
455    /// The playback instant at `t`.
456    pub fn at(&self, t: Duration) -> Frame {
457        Frame {
458            tracks: self.tracks.iter().map(|track| track.at(t)).collect(),
459            ghost: self.pantomime.as_ref().map(|mime| mime.at(t)),
460        }
461    }
462
463    /// The reviewable text form the goldens pin: every fact a reviewer needs
464    /// to judge a depiction, and nothing environment-dependent.
465    pub fn describe(&self) -> String {
466        let mut lines = vec![
467            format!("label: {}", self.label),
468            format!("duration: {}ms", ms(self.duration)),
469        ];
470        lines.push(match self.camera {
471            None => "camera: none".to_owned(),
472            Some(CameraPlan { scope, region }) => {
473                let scope = match scope {
474                    Scope::Root => "root".to_owned(),
475                    Scope::Block(id) => format!("block {id}"),
476                };
477                format!("camera: {scope} {}", rect(region))
478            }
479        });
480        for track in &self.tracks {
481            lines.push(format!("track {} {}", track.subject, kind_name(track.kind)));
482            keys(&mut lines, &track.keys);
483        }
484        match &self.pantomime {
485            None => lines.push("pantomime: none".to_owned()),
486            Some(mime) => {
487                lines.push(format!(
488                    "pantomime: tool {}, {}",
489                    match mime.tool {
490                        None => "none".to_owned(),
491                        Some(tool) => format!("{tool:?}"),
492                    },
493                    affordance_name(mime.affordance),
494                ));
495                keys(&mut lines, &mime.cursor);
496            }
497        }
498        lines.push(String::new());
499        lines.join("\n")
500    }
501}
502
503fn keys(lines: &mut Vec<String>, keys: &[Keyframe]) {
504    for key in keys {
505        lines.push(format!(
506            "  {}ms {} {}",
507            ms(key.at),
508            easing_name(key.easing),
509            value(&key.value),
510        ));
511    }
512}
513
514fn affordance_name(affordance: Affordance) -> String {
515    match affordance {
516        Affordance::Canvas => "canvas".to_owned(),
517        Affordance::Body(at) => format!("body {at}"),
518        Affordance::Handle(at) => format!("handle {at}"),
519        Affordance::Stub(at) => format!("stub {at}"),
520        Affordance::Writing(at) => format!("writing {at}"),
521    }
522}
523
524fn ms(d: Duration) -> u128 {
525    d.as_millis()
526}
527
528fn rect(r: GridRect) -> String {
529    format!(
530        "[{} {} {}x{}]",
531        r.top_left.x, r.top_left.y, r.size.w, r.size.h
532    )
533}
534
535fn point(p: GridPoint) -> String {
536    format!("[{} {}]", p.x, p.y)
537}
538
539/// Two decimals: world-pixel geometry is fixed-point, and a golden must
540/// not turn on the last bit of an `f32` rendering.
541fn artwork(r: ScreenRect) -> String {
542    let (x, y) = (f32::from(r.top_left.x), f32::from(r.top_left.y));
543    let (w, h) = (f32::from(r.size.w), f32::from(r.size.h));
544    format!("[{x:.2} {y:.2} {w:.2}x{h:.2}]")
545}
546
547fn value(v: &TrackValue) -> String {
548    match v {
549        TrackValue::Rect(r) => format!("rect {}", rect(*r)),
550        TrackValue::Point(p) => format!("point {}", point(*p)),
551        TrackValue::Artwork(r) => format!("artwork {}", artwork(*r)),
552        TrackValue::Path(points) => {
553            let corners: Vec<String> = points.iter().map(|p| point(*p)).collect();
554            format!("path {}", corners.join(" -> "))
555        }
556        TrackValue::Along { route, at } => {
557            format!("along route {route} at {:.2}", f32::from(*at))
558        }
559        TrackValue::Label { slot, side, offset } => format!(
560            "label {} {} at {:.2}",
561            slot_name(*slot),
562            side_name(*side),
563            f32::from(*offset),
564        ),
565        TrackValue::Text { of, text } => {
566            format!("text {} {} {text:?}", site_name(of.at), line_name(of.line))
567        }
568        TrackValue::Flag { at, state } => {
569            format!("flag {} {}", site_name(*at), flag_name(*state))
570        }
571        TrackValue::Settled => "settled".to_owned(),
572    }
573}
574
575fn site_name(at: Site) -> String {
576    match at {
577        Site::Shape(r) => rect(r),
578        Site::Anchor(p) => point(p),
579        Site::Wire => "wire".to_owned(),
580        Site::Sheet => "sheet".to_owned(),
581    }
582}
583
584fn line_name(line: TextLine) -> &'static str {
585    match line {
586        TextLine::Label(slot) => slot_name(slot),
587        TextLine::Pin(PinLine::Name) => "pin-name",
588        TextLine::Pin(PinLine::Type) => "pin-type",
589        TextLine::Pin(PinLine::Tag) => "pin-tag",
590        TextLine::Content => "content",
591        TextLine::WireName => "wire-name",
592        TextLine::DocumentName => "document-name",
593    }
594}
595
596fn flag_name(state: FlagState) -> String {
597    match state {
598        FlagState::Accent(role) => format!("role {}", role_name(role)),
599        FlagState::Lock(InterfaceLock::Locked) => "lock locked".to_owned(),
600        FlagState::Lock(InterfaceLock::Unlocked) => "lock unlocked".to_owned(),
601        FlagState::Direction(dir) => format!("dir {}", dir_name(dir)),
602        FlagState::Tag(TagVisibility::Shown) => "tag shown".to_owned(),
603        FlagState::Tag(TagVisibility::Hidden) => "tag hidden".to_owned(),
604        FlagState::Label(slot, LabelVisibility::Shown) => {
605            format!("{} shown", slot_name(slot))
606        }
607        FlagState::Label(slot, LabelVisibility::Hidden) => {
608            format!("{} hidden", slot_name(slot))
609        }
610    }
611}
612
613fn role_name(role: Role) -> &'static str {
614    match role {
615        Role::Accent0 => "plain",
616        Role::Accent1 => "accent1",
617        Role::Accent2 => "accent2",
618        Role::Accent3 => "accent3",
619        Role::Accent4 => "accent4",
620        Role::Accent5 => "accent5",
621        Role::Accent6 => "accent6",
622        Role::Accent7 => "accent7",
623        Role::Accent8 => "accent8",
624    }
625}
626
627fn dir_name(dir: PinDir) -> &'static str {
628    match dir {
629        PinDir::Input => "input",
630        PinDir::Output => "output",
631        PinDir::InOut => "in-out",
632    }
633}
634
635fn slot_name(slot: LabelSlot) -> &'static str {
636    match slot {
637        LabelSlot::Title => "title",
638        LabelSlot::TypeLabel => "type",
639    }
640}
641
642fn side_name(side: LabelSide) -> &'static str {
643    match side {
644        LabelSide::Top => "top",
645        LabelSide::Center => "center",
646        LabelSide::Bottom => "bottom",
647    }
648}
649
650fn kind_name(kind: TrackKind) -> &'static str {
651    match kind {
652        TrackKind::Appear => "appear",
653        TrackKind::Vanish => "vanish",
654        TrackKind::Morph => "morph",
655        TrackKind::Emphasis => "emphasis",
656        TrackKind::Overlay => "overlay",
657        TrackKind::Pantomime => "pantomime",
658    }
659}
660
661fn easing_name(e: Easing) -> &'static str {
662    match e {
663        Easing::Linear => "linear",
664        Easing::EaseOut => "ease-out",
665    }
666}
667
668impl Track {
669    fn end(&self) -> Duration {
670        ends(&self.keys).1
671    }
672
673    /// When this track's depiction begins — read by the stagger's proof.
674    #[cfg(test)]
675    fn start(&self) -> Duration {
676        ends(&self.keys).0
677    }
678
679    /// This track's state at `t`.
680    fn at(&self, t: Duration) -> FrameTrack {
681        let (start, end) = ends(&self.keys);
682        FrameTrack {
683            subject: self.subject,
684            kind: self.kind,
685            value: sample(&self.keys, t),
686            progress: Progress::through(t.saturating_sub(start), end.saturating_sub(start)),
687        }
688    }
689}
690
691/// The window a key list occupies. A lone key holds the value its window
692/// *ends* on, so it runs from the top; only the delete cascade's stagger
693/// ([`Cascade`]) moves a start off zero.
694fn ends(keys: &[Keyframe]) -> (Duration, Duration) {
695    let end = keys.last().map_or(Duration::ZERO, |k| k.at);
696    match keys {
697        [first, _, ..] => (first.at, end),
698        _ => (Duration::ZERO, end),
699    }
700}
701
702/// A key list's value at `t`: the bracketing keys interpolated under the
703/// later key's easing, clamped to the ends. Shared by the tracks and the
704/// ghost cursor, which is a key list like any other.
705fn sample(keys: &[Keyframe], t: Duration) -> FrameValue {
706    match keys {
707        [] => FrameValue::Settled,
708        [only] => frame_value(&only.value),
709        keys => match keys.iter().position(|k| k.at > t) {
710            None => frame_value(&keys[keys.len() - 1].value),
711            Some(0) => frame_value(&keys[0].value),
712            Some(i) => {
713                let (from, to) = (&keys[i - 1], &keys[i]);
714                let local =
715                    Progress::through(t.saturating_sub(from.at), to.at.saturating_sub(from.at));
716                lerp(&from.value, &to.value, to.easing.apply(local))
717            }
718        },
719    }
720}
721
722fn cell(v: i32) -> f32 {
723    v as f32
724}
725
726fn corners(r: GridRect) -> ([f32; 2], [f32; 2]) {
727    (
728        [cell(r.left()), cell(r.top())],
729        [cell(r.right()), cell(r.bottom())],
730    )
731}
732
733fn px_corners(r: ScreenRect) -> ([f32; 2], [f32; 2]) {
734    let min = [f32::from(r.top_left.x), f32::from(r.top_left.y)];
735    (
736        min,
737        [min[0] + f32::from(r.size.w), min[1] + f32::from(r.size.h)],
738    )
739}
740
741fn frame_value(v: &TrackValue) -> FrameValue {
742    match v {
743        TrackValue::Settled => FrameValue::Settled,
744        TrackValue::Rect(r) => {
745            let (min, max) = corners(*r);
746            FrameValue::Rect { min, max }
747        }
748        TrackValue::Point(p) => FrameValue::Point {
749            at: [cell(p.x), cell(p.y)],
750        },
751        TrackValue::Artwork(r) => {
752            let (min, max) = px_corners(*r);
753            FrameValue::Artwork { min, max }
754        }
755        TrackValue::Path(points) => {
756            FrameValue::Path(points.iter().map(|p| [cell(p.x), cell(p.y)]).collect())
757        }
758        TrackValue::Along { route, at } => FrameValue::Along {
759            route: *route,
760            at: f32::from(*at),
761        },
762        TrackValue::Label { slot, side, offset } => FrameValue::Label {
763            slot: *slot,
764            side: *side,
765            offset: f32::from(*offset),
766        },
767        // A lone text key is a crossfade that has already run: the string
768        // it holds is both ends of it.
769        TrackValue::Text { of, text } => FrameValue::Text {
770            of: *of,
771            from: text.clone(),
772            to: text.clone(),
773            mix: Progress::one(),
774        },
775        TrackValue::Flag { at, state } => FrameValue::Flag {
776            at: *at,
777            state: *state,
778        },
779    }
780}
781
782fn lerp(from: &TrackValue, to: &TrackValue, p: Progress) -> FrameValue {
783    let t = f32::from(p);
784    let mix = |a: [f32; 2], b: [f32; 2]| [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
785    match (frame_value(from), frame_value(to)) {
786        (FrameValue::Rect { min: a0, max: a1 }, FrameValue::Rect { min: b0, max: b1 }) => {
787            FrameValue::Rect {
788                min: mix(a0, b0),
789                max: mix(a1, b1),
790            }
791        }
792        (FrameValue::Artwork { min: a0, max: a1 }, FrameValue::Artwork { min: b0, max: b1 }) => {
793            FrameValue::Artwork {
794                min: mix(a0, b0),
795                max: mix(a1, b1),
796            }
797        }
798        (FrameValue::Point { at: a }, FrameValue::Point { at: b }) => {
799            FrameValue::Point { at: mix(a, b) }
800        }
801        (FrameValue::Path(from), FrameValue::Path(to)) => FrameValue::Path(wipe(&from, &to, t)),
802        (FrameValue::Along { route: a, at: x }, FrameValue::Along { route: b, at: y })
803            if a == b =>
804        {
805            FrameValue::Along {
806                route: a,
807                at: x + (y - x) * t,
808            }
809        }
810        // A side change is a step, not a slide: the label lands on its new
811        // side and travels the offset along it.
812        (
813            FrameValue::Label { offset: x, .. },
814            FrameValue::Label {
815                slot,
816                side,
817                offset: y,
818            },
819        ) => FrameValue::Label {
820            slot,
821            side,
822            offset: x + (y - x) * t,
823        },
824        // A text morph is a crossfade: both strings reach the painter, so
825        // what it draws is the one turning into the other.
826        (FrameValue::Text { from, .. }, FrameValue::Text { of, to: now, .. }) => FrameValue::Text {
827            of,
828            from,
829            to: now,
830            mix: p,
831        },
832        // A discrete register steps at its arrival: there is nothing
833        // between two enum values to draw on the way.
834        (FrameValue::Flag { state: was, .. }, FrameValue::Flag { at, state: now }) => {
835            FrameValue::Flag {
836                at,
837                state: if p.is_complete() { now } else { was },
838            }
839        }
840        // A track never mixes value shapes; anything else settles.
841        (_, settled) => settled,
842    }
843}
844
845/// A path keyframe pair is an **arc-length wipe**, never a corner-by-corner
846/// mix: what is drawn is always a prefix of one real polyline, so every leg
847/// stays orthogonal and no wire is shown in a shape no wire can have. A
848/// pair ending on a seed — a one-point list — retracts the path it starts
849/// from; every other pair draws the one it ends on. A wire whose corner
850/// list is rewritten therefore stages: it is rubbed out and redrawn, which
851/// is what its op does to the list, and it needs no answer to the question
852/// of which corner of one solve became which corner of the other.
853fn wipe(from: &[[f32; 2]], to: &[[f32; 2]], t: f32) -> Vec<[f32; 2]> {
854    if to.len() < 2 && from.len() > 1 {
855        reveal(from, 1.0 - t)
856    } else {
857        reveal(to, t)
858    }
859}
860
861/// The prefix of `points` covering fraction `t` of its arc length, ending
862/// part-way along a leg where the fraction falls there.
863fn reveal(points: &[[f32; 2]], t: f32) -> Vec<[f32; 2]> {
864    if t >= 1.0 {
865        return points.to_vec();
866    }
867    let leg = |a: [f32; 2], b: [f32; 2]| ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
868    let Some(&start) = points.first() else {
869        return Vec::new();
870    };
871    let mut drawn = vec![start];
872    let mut left = points
873        .windows(2)
874        .map(|pair| leg(pair[0], pair[1]))
875        .sum::<f32>()
876        * t.max(0.0);
877    for pair in points.windows(2) {
878        let length = leg(pair[0], pair[1]);
879        if length <= left {
880            drawn.push(pair[1]);
881            left -= length;
882            continue;
883        }
884        if left > 0.0 {
885            let f = left / length;
886            drawn.push([
887                pair[0][0] + (pair[1][0] - pair[0][0]) * f,
888                pair[0][1] + (pair[1][1] - pair[0][1]) * f,
889            ]);
890        }
891        break;
892    }
893    drawn
894}
895
896impl Pantomime {
897    /// Where the ghost hand is at `t`, and what it is over.
898    fn at(&self, t: Duration) -> Ghost {
899        Ghost {
900            tool: self.tool,
901            affordance: self.affordance,
902            at: match sample(&self.cursor, t) {
903                FrameValue::Point { at } => at,
904                // [`cursor`] builds nothing but `Point` keys, and never an
905                // empty list.
906                _ => [0.0, 0.0],
907            },
908        }
909    }
910}
911
912/// L1, appended once per timeline (7·6): the hand behind the commit.
913///
914/// Two inputs, and the split between them is the contract. The **label**
915/// says what the user reached for — this is L1's one sanctioned reading of
916/// it (C2), and only its leading verb is read. The **L0 tracks** say what
917/// the hand touched and where, so a pantomime never claims geometry the
918/// rules did not already derive.
919///
920/// A label whose verb no *tool* says is not evidence a hand did anything
921/// on the canvas: a delete, a paste, a flag from a menu were given from
922/// the chrome, and there is no gesture to mime. Those get no pantomime,
923/// and neither does a commit that depicts nothing to point at.
924fn pantomime(scene: &Scene<'_>, label: &str, tracks: &[Track]) -> Option<Pantomime> {
925    let verb = Verb::opening(label)?;
926    let depicted = || {
927        tracks
928            .iter()
929            .filter_map(|track| Some((track, track_region(scene, track)?)))
930    };
931    // The subject the verb answers to, where the commit has one: a wire
932    // that stamps its own destination pin writes both, and it is the wire
933    // the hand was drawing.
934    let (track, region) = depicted()
935        .find(|(track, _)| verb.armed(track).is_some())
936        .or_else(|| depicted().next())?;
937    Some(Pantomime {
938        tool: verb.armed(track),
939        affordance: affordance(scene, track),
940        cursor: cursor(track, region),
941    })
942}
943
944/// The verb a commit's label opens with, which is the only part of a label
945/// L1 reads. The vocabulary is exactly [`ToolName::verb`]'s outputs plus
946/// the two verbs the editor says without a tool behind them — the
947/// substitute `edit::describe` makes when a gesture names something that
948/// had no name, and the arrow keys' nudge. A label that begins with
949/// anything else was written by a command, an import, or a restore, and
950/// no hand is mimed for it.
951#[derive(Clone, Copy, PartialEq, Eq, Debug)]
952enum Verb {
953    Add,
954    Create,
955    Edit,
956    Modify,
957    Move,
958    Rename,
959    Resize,
960    Retype,
961}
962
963impl Verb {
964    fn opening(label: &str) -> Option<Self> {
965        let word = label.split_whitespace().next()?.to_ascii_lowercase();
966        Some(match word.as_str() {
967            "add" => Verb::Add,
968            "create" => Verb::Create,
969            "edit" => Verb::Edit,
970            "modify" => Verb::Modify,
971            // The arrow keys are the one hand gesture no tool is armed
972            // for, and they do what a drag does.
973            "move" | "nudge" => Verb::Move,
974            // "Name" is what `edit::describe` says instead of "Rename"
975            // when the register it names had nothing in it.
976            "rename" | "name" => Verb::Rename,
977            "resize" => Verb::Resize,
978            "retype" => Verb::Retype,
979            _ => return None,
980        })
981    }
982
983    /// The tool to ring: the one table, total over the verbs a label can
984    /// open with crossed with the subjects L0 found. `None` where the two
985    /// name no tool — the hand still travels, it simply rings nothing.
986    fn armed(self, track: &Track) -> Option<ToolName> {
987        use EntityRef as E;
988        let artwork = track
989            .keys
990            .iter()
991            .any(|key| matches!(key.value, TrackValue::Artwork(_)));
992        Some(match (self, track.subject) {
993            // A block's one piece of artwork is its icon.
994            (Verb::Add, E::Block(_)) if artwork => ToolName::Icon,
995            (Verb::Add, E::Block(_)) => ToolName::NewBlock,
996            (Verb::Add, E::Area(_)) => ToolName::NewArea,
997            (Verb::Add, E::Pin(_)) => ToolName::AddPort,
998            (Verb::Add, E::Image(_)) => ToolName::NewImage,
999            (Verb::Add, E::Text(_)) => ToolName::AddText,
1000            (Verb::Add, E::RouteLabel(_)) => ToolName::AddRouteLabel,
1001            (Verb::Create, E::Route(_)) => ToolName::Route,
1002            (Verb::Move, E::Block(_)) => match written_slot(track) {
1003                Some(LabelSlot::Title) => ToolName::MoveTitle,
1004                Some(LabelSlot::TypeLabel) => ToolName::MoveBlockType,
1005                // One tool drags every shape, whatever the toolbar calls
1006                // it.
1007                None => ToolName::MoveBlock,
1008            },
1009            (Verb::Move, E::Area(_) | E::Text(_) | E::Image(_)) => ToolName::MoveBlock,
1010            (Verb::Move, E::Pin(_)) => ToolName::MovePin,
1011            (Verb::Move, E::RouteLabel(_)) => ToolName::MoveLabel,
1012            (Verb::Resize, E::Block(_) | E::Area(_) | E::Image(_)) => ToolName::ResizeBlock,
1013            (Verb::Modify, E::Route(_)) => ToolName::EditRoute,
1014            (Verb::Rename, E::Pin(_)) => ToolName::RenamePin,
1015            (Verb::Rename, E::Route(_)) => ToolName::RenameRoute,
1016            (Verb::Rename, E::Block(_) | E::Area(_)) => ToolName::RenameTitle,
1017            (Verb::Retype, E::Pin(_)) => ToolName::RetypePin,
1018            (Verb::Retype, E::Block(_)) => ToolName::RenameBlockType,
1019            (Verb::Edit, E::Text(_)) => ToolName::EditTextBox,
1020            // A verb the subject does not answer to: the gesture reached
1021            // the document some other way, and the toolbar shows nothing.
1022            _ => return None,
1023        })
1024    }
1025}
1026
1027/// Which of a shape's two placed labels a track writes or places, where it
1028/// does either.
1029fn written_slot(track: &Track) -> Option<LabelSlot> {
1030    track.keys.iter().find_map(|key| match key.value {
1031        TrackValue::Label { slot, .. }
1032        | TrackValue::Text {
1033            of:
1034                Written {
1035                    line: TextLine::Label(slot),
1036                    ..
1037                },
1038            ..
1039        } => Some(slot),
1040        _ => None,
1041    })
1042}
1043
1044/// What the gesture took hold of, read off the depiction rather than off
1045/// the op: a written line is grabbed by its writing, a pin by the stub it
1046/// is seen on (7·2) unless the op moves its port body, and a travel that
1047/// changes a footprint's *size* was made by its corner rather than by its
1048/// middle. An arrival is drawn on bare canvas — except where the pre-image
1049/// already holds the subject, which is the icon a block gains.
1050fn affordance(scene: &Scene<'_>, track: &Track) -> Affordance {
1051    let subject = track.subject;
1052    let holds = |of: fn(&TrackValue) -> bool| track.keys.iter().any(|key| of(&key.value));
1053    if holds(|value| {
1054        matches!(
1055            value,
1056            TrackValue::Text { .. } | TrackValue::Along { .. } | TrackValue::Label { .. }
1057        )
1058    }) {
1059        return Affordance::Writing(subject);
1060    }
1061    if matches!(subject, EntityRef::Pin(_)) && !holds(|value| matches!(value, TrackValue::Rect(_)))
1062    {
1063        return Affordance::Stub(subject);
1064    }
1065    match track.kind {
1066        TrackKind::Appear if !scene.held(subject) => Affordance::Canvas,
1067        TrackKind::Morph if resizes(track) => Affordance::Handle(subject),
1068        _ => Affordance::Body(subject),
1069    }
1070}
1071
1072/// Whether a travel changes its subject's extent rather than only its
1073/// place.
1074fn resizes(track: &Track) -> bool {
1075    let extent = |key: &Keyframe| key.value.bounds().map(|r| r.size);
1076    match (track.keys.first(), track.keys.last()) {
1077        (Some(first), Some(last)) => extent(first) != extent(last),
1078        _ => false,
1079    }
1080}
1081
1082/// The ghost cursor's path, decided once per [`TrackKind`] so no rule
1083/// invents one:
1084///
1085/// - what **arrives** is drawn into place — the hand enters at the framed
1086///   region's edge, puts its pen down on the seed the shape grows out of,
1087///   and drags to the far corner it ends on;
1088/// - what **travels** is carried — the hand starts on the pre-image and
1089///   goes the whole way with it;
1090/// - what **goes**, and what merely **changes**, is reached for and dwelt
1091///   on — the hand arrives half way through and stays while the change
1092///   lands, because there is nothing for it to drag.
1093fn cursor(track: &Track, region: GridRect) -> Vec<Keyframe> {
1094    let (start, end) = ends(&track.keys);
1095    let bounds = |key: &Keyframe| key.value.bounds().unwrap_or(region);
1096    let (first, last) = match (track.keys.first(), track.keys.last()) {
1097        (Some(first), Some(last)) => (bounds(first), bounds(last)),
1098        _ => (region, region),
1099    };
1100    let reached = start + end.saturating_sub(start) / 2;
1101    let hold = |at: GridPoint| {
1102        vec![
1103            step(start, entry(region, at)),
1104            step(reached, at),
1105            step(end, at),
1106        ]
1107    };
1108    match track.kind {
1109        TrackKind::Appear => {
1110            let seed = first.top_left;
1111            let far = track.keys.last().map_or(region.top_left, |key| {
1112                landing(&key.value).unwrap_or(GridPoint {
1113                    x: last.right(),
1114                    y: last.bottom(),
1115                })
1116            });
1117            vec![
1118                step(start, entry(region, seed)),
1119                step(reached, seed),
1120                step(end, far),
1121            ]
1122        }
1123        TrackKind::Morph => vec![step(start, center(first)), step(end, center(last))],
1124        TrackKind::Vanish | TrackKind::Emphasis | TrackKind::Overlay => hold(center(first)),
1125        // A pantomime is the ghost; it is never an L0 track, and the arm
1126        // is here so the policy stays total over the kinds.
1127        TrackKind::Pantomime => hold(center(last)),
1128    }
1129}
1130
1131/// Where a hand that drew this value lets go: the far end of a stroke,
1132/// which is a real point on the wire rather than a corner of the box
1133/// around it. Every other value is a region, and its far corner is the
1134/// caller's business.
1135fn landing(value: &TrackValue) -> Option<GridPoint> {
1136    match value {
1137        TrackValue::Path(points) => points.last().copied(),
1138        _ => None,
1139    }
1140}
1141
1142fn step(at: Duration, to: GridPoint) -> Keyframe {
1143    Keyframe {
1144        at,
1145        value: TrackValue::Point(to),
1146        easing: Easing::EaseOut,
1147    }
1148}
1149
1150/// Where the hand comes in from: the nearer of the framed region's two
1151/// vertical edges, at the row it is heading for. A hand already at the
1152/// edge simply starts there.
1153fn entry(region: GridRect, toward: GridPoint) -> GridPoint {
1154    let (left, right) = (region.left(), region.right());
1155    GridPoint {
1156        x: if toward.x - left <= right - toward.x {
1157            left
1158        } else {
1159            right
1160        },
1161        y: toward.y,
1162    }
1163}
1164
1165/// The whole substrate in one call (C1): pure over the document the commit
1166/// was written against and the commit itself.
1167pub fn synthesize(before: &IndexedDocument<'_>, commit: &Commit) -> Timeline {
1168    let after = before.doc.try_apply(commit).ok();
1169    let scene = Scene {
1170        span: span(before, commit),
1171        paths: Paths::of(before.doc, after.as_ref(), commit),
1172        indexed: before,
1173        after,
1174        commit,
1175    };
1176    let cascade = Cascade::of(commit);
1177    let tracks: Vec<Track> = commit
1178        .ops()
1179        .iter()
1180        .map(|op| delayed(rule(&scene, op), cascade.start(&scene, op)))
1181        .collect();
1182    let duration = tracks
1183        .iter()
1184        .map(Track::end)
1185        .max()
1186        .unwrap_or(Duration::ZERO);
1187    let camera = camera_plan(&scene, &tracks);
1188    let pantomime = pantomime(&scene, commit.label(), &tracks);
1189    Timeline {
1190        label: commit.label().to_owned(),
1191        duration,
1192        camera,
1193        tracks,
1194        pantomime,
1195    }
1196}
1197
1198/// What a rule may read (C1): the document the commit was written against,
1199/// the document it produces, the commit itself, the window their tracks
1200/// share, and the polylines the router solved on either side of it. A
1201/// pre-image comes from the first, an arrival from the second — a pin
1202/// re-slotted on a block the same commit resized lands on the boundary the
1203/// document will hold, not on the one it had.
1204///
1205/// A freshly minted entity exists in neither document nor in its own op
1206/// alone — a wire that stamps its own destination pin writes both in one
1207/// commit — so a sibling op is the only pre-image such an entity has.
1208struct Scene<'a> {
1209    indexed: &'a IndexedDocument<'a>,
1210    /// Absent when the fold refuses the commit; the rules then depict
1211    /// arrivals from the pre-image and say nothing they cannot know.
1212    after: Option<Document>,
1213    commit: &'a Commit,
1214    span: Duration,
1215    paths: Paths,
1216}
1217
1218/// The polylines the router solved for the wires this commit draws,
1219/// redraws, retracts or brings back, on both sides of it.
1220///
1221/// C1 (amended 2026-08-30) admits the router because it is a pure,
1222/// deterministic function of document state — the same call the drag
1223/// previews make every frame without mutating anything — and a depiction
1224/// whose end state is not the geometry the document will show is a
1225/// depiction of something that did not happen. The seam is
1226/// [`Presentation::refresh_routes`]: an [`IndexedDocument`] in, solved
1227/// grid-space edges out, no painter and no UI toolkit between.
1228struct Paths {
1229    before: HashMap<RouteId, Arc<[GridPoint]>>,
1230    after: HashMap<RouteId, Arc<[GridPoint]>>,
1231}
1232
1233impl Paths {
1234    fn of(before: &Document, after: Option<&Document>, commit: &Commit) -> Self {
1235        // A wire the commit mints or revives has no path in the pre-image
1236        // to ask for, and one it tombstones has none in the arrival.
1237        let mut was = Vec::new();
1238        let mut now = Vec::new();
1239        for op in commit.ops() {
1240            match op {
1241                OpCodes::Route(id, Crud::Create(_) | Crud::Restore) => now.push(*id),
1242                OpCodes::Route(id, Crud::Update(RouteUpdate::Waypoints(_))) => {
1243                    was.push(*id);
1244                    now.push(*id);
1245                }
1246                // A delete retracts along the pre-image side. And a write
1247                // ON a wire that moves no geometry — its name — still
1248                // needs the polyline, because the camera's fallback frames
1249                // the wire the writing sits on: identical on both sides,
1250                // solved on the pre-image side it certainly exists on.
1251                OpCodes::Route(id, Crud::Delete | Crud::Update(_)) => was.push(*id),
1252                OpCodes::RouteLabel(id, _) => {
1253                    if let Some(live) = before.route_label(id) {
1254                        was.push(*live.as_ref().owner.as_ref());
1255                    }
1256                }
1257                _ => {}
1258            }
1259        }
1260        Self {
1261            before: solved(before, &was),
1262            after: after.map(|doc| solved(doc, &now)).unwrap_or_default(),
1263        }
1264    }
1265}
1266
1267/// The corner list each wanted wire takes in `doc`. A wire the solve
1268/// leaves unplaced yields nothing and the rule says so.
1269fn solved(doc: &Document, wanted: &[RouteId]) -> HashMap<RouteId, Arc<[GridPoint]>> {
1270    if wanted.is_empty() {
1271        return HashMap::default();
1272    }
1273    let mut index = DocIndex::default();
1274    let mut presentation = Presentation::default();
1275    presentation.refresh_routes(&index.view(doc));
1276    wanted
1277        .iter()
1278        .filter_map(|id| {
1279            let geometry = presentation.routes.get(id)?;
1280            let mut corners = vec![geometry.start_pos];
1281            corners.extend(geometry.iter_edges().map(|(_, edge)| edge.end));
1282            (corners.len() > 1).then(|| (*id, Arc::from(corners)))
1283        })
1284        .collect()
1285}
1286
1287/// Where a pin shows: the point on its owner's boundary that its slot
1288/// picks. A pin the document root holds has no boundary to sit on (F9)
1289/// and neither has one whose owner the document cannot place, so its own
1290/// drawn body is the only place it can be said to sit.
1291fn slot_anchor(owner: Option<GridRect>, slot: PinSlot, body: GridRect) -> GridPoint {
1292    let Some(rect) = owner else {
1293        return center(body);
1294    };
1295    GridPoint {
1296        x: match slot.side {
1297            PinSide::West => rect.left(),
1298            PinSide::East => rect.right(),
1299        },
1300        y: pin_slot_row(rect.top(), slot.offset),
1301    }
1302}
1303
1304/// The geometry readers ask the document, never the liveness flag: a
1305/// delete tombstones an entity but retains its values, so `before` can
1306/// still say where a dead thing stood — which is the footprint a `Vanish`
1307/// fades from and a `Restore` travels out of (7·5). Only an entity the
1308/// document never held has nothing to depict.
1309fn block_icon(doc: &Document, id: BlockId) -> Option<ScreenRect> {
1310    icon_box(doc.block(&id)?.as_ref().icon.as_ref())
1311}
1312
1313fn area_rect(doc: &Document, id: AreaId) -> Option<GridRect> {
1314    Some(*doc.area(&id)?.as_ref().rect.as_ref())
1315}
1316
1317fn text_pos(doc: &Document, id: TextId) -> Option<GridPoint> {
1318    Some(*doc.text(&id)?.as_ref().pos.as_ref())
1319}
1320
1321fn image_rect(doc: &Document, id: ImageId) -> Option<ScreenRect> {
1322    Some(*doc.image(&id)?.as_ref().rect.as_ref())
1323}
1324
1325/// A pin's seat: the block it sits on, the slot it took there, and the
1326/// port body it draws in that block's interior.
1327fn pin_seat(doc: &Document, id: PinId) -> Option<(BlockId, PinSlot, GridRect)> {
1328    let pin = doc.pin(&id)?.as_ref();
1329    Some((*pin.owner.as_ref(), *pin.slot.as_ref(), *pin.rect.as_ref()))
1330}
1331
1332impl Scene<'_> {
1333    /// The document the commit was written against.
1334    fn before(&self) -> &Document {
1335        self.indexed.doc
1336    }
1337
1338    /// The document the commit produces, or the pre-image where the fold
1339    /// refused it.
1340    fn after(&self) -> &Document {
1341        self.after.as_ref().unwrap_or(self.indexed.doc)
1342    }
1343
1344    /// The create this commit mints a block with — the only pre-image a
1345    /// block the pre-image never held can have, which is the shape a
1346    /// paste lands in (7·2).
1347    fn minted(&self, id: BlockId) -> Option<&BlockInit> {
1348        self.commit.ops().iter().find_map(|op| match op {
1349            OpCodes::Block(other, Crud::Create(init)) if *other == id => Some(init),
1350            _ => None,
1351        })
1352    }
1353
1354    /// A block's footprint before the commit, or from the same commit's
1355    /// create.
1356    fn block_rect(&self, id: BlockId) -> Option<GridRect> {
1357        if let Some(live) = self.before().block(&id) {
1358            return Some(*live.as_ref().rect.as_ref());
1359        }
1360        self.minted(id).map(|init| init.rect)
1361    }
1362
1363    /// A block's footprint after the commit — where a pin this commit
1364    /// re-slots on it actually lands.
1365    fn block_rect_after(&self, id: BlockId) -> Option<GridRect> {
1366        self.after()
1367            .block(&id)
1368            .map(|live| *live.as_ref().rect.as_ref())
1369            .or_else(|| self.block_rect(id))
1370    }
1371
1372    /// Where a pin's written lines and discrete registers read: the slot
1373    /// anchor on its owner's boundary, which is where the user sees the
1374    /// pin at all (7·2). Its port body is interior geometry and is not.
1375    fn pin_site(&self, id: PinId) -> Option<Site> {
1376        let (owner, slot, body) = pin_seat(self.before(), id)?;
1377        Some(Site::Anchor(slot_anchor(
1378            self.block_rect(owner),
1379            slot,
1380            body,
1381        )))
1382    }
1383
1384    /// Where the document says an entity stands on one side of the
1385    /// commit: the footprint a `Vanish` fades from, a `Restore` travels
1386    /// between, and a re-point is held up at. A pin shows at its slot
1387    /// anchor and a wire along its solved polyline — the same places the
1388    /// create family draws them into.
1389    fn footprint(&self, side: Side, subject: EntityRef) -> TrackValue {
1390        let doc = side.doc(self);
1391        let value = match subject {
1392            EntityRef::Block(id) => side.block_rect(self, id).map(TrackValue::Rect),
1393            EntityRef::Pin(id) => pin_seat(doc, id).map(|(owner, slot, body)| {
1394                TrackValue::Point(slot_anchor(side.block_rect(self, owner), slot, body))
1395            }),
1396            EntityRef::Route(id) => side.paths(self).get(&id).cloned().map(TrackValue::Path),
1397            EntityRef::RouteLabel(id) => doc.route_label(&id).map(|live| TrackValue::Along {
1398                route: *live.as_ref().owner.as_ref(),
1399                at: *live.as_ref().pos.as_ref(),
1400            }),
1401            EntityRef::Text(id) => text_pos(doc, id).map(TrackValue::Point),
1402            EntityRef::Area(id) => area_rect(doc, id).map(TrackValue::Rect),
1403            EntityRef::Image(id) => image_rect(doc, id).map(TrackValue::Artwork),
1404            EntityRef::Document | EntityRef::Asset(_) => None,
1405        };
1406        value.unwrap_or(TrackValue::Settled)
1407    }
1408
1409    /// Whether the pre-image holds this entity at all. A tombstone counts
1410    /// — the document still says where it stood — and only the entity the
1411    /// commit itself mints is absent.
1412    fn held(&self, subject: EntityRef) -> bool {
1413        let doc = self.before();
1414        match subject {
1415            EntityRef::Block(id) => doc.block(&id).is_some(),
1416            EntityRef::Pin(id) => doc.pin(&id).is_some(),
1417            EntityRef::Route(id) => doc.route(&id).is_some(),
1418            EntityRef::RouteLabel(id) => doc.route_label(&id).is_some(),
1419            EntityRef::Text(id) => doc.text(&id).is_some(),
1420            EntityRef::Area(id) => doc.area(&id).is_some(),
1421            EntityRef::Image(id) => doc.image(&id).is_some(),
1422            EntityRef::Document | EntityRef::Asset(_) => true,
1423        }
1424    }
1425
1426    /// The block an op's subject sits inside: a shape's owner, a block's
1427    /// parent, a wire label's wire's owner — read from the pre-image
1428    /// where the op does not carry it. The document itself and an asset
1429    /// sit in nothing.
1430    fn container(&self, op: &OpCodes) -> Option<BlockId> {
1431        let doc = self.before();
1432        Some(match op {
1433            OpCodes::Block(_, Crud::Create(init)) => init.parent,
1434            OpCodes::Block(id, _) => *doc.block(id)?.as_ref().parent.as_ref(),
1435            OpCodes::Area(_, Crud::Create(init)) => init.owner,
1436            OpCodes::Area(id, _) => *doc.area(id)?.as_ref().owner.as_ref(),
1437            OpCodes::Text(_, Crud::Create(init)) => init.owner,
1438            OpCodes::Text(id, _) => *doc.text(id)?.as_ref().owner.as_ref(),
1439            OpCodes::Image(_, Crud::Create(init)) => init.owner,
1440            OpCodes::Image(id, _) => *doc.image(id)?.as_ref().owner.as_ref(),
1441            OpCodes::Route(_, Crud::Create(init)) => init.owner,
1442            OpCodes::Route(id, _) => *doc.route(id)?.as_ref().owner.as_ref(),
1443            OpCodes::RouteLabel(_, Crud::Create(init)) => {
1444                *doc.route(&init.owner)?.as_ref().owner.as_ref()
1445            }
1446            OpCodes::RouteLabel(id, _) => {
1447                let route = *doc.route_label(id)?.as_ref().owner.as_ref();
1448                *doc.route(&route)?.as_ref().owner.as_ref()
1449            }
1450            OpCodes::Pin(_, Crud::Create(init)) => init.owner,
1451            OpCodes::Pin(id, _) => *doc.pin(id)?.as_ref().owner.as_ref(),
1452            OpCodes::Document(_) | OpCodes::Asset(..) => return None,
1453        })
1454    }
1455
1456    /// The scope an op's subject is drawn in. A pin is the exception: it
1457    /// is depicted at its slot anchor, which is on its owner's *outside*,
1458    /// so it belongs to the scope the owner itself is a child of — except
1459    /// where the op moves the port *body*, which is interior geometry.
1460    fn scope_of(&self, op: &OpCodes) -> Option<Scope> {
1461        let inside = self.container(op)?;
1462        Some(Scope::from_wire(match op {
1463            OpCodes::Pin(_, Crud::Update(PinUpdate::Rect(_) | PinUpdate::FlipLR(_))) => inside,
1464            OpCodes::Pin(..) => self.parent_of(inside),
1465            _ => inside,
1466        }))
1467    }
1468
1469    /// The scope a block is a child of. The root's own parent is the root.
1470    fn parent_of(&self, block: BlockId) -> BlockId {
1471        self.before()
1472            .block(&block)
1473            .map(|live| *live.as_ref().parent.as_ref())
1474            .or_else(|| self.minted(block).map(|init| init.parent))
1475            .unwrap_or(BlockId::NULL)
1476    }
1477}
1478
1479/// Which side of the commit a footprint is read from.
1480#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1481enum Side {
1482    Before,
1483    After,
1484}
1485
1486impl Side {
1487    fn doc<'a>(self, scene: &'a Scene<'_>) -> &'a Document {
1488        match self {
1489            Side::Before => scene.before(),
1490            Side::After => scene.after(),
1491        }
1492    }
1493
1494    fn paths<'a>(self, scene: &'a Scene<'_>) -> &'a HashMap<RouteId, Arc<[GridPoint]>> {
1495        match self {
1496            Side::Before => &scene.paths.before,
1497            Side::After => &scene.paths.after,
1498        }
1499    }
1500
1501    fn block_rect(self, scene: &Scene<'_>, id: BlockId) -> Option<GridRect> {
1502        match self {
1503            Side::Before => scene.block_rect(id),
1504            Side::After => scene.block_rect_after(id),
1505        }
1506    }
1507}
1508
1509/// The delete family's stagger, as policy rather than per-rule numbers: a
1510/// tombstone's depiction starts one [`STAGGER`] later for every block of
1511/// *this commit's own* cascade that contains it, so a subtree fades
1512/// outside-in and the user sees how far the delete reached. Measuring
1513/// inside the commit is what keeps a lone deleted text box starting at
1514/// once, however deep it happens to sit.
1515struct Cascade {
1516    doomed: HashSet<BlockId>,
1517}
1518
1519impl Cascade {
1520    fn of(commit: &Commit) -> Self {
1521        Self {
1522            doomed: commit
1523                .ops()
1524                .iter()
1525                .filter_map(|op| match op {
1526                    OpCodes::Block(id, Crud::Delete) => Some(*id),
1527                    _ => None,
1528                })
1529                .collect(),
1530        }
1531    }
1532
1533    fn start(&self, scene: &Scene<'_>, op: &OpCodes) -> Duration {
1534        if self.doomed.is_empty() {
1535            return Duration::ZERO;
1536        }
1537        let mut rank = 0;
1538        let mut inside = scene.container(op);
1539        while let Some(block) = inside {
1540            rank += u32::from(self.doomed.contains(&block));
1541            inside = (block != BlockId::NULL).then(|| scene.parent_of(block));
1542        }
1543        STAGGER * rank
1544    }
1545}
1546
1547/// The window every track in one commit shares: an arrival takes longer
1548/// than a travel, and a rider — the block that grew to make room for a new
1549/// port — moves *with* what it makes room for rather than after it.
1550fn span(before: &IndexedDocument<'_>, commit: &Commit) -> Duration {
1551    if commit.ops().iter().any(|op| arrives(before.doc, op)) {
1552        APPEAR
1553    } else {
1554        MORPH
1555    }
1556}
1557
1558/// Whether an op brings something into view: any entity's create, and the
1559/// icon write that gives a block a picture it did not have — the same
1560/// write on a block that already has one is the icon riding a move or a
1561/// resize, which travels rather than arrives.
1562fn arrives(before: &Document, op: &OpCodes) -> bool {
1563    fn creates<I, U>(crud: &Crud<I, U>) -> bool {
1564        matches!(crud, Crud::Create(_))
1565    }
1566    match op {
1567        OpCodes::Block(id, Crud::Update(BlockUpdate::Icon(icon))) => {
1568            icon_box(icon).is_some() && block_icon(before, *id).is_none()
1569        }
1570        OpCodes::Block(_, crud) => creates(crud),
1571        OpCodes::Pin(_, crud) => creates(crud),
1572        OpCodes::Route(_, crud) => creates(crud),
1573        OpCodes::RouteLabel(_, crud) => creates(crud),
1574        OpCodes::Text(_, crud) => creates(crud),
1575        OpCodes::Area(_, crud) => creates(crud),
1576        OpCodes::Image(_, crud) => creates(crud),
1577        OpCodes::Document(_) | OpCodes::Asset(..) => false,
1578    }
1579}
1580
1581/// The box a block's icon shows in, or `None` for the model's zero icon —
1582/// which means "no picture", and is the value Delete Icon writes (7·5).
1583fn icon_box(icon: &Icon) -> Option<ScreenRect> {
1584    (icon != &Icon::default()).then_some(icon.rect)
1585}
1586
1587/// One op, one track (C6's dispatch). The arms that answer with
1588/// [`settled`] are the rows whose rules land with their families (7·3–7·6);
1589/// the exhaustive `OpCodes` match is already total, so a new op *kind* is a
1590/// compile error today.
1591fn rule(scene: &Scene<'_>, op: &OpCodes) -> Track {
1592    let span = scene.span;
1593    match op {
1594        // "New Block", "New Area": a drawn footprint grows in.
1595        OpCodes::Block(id, Crud::Create(init)) => {
1596            grow_in(EntityRef::Block(*id), init.rect, span, op)
1597        }
1598        OpCodes::Area(id, Crud::Create(init)) => grow_in(EntityRef::Area(*id), init.rect, span, op),
1599        // "New Image": the one geometry the document keeps unsnapped.
1600        OpCodes::Image(id, Crud::Create(init)) => {
1601            grow_in_artwork(EntityRef::Image(*id), init.rect, span, op)
1602        }
1603        // "New Text Box": a text's extent is measured, never authored, so
1604        // its anchor is the whole of what the document can say arrived.
1605        OpCodes::Text(id, Crud::Create(init)) => {
1606            arrive(EntityRef::Text(*id), TrackValue::Point(init.pos), span, op)
1607        }
1608        // "Add Pin", "Add Port": a pin arrives at the slot it took on its
1609        // owner's boundary — where the user sees it, not where its port
1610        // body sits in the owner's own interior view.
1611        OpCodes::Pin(id, Crud::Create(init)) => arrive(
1612            EntityRef::Pin(*id),
1613            TrackValue::Point(slot_anchor(
1614                scene.block_rect_after(init.owner),
1615                init.slot,
1616                init.rect,
1617            )),
1618            span,
1619            op,
1620        ),
1621        // "New Route (wire)".
1622        OpCodes::Route(id, Crud::Create(_)) => wire(EntityRef::Route(*id), scene, *id, op),
1623        // "Add Wire Label": handed over as the pair the document authored,
1624        // because the projection that would resolve it to a point speaks
1625        // the UI toolkit's geometry, which this module may not (C5).
1626        OpCodes::RouteLabel(id, Crud::Create(init)) => arrive(
1627            EntityRef::RouteLabel(*id),
1628            TrackValue::Along {
1629                route: init.owner,
1630                at: init.pos,
1631            },
1632            span,
1633            op,
1634        ),
1635        // "Set Block Icon": the picture arrives inside its block — unless
1636        // the block already had one, when this is the icon riding a move
1637        // or a resize. The zero icon is Delete Icon's write (7·5).
1638        OpCodes::Block(id, Crud::Update(BlockUpdate::Icon(icon))) => {
1639            match (block_icon(scene.before(), *id), icon_box(icon)) {
1640                // "Delete Icon": the zero icon is no picture, so the box
1641                // the block carried fades where it was drawn.
1642                (Some(from), None) => {
1643                    vanish(EntityRef::Block(*id), TrackValue::Artwork(from), span, op)
1644                }
1645                (None, None) => settled(op),
1646                (None, Some(to)) => grow_in_artwork(EntityRef::Block(*id), to, span, op),
1647                (Some(from), Some(to)) => morph(
1648                    EntityRef::Block(*id),
1649                    TrackValue::Artwork(from),
1650                    TrackValue::Artwork(to),
1651                    span,
1652                    op,
1653                ),
1654            }
1655        }
1656        // "Move Shape" (the block arm), "Move Group", "Keyboard Nudge",
1657        // "Resize Shape", and the growth rider that makes room for a new
1658        // port — the same travel, over the same window.
1659        OpCodes::Block(id, Crud::Update(BlockUpdate::Rect(to))) => morph(
1660            EntityRef::Block(*id),
1661            TrackValue::Rect(scene.block_rect(*id).unwrap_or(*to)),
1662            TrackValue::Rect(*to),
1663            span,
1664            op,
1665        ),
1666        // "Move Shape" and "Move Group" for the shapes that are not
1667        // blocks, and "Resize Shape" for the two that carry handles.
1668        OpCodes::Area(id, Crud::Update(AreaUpdate::Rect(to))) => morph(
1669            EntityRef::Area(*id),
1670            TrackValue::Rect(area_rect(scene.before(), *id).unwrap_or(*to)),
1671            TrackValue::Rect(*to),
1672            span,
1673            op,
1674        ),
1675        OpCodes::Text(id, Crud::Update(TextUpdate::Pos(to))) => morph(
1676            EntityRef::Text(*id),
1677            TrackValue::Point(text_pos(scene.before(), *id).unwrap_or(*to)),
1678            TrackValue::Point(*to),
1679            span,
1680            op,
1681        ),
1682        OpCodes::Image(id, Crud::Update(ImageUpdate::Rect(to))) => morph(
1683            EntityRef::Image(*id),
1684            TrackValue::Artwork(image_rect(scene.before(), *id).unwrap_or(*to)),
1685            TrackValue::Artwork(*to),
1686            span,
1687            op,
1688        ),
1689        // A port body travels in its owner's interior, the scope it is
1690        // drawn in — never on the boundary its slot anchors to.
1691        OpCodes::Pin(id, Crud::Update(PinUpdate::Rect(to))) => morph(
1692            EntityRef::Pin(*id),
1693            TrackValue::Rect(pin_seat(scene.before(), *id).map_or(*to, |(_, _, body)| body)),
1694            TrackValue::Rect(*to),
1695            span,
1696            op,
1697        ),
1698        // "Move Pin", "Relocate Pin Group", "Nudge Pins", the slot half of
1699        // "Flip Shape Pins", "Flip Block Vertical", and the pins a resize
1700        // carries up.
1701        OpCodes::Pin(id, Crud::Update(PinUpdate::Slot(to))) => reseated(scene, *id, *to, op),
1702        // The other half of "Flip Shape Pins": a body told to face the
1703        // other way keeps every coordinate it had, so it is held up rather
1704        // than moved. A block flip toggles the flag on every pin precisely
1705        // *because* the interior must not appear to change.
1706        OpCodes::Pin(id, Crud::Update(PinUpdate::FlipLR(_))) => held(
1707            EntityRef::Pin(*id),
1708            pin_seat(scene.before(), *id)
1709                .map_or(TrackValue::Settled, |(_, _, body)| TrackValue::Rect(body)),
1710            span,
1711            op,
1712        ),
1713        // "Edit Route", "Reroute Wire / Block", and the waypoint riders a
1714        // move owes the wires it drags: one authored list write, one
1715        // track, along the paths the two lists solve to.
1716        OpCodes::Route(id, Crud::Update(RouteUpdate::Waypoints(_))) => rewired(scene, *id, op),
1717        // "Move Wire Label": the label slides along its own wire. Both
1718        // arc lengths are the document's own — the emitter re-anchors a
1719        // label onto the geometry the same commit solves — so the pair
1720        // travels and the painter resolves each end against the wire it
1721        // is drawing.
1722        OpCodes::RouteLabel(id, Crud::Update(RouteLabelUpdate::Pos(to))) => {
1723            slid(scene, *id, *to, op)
1724        }
1725        // "Move Title / Type Label", "Rename Title", "Rename Block Type".
1726        OpCodes::Block(id, Crud::Update(BlockUpdate::Title(update))) => {
1727            label_write(scene, LabelAt::BlockTitle(*id), update, op)
1728        }
1729        OpCodes::Block(id, Crud::Update(BlockUpdate::TypeLabel(update))) => {
1730            label_write(scene, LabelAt::BlockType(*id), update, op)
1731        }
1732        OpCodes::Area(id, Crud::Update(AreaUpdate::Title(update))) => {
1733            label_write(scene, LabelAt::AreaTitle(*id), update, op)
1734        }
1735        // "Rename Pin", "Retype Pin", "Set Pin Tag": a pin's three written
1736        // lines, crossfading at the anchor the user reads them by. The
1737        // body a rename widens to fit them is the `Rect` rider above,
1738        // drawn one scope in.
1739        OpCodes::Pin(id, Crud::Update(PinUpdate::Name(to))) => {
1740            pin_line(scene, *id, PinLine::Name, to, op)
1741        }
1742        OpCodes::Pin(id, Crud::Update(PinUpdate::TypeName(to))) => {
1743            pin_line(scene, *id, PinLine::Type, to, op)
1744        }
1745        OpCodes::Pin(id, Crud::Update(PinUpdate::Tag(to))) => {
1746            pin_line(scene, *id, PinLine::Tag, to, op)
1747        }
1748        // "Cycle Pin Direction", "Set Pin Direction", "Show/Hide Pin
1749        // Tags", and the port arm of "Set Accent".
1750        OpCodes::Pin(id, Crud::Update(PinUpdate::Dir(to))) => pin_flag(
1751            scene,
1752            *id,
1753            |pin| FlagState::Direction(*pin.dir.as_ref()),
1754            FlagState::Direction(*to),
1755            op,
1756        ),
1757        OpCodes::Pin(id, Crud::Update(PinUpdate::TagHidden(to))) => pin_flag(
1758            scene,
1759            *id,
1760            |pin| FlagState::Tag(TagVisibility::from(*pin.tag_hidden.as_ref())),
1761            FlagState::Tag(TagVisibility::from(*to)),
1762            op,
1763        ),
1764        OpCodes::Pin(id, Crud::Update(PinUpdate::PortAccent(to))) => pin_flag(
1765            scene,
1766            *id,
1767            |pin| FlagState::Accent(*pin.port_accent.as_ref()),
1768            FlagState::Accent(*to),
1769            op,
1770        ),
1771        // "Set Accent" and "Lock/Unlock Block": what a shape's footprint
1772        // is drawn *with*, held up on that footprint.
1773        OpCodes::Block(id, Crud::Update(BlockUpdate::Role(to))) => block_flag(
1774            scene,
1775            *id,
1776            |block| FlagState::Accent(*block.role.as_ref()),
1777            FlagState::Accent(*to),
1778            op,
1779        ),
1780        OpCodes::Block(id, Crud::Update(BlockUpdate::Locked(to))) => block_flag(
1781            scene,
1782            *id,
1783            |block| FlagState::Lock(InterfaceLock::from(*block.locked.as_ref())),
1784            FlagState::Lock(InterfaceLock::from(*to)),
1785            op,
1786        ),
1787        OpCodes::Area(id, Crud::Update(AreaUpdate::Role(to))) => or_settled(
1788            scene
1789                .before()
1790                .area(id)
1791                .filter(|area| area.is_alive())
1792                .map(|live| {
1793                    let area = live.as_ref();
1794                    stepped(
1795                        Site::Shape(*area.rect.as_ref()),
1796                        FlagState::Accent(*area.role.as_ref()),
1797                        FlagState::Accent(*to),
1798                        scene.span,
1799                        op,
1800                    )
1801                }),
1802            op,
1803        ),
1804        OpCodes::Text(id, Crud::Update(TextUpdate::Role(to))) => or_settled(
1805            scene
1806                .before()
1807                .text(id)
1808                .filter(|live| live.is_alive())
1809                .map(|live| {
1810                    let box_ = live.as_ref();
1811                    stepped(
1812                        Site::Anchor(*box_.pos.as_ref()),
1813                        FlagState::Accent(*box_.role.as_ref()),
1814                        FlagState::Accent(*to),
1815                        scene.span,
1816                        op,
1817                    )
1818                }),
1819            op,
1820        ),
1821        OpCodes::Route(id, Crud::Update(RouteUpdate::Role(to))) => or_settled(
1822            scene
1823                .before()
1824                .route(id)
1825                .filter(|route| route.is_alive())
1826                .map(|live| {
1827                    stepped(
1828                        Site::Wire,
1829                        FlagState::Accent(*live.as_ref().role.as_ref()),
1830                        FlagState::Accent(*to),
1831                        scene.span,
1832                        op,
1833                    )
1834                }),
1835            op,
1836        ),
1837        // "Rename Route", and the naming half of "Delete Wire Label":
1838        // clearing the last label's text unnames the wire, which is this
1839        // same crossfade ending on nothing.
1840        OpCodes::Route(id, Crud::Update(RouteUpdate::Name(to))) => or_settled(
1841            scene
1842                .before()
1843                .route(id)
1844                .filter(|route| route.is_alive())
1845                .map(|live| {
1846                    let of = Written {
1847                        at: Site::Wire,
1848                        line: TextLine::WireName,
1849                    };
1850                    crossfade(of, live.as_ref().name.as_ref(), to, scene.span, op)
1851                }),
1852            op,
1853        ),
1854        // "Edit Text Box".
1855        OpCodes::Text(id, Crud::Update(TextUpdate::Text(to))) => or_settled(
1856            scene
1857                .before()
1858                .text(id)
1859                .filter(|live| live.is_alive())
1860                .map(|live| {
1861                    let box_ = live.as_ref();
1862                    let of = Written {
1863                        at: Site::Anchor(*box_.pos.as_ref()),
1864                        line: TextLine::Content,
1865                    };
1866                    crossfade(of, box_.text.as_ref(), to, scene.span, op)
1867                }),
1868            op,
1869        ),
1870        // "Delete Text Box (emptied)" and "Delete Text": one op, one arm —
1871        // a text box is nothing but its content, so it goes the way it was
1872        // emptied however the gesture reached it (7·4, reconciled 7·5).
1873        OpCodes::Text(id, Crud::Delete) => emptied(scene, *id, op),
1874        // "Delete Block" and its cascade, "Delete Port", "Delete Pins",
1875        // "Delete Area", "Delete Image", "Delete Route", "Delete
1876        // Selection", and the delete half of "Cut": what a commit
1877        // tombstones fades out where the pre-image says it stood, one
1878        // level of the cascade after another ([`Cascade`]).
1879        OpCodes::Block(_, Crud::Delete)
1880        | OpCodes::Pin(_, Crud::Delete)
1881        | OpCodes::Route(_, Crud::Delete)
1882        | OpCodes::RouteLabel(_, Crud::Delete)
1883        | OpCodes::Area(_, Crud::Delete)
1884        | OpCodes::Image(_, Crud::Delete) => vanish(
1885            op.target(),
1886            scene.footprint(Side::Before, op.target()),
1887            span,
1888            op,
1889        ),
1890        // "Paste", in its identity-preserving form: a cut and its first
1891        // paste are one move, and the restore is the tell. The entity
1892        // travels out of the tombstone the cut left — `before` still
1893        // holds where it stood — and lands where this commit re-points
1894        // it. Never a death and a birth.
1895        OpCodes::Block(_, Crud::Restore)
1896        | OpCodes::Pin(_, Crud::Restore)
1897        | OpCodes::Route(_, Crud::Restore)
1898        | OpCodes::RouteLabel(_, Crud::Restore)
1899        | OpCodes::Text(_, Crud::Restore)
1900        | OpCodes::Area(_, Crud::Restore)
1901        | OpCodes::Image(_, Crud::Restore) => restored(scene, op),
1902        // "Wrap Top": the content that was re-parented is held up where it
1903        // stands, and the title block's repoint holds up its new top. The
1904        // same reading serves a move's re-point: the restore beside it is
1905        // the travel, so the handover is held at where it came from.
1906        OpCodes::Block(_, Crud::Update(BlockUpdate::Parent(_)))
1907        | OpCodes::Pin(_, Crud::Update(PinUpdate::Owner(_)))
1908        | OpCodes::Route(_, Crud::Update(RouteUpdate::Owner(_)))
1909        | OpCodes::Text(_, Crud::Update(TextUpdate::Owner(_)))
1910        | OpCodes::Area(_, Crud::Update(AreaUpdate::Owner(_)))
1911        | OpCodes::Image(_, Crud::Update(ImageUpdate::Owner(_))) => held(
1912            op.target(),
1913            scene.footprint(Side::Before, op.target()),
1914            span,
1915            op,
1916        ),
1917        OpCodes::Document(TitleBlockUpdate::Top(id)) => held(
1918            EntityRef::Document,
1919            scene
1920                .block_rect(*id)
1921                .map_or(TrackValue::Settled, TrackValue::Rect),
1922            span,
1923            op,
1924        ),
1925        // The drawing's own name crossfades where it is read — the title
1926        // block in the sheet's corner, which is chrome rather than
1927        // canvas, so it frames nothing. Written by an import
1928        // (`schema::lower`) and carried by `edit::restore`.
1929        OpCodes::Document(TitleBlockUpdate::Name(to)) => crossfade(
1930            Written {
1931                at: Site::Sheet,
1932                line: TextLine::DocumentName,
1933            },
1934            scene.before().title_block().name.as_ref(),
1935            to,
1936            span,
1937            op,
1938        ),
1939        // A wire label re-homed onto another wire: two wires have no
1940        // ground between them for a label to slide along, so it is held
1941        // at the placement it lands on — the reading a label's `Side`
1942        // step already has (7·3). Written only by `edit::restore`.
1943        OpCodes::RouteLabel(id, Crud::Update(RouteLabelUpdate::Owner(to))) => held(
1944            EntityRef::RouteLabel(*id),
1945            scene
1946                .before()
1947                .route_label(id)
1948                .map_or(TrackValue::Settled, |live| TrackValue::Along {
1949                    route: *to,
1950                    at: *live.as_ref().pos.as_ref(),
1951                }),
1952            span,
1953            op,
1954        ),
1955        // The picture inside an image box is replaced: the box does not
1956        // move, so it is held up while what is drawn in it changes.
1957        // Written by a paste re-stamping a copied image's payload and by
1958        // `edit::restore`.
1959        OpCodes::Image(id, Crud::Update(ImageUpdate::Asset(_))) => held(
1960            EntityRef::Image(*id),
1961            scene.footprint(Side::After, EntityRef::Image(*id)),
1962            span,
1963            op,
1964        ),
1965        // The payload rider (`edit::assets`): bytes arriving under their
1966        // content hash. Nothing of it appears — what the user sees is the
1967        // image or icon referencing them — so the track carries the op and
1968        // depicts no geometry. The one op with nothing to depict by
1969        // design, and the one [`Timeline::undepicted`] excuses.
1970        OpCodes::Asset(..) => held(op.target(), TrackValue::Settled, span, op),
1971    }
1972}
1973
1974/// The create family's idiom: what arrives grows out of its own top-left
1975/// corner, right and down — the direction a hand draws a box (user, 2026-
1976/// 08-30: "more natural").
1977fn grow_in(subject: EntityRef, written: GridRect, span: Duration, op: &OpCodes) -> Track {
1978    let seed = GridRect {
1979        top_left: written.top_left,
1980        size: GridSize::default(),
1981    };
1982    appearing(
1983        subject,
1984        TrackValue::Rect(seed),
1985        TrackValue::Rect(written),
1986        span,
1987        op,
1988    )
1989}
1990
1991/// [`grow_in`] for the document's unsnapped artwork boxes.
1992fn grow_in_artwork(subject: EntityRef, written: ScreenRect, span: Duration, op: &OpCodes) -> Track {
1993    let seed = ScreenRect {
1994        top_left: written.top_left,
1995        size: ScreenSize::default(),
1996    };
1997    appearing(
1998        subject,
1999        TrackValue::Artwork(seed),
2000        TrackValue::Artwork(written),
2001        span,
2002        op,
2003    )
2004}
2005
2006/// Inventory row "New Route (wire)": the wire draws itself along the path
2007/// the router solved for it, from its start anchor outward.
2008fn wire(subject: EntityRef, scene: &Scene<'_>, id: RouteId, op: &OpCodes) -> Track {
2009    let Some(path) = scene.paths.after.get(&id) else {
2010        // A wire the solve leaves unplaced — a suppressed endpoint, a
2011        // commit the fold refuses — has no path to draw.
2012        return arrive(subject, TrackValue::Settled, scene.span, op);
2013    };
2014    appearing(
2015        subject,
2016        TrackValue::Path(Arc::from([path[0]])),
2017        TrackValue::Path(path.clone()),
2018        scene.span,
2019        op,
2020    )
2021}
2022
2023/// The two ends of a two-key track: where the depiction starts, and the
2024/// value the commit wrote.
2025struct Ends {
2026    from: TrackValue,
2027    to: TrackValue,
2028}
2029
2030/// A pair of keys under one kind, the second reached over `span`.
2031fn between(subject: EntityRef, kind: TrackKind, ends: Ends, span: Duration, op: &OpCodes) -> Track {
2032    Track {
2033        subject,
2034        kind,
2035        op: op.clone(),
2036        keys: vec![
2037            Keyframe {
2038                at: Duration::ZERO,
2039                value: ends.from,
2040                easing: Easing::Linear,
2041            },
2042            Keyframe {
2043                at: span,
2044                value: ends.to,
2045                easing: kind.arrival(),
2046            },
2047        ],
2048    }
2049}
2050
2051/// An arrival that grows: a seed at the start, the written value at the
2052/// end of the gesture's window.
2053fn appearing(
2054    subject: EntityRef,
2055    seed: TrackValue,
2056    written: TrackValue,
2057    span: Duration,
2058    op: &OpCodes,
2059) -> Track {
2060    between(
2061        subject,
2062        TrackKind::Appear,
2063        Ends {
2064            from: seed,
2065            to: written,
2066        },
2067        span,
2068        op,
2069    )
2070}
2071
2072/// An arrival with nothing to grow from — an anchor point, a place along a
2073/// wire. One key, so the depiction is where it lands and the progress it
2074/// lands over.
2075fn arrive(subject: EntityRef, value: TrackValue, span: Duration, op: &OpCodes) -> Track {
2076    Track {
2077        subject,
2078        kind: TrackKind::Appear,
2079        op: op.clone(),
2080        keys: vec![Keyframe {
2081            at: span,
2082            value,
2083            easing: Easing::EaseOut,
2084        }],
2085    }
2086}
2087
2088/// Held up rather than moved or minted: what a gesture changed about an
2089/// entity that stays where it is.
2090fn held(subject: EntityRef, value: TrackValue, span: Duration, op: &OpCodes) -> Track {
2091    Track {
2092        subject,
2093        kind: TrackKind::Emphasis,
2094        op: op.clone(),
2095        keys: vec![Keyframe {
2096            at: span,
2097            value,
2098            easing: Easing::Linear,
2099        }],
2100    }
2101}
2102
2103/// The delete family's idiom: what a commit tombstones keeps the
2104/// footprint the pre-image gave it — the entity is alive there — and
2105/// fades over the window. A wire is the one exception: it is a stroke
2106/// rather than a footprint, so it retracts into its own start, which is
2107/// the 7·3 wipe read the other way round.
2108fn vanish(subject: EntityRef, footprint: TrackValue, span: Duration, op: &OpCodes) -> Track {
2109    let gone = match &footprint {
2110        TrackValue::Path(path) if !path.is_empty() => TrackValue::Path(Arc::from([path[0]])),
2111        standing => standing.clone(),
2112    };
2113    between(
2114        subject,
2115        TrackKind::Vanish,
2116        Ends {
2117            from: footprint,
2118            to: gone,
2119        },
2120        span,
2121        op,
2122    )
2123}
2124
2125/// The clipboard's move: an entity comes back out of the tombstone the
2126/// cut left it in and travels to where this commit re-points it. What the
2127/// pre-image cannot place at all — a wire whose endpoints were tombstoned
2128/// with it, so no solve reaches it — starts where it lands.
2129fn restored(scene: &Scene<'_>, op: &OpCodes) -> Track {
2130    let subject = op.target();
2131    let to = scene.footprint(Side::After, subject);
2132    let from = match scene.footprint(Side::Before, subject) {
2133        TrackValue::Settled => to.clone(),
2134        stood => stood,
2135    };
2136    morph(subject, from, to, scene.span, op)
2137}
2138
2139/// The stagger applied to a finished track, so a rule never has to know
2140/// what else the commit deleted.
2141fn delayed(mut track: Track, by: Duration) -> Track {
2142    if !by.is_zero() {
2143        for key in &mut track.keys {
2144            key.at += by;
2145        }
2146    }
2147    track
2148}
2149
2150/// The geometry family's idiom: one travel from the pre-image to the value
2151/// the commit writes, easing into its arrival.
2152fn morph(
2153    subject: EntityRef,
2154    from: TrackValue,
2155    to: TrackValue,
2156    span: Duration,
2157    op: &OpCodes,
2158) -> Track {
2159    between(subject, TrackKind::Morph, Ends { from, to }, span, op)
2160}
2161
2162/// The naming family's idiom: a crossfade whose keyframes hold both
2163/// strings, so a rename still says what the register became (C3).
2164fn crossfade(of: Written, was: &str, now: &str, span: Duration, op: &OpCodes) -> Track {
2165    morph(op.target(), text(of, was), text(of, now), span, op)
2166}
2167
2168fn text(of: Written, value: &str) -> TrackValue {
2169    TrackValue::Text {
2170        of,
2171        text: Arc::from(value),
2172    }
2173}
2174
2175/// The flag family's idiom: a discrete register is held up where it shows
2176/// and steps to the value the commit wrote at the end key.
2177fn stepped(at: Site, was: FlagState, now: FlagState, span: Duration, op: &OpCodes) -> Track {
2178    between(
2179        op.target(),
2180        TrackKind::Emphasis,
2181        Ends {
2182            from: TrackValue::Flag { at, state: was },
2183            to: TrackValue::Flag { at, state: now },
2184        },
2185        span,
2186        op,
2187    )
2188}
2189
2190/// The pin rows that rewrite one of its three written lines.
2191fn pin_line(scene: &Scene<'_>, id: PinId, line: PinLine, now: &str, op: &OpCodes) -> Track {
2192    let (Some(at), Some(live)) = (scene.pin_site(id), scene.before().pin(&id)) else {
2193        return settled(op);
2194    };
2195    let of = Written {
2196        at,
2197        line: TextLine::Pin(line),
2198    };
2199    crossfade(of, line.of(live.as_ref()), now, scene.span, op)
2200}
2201
2202/// The pin rows that write one of its discrete registers.
2203fn pin_flag(
2204    scene: &Scene<'_>,
2205    id: PinId,
2206    was: impl Fn(&Pin) -> FlagState,
2207    now: FlagState,
2208    op: &OpCodes,
2209) -> Track {
2210    let (Some(at), Some(live)) = (scene.pin_site(id), scene.before().pin(&id)) else {
2211        return settled(op);
2212    };
2213    stepped(at, was(live.as_ref()), now, scene.span, op)
2214}
2215
2216/// The block rows that write one of its discrete registers.
2217fn block_flag(
2218    scene: &Scene<'_>,
2219    id: BlockId,
2220    was: impl Fn(&Block) -> FlagState,
2221    now: FlagState,
2222    op: &OpCodes,
2223) -> Track {
2224    let (Some(rect), Some(live)) = (
2225        scene.block_rect(id),
2226        scene.before().block(&id).filter(|b| b.is_alive()),
2227    ) else {
2228        return settled(op);
2229    };
2230    stepped(Site::Shape(rect), was(live.as_ref()), now, scene.span, op)
2231}
2232
2233/// A rule whose subject the pre-image does not hold has nothing to depict
2234/// but the op it carries.
2235fn or_settled(track: Option<Track>, op: &OpCodes) -> Track {
2236    track.unwrap_or_else(|| settled(op))
2237}
2238
2239/// Inventory row "Delete Text Box (emptied)": a text box is nothing but
2240/// its content, so it goes the way it was emptied — that content fading
2241/// out at the anchor it stood on. The op is a plain delete, and 7·5's
2242/// "Delete Text" is the same act reached by pressing a key instead of
2243/// clearing the buffer, so the two rows share this arm.
2244fn emptied(scene: &Scene<'_>, id: TextId, op: &OpCodes) -> Track {
2245    let Some(live) = scene.before().text(&id).filter(|t| t.is_alive()) else {
2246        return settled(op);
2247    };
2248    let of = Written {
2249        at: Site::Anchor(*live.as_ref().pos.as_ref()),
2250        line: TextLine::Content,
2251    };
2252    between(
2253        EntityRef::Text(id),
2254        TrackKind::Vanish,
2255        Ends {
2256            from: text(of, live.as_ref().text.as_ref()),
2257            to: text(of, ""),
2258        },
2259        scene.span,
2260        op,
2261    )
2262}
2263
2264/// The pin rows that write a slot: the pin travels between the two points
2265/// its old and new slots pick on its owner's boundary — the boundary the
2266/// document will hold, which a resize in the same commit may have moved.
2267/// A flip is this travel and nothing more: the ops swap sides, so the pins
2268/// cross, and no mirror is drawn that the commit does not state.
2269fn reseated(scene: &Scene<'_>, id: PinId, to: PinSlot, op: &OpCodes) -> Track {
2270    let Some((owner, was, body)) = pin_seat(scene.before(), id) else {
2271        return settled(op);
2272    };
2273    let landed = pin_seat(scene.after(), id).map_or(body, |(_, _, body)| body);
2274    morph(
2275        EntityRef::Pin(id),
2276        TrackValue::Point(slot_anchor(scene.block_rect(owner), was, body)),
2277        TrackValue::Point(slot_anchor(scene.block_rect_after(owner), to, landed)),
2278        scene.span,
2279        op,
2280    )
2281}
2282
2283/// The rows that rewrite a wire's corner list: the wire is rubbed out
2284/// along the path its old list solved to and drawn again along the new
2285/// one ([`wipe`]), because that is what the write does to the list.
2286/// A list the solver answers the same way on both sides moved nothing, and
2287/// is held up instead.
2288fn rewired(scene: &Scene<'_>, id: RouteId, op: &OpCodes) -> Track {
2289    let subject = EntityRef::Route(id);
2290    let (Some(was), Some(now)) = (scene.paths.before.get(&id), scene.paths.after.get(&id)) else {
2291        return held(subject, TrackValue::Settled, scene.span, op);
2292    };
2293    if was == now {
2294        return held(subject, TrackValue::Path(now.clone()), scene.span, op);
2295    }
2296    Track {
2297        subject,
2298        kind: TrackKind::Morph,
2299        op: op.clone(),
2300        keys: vec![
2301            Keyframe {
2302                at: Duration::ZERO,
2303                value: TrackValue::Path(was.clone()),
2304                easing: Easing::Linear,
2305            },
2306            Keyframe {
2307                at: scene.span / 2,
2308                value: TrackValue::Path(Arc::from([was[0]])),
2309                easing: Easing::Linear,
2310            },
2311            Keyframe {
2312                at: scene.span,
2313                value: TrackValue::Path(now.clone()),
2314                easing: Easing::EaseOut,
2315            },
2316        ],
2317    }
2318}
2319
2320/// Inventory row "Move Wire Label": the label slides along its wire.
2321fn slid(scene: &Scene<'_>, id: RouteLabelId, to: FracVal, op: &OpCodes) -> Track {
2322    let Some(live) = scene.before().route_label(&id).filter(|l| l.is_alive()) else {
2323        return settled(op);
2324    };
2325    let route = *live.as_ref().owner.as_ref();
2326    let from = *live.as_ref().pos.as_ref();
2327    morph(
2328        EntityRef::RouteLabel(id),
2329        TrackValue::Along { route, at: from },
2330        TrackValue::Along { route, at: to },
2331        scene.span,
2332        op,
2333    )
2334}
2335
2336/// Which placed label an op moves, and how to read it out of a document.
2337#[derive(Clone, Copy)]
2338enum LabelAt {
2339    BlockTitle(BlockId),
2340    BlockType(BlockId),
2341    AreaTitle(AreaId),
2342}
2343
2344impl LabelAt {
2345    fn subject(self) -> EntityRef {
2346        match self {
2347            LabelAt::BlockTitle(id) | LabelAt::BlockType(id) => EntityRef::Block(id),
2348            LabelAt::AreaTitle(id) => EntityRef::Area(id),
2349        }
2350    }
2351
2352    fn slot(self) -> LabelSlot {
2353        match self {
2354            LabelAt::BlockTitle(_) | LabelAt::AreaTitle(_) => LabelSlot::Title,
2355            LabelAt::BlockType(_) => LabelSlot::TypeLabel,
2356        }
2357    }
2358
2359    fn label(self, doc: &Document) -> Option<&Label> {
2360        Some(match self {
2361            LabelAt::BlockTitle(id) => &doc.block(&id).filter(|b| b.is_alive())?.as_ref().title,
2362            LabelAt::BlockType(id) => &doc.block(&id).filter(|b| b.is_alive())?.as_ref().type_label,
2363            LabelAt::AreaTitle(id) => &doc.area(&id).filter(|a| a.is_alive())?.as_ref().title,
2364        })
2365    }
2366
2367    /// The footprint the label is drawn on — the whole of what the
2368    /// document can say about where its text appears, since resolving a
2369    /// side and an offset to a point needs a measured text width.
2370    fn site(self, doc: &Document) -> Option<Site> {
2371        match self {
2372            LabelAt::BlockTitle(id) | LabelAt::BlockType(id) => doc
2373                .block(&id)
2374                .filter(|b| b.is_alive())
2375                .map(|live| *live.as_ref().rect.as_ref()),
2376            LabelAt::AreaTitle(id) => area_rect(doc, id),
2377        }
2378        .map(Site::Shape)
2379    }
2380}
2381
2382/// Inventory rows "Move Title / Type Label", "Rename Title" and "Rename
2383/// Block Type": the registers of a shape's placed label.
2384///
2385/// The offset travels along the side the label ends on; a side change is a
2386/// step and is *held*, because two edges of a shape have no ground between
2387/// them for a label to cross. A rename is the family crossfade, on the
2388/// footprint that carries the label.
2389fn label_write(scene: &Scene<'_>, at: LabelAt, update: &LabelUpdate, op: &OpCodes) -> Track {
2390    let Some(label) = at.label(scene.before()) else {
2391        return settled(op);
2392    };
2393    let placed = |label: &Label| TrackValue::Label {
2394        slot: at.slot(),
2395        side: *label.side.as_ref(),
2396        offset: *label.offset.as_ref(),
2397    };
2398    let from = placed(label);
2399    let to = at.label(scene.after()).map_or_else(|| from.clone(), placed);
2400    match update {
2401        LabelUpdate::Offset(_) => morph(at.subject(), from, to, scene.span, op),
2402        LabelUpdate::Side(_) => held(at.subject(), to, scene.span, op),
2403        LabelUpdate::Name(now) => or_settled(
2404            at.site(scene.before()).map(|site| {
2405                let of = Written {
2406                    at: site,
2407                    line: TextLine::Label(at.slot()),
2408                };
2409                crossfade(of, label.name.as_ref(), now, scene.span, op)
2410            }),
2411            op,
2412        ),
2413        // A label that stops being drawn is a flag like any other, so it
2414        // steps on the footprint that carries it. No *gesture* writes it:
2415        // the register reaches a commit only through an authored document
2416        // that hid a label and the restore that carries such a document
2417        // back, which is the emitter this arm is proven through.
2418        LabelUpdate::Hidden(now) => or_settled(
2419            at.site(scene.before()).map(|site| {
2420                stepped(
2421                    site,
2422                    FlagState::Label(at.slot(), LabelVisibility::from(*label.hidden.as_ref())),
2423                    FlagState::Label(at.slot(), LabelVisibility::from(*now)),
2424                    scene.span,
2425                    op,
2426                )
2427            }),
2428            op,
2429        ),
2430    }
2431}
2432
2433/// Nothing to depict: the op carried, no geometry. Most consumers are rows
2434/// 7·4–7·6 still owe a rule; the rest are ops whose subject the pre-image
2435/// does not hold, which have no travel anyone can name.
2436fn settled(op: &OpCodes) -> Track {
2437    Track {
2438        subject: op.target(),
2439        kind: TrackKind::Emphasis,
2440        op: op.clone(),
2441        keys: vec![Keyframe {
2442            at: Duration::ZERO,
2443            value: TrackValue::Settled,
2444            easing: Easing::Linear,
2445        }],
2446    }
2447}
2448
2449/// The zero-size region at a point — what a bare anchor contributes to the
2450/// camera's plan.
2451fn spot(at: GridPoint) -> GridRect {
2452    GridRect {
2453        top_left: at,
2454        size: GridSize::default(),
2455    }
2456}
2457
2458fn center(r: GridRect) -> GridPoint {
2459    GridPoint {
2460        x: r.top_left.x + (r.size.w / 2) as i32,
2461        y: r.top_left.y + (r.size.h / 2) as i32,
2462    }
2463}
2464
2465/// The whole cells an unsnapped artwork box covers — the camera plans in
2466/// grid space, and artwork is the one geometry stored in world pixels.
2467fn grid_bounds(r: ScreenRect) -> GridRect {
2468    let (min, max) = px_corners(r);
2469    let cells = |v: f32, up: bool| {
2470        let scaled = v / GRID_SIZE;
2471        if up { scaled.ceil() } else { scaled.floor() }
2472    };
2473    let (left, top) = (cells(min[0], false), cells(min[1], false));
2474    let (right, bottom) = (cells(max[0], true), cells(max[1], true));
2475    GridRect {
2476        top_left: GridPoint {
2477            x: left as i32,
2478            y: top as i32,
2479        },
2480        size: GridSize {
2481            w: (right - left) as u32,
2482            h: (bottom - top) as u32,
2483        },
2484    }
2485}
2486
2487/// The union of everything the tracks depict, in the scope the first
2488/// depicted subject lives in. Advisory (never in the oracle), and absent
2489/// when nothing depicts a region yet.
2490///
2491/// A region is measured in the scope it frames: a scope is a coordinate
2492/// space of its own, so a track drawn in another one — the port body whose
2493/// pin re-slots on the boundary a level up — is left out of the union.
2494/// Ops with no scope at all (the document's own, an asset's) are in it,
2495/// since they depict their subject wherever the rest of the commit is.
2496fn camera_plan(scene: &Scene<'_>, tracks: &[Track]) -> Option<CameraPlan> {
2497    let scope = tracks
2498        .iter()
2499        .find_map(|track| scene.scope_of(&track.op))
2500        .unwrap_or(Scope::Root);
2501    let region = tracks
2502        .iter()
2503        .filter(|track| scene.scope_of(&track.op).is_none_or(|owner| owner == scope))
2504        .filter_map(|track| track_region(scene, track))
2505        .reduce(union)?;
2506    Some(CameraPlan { scope, region })
2507}
2508
2509/// The grid region one track depicts in: what its keys cover, or — where
2510/// they cover nothing the camera can frame — the [`fallback_region`] its
2511/// subject stands in. The camera unions these, and the pantomime picks
2512/// the first of them as the thing the hand went for.
2513fn track_region(scene: &Scene<'_>, track: &Track) -> Option<GridRect> {
2514    track
2515        .keys
2516        .iter()
2517        .filter_map(|key| key.value.bounds())
2518        .reduce(union)
2519        .or_else(|| fallback_region(scene, track.subject))
2520}
2521
2522/// The advisory region for a track whose keys depict no bounds of their
2523/// own — a crossfade at a label, a travel along a wire: the subject's own
2524/// footprint, chasing a wire label onto its route's solved polyline. This
2525/// is why no row needs the measured-text anchor projections the headless
2526/// gate refuses — the camera frames the thing whose writing moved, which
2527/// is what a viewer wants framed anyway.
2528fn fallback_region(scene: &Scene<'_>, subject: EntityRef) -> Option<GridRect> {
2529    let resolve = |side| match scene.footprint(side, subject) {
2530        TrackValue::Along { route, .. } => scene.footprint(side, EntityRef::Route(route)).bounds(),
2531        value => value.bounds(),
2532    };
2533    resolve(Side::Before).or_else(|| resolve(Side::After))
2534}
2535
2536fn union(a: GridRect, b: GridRect) -> GridRect {
2537    let x0 = a.top_left.x.min(b.top_left.x);
2538    let y0 = a.top_left.y.min(b.top_left.y);
2539    let x1 = a.right().max(b.right());
2540    let y1 = a.bottom().max(b.bottom());
2541    GridRect {
2542        top_left: GridPoint { x: x0, y: y0 },
2543        size: GridSize {
2544            w: (x1 - x0) as u32,
2545            h: (y1 - y0) as u32,
2546        },
2547    }
2548}
2549
2550#[cfg(test)]
2551pub(crate) mod tests;