Skip to main content

blockworx/edit/
create.rs

1//! The create family (`docs/op-emitter-playbook.md`, 10c): the emitters
2//! that mint entities. Every one takes its new ids pre-minted — ops land
3//! with concrete ids, so a compound gesture (a wire and the pin it lands
4//! on) is one commit.
5
6use ahash::HashSet;
7use blockworx_doc::{
8    block_model::{Area, Block, BlockUpdate, Icon, Label, Pin, Route, RouteLabel, Text},
9    commit::CommitBuilder,
10    document::{Document, IndexedDocument, TitleBlockUpdate},
11    geometry::{FracVal, GridPoint, GridRect, GridSize, GridVec, PinSlot, Waypoint},
12    id::{AreaId, BlockId, PinId, RouteId, RouteLabelId, TextId},
13    opcode::{Crud, OpCodes},
14    values::{LabelSide, PinDir, PinSide, Role},
15};
16use blockworx_geom::{Pos2, Rect};
17
18use crate::edit::lock::UnlockedScope;
19use crate::edit::lower::{block_rect, slot_capacity};
20use crate::grid::{
21    DEFAULT_SCALE_FOR_NEW_VIEW, GRID_SIZE, PIN_PITCH_GRID, PIN_TOP_MARGIN, ceil_block_height,
22    grid_point, grid_rect, pin_offset_y, px_rect, snap_block_height_cells,
23};
24use crate::path::{Resolved, Scope, resolve};
25use crate::shape::port::{PORT_HEIGHT, width_for_labels};
26
27/// The name a freshly drawn area carries into its rename editor. A block
28/// gets a numbered one instead ([`BLOCK_PREFIX`]) — an area never appears
29/// in a breadcrumb, a hierarchy row or a PDF outline, which is where
30/// same-named neighbours were confusing.
31const UNTITLED: &str = "Untitled";
32
33/// The auto-name a freshly drawn block carries, numbered across the whole
34/// document. The user ruled out the shared "Untitled" on 2026-08-31 — *"If
35/// the user declines to name a block, the fact that they are all named
36/// 'Untitled' is confusing"* — so a block is born with a name that already
37/// tells it apart (playbook R17).
38const BLOCK_PREFIX: &str = "Block ";
39
40/// The auto-name every stamped pin takes, numbered per owner.
41const PORT_PREFIX: &str = "Port ";
42
43/// A fresh label at its kind's legacy resting side: a block title sits at
44/// the bottom (`BlockLabel::default`), a type label and an area title at
45/// the top (`BlockLabel::upper_left`, `Area::new`).
46fn label(name: &str, side: LabelSide) -> Label {
47    Label {
48        name: name.into(),
49        side,
50        offset: FracVal::default(),
51        hidden: false,
52    }
53}
54
55/// The block a drag drew: where it lands, and the two corners it spans.
56/// A struct like [`NewPin`] and [`NewPort`], because the emitter also needs
57/// the document to name it and seven loose arguments say nothing.
58#[derive(Clone, Copy, Debug)]
59pub struct NewBlock {
60    pub id: BlockId,
61    pub scope: Scope,
62    pub start: Pos2,
63    pub end: Pos2,
64}
65
66/// Inventory row "New Block": a drag stamps a child block on `scope` — the
67/// document root is a scope like any other. The parent's child list is
68/// derived, so `parent` is the whole structural write.
69///
70/// The block is born named (R17), which is why this needs the document: the
71/// number is the first one no other block's title claims. Naming it here
72/// rather than at each surface means the canvas, the breadcrumb, the
73/// hierarchy, the palette and the PDF outline are all fixed by one policy,
74/// and the name is a real, editable, journalled one rather than a label
75/// five renderers have to agree on.
76pub fn block(doc: &Document, new: NewBlock, builder: &mut CommitBuilder) {
77    let NewBlock {
78        id,
79        scope,
80        start,
81        end,
82    } = new;
83    builder.push(OpCodes::Block(
84        id,
85        Crud::Create(Block {
86            parent: scope.wire_id(),
87            rect: block_rect(start, end),
88            locked: false,
89            role: Role::default(),
90            title: label(&next_free_title(doc, BLOCK_PREFIX), LabelSide::Bottom),
91            type_label: label("", LabelSide::Top),
92            icon: Icon::default(),
93        }),
94    ));
95}
96
97/// The box a block dropped on the canvas takes, anchored top-left at the
98/// drop point. A drop names one corner and a block needs two, so the extent
99/// is the one the document already gives a block nobody sized
100/// ([`TOP_BLOCK_DEFAULT_RECT`]) — deliberately small and fixed.
101pub fn stamped_block(at: Pos2) -> Rect {
102    stamped(at, TOP_BLOCK_DEFAULT_RECT.size)
103}
104
105/// A default extent hung off a grid-snapped drop point — the arithmetic the
106/// `stamped_*` defaults share.
107fn stamped(at: Pos2, size: GridSize) -> Rect {
108    px_rect(GridRect {
109        top_left: grid_point(at),
110        size,
111    })
112}
113
114/// Inventory row "New Area": a drag stamps a boundary annotation box.
115/// Unlike a block, an area holds no pins, so its height is whatever the
116/// drag drew.
117pub fn area(id: AreaId, owner: Scope, start: Pos2, end: Pos2, builder: &mut CommitBuilder) {
118    builder.push(OpCodes::Area(
119        id,
120        Crud::Create(Area {
121            owner: owner.wire_id(),
122            rect: grid_rect(start, end),
123            role: Role::default(),
124            title: label(UNTITLED, LabelSide::Top),
125        }),
126    ));
127}
128
129/// The box an area dropped on the canvas takes, on [`stamped_block`]'s
130/// terms. Wider than a block and free of its pin ladder: an area's job is to
131/// enclose a few of them.
132pub fn stamped_area(at: Pos2) -> Rect {
133    stamped(at, STAMPED_AREA)
134}
135
136const STAMPED_AREA: GridSize = GridSize { w: 20, h: 14 };
137
138/// Inventory row "New Text Box": a click places an empty text annotation
139/// and opens its editor; emptying it there is what deletes it
140/// (`naming::edit_text`).
141pub fn text_box(id: TextId, owner: Scope, pos: Pos2, builder: &mut CommitBuilder) {
142    builder.push(OpCodes::Text(
143        id,
144        Crud::Create(Text {
145            owner: owner.wire_id(),
146            text: String::new(),
147            pos: grid_point(pos),
148            role: Role::default(),
149        }),
150    ));
151}
152
153/// A pin the gesture stamped on a block edge: [`pin`]'s parameters, and
154/// the rider [`route`] carries when a wire lands on a free slot.
155#[derive(Clone, Copy, Debug)]
156pub struct NewPin {
157    pub id: PinId,
158    /// The scope that takes it — as proof rather than as a name, because
159    /// adding a pin is a material edit and a locked block refuses one
160    /// (`crate::edit::lock`). A `NewPin` for a locked owner is
161    /// unconstructible.
162    pub owner: UnlockedScope,
163    pub slot: PinSlot,
164}
165
166/// A boundary port the gesture stamped: the block whose boundary it joins,
167/// and the two corners of the body box drawn for it. Its slot is searched,
168/// not given.
169#[derive(Clone, Copy, Debug)]
170pub struct NewPort {
171    pub id: PinId,
172    /// As [`NewPin::owner`].
173    pub owner: UnlockedScope,
174    pub start: Pos2,
175    pub end: Pos2,
176}
177
178pub(crate) fn owner_pins<'a>(
179    indexed: &'a IndexedDocument<'a>,
180    owner: Scope,
181) -> impl Iterator<Item = &'a Pin> + 'a {
182    indexed
183        .index
184        .blocks
185        .get(&owner.wire_id())
186        .into_iter()
187        .flat_map(|entry| entry.pins.iter())
188        .filter_map(|id| indexed.doc.pin(id))
189}
190
191/// The auto-name a stamped pin takes (`"Port N"`, tag `"N"` —
192/// `Drawing::add_port_auto_named`, `add_named_pin`). Ids count per
193/// document, not per block, so the number is read back off the owner's
194/// pins: one past the highest they already carry.
195fn next_pin_ordinal(indexed: &IndexedDocument<'_>, owner: Scope) -> u32 {
196    owner_pins(indexed, owner)
197        .filter_map(|pin| {
198            pin.name
199                .strip_prefix(PORT_PREFIX)
200                .and_then(|ordinal| ordinal.parse::<u32>().ok())
201        })
202        .max()
203        .unwrap_or(0)
204        + 1
205}
206
207/// Where a stamped pin's port body sits inside its owner's own interior
208/// view, ported from `Block::default_port_rect` (`src/shape/block.rs:
209/// 288-318`): the owner's rect magnified by [`DEFAULT_SCALE_FOR_NEW_VIEW`]
210/// so the expanded view has room for child blocks, the body hugging the
211/// edge its slot sits on and tracking the slot down that edge. A body
212/// landing on an existing one steps outward by its own width until clear —
213/// bounded, like the legacy, so a pathological interior cannot spin.
214///
215/// Shared with the port emitters, which
216/// reconstructs the same placement for a pin whose file omitted it, the
217/// way `Block::fill_missing_port_rects` does on the legacy load.
218pub(crate) fn default_port_rect(
219    rect: GridRect,
220    siblings: &[GridRect],
221    name: &str,
222    slot: PinSlot,
223) -> GridRect {
224    let origin = rect.top_left;
225    let scaled_w = rect.size.w as i32 * DEFAULT_SCALE_FOR_NEW_VIEW;
226    let width = width_for_labels(name, "");
227    let top = origin.y + slot.offset as i32 * PIN_PITCH_GRID * DEFAULT_SCALE_FOR_NEW_VIEW;
228    let (left, step) = match slot.side {
229        PinSide::East => (
230            (origin.x + scaled_w - width as i32).max(origin.x),
231            width as i32,
232        ),
233        PinSide::West => (origin.x, -(width as i32)),
234    };
235    let mut candidate = GridRect {
236        top_left: GridPoint { x: left, y: top },
237        size: GridSize {
238            w: width,
239            h: PORT_HEIGHT,
240        },
241    };
242    let mut steps = 0;
243    while siblings.iter().any(|body| candidate.intersects(*body)) && steps <= siblings.len() {
244        candidate = candidate.translate(GridVec::new(step, 0));
245        steps += 1;
246    }
247    candidate
248}
249
250/// The pin a stamp lands as.
251///
252/// The owner is a [`UnlockedScope`], so "a locked block keeps its interface
253/// frozen" and "a block the document does not hold takes nothing" are both
254/// answered before this is called — there is no `NewPin` for either.
255fn pin_init(indexed: &IndexedDocument<'_>, new: NewPin) -> Pin {
256    // The document root is a scope without an entity behind it (F9): it holds
257    // pins, is never locked, and has no rect to place a body against — the
258    // only gesture that stamps one there is [`port`], which draws the body
259    // itself and overwrites the placement below.
260    let owner = resolve(indexed, new.owner.scope());
261    let ordinal = next_pin_ordinal(indexed, new.owner.scope());
262    let name = format!("{PORT_PREFIX}{ordinal}");
263    let siblings: Vec<GridRect> = owner_pins(indexed, new.owner.scope())
264        .map(|pin| pin.rect)
265        .collect();
266    Pin {
267        owner: new.owner.scope().wire_id(),
268        rect: match owner {
269            Resolved::Block(block) => default_port_rect(block.rect, &siblings, &name, new.slot),
270            Resolved::Root | Resolved::Absent => GridRect::default(),
271        },
272        name,
273        type_name: String::new(),
274        tag: ordinal.to_string(),
275        tag_hidden: false,
276        slot: new.slot,
277        dir: PinDir::InOut,
278        port_accent: Role::default(),
279        flip_lr: false,
280    }
281}
282
283/// Inventory row "Add Pin (block edge)": a click on a slot marker — or a
284/// wire pulled out of one — stamps a pin there.
285pub fn pin(indexed: &IndexedDocument<'_>, new: NewPin, builder: &mut CommitBuilder) {
286    let init = pin_init(indexed, new);
287    builder.push(OpCodes::Pin(new.id, Crud::Create(init)));
288}
289
290/// The shortest block, in cells, that still offers slot `slot`: the slot's
291/// own row plus the clearance [`max_pin_slot`](crate::grid::max_pin_slot)
292/// keeps below it.
293fn height_for_slot(slot: u32) -> u32 {
294    (ceil_block_height(pin_offset_y(0.0, slot) + PIN_TOP_MARGIN) / GRID_SIZE).round() as u32
295}
296
297/// The first slot the owner's pins leave free, scanning top-down and West
298/// before East (the legacy `Block::new_pin_locations` order). Total: each
299/// offset offers two slots, so some slot at an offset in `0..=n` escapes
300/// `n` occupants.
301///
302/// Shared with the clipboard (10g): a paste slots its pins by the same
303/// policy a stamp does, group at a time.
304pub(crate) fn first_free_slot(occupied: &HashSet<PinSlot>) -> PinSlot {
305    (0..=occupied.len() as u32)
306        .flat_map(|offset| [PinSide::West, PinSide::East].map(|side| PinSlot { side, offset }))
307        .find(|slot| !occupied.contains(slot))
308        .unwrap_or_default()
309}
310
311/// The growth rider: a block too short for `slot` grows two cells at a
312/// time until it fits (legacy `add_port_auto_named`'s loop). `None` when
313/// it already fits.
314pub(crate) fn grown_to_fit(block: &Block, slot: PinSlot) -> Option<GridRect> {
315    let rect = block.rect;
316    let mut height = rect.size.h;
317    while slot_capacity(height) < slot.offset {
318        height += 2;
319    }
320    (height != rect.size.h).then_some(GridRect {
321        size: GridSize {
322            h: height,
323            ..rect.size
324        },
325        ..rect
326    })
327}
328
329/// The body box a port dropped on the canvas takes, on [`stamped_block`]'s
330/// terms — and the width a port *drawn* falls back to when the drag is
331/// narrower than one. The height is [`PORT_HEIGHT`], which [`port`] forces
332/// on whatever box it is handed anyway.
333pub fn stamped_port(at: Pos2) -> Rect {
334    stamped(
335        at,
336        GridSize {
337            w: STAMPED_PORT_WIDTH,
338            h: PORT_HEIGHT,
339        },
340    )
341}
342
343pub const STAMPED_PORT_WIDTH: u32 = 4;
344
345/// Inventory row "Add Port (boundary)": stamp a port on the scope's own
346/// boundary at the first free slot, growing the block until that slot
347/// fits.
348pub fn port(indexed: &IndexedDocument<'_>, new: NewPort, builder: &mut CommitBuilder) {
349    let owner = resolve(indexed, new.owner.scope());
350    if let Resolved::Absent = owner {
351        return;
352    }
353    let occupied: HashSet<PinSlot> = owner_pins(indexed, new.owner.scope())
354        .map(|pin| pin.slot)
355        .collect();
356    let slot = first_free_slot(&occupied);
357    let stamp = NewPin {
358        id: new.id,
359        owner: new.owner,
360        slot,
361    };
362    let init = pin_init(indexed, stamp);
363    let body = grid_rect(new.start, new.end);
364    builder.push(OpCodes::Pin(
365        new.id,
366        Crud::Create(Pin {
367            // A port is drawn, not auto-placed: the gesture box replaces
368            // the interior placement, keeping only the fixed body height.
369            rect: GridRect {
370                size: GridSize {
371                    h: PORT_HEIGHT,
372                    ..body.size
373                },
374                ..body
375            },
376            ..init
377        }),
378    ));
379    // The root scope has no box to grow: its ports sit where they are drawn.
380    if let Resolved::Block(block) = owner
381        && let Some(rect) = grown_to_fit(block, slot)
382    {
383        builder.push(OpCodes::Block(
384            new.owner.scope().wire_id(),
385            Crud::Update(BlockUpdate::Rect(rect)),
386        ));
387    }
388}
389
390/// Where a drawn wire landed: on a pin that already exists, or on a free
391/// slot marker, which stamps the destination pin in the same commit.
392#[derive(Clone, Copy, Debug)]
393pub enum RouteEnd {
394    Pin(PinId),
395    Fresh(NewPin),
396}
397
398/// One wire the gesture drew.
399#[derive(Clone, Debug)]
400pub struct NewRoute {
401    pub id: RouteId,
402    pub owner: Scope,
403    pub from: PinId,
404    pub to: RouteEnd,
405    /// The corners the router solved, in path order.
406    pub waypoints: Vec<Waypoint>,
407}
408
409/// Inventory row "New Route (wire)": connect two pins along the solved
410/// path. A wire landing on a free slot carries its destination pin, and a
411/// destination the owner refuses takes the wire with it — a rejected route
412/// must never leave an orphan pin (legacy `RouteTool::commit`).
413pub fn route(indexed: &IndexedDocument<'_>, new: NewRoute, builder: &mut CommitBuilder) {
414    let to = match new.to {
415        RouteEnd::Pin(id) => id,
416        RouteEnd::Fresh(stamp) => {
417            // No refusal to handle: the stamp carries an `UnlockedScope`, so
418            // a wire cannot arrive here proposing a pin on a frozen block.
419            let init = pin_init(indexed, stamp);
420            builder.push(OpCodes::Pin(stamp.id, Crud::Create(init)));
421            stamp.id
422        }
423    };
424    builder.push(OpCodes::Route(
425        new.id,
426        Crud::Create(Route {
427            owner: new.owner.wire_id(),
428            name: String::new(),
429            from: new.from,
430            to,
431            role: Role::default(),
432            waypoints: new.waypoints,
433        }),
434    ));
435}
436
437/// Where a grabbed corner lands in a route's polyline — the arc-length
438/// ordinal the gesture classified against solved geometry the emitter
439/// cannot see. Past the end clamps to the end.
440#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
441pub struct PathOrdinal(usize);
442
443impl PathOrdinal {
444    pub fn new(index: usize) -> Self {
445        Self(index)
446    }
447}
448
449impl From<PathOrdinal> for usize {
450    fn from(ordinal: PathOrdinal) -> usize {
451        ordinal.0
452    }
453}
454
455/// Inventory row "Add Wire Label": drop a name label on a wire, at the arc
456/// length the click projected onto its polyline.
457pub fn wire_label(
458    doc: &Document,
459    id: RouteLabelId,
460    route: RouteId,
461    pos: FracVal,
462    builder: &mut CommitBuilder,
463) {
464    if doc.route(&route).is_none() {
465        return;
466    }
467    builder.push(OpCodes::RouteLabel(
468        id,
469        Crud::Create(RouteLabel { owner: route, pos }),
470    ));
471}
472
473pub(crate) const TOP_BLOCK_DEFAULT_WIDTH: u32 = 8;
474pub(crate) const TOP_BLOCK_DEFAULT_HEIGHT: u32 = 16;
475
476/// The rect a top block is born with: deliberately small and fixed rather
477/// than derived from what it holds.
478const TOP_BLOCK_DEFAULT_RECT: GridRect = GridRect {
479    top_left: GridPoint { x: 0, y: 0 },
480    size: GridSize {
481        w: TOP_BLOCK_DEFAULT_WIDTH,
482        h: TOP_BLOCK_DEFAULT_HEIGHT,
483    },
484};
485
486/// The auto-name a wrapped level takes (legacy `next_top_name`).
487const TOP_PREFIX: &str = "top_";
488
489/// The lowest `{prefix}<n>` from 1 up that no live block titles itself, so
490/// two blocks the user has not named are still told apart wherever their
491/// titles are listed — the nav breadcrumb, the hierarchy, the PDF outline.
492///
493/// The search reads the titles the document actually holds rather than
494/// counting blocks, so it stays deterministic and collision-free after a
495/// delete or a paste: a document that lost `Block 2` mints that name again
496/// instead of climbing past it forever.
497fn next_free_title(doc: &Document, prefix: &str) -> String {
498    let taken: HashSet<&str> = doc
499        .blocks()
500        .map(|(_, block)| block.title.name.as_str())
501        .collect();
502    // One more candidate than there are blocks, so at least one is free;
503    // the fallback keeps the search total instead of unwrapping.
504    let last = taken.len() + 1;
505    (1..=last)
506        .map(|n| format!("{prefix}{n}"))
507        .find(|candidate| !taken.contains(candidate.as_str()))
508        .unwrap_or_else(|| format!("{prefix}{}", last + 1))
509}
510
511/// The rect the old root shows as once demoted: the one it already has,
512/// made tall enough that its lowest pin slot still fits. Its size is *not*
513/// derived from what it contains — a block as large as the whole drawing
514/// is unwieldy — and a degenerate rect (a root written before tops were
515/// born with a size) falls back to the default rather than demoting to an
516/// invisible sliver.
517fn demoted_rect(block: &Block, lowest_slot: u32) -> GridRect {
518    let own = block.rect;
519    let width = if own.size.w == 0 {
520        TOP_BLOCK_DEFAULT_WIDTH
521    } else {
522        own.size.w
523    };
524    let height = if own.size.h == 0 {
525        TOP_BLOCK_DEFAULT_HEIGHT
526    } else {
527        own.size.h
528    };
529    GridRect {
530        top_left: own.top_left,
531        size: GridSize {
532            w: width,
533            h: snap_block_height_cells(height.max(height_for_slot(lowest_slot))),
534        },
535    }
536}
537
538/// Inventory row "Wrap Top": add a level above the whole document — a
539/// fresh root whose sole child is the old one, which demotes to a child
540/// rect. A document with no root yet (`top` is `NULL`) has nothing to
541/// demote, so the wrap is just the new root.
542pub fn wrap_top(indexed: &IndexedDocument<'_>, new_root: BlockId, builder: &mut CommitBuilder) {
543    builder.push(OpCodes::Block(
544        new_root,
545        Crud::Create(Block {
546            parent: Scope::Root.wire_id(),
547            rect: TOP_BLOCK_DEFAULT_RECT,
548            locked: false,
549            role: Role::default(),
550            title: label(&next_free_title(indexed.doc, TOP_PREFIX), LabelSide::Bottom),
551            type_label: label("", LabelSide::Top),
552            icon: Icon::default(),
553        }),
554    ));
555    let old_root = indexed.doc.title_block().top;
556    if let Some(old) = indexed.doc.block(&old_root) {
557        builder.push(OpCodes::Block(
558            old_root,
559            Crud::Update(BlockUpdate::Parent(new_root)),
560        ));
561        let lowest_slot = owner_pins(indexed, Scope::Block(old_root))
562            .map(|pin| pin.slot.offset)
563            .max()
564            .unwrap_or(0);
565        let demoted = demoted_rect(old, lowest_slot);
566        if demoted != old.rect {
567            builder.push(OpCodes::Block(
568                old_root,
569                Crud::Update(BlockUpdate::Rect(demoted)),
570            ));
571        }
572    }
573    builder.push(OpCodes::Document(TitleBlockUpdate::Top(new_root)));
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use blockworx_geom::pos2;
580
581    /// The proof a create takes, for a test whose owner is unlocked. A locked
582    /// one is `UnlockedScope::of(...) == None`, which is the point of the
583    /// type and is asserted where it matters.
584    fn scope(indexed: &IndexedDocument<'_>, owner: Scope) -> UnlockedScope {
585        UnlockedScope::of(indexed, owner).expect("the owner is unlocked")
586    }
587    use crate::edit::harness::{fold, seals_to_nothing, wired};
588    use blockworx_doc::document::DocIndex;
589    use blockworx_doc::{
590        block_model::PinUpdate,
591        fixtures::{area_id, block_id, pin_id, route_id, route_label_id, text_id},
592    };
593
594    /// A gesture point, in whole grid cells.
595    fn at(x: f32, y: f32) -> Pos2 {
596        pos2(x * GRID_SIZE, y * GRID_SIZE)
597    }
598
599    fn rect(x: i32, y: i32, w: u32, h: u32) -> GridRect {
600        GridRect {
601            top_left: GridPoint { x, y },
602            size: GridSize { w, h },
603        }
604    }
605
606    fn slot(side: PinSide, offset: u32) -> PinSlot {
607        PinSlot { side, offset }
608    }
609
610    /// A corner the router solved, free to be re-solved.
611    fn corner(x: i32, y: i32) -> Waypoint {
612        Waypoint {
613            pos: GridPoint { x, y },
614            locked: false,
615        }
616    }
617
618    /// A corner the user put where it is, which re-routing must respect.
619    fn pinned(x: i32, y: i32) -> Waypoint {
620        Waypoint {
621            locked: true,
622            ..corner(x, y)
623        }
624    }
625
626    fn block_of(doc: &Document, id: u32) -> &Block {
627        doc.block(&block_id(id)).expect("the scene's block exists")
628    }
629
630    fn pin_of(doc: &Document, id: u32) -> &Pin {
631        doc.pin(&pin_id(id)).expect("the scene's pin exists")
632    }
633
634    /// The width every stamp in these tests places, since each takes a
635    /// six-character auto-name.
636    fn body_width() -> u32 {
637        width_for_labels("Port 1", "")
638    }
639
640    /// Block 1 at a real size — the interior a stamped pin's body is
641    /// placed in.
642    fn sized() -> Document {
643        let doc = wired();
644        let mut builder = CommitBuilder::new("Sized the block");
645        builder.push(OpCodes::Block(
646            block_id(1),
647            Crud::Update(BlockUpdate::Rect(rect(0, 0, 8, 16))),
648        ));
649        let doc = fold(builder, &doc);
650        assert!(
651            8 * DEFAULT_SCALE_FOR_NEW_VIEW > body_width() as i32,
652            "precondition: the scaled interior is wider than a port body, \
653             so the two edges are distinguishable placements"
654        );
655        doc
656    }
657
658    /// Block 1 four cells tall — one pin slot — with both of that slot's
659    /// places taken, so a fresh port has to grow it.
660    fn packed() -> Document {
661        let doc = wired();
662        let mut builder = CommitBuilder::new("Packed the boundary");
663        builder.push(OpCodes::Block(
664            block_id(1),
665            Crud::Update(BlockUpdate::Rect(rect(0, 0, 8, 4))),
666        ));
667        builder.push(OpCodes::Pin(
668            pin_id(3),
669            Crud::Update(PinUpdate::Slot(slot(PinSide::West, 0))),
670        ));
671        builder.push(OpCodes::Pin(
672            pin_id(4),
673            Crud::Update(PinUpdate::Slot(slot(PinSide::East, 0))),
674        ));
675        let doc = fold(builder, &doc);
676        assert_eq!(
677            slot_capacity(4),
678            0,
679            "precondition: a four-cell block offers slot 0 and nothing else"
680        );
681        doc
682    }
683
684    /// The scene with its block's pin interface frozen — the arm every pin
685    /// stamp must decline.
686    fn locked_scene() -> Document {
687        let doc = wired();
688        let mut builder = CommitBuilder::new("Locked the block");
689        builder.push(OpCodes::Block(
690            block_id(1),
691            Crud::Update(BlockUpdate::Locked(true)),
692        ));
693        let doc = fold(builder, &doc);
694        assert!(
695            block_of(&doc, 1).locked,
696            "precondition: the owner is locked"
697        );
698        doc
699    }
700
701    #[test]
702    fn block_stamps_a_numbered_child_on_the_height_ladder() {
703        let doc = wired();
704        let mut builder = CommitBuilder::new("Drew a block");
705        block(
706            &doc,
707            NewBlock {
708                id: block_id(10),
709                scope: Scope::Block(block_id(1)),
710                start: at(1.0, 2.0),
711                end: at(9.0, 7.0),
712            },
713            &mut builder,
714        );
715        let doc = fold(builder, &doc);
716
717        let new = block_of(&doc, 10);
718        assert_eq!(new.parent, block_id(1));
719        assert_eq!(
720            new.rect,
721            rect(1, 2, 8, 4),
722            "the drag's five-cell height settles onto the ladder's first rung"
723        );
724        assert_eq!(new.title.name, "Block 1");
725        assert_eq!(new.title.side, LabelSide::Bottom);
726        assert_eq!(new.type_label.side, LabelSide::Top);
727        assert_eq!(new.type_label.name, "");
728        assert!(!new.locked);
729        assert_eq!(new.role, Role::Accent0);
730        assert_eq!(new.icon, Icon::default());
731        assert!(
732            DocIndex::of(&doc).blocks[&block_id(1)]
733                .children
734                .contains(&block_id(10)),
735            "the scope's child list is presentation from the parent register"
736        );
737    }
738
739    #[test]
740    fn a_block_at_the_document_root_takes_the_null_parent() {
741        let doc = wired();
742        let mut builder = CommitBuilder::new("Drew a top-level block");
743        block(
744            &doc,
745            NewBlock {
746                id: block_id(10),
747                scope: Scope::Root,
748                start: at(0.0, 0.0),
749                end: at(4.0, 4.0),
750            },
751            &mut builder,
752        );
753        let doc = fold(builder, &doc);
754
755        assert_eq!(Scope::from_wire(block_of(&doc, 10).parent), Scope::Root);
756        assert!(
757            DocIndex::of(&doc).blocks[&Scope::Root.wire_id()]
758                .children
759                .contains(&block_id(10))
760        );
761    }
762
763    /// R17: an unnamed block is told apart by its own title, not by the
764    /// surface listing it. The number is the lowest one free, so consecutive
765    /// stamps count up — and a stamp after a delete refills the gap rather
766    /// than climbing past it, which is what makes the name deterministic
767    /// however the document got to where it is.
768    #[test]
769    fn a_stamped_block_takes_the_lowest_free_number_and_refills_gaps() {
770        let stamp = |doc: &Document, id: u32| {
771            let mut builder = CommitBuilder::new("Drew a block");
772            block(
773                doc,
774                NewBlock {
775                    id: block_id(id),
776                    scope: Scope::Root,
777                    start: at(0.0, 0.0),
778                    end: at(4.0, 4.0),
779                },
780                &mut builder,
781            );
782            fold(builder, doc)
783        };
784        let doc = stamp(&wired(), 10);
785        assert_eq!(block_of(&doc, 10).title.name, "Block 1");
786
787        let doc = stamp(&doc, 11);
788        assert_eq!(
789            block_of(&doc, 11).title.name,
790            "Block 2",
791            "a second stamp reused the first block's name",
792        );
793
794        let mut builder = CommitBuilder::new("Deleted a block");
795        let mut index = DocIndex::default();
796        crate::edit::delete::selection(
797            &index.view(&doc),
798            &[crate::edit::delete::Target::Block(block_id(10))],
799            &mut builder,
800        );
801        let doc = fold(builder, &doc);
802        assert!(
803            doc.block(&block_id(10)).is_none(),
804            "precondition: the block holding \"Block 1\" is gone",
805        );
806
807        let doc = stamp(&doc, 12);
808        assert_eq!(
809            block_of(&doc, 12).title.name,
810            "Block 1",
811            "the freed number was climbed past instead of reused",
812        );
813    }
814
815    #[test]
816    fn area_stamps_an_untitled_box_off_the_height_ladder() {
817        let doc = wired();
818        let mut builder = CommitBuilder::new("Drew an area");
819        area(
820            area_id(10),
821            Scope::Block(block_id(1)),
822            at(1.0, 2.0),
823            at(9.0, 7.0),
824            &mut builder,
825        );
826        let doc = fold(builder, &doc);
827
828        let new = doc.area(&area_id(10)).expect("the area exists");
829        assert_eq!(new.owner, block_id(1));
830        assert_eq!(
831            new.rect,
832            rect(1, 2, 8, 5),
833            "an area holds no pins, so its height is the one drawn"
834        );
835        assert_eq!(new.title.name, "Untitled");
836        assert_eq!(new.title.side, LabelSide::Top);
837        assert_eq!(new.role, Role::Accent0);
838    }
839
840    #[test]
841    fn text_box_stamps_an_empty_box_at_the_snapped_click() {
842        let doc = wired();
843        let mut builder = CommitBuilder::new("Placed a text box");
844        text_box(
845            text_id(10),
846            Scope::Block(block_id(1)),
847            at(3.4, 5.6),
848            &mut builder,
849        );
850        let doc = fold(builder, &doc);
851
852        let new = doc.text(&text_id(10)).expect("the text exists");
853        assert_eq!(new.owner, block_id(1));
854        assert_eq!(new.text, "");
855        assert_eq!(new.pos, GridPoint { x: 3, y: 6 });
856        assert_eq!(new.role, Role::Accent0);
857    }
858
859    #[test]
860    fn port_takes_the_first_free_slot_and_grows_the_block_to_fit() {
861        let doc = packed();
862        let mut index = DocIndex::default();
863        let mut builder = CommitBuilder::new("Stamped a port");
864        let view = index.view(&doc);
865        port(
866            &view,
867            NewPort {
868                id: pin_id(10),
869                owner: scope(&view, Scope::Block(block_id(1))),
870                start: at(2.0, 2.0),
871                end: at(7.0, 5.0),
872            },
873            &mut builder,
874        );
875        let doc = fold(builder, &doc);
876
877        let new = pin_of(&doc, 10);
878        assert_eq!(
879            new.slot,
880            slot(PinSide::West, 1),
881            "slot 0 is taken on both sides, so the next offset's West place wins"
882        );
883        assert_eq!(new.name, "Port 1");
884        assert_eq!(new.tag, "1");
885        assert_eq!(new.dir, PinDir::InOut);
886        assert_eq!(
887            new.rect,
888            rect(2, 2, 5, PORT_HEIGHT),
889            "the gesture box places and widens the body; its height is fixed"
890        );
891
892        assert_eq!(
893            slot_capacity(6),
894            0,
895            "precondition: one two-cell step is not enough, so the loop must run twice"
896        );
897        assert_eq!(
898            block_of(&doc, 1).rect.size,
899            GridSize { w: 8, h: 8 },
900            "the owner grew two cells at a time until the slot fit"
901        );
902    }
903
904    #[test]
905    fn ports_number_themselves_from_the_owners_pins() {
906        let doc = packed();
907        let stamp = |doc: &Document, id: u32| {
908            let mut builder = CommitBuilder::new("Stamped a port");
909            let mut index = DocIndex::default();
910            let view = index.view(doc);
911            port(
912                &view,
913                NewPort {
914                    id: pin_id(id),
915                    owner: scope(&view, Scope::Block(block_id(1))),
916                    start: at(0.0, 0.0),
917                    end: at(5.0, 2.0),
918                },
919                &mut builder,
920            );
921            fold(builder, doc)
922        };
923        assert!(
924            !pin_of(&doc, 3).name.starts_with(PORT_PREFIX),
925            "precondition: the scene's hand-named pins carry no ordinal"
926        );
927
928        let doc = stamp(&doc, 10);
929        let doc = stamp(&doc, 11);
930        assert_eq!(pin_of(&doc, 10).name, "Port 1");
931        assert_eq!(pin_of(&doc, 11).name, "Port 2");
932        assert_eq!(pin_of(&doc, 11).tag, "2");
933        assert_ne!(
934            pin_of(&doc, 10).slot,
935            pin_of(&doc, 11).slot,
936            "each stamp takes a slot the last one left free"
937        );
938    }
939
940    #[test]
941    fn pin_stamps_at_the_given_slot() {
942        let doc = sized();
943        let mut index = DocIndex::default();
944        let mut builder = CommitBuilder::new("Stamped a pin");
945        let view = index.view(&doc);
946        pin(
947            &view,
948            NewPin {
949                id: pin_id(10),
950                owner: scope(&view, Scope::Block(block_id(1))),
951                slot: slot(PinSide::East, 2),
952            },
953            &mut builder,
954        );
955        let doc = fold(builder, &doc);
956
957        let new = pin_of(&doc, 10);
958        assert_eq!(new.owner, block_id(1));
959        assert_eq!(new.slot, slot(PinSide::East, 2));
960        assert_eq!(new.name, "Port 1");
961        assert_eq!(new.tag, "1");
962        assert_eq!(new.dir, PinDir::InOut);
963        assert!(!new.tag_hidden);
964        assert!(!new.flip_lr);
965        assert_eq!(
966            block_of(&doc, 1).rect.size,
967            GridSize { w: 8, h: 16 },
968            "a pin at a named slot never grows its block"
969        );
970    }
971
972    #[test]
973    fn a_stamped_pins_body_hugs_the_edge_its_slot_sits_on() {
974        let doc = sized();
975        let stamp = |doc: &Document, id: u32, side| {
976            let mut builder = CommitBuilder::new("Stamped a pin");
977            let mut index = DocIndex::default();
978            let view = index.view(doc);
979            pin(
980                &view,
981                NewPin {
982                    id: pin_id(id),
983                    owner: scope(&view, Scope::Block(block_id(1))),
984                    slot: slot(side, 1),
985                },
986                &mut builder,
987            );
988            fold(builder, doc)
989        };
990
991        let doc = stamp(&doc, 10, PinSide::West);
992        let doc = stamp(&doc, 11, PinSide::East);
993        let interior = block_of(&doc, 1).rect;
994        let west = pin_of(&doc, 10).rect;
995        let east = pin_of(&doc, 11).rect;
996
997        assert_eq!(
998            west,
999            rect(
1000                interior.left(),
1001                interior.top() + PIN_PITCH_GRID * DEFAULT_SCALE_FOR_NEW_VIEW,
1002                body_width(),
1003                PORT_HEIGHT
1004            ),
1005            "a West slot's body hugs the left of the magnified interior, one slot down"
1006        );
1007        assert_eq!(
1008            east.right(),
1009            interior.left() + interior.size.w as i32 * DEFAULT_SCALE_FOR_NEW_VIEW,
1010            "an East slot's body hugs the right of the magnified interior"
1011        );
1012        assert_eq!(east.top(), west.top(), "both track the same slot");
1013        assert!(
1014            west.right() < east.left(),
1015            "the two edges are placements apart, not the same box"
1016        );
1017    }
1018
1019    #[test]
1020    fn a_body_that_would_cover_a_sibling_steps_outward() {
1021        let doc = sized();
1022        let mut index = DocIndex::default();
1023        let taken = default_port_rect(
1024            block_of(&doc, 1).rect,
1025            &[],
1026            "Port 1",
1027            slot(PinSide::West, 1),
1028        );
1029        let mut builder = CommitBuilder::new("Parked a body in the way");
1030        builder.push(OpCodes::Pin(
1031            pin_id(3),
1032            Crud::Update(PinUpdate::Rect(taken)),
1033        ));
1034        let doc = fold(builder, &doc);
1035        assert!(
1036            default_port_rect(
1037                block_of(&doc, 1).rect,
1038                &[],
1039                "Port 1",
1040                slot(PinSide::West, 1),
1041            )
1042            .intersects(taken),
1043            "precondition: unstepped, the stamp would land on the parked body"
1044        );
1045
1046        let mut builder = CommitBuilder::new("Stamped a pin behind it");
1047        let view = index.view(&doc);
1048        pin(
1049            &view,
1050            NewPin {
1051                id: pin_id(10),
1052                owner: scope(&view, Scope::Block(block_id(1))),
1053                slot: slot(PinSide::West, 1),
1054            },
1055            &mut builder,
1056        );
1057        let doc = fold(builder, &doc);
1058
1059        let stepped = pin_of(&doc, 10).rect;
1060        assert!(
1061            !stepped.intersects(taken),
1062            "the body stepped clear of its sibling"
1063        );
1064        assert_eq!(
1065            stepped,
1066            taken.translate(GridVec::new(-(body_width() as i32), 0)),
1067            "a West body steps outward by its own width — away from the interior"
1068        );
1069    }
1070
1071    /// F9: the document root is a scope with no entity behind it, and a
1072    /// serverless boot starts there — so the port stamp has to land without
1073    /// an owner block to read a lock, a box, or a growth from.
1074    #[test]
1075    fn the_document_root_takes_a_port_though_it_has_no_block() {
1076        let doc = packed();
1077        let mut index = DocIndex::default();
1078        assert!(
1079            doc.block(&Scope::Root.wire_id()).is_none(),
1080            "precondition: the root is a scope, not an entity"
1081        );
1082
1083        let mut builder = CommitBuilder::new("Stamped a port on the root");
1084        let view = index.view(&doc);
1085        port(
1086            &view,
1087            NewPort {
1088                id: pin_id(10),
1089                owner: scope(&view, Scope::Root),
1090                start: at(2.0, 2.0),
1091                end: at(7.0, 5.0),
1092            },
1093            &mut builder,
1094        );
1095        let doc = fold(builder, &doc);
1096
1097        let new = pin_of(&doc, 10);
1098        assert_eq!(Scope::from_wire(new.owner), Scope::Root);
1099        assert_eq!(
1100            new.slot,
1101            slot(PinSide::West, 0),
1102            "the root held no pins, so the first slot is free"
1103        );
1104        assert_eq!(
1105            new.rect,
1106            rect(2, 2, 5, PORT_HEIGHT),
1107            "the gesture box places the body; the root has none to place it in"
1108        );
1109    }
1110
1111    /// A locked owner stamps nothing — and now says so by refusing the proof,
1112    /// so there is no `NewPin` to hand the emitter at all.
1113    #[test]
1114    fn a_locked_owner_stamps_nothing() {
1115        let doc = locked_scene();
1116        let mut index = DocIndex::default();
1117        let view = index.view(&doc);
1118        assert_eq!(
1119            UnlockedScope::of(&view, Scope::Block(block_id(1))),
1120            None,
1121            "a locked block hands out no proof, so a stamp cannot name it",
1122        );
1123
1124        // A proof carries the scope it was minted from, so it cannot be
1125        // pointed at the frozen block: stamping with the root's proof lands
1126        // the pin at the root, which is the only place that proof allows.
1127        let stamp = NewPin {
1128            id: pin_id(10),
1129            owner: scope(&view, Scope::Root),
1130            slot: slot(PinSide::East, 2),
1131        };
1132        let mut builder = CommitBuilder::new("Stamped a pin at the root");
1133        pin(&view, stamp, &mut builder);
1134        let doc = fold(builder, &doc);
1135        assert_eq!(
1136            Scope::from_wire(pin_of(&doc, 10).owner),
1137            Scope::Root,
1138            "the stamp landed where the proof came from, not where it was aimed",
1139        );
1140        assert!(
1141            block_of(&doc, 1).locked,
1142            "and the frozen block is untouched",
1143        );
1144    }
1145
1146    /// A port on a locked block is the same refusal, one emitter along.
1147    #[test]
1148    fn a_locked_owner_takes_no_port_either() {
1149        let doc = locked_scene();
1150        let mut index = DocIndex::default();
1151        let view = index.view(&doc);
1152        assert_eq!(UnlockedScope::of(&view, Scope::Block(block_id(1))), None);
1153    }
1154
1155    #[test]
1156    fn an_absent_owner_hands_out_no_proof() {
1157        let doc = wired();
1158        let mut index = DocIndex::default();
1159        let view = index.view(&doc);
1160        assert_eq!(
1161            UnlockedScope::of(&view, Scope::Block(block_id(99))),
1162            None,
1163            "a block the document does not hold takes no pin, material or not",
1164        );
1165    }
1166
1167    #[test]
1168    fn route_stamps_the_wire_along_the_solved_corners() {
1169        let doc = wired();
1170        let mut index = DocIndex::default();
1171        let corners = vec![corner(1, 1), pinned(2, 2)];
1172        let mut builder = CommitBuilder::new("Drew a wire");
1173        let view = index.view(&doc);
1174        route(
1175            &view,
1176            NewRoute {
1177                id: route_id(10),
1178                owner: Scope::Block(block_id(1)),
1179                from: pin_id(3),
1180                to: RouteEnd::Pin(pin_id(4)),
1181                waypoints: corners.clone(),
1182            },
1183            &mut builder,
1184        );
1185        let doc = fold(builder, &doc);
1186
1187        let new = doc.route(&route_id(10)).expect("the wire exists");
1188        assert_eq!(new.owner, block_id(1));
1189        assert_eq!(new.from, pin_id(3));
1190        assert_eq!(new.to, pin_id(4));
1191        assert_eq!(
1192            new.name, "",
1193            "a wire is born unnamed; naming it is the label's job"
1194        );
1195        assert_eq!(new.role, Role::Accent0);
1196        assert_eq!(new.waypoints, corners);
1197        assert!(
1198            [new.from, new.to].iter().all(|pin| doc.pin(pin).is_some()),
1199            "both endpoints stand, so the wire draws"
1200        );
1201    }
1202
1203    #[test]
1204    fn a_wire_landing_on_a_free_slot_stamps_its_destination_pin() {
1205        let doc = wired();
1206        let mut index = DocIndex::default();
1207        let mut builder = CommitBuilder::new("Drew a wire onto a fresh pin");
1208        let view = index.view(&doc);
1209        route(
1210            &view,
1211            NewRoute {
1212                id: route_id(11),
1213                owner: Scope::Block(block_id(1)),
1214                from: pin_id(3),
1215                to: RouteEnd::Fresh(NewPin {
1216                    id: pin_id(10),
1217                    owner: scope(&view, Scope::Block(block_id(1))),
1218                    slot: slot(PinSide::East, 1),
1219                }),
1220                waypoints: Vec::new(),
1221            },
1222            &mut builder,
1223        );
1224        let doc = fold(builder, &doc);
1225
1226        assert_eq!(pin_of(&doc, 10).slot, slot(PinSide::East, 1));
1227        assert_eq!(
1228            doc.route(&route_id(11)).expect("the wire exists").to,
1229            pin_id(10),
1230            "the wire and the pin it landed on arrive in one commit"
1231        );
1232    }
1233
1234    #[test]
1235    fn an_absent_wire_takes_no_label() {
1236        let doc = wired();
1237        let mut builder = CommitBuilder::new("Deleted the wire");
1238        builder.push(OpCodes::Route(route_id(5), Crud::Delete));
1239        let doc = fold(builder, &doc);
1240        assert!(
1241            doc.route(&route_id(5)).is_none(),
1242            "precondition: the wire is gone"
1243        );
1244
1245        let mut builder = CommitBuilder::new("Labelled a dead wire");
1246        wire_label(
1247            &doc,
1248            route_label_id(10),
1249            route_id(5),
1250            FracVal::from(3.5),
1251            &mut builder,
1252        );
1253        seals_to_nothing(builder);
1254
1255        let mut builder = CommitBuilder::new("Labelled a wire that never was");
1256        wire_label(
1257            &doc,
1258            route_label_id(10),
1259            route_id(99),
1260            FracVal::from(3.5),
1261            &mut builder,
1262        );
1263        seals_to_nothing(builder);
1264    }
1265
1266    #[test]
1267    fn wire_label_anchors_at_the_arc_length_the_click_projected() {
1268        let doc = wired();
1269        let mut builder = CommitBuilder::new("Labelled a wire");
1270        wire_label(
1271            &doc,
1272            route_label_id(10),
1273            route_id(5),
1274            FracVal::from(37.5),
1275            &mut builder,
1276        );
1277        let doc = fold(builder, &doc);
1278
1279        let new = doc
1280            .route_label(&route_label_id(10))
1281            .expect("the label exists");
1282        assert_eq!(new.owner, route_id(5));
1283        assert_eq!(new.pos, FracVal::from(37.5));
1284        assert!(
1285            DocIndex::of(&doc).routes[&route_id(5)]
1286                .labels
1287                .contains(&route_label_id(10))
1288        );
1289    }
1290
1291    /// The scene with block 1 designated as the document's root — what
1292    /// Wrap Top demotes.
1293    fn rooted() -> Document {
1294        let doc = wired();
1295        let mut builder = CommitBuilder::new("Designated the root");
1296        builder.push(OpCodes::Document(TitleBlockUpdate::Top(block_id(1))));
1297        let doc = fold(builder, &doc);
1298        assert_eq!(
1299            doc.title_block().top,
1300            block_id(1),
1301            "precondition: the document has a root to demote"
1302        );
1303        doc
1304    }
1305
1306    #[test]
1307    fn wrap_top_creates_a_root_demotes_the_old_one_and_repoints_the_top() {
1308        let doc = rooted();
1309        let mut index = DocIndex::default();
1310        let mut builder = CommitBuilder::new("Wrapped the top");
1311        wrap_top(&index.view(&doc), block_id(10), &mut builder);
1312        let doc = fold(builder, &doc);
1313
1314        assert_eq!(doc.title_block().top, block_id(10));
1315        let new_root = block_of(&doc, 10);
1316        assert_eq!(Scope::from_wire(new_root.parent), Scope::Root);
1317        assert_eq!(new_root.rect, TOP_BLOCK_DEFAULT_RECT);
1318        assert_eq!(new_root.title.name, "top_1");
1319        assert_eq!(
1320            block_of(&doc, 1).parent,
1321            block_id(10),
1322            "the old root demotes to the new one's child"
1323        );
1324        assert_eq!(
1325            block_of(&doc, 1).rect,
1326            rect(0, 0, TOP_BLOCK_DEFAULT_WIDTH, TOP_BLOCK_DEFAULT_HEIGHT),
1327            "a root with no size of its own demotes to a visible child, not a sliver"
1328        );
1329        assert_eq!(
1330            DocIndex::of(&doc).blocks[&Scope::Root.wire_id()].children,
1331            [block_id(10)].into_iter().collect(),
1332            "the document root holds exactly the new level"
1333        );
1334    }
1335
1336    #[test]
1337    fn repeated_wraps_name_each_level_distinctly() {
1338        let doc = rooted();
1339        let mut index = DocIndex::default();
1340        let mut builder = CommitBuilder::new("Wrapped the top");
1341        wrap_top(&index.view(&doc), block_id(10), &mut builder);
1342        let doc = fold(builder, &doc);
1343
1344        let mut builder = CommitBuilder::new("Wrapped it again");
1345        wrap_top(&index.view(&doc), block_id(11), &mut builder);
1346        let commit = builder.seal().expect("the wrap produced ops");
1347        assert_eq!(
1348            commit.ops().len(),
1349            3,
1350            "a root already at its demoted rect takes no geometry write"
1351        );
1352        let doc = doc
1353            .try_apply(&commit)
1354            .expect("the fold accepts the gesture");
1355
1356        assert_eq!(block_of(&doc, 11).title.name, "top_2");
1357        assert_eq!(doc.title_block().top, block_id(11));
1358        assert_eq!(block_of(&doc, 10).parent, block_id(11));
1359    }
1360
1361    #[test]
1362    fn wrapping_a_rootless_document_just_names_a_root() {
1363        let doc = wired();
1364        let mut index = DocIndex::default();
1365        assert_eq!(
1366            Scope::from_wire(doc.title_block().top),
1367            Scope::Root,
1368            "precondition: the document has no root yet"
1369        );
1370
1371        let mut builder = CommitBuilder::new("Wrapped the top");
1372        wrap_top(&index.view(&doc), block_id(10), &mut builder);
1373        let commit = builder.seal().expect("the wrap produced ops");
1374        assert_eq!(commit.ops().len(), 2, "there is nothing to demote");
1375        let doc = doc
1376            .try_apply(&commit)
1377            .expect("the fold accepts the gesture");
1378
1379        assert_eq!(doc.title_block().top, block_id(10));
1380        assert_eq!(
1381            Scope::from_wire(block_of(&doc, 1).parent),
1382            Scope::Root,
1383            "a block that was never the root is left where it is"
1384        );
1385    }
1386
1387    #[test]
1388    fn the_demoted_root_grows_to_fit_its_lowest_pin() {
1389        let doc = rooted();
1390        let mut index = DocIndex::default();
1391        let mut builder = CommitBuilder::new("Sized the root and its pins");
1392        builder.push(OpCodes::Block(
1393            block_id(1),
1394            Crud::Update(BlockUpdate::Rect(rect(0, 0, 8, 4))),
1395        ));
1396        builder.push(OpCodes::Pin(
1397            pin_id(3),
1398            Crud::Update(PinUpdate::Slot(slot(PinSide::West, 3))),
1399        ));
1400        let doc = fold(builder, &doc);
1401        assert!(
1402            slot_capacity(4) < 3,
1403            "precondition: the root is too short for its own lowest pin"
1404        );
1405
1406        let mut builder = CommitBuilder::new("Wrapped the top");
1407        wrap_top(&index.view(&doc), block_id(10), &mut builder);
1408        let doc = fold(builder, &doc);
1409
1410        let demoted = block_of(&doc, 1).rect;
1411        assert_eq!(demoted.size.w, 8, "the demoted root keeps its own width");
1412        assert!(
1413            slot_capacity(demoted.size.h) >= 3,
1414            "the demoted height offers the lowest pin's slot"
1415        );
1416        assert_eq!(demoted.size.h, height_for_slot(3));
1417    }
1418
1419    #[test]
1420    fn the_free_slot_search_finds_the_hole_in_a_packed_boundary() {
1421        let both = |offset| [PinSide::West, PinSide::East].map(|side| slot(side, offset));
1422        let mut occupied: HashSet<PinSlot> = (0..4).flat_map(both).collect();
1423        occupied.remove(&slot(PinSide::East, 2));
1424        assert_eq!(first_free_slot(&occupied), slot(PinSide::East, 2));
1425
1426        assert_eq!(
1427            first_free_slot(&HashSet::default()),
1428            slot(PinSide::West, 0),
1429            "West comes before East at each offset"
1430        );
1431        let full: HashSet<PinSlot> = (0..4).flat_map(both).collect();
1432        assert_eq!(
1433            first_free_slot(&full),
1434            slot(PinSide::West, 4),
1435            "a full boundary hands back the first slot past its end"
1436        );
1437    }
1438
1439    #[test]
1440    fn every_rung_of_the_height_ladder_offers_exactly_its_slot() {
1441        assert_eq!(height_for_slot(0), 4, "the shortest block holds one slot");
1442        for offset in 0..6 {
1443            let height = height_for_slot(offset);
1444            assert_eq!(slot_capacity(height), offset);
1445            assert_eq!(
1446                snap_block_height_cells(height),
1447                height,
1448                "the fitting height is itself a rung"
1449            );
1450        }
1451    }
1452
1453    #[test]
1454    fn a_top_block_is_born_at_a_height_the_resize_snap_keeps() {
1455        assert_eq!(
1456            snap_block_height_cells(TOP_BLOCK_DEFAULT_HEIGHT),
1457            TOP_BLOCK_DEFAULT_HEIGHT
1458        );
1459    }
1460}