Skip to main content

blockworx_editor/edit/
create.rs

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