monospace-diagram 0.1.2

An ordered set of shapes that draws as an ASCII or box-drawing diagram
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
//! A gallery of what a diagram adds over the figures, and the one completeness guarantee the
//! core's cannot have.
//!
//! A `Shape` here is a closed set of three kinds, so [`kind_of`] matches over it with no arm for
//! anything else — and **that match is the guarantee**. A fourth kind added and drawn compiles the
//! library and stops the build here, at the line that has to name it. Verified by adding a variant,
//! implementing its `draw`, and watching the library succeed and this file fail.
//!
//! Two of the three files earn their place by showing something the core's gallery cannot. The
//! order is the only part of `docs/diagram-model.md` that leaves a trace in a buffer, and moving
//! `1` one place toward the front is the only operation in the project that changes a picture
//! without changing a figure. The other is the arm terminal meeting a box border, which reads `├`
//! and is written by neither figure.
//!
//! The cost is one place the rule from the core's gallery does not hold. A `Diagram` keeps its
//! figures in a private map keyed by their identities and offers one query by an identity, with no
//! listing of what it holds, so there is nothing to derive a label from and the block says which
//! shapes it holds in a string written by hand. A shape's own rectangle was withdrawn from the
//! shape abstraction for the same reason — a shape with no reader — and the reader that has since
//! arrived is that one query, not the read iterator that was declined and not the listing beside it.
//!
//! **A figure's picture needs this carrier and not a `<!-- render: -->` marker, and the split is
//! reach rather than preference.** A marker is a description a subprocess reads, so whatever the
//! crate keeps private is outside its reach by construction — the fragments compose the figures, and
//! no JSON gets to them — while a gallery draws the values themselves, which is the only way to see
//! what a fragment stamps. The marker is not displaced by this and keeps its own reach: it reads a
//! description, and a description can name the three diagram shapes at any position in any
//! composition. Neither substitutes for the other, and a figure's picture is the one thing the
//! marker cannot hold.

use std::fmt::Write as _;

use monospace_core::{
    Arm, Buffer, Cell, CellReader, Direction, Glyph, GlyphCatalog, Pos, Size, Stroke,
    SymbolStyleName, Terminal, Window, render,
};

use std::num::NonZeroU16;

use crate::{Anchor, Delta, Diagram, Endpoint, Reference, Shape, ShapeId};

/// One description for every file, so the prose is written once rather than per level.
const WHAT: &str = concat!(
    "A gallery: a chosen set of examples, one block each, for reading rather than for coverage. ",
    "Each label lists the shapes in the order they were added, each one derived from its own ",
    "Debug, so a label cannot disagree with the values it names; what was done between two ",
    "drawings of one diagram is the second line, and it is named for the change rather than for ",
    "the order, which 080 could make and a displacement does not. It is written by hand, because ",
    "a diagram offers one query by an identity and no listing of the shapes it holds. Under each ",
    "picture is the surface that picture came from: a count of the positions the diagram wrote, ",
    "then a row for each of them, saying what that position renders, what each of its four arms ",
    "holds, and the base stroke every Set arm is drawn in. A Set arm names the stroke; Unset and ",
    "Closed are the model's own two words for the other two. A position left unwritten is a hole ",
    "in the picture rather than a row here, and the count is how many there are. A literal or a ",
    "symbol has no arms, so its arm columns are empty. A block moving means a figure or an order ",
    "changed, and what moved is the thing to look at — the same acceptance a characterization ",
    "gets, without its claim of covering a range.",
);

const COLUMNS: [&str; 7] = ["at", "glyph", "top", "right", "bottom", "left", "base"];

fn arm_text(arm: &Arm) -> String {
    match arm {
        Arm::Set(stroke) => stroke.0.clone(),
        Arm::Closed => "Closed".to_string(),
        Arm::Unset => "Unset".to_string(),
    }
}

fn at(x: i16, y: i16) -> Pos {
    Pos { x, y }
}

fn light() -> Stroke {
    Stroke::from("light")
}

fn glyph(text: &str) -> Glyph {
    Glyph::new(text).expect("one glyph")
}

/// The identity of the ordinal `ordinal`, which is how every label below names one.
///
/// **Hand-written rather than read back from the diagram**, which is what the gallery's own `WHAT`
/// says: a diagram offers one query by an identity and no listing of the shapes it holds, so a label
/// naming one is a claim about what was added rather than a reading of what is held.
fn id(ordinal: u16) -> ShapeId {
    ShapeId::new(NonZeroU16::new(ordinal).expect("no gallery names zero"))
}

fn window(width: u16, height: u16) -> Size {
    Size { width, height }
}

/// The kind's own name, by a `match` with no arm for anything else. **This is the check.** The
/// core has no equivalent and cannot have one: its figures are an open trait, so there is no set
/// to be exhaustive over. Here the set is closed, so adding a fourth kind makes this function
/// stop compiling until it is given a name — and a name is what a catalog entry needs.
fn kind_of(shape: &Shape) -> &'static str {
    match shape {
        Shape::Box { .. } => "Box",
        Shape::Line { .. } => "Line",
        Shape::Connector { .. } => "Connector",
    }
}

/// A box, a line and a connector: the closed set, each named by [`kind_of`]. A factory rather than
/// a list of values, because the closed set is then three calls rather than three values and the
/// three clones they would need. `Shape` had no `Clone` when this was written and the factory was
/// the only way to have the same figure twice; it is `Clone` now, so the choice is free rather
/// than forced, and what it buys is that each kind is constructed in one place.
fn the_kinds() -> Vec<(&'static str, Shape)> {
    vec![
        (
            kind_of(&box_shape(at(0, 0), None)),
            box_shape(at(0, 0), None),
        ),
        (kind_of(&line_shape(at(0, 0), 6)), line_shape(at(0, 0), 6)),
        (
            kind_of(&connector_shape(at(0, 0), at(6, 0))),
            connector_shape(at(0, 0), at(6, 0)),
        ),
    ]
}

fn box_shape(at_: Pos, fill: Option<&str>) -> Shape {
    Shape::Box {
        at: at_,
        size: window(6, 3),
        stroke: light(),
        fill: fill.map(glyph),
        text: None,
    }
}

fn line_shape(at_: Pos, len: u16) -> Shape {
    Shape::Line {
        at: at_,
        len,
        orientation: monospace_core::Orientation::Horizontal,
        stroke: light(),
    }
}

fn connector_shape(from_at: Pos, to_at: Pos) -> Shape {
    Shape::Connector {
        from: Endpoint::absolute(
            from_at,
            Direction::Right,
            monospace_core::Terminal::Glyph {
                glyph: glyph("◄")
            },
        ),
        to: Endpoint::absolute(
            to_at,
            Direction::Left,
            monospace_core::Terminal::Glyph {
                glyph: glyph("►")
            },
        ),
        stroke: light(),
    }
}

fn small_box(at_: Pos, fill: Option<&str>) -> Shape {
    Shape::Box {
        at: at_,
        size: window(4, 3),
        stroke: light(),
        fill: fill.map(glyph),
        text: None,
    }
}

fn catalog() -> GlyphCatalog {
    GlyphCatalog::union([GlyphCatalog::light(), GlyphCatalog::solid()])
}

/// Draws `diagram` into a fresh window of `size` and returns the picture and the surface.
fn draw(size: Size, diagram: &Diagram) -> (String, String) {
    let origin = at(0, 0);
    let mut buffer = Buffer::new(Window { origin, size });
    diagram.draw(&mut buffer);
    let picture = render(&buffer, &catalog(), buffer.window());
    (picture, surface(&buffer, size))
}

fn surface(buffer: &Buffer, size: Size) -> String {
    let catalog = catalog();
    let mut rows: Vec<[String; 7]> = Vec::new();
    let total = size.width as usize * size.height as usize;

    for y in 0..size.height.cast_signed() {
        for x in 0..size.width.cast_signed() {
            let cell = buffer.cell(at(x, y));
            let (glyph, top, right, bottom, left, base) = match &cell {
                None => continue,
                Some(Cell::Literal(g)) => (
                    g.as_str().to_string(),
                    String::new(),
                    String::new(),
                    String::new(),
                    String::new(),
                    String::new(),
                ),
                Some(Cell::Symbol(s)) => (
                    catalog
                        .symbol(s)
                        .map_or("?".to_string(), |g| g.as_str().to_string()),
                    String::new(),
                    String::new(),
                    String::new(),
                    String::new(),
                    String::new(),
                ),
                Some(Cell::Strokes(s)) => (
                    catalog
                        .glyph(&s.key())
                        .map_or("?".to_string(), |g| g.as_str().to_string()),
                    arm_text(&s.top),
                    arm_text(&s.right),
                    arm_text(&s.bottom),
                    arm_text(&s.left),
                    s.base.0.clone(),
                ),
            };
            rows.push([format!("{x},{y}"), glyph, top, right, bottom, left, base]);
        }
    }

    let widths: Vec<usize> = (0..COLUMNS.len())
        .map(|column| {
            rows.iter()
                .map(|row| row[column].chars().count())
                .chain(std::iter::once(COLUMNS[column].len()))
                .max()
                .unwrap_or(0)
        })
        .collect();

    let line = |cells: &[String]| {
        let padded: Vec<String> = cells
            .iter()
            .zip(&widths)
            .map(|(cell, width)| format!("{cell:<width$}"))
            .collect();
        format!("  | {} |", padded.join(" | "))
    };

    let mut out = format!("  wrote {} of {total} positions\n", rows.len());
    let header: Vec<String> = COLUMNS.iter().map(|name| (*name).to_string()).collect();
    let _ = writeln!(out, "{}", line(&header));
    let rule: Vec<String> = widths.iter().map(|width| "-".repeat(*width)).collect();
    let _ = writeln!(out, "  | {} |", rule.join(" | "));
    for row in &rows {
        let _ = writeln!(out, "{}", line(row));
    }
    out
}

/// One block: what the diagram holds, then what was done between the two drawings, then the
/// picture, then the surface. `shapes` is a label rather than the values, because a `Diagram` offers
/// one query by an identity and no listing of the shapes it holds, so there is nothing here to
/// derive a label from. `change` is written by hand for the same reason, and is named for the
/// change rather than for the order because a figure's displacement is a change too.
fn block(shapes: &str, change: &str, size: Size, diagram: &Diagram) -> String {
    let mut out = String::new();
    let _ = writeln!(out, "  shapes: {shapes}");
    let _ = writeln!(out, "  change: {change}");

    let (picture, surface) = draw(size, diagram);
    for line in picture.lines() {
        let _ = writeln!(out, "{}", format!("  {line}").trim_end());
    }
    let _ = write!(out, "{surface}");
    out
}

/// Writes one snapshot. The path is set rather than left to `insta`'s default, which puts a
/// snapshot beside the source that wrote it — the characterization does the same, and the two
/// kinds are kept apart by directory.
fn snap(name: &str, body: &str) {
    let mut settings = insta::Settings::clone_current();
    settings.set_snapshot_path(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/src/snapshots/gallery"
    ));
    settings.set_description(WHAT);
    settings.bind(|| insta::assert_snapshot!(name, body));
}

// ------------------------------------------------------------ the three kinds, one each

#[test]
fn the_three_kinds() {
    let mut out = String::new();
    for (kind, shape) in the_kinds() {
        let label = format!("Shape::{kind} {shape:?}");
        let mut diagram = Diagram::new();
        diagram = diagram.add(shape).0;
        let _ = writeln!(out, "{label}");
        let (picture, surface) = draw(window(7, 3), &diagram);
        for line in picture.lines() {
            let _ = writeln!(out, "{}", format!("  {line}").trim_end());
        }
        let _ = write!(out, "{surface}");
        let _ = writeln!(out);
    }
    snap("the_three_kinds", &out);
}

// ----------------------------------------------------------------- what the order is for

#[test]
fn the_order_decides_a_shared_cell() {
    // As written: `1` behind, `2` in front, so `▓` covers where they share. Rebuilt rather than
    // cloned: `Shape` is `Clone` now, and rebuilding from the factory is what keeps both blocks
    // drawing the two boxes as written rather than one of them drawing a second pair.
    let labelled = "[small_box(0,0,fill=░), small_box(2,1,fill=▓)]";
    let mut diagram = Diagram::new();
    diagram = diagram.add(small_box(at(0, 0), Some("░"))).0;
    diagram = diagram.add(small_box(at(2, 1), Some("▓"))).0;
    let first = block(labelled, "as written", window(6, 4), &diagram);

    // One place toward the front, which puts `░` over `▓` and changes three cells.
    diagram = diagram.forward(id(1));
    let moved = block(
        labelled,
        "1 moved one place toward the front",
        window(6, 4),
        &diagram,
    );

    snap(
        "the_order_decides_a_shared_cell",
        &format!("{first}\n{moved}"),
    );
}

// ------------------------------------------------- an arm terminal composing with a border

#[test]
fn an_arm_terminal_composes_where_a_border_is() {
    let labelled = "[small_box(0,0,no fill), arm_connector(3,1 -> 7,1)]";
    let mut diagram = Diagram::new();
    diagram = diagram.add(small_box(at(0, 0), None)).0;
    diagram = diagram.add(arm_connector(at(3, 1), at(7, 1))).0;
    snap(
        "an_arm_terminal_composes_where_a_border_is",
        &block(labelled, "as written", window(8, 3), &diagram),
    );
}

fn arm_connector(from_at: Pos, to_at: Pos) -> Shape {
    Shape::Connector {
        from: Endpoint::absolute(from_at, Direction::Right, monospace_core::Terminal::Arm),
        to: Endpoint::absolute(to_at, Direction::Left, monospace_core::Terminal::Arm),
        stroke: light(),
    }
}

// ------------------------------------------ an endpoint hanging from a figure's side

/// The connector of _an endpoint hangs from a side_ with its `from` named as a **reference** to the
/// box's right side rather than as the point that side resolves to.
fn hanging_connector(box_id: ShapeId) -> Shape {
    Shape::Connector {
        from: Endpoint::reference(
            Reference::new(box_id, Anchor::Right, Delta { dx: 0, dy: 0 }, 0),
            monospace_core::Terminal::Arm,
        ),
        to: Endpoint::absolute(at(7, 1), Direction::Left, monospace_core::Terminal::Arm),
        stroke: light(),
    }
}

/// The first block is the model's §6 _Attachment_ reached with a reference where the model spells
/// the point; the second is the same diagram with the box displaced four cells right, so the arrow
/// follows the side it hangs from; the third is the arrangement as written with the **connector**
/// displaced two cells down, so the arrow slides off that side and draws as a translation of itself;
/// and the fourth is the arrangement as written with the box **taken out**, so the arrow stands
/// exactly where it stood and what is left of it is the arrow alone.
///
/// All four are in one snapshot because what a reader is meant to see is them together, and the three
/// changes are three directions of one rule: displacing the figure a reference hangs from carries the
/// endpoint, displacing the figure that holds the reference moves it, and taking the figure away
/// freezes the endpoint where it stood. The pair of displacements and the removal are what makes the
/// difference visible rather than asserted. **The fourth block is drawn by `remove` itself** rather
/// than built by hand from the two positions the rule yields, and that is the whole reason it is in
/// the gallery: a freeze that stopped holding would drop this snapshot instead of leaving a picture
/// that no longer matches the code.
///
/// **The three later blocks are each reached from a diagram built for that block**, and the cost is
/// a second and a third `Diagram` in this test rather than a `get` and a `replace`. The reason is
/// measured rather than stylistic: the second block leaves the box's right side centre on `{7, 1}`,
/// which is where the connector's own free end already stood, so a change applied to *that* diagram
/// would be a change to something degenerate; and a removal of a figure the arrow's reference is
/// already resolving somewhere else would freeze the arrow **displaced**, which is a fourth picture
/// of a different claim. Reaching the arrangement as written is also what keeps every block
/// comparable with the first: they differ by the one change between them and by nothing else.
///
/// **The label names three values now, because the reference holds three.** A block cannot print a
/// shape for itself — a `Diagram` offers one query by an identity and no listing — so this string
/// is written by hand, and a hand-written label that named two of a reference's three values would
/// let a reader assume a third default. `offset (0, 0)` is what says the arrow stands *on* the
/// side rather than beside it. The offset is drawn nowhere in any of the four pictures, and §6's
/// stays exactly as the model spells it.
///
/// This is no longer the only carrier in the crate that can reach a reference: a
/// `<!-- render: -->` marker reads a description, and a description's `at` may hold one. The block
/// stays here rather than moving to a marker because the four blocks are one snapshot and a marker
/// renders one picture per description. It is also the only carrier that can reach a
/// **displacement** or a **removal** at all: a marker reads a description, and a description
/// carries no field for either, so no marker anywhere in the repository can hold a picture of
/// either rule.
#[test]
fn an_endpoint_hangs_from_a_side_and_follows_it() {
    let labelled =
        "[small_box(0,0,no fill), arm_connector(from = Reference(1, Right, offset (0,0)) -> 7,1)]";
    let diagram = Diagram::new();
    let (mut diagram, box_id) = diagram.add(small_box(at(0, 0), None));
    diagram = diagram.add(hanging_connector(box_id)).0;
    let first = block(labelled, "as written", window(8, 3), &diagram);

    // The box moves four cells right, so its right side center moves from `{3, 1}` to `{7, 1}`
    // and the arrow lands on the new side without anything naming where that is.
    let moved = diagram
        .get(box_id)
        .expect("the box is in the diagram")
        .displaced_by(Delta { dx: 4, dy: 0 });
    diagram = diagram.replace(box_id, moved);
    let second = block(
        labelled,
        "the box displaced four cells right",
        window(8, 3),
        &diagram,
    );

    // And the third displaces the **connector**, which is the other direction and the other rule:
    // the endpoint slides off the side rather than following it, and the arrow draws as a
    // translation of itself.
    //
    // **Reached from the arrangement as written rather than from the block beside it**, and a second
    // diagram in this test is what that costs — six lines rather than a `get` and a `replace`. The
    // reason is measured rather than stylistic: the second block above left the box's right side
    // centre on `{7, 1}`, which is where the connector's own free end already stood, so a
    // displacement applied to *that* diagram would have put both endpoints on `{7, 3}` and drawn
    // nothing at all — degenerate in the same way the second block above is, one step further
    // along. Reaching the arrangement as written is also what keeps the third block comparable with
    // the first: the two differ by the one displacement between them and by nothing else.
    let from_as_written = Diagram::new();
    let (from_as_written, second_box_id) = from_as_written.add(small_box(at(0, 0), None));
    let (mut from_as_written, arrow_id) = from_as_written.add(hanging_connector(second_box_id));
    let moved_arrow = from_as_written
        .get(arrow_id)
        .expect("the connector is in the diagram")
        .displaced_by(Delta { dx: 0, dy: 2 });
    from_as_written = from_as_written.replace(arrow_id, moved_arrow);

    // **`window(8, 4)`, and the fourth row is the reason.** The displaced connector lands on the
    // fourth row, so a three-row window would clip it out entirely and the block would measure to
    // nothing. A snapshot that grew a fifth row of nothing is the signature of the window being too
    // short, so the row is worth naming rather than discovering.
    let third = block(
        labelled,
        "the connector displaced two cells down",
        window(8, 4),
        &from_as_written,
    );

    // And the fourth takes the box away, which is the rule 142 lands: **the arrow stands exactly
    // where it stood**, because the end that referenced the box froze at the point it was resolving
    // to rather than dropping the connector or re-routing it to nothing.
    //
    // **Reached from a third diagram in this test**, for the reason the third block needed a second:
    // the two blocks above leave `1` displaced four cells right, and a removal of a figure the
    // arrow's reference is already resolving somewhere else freezes the arrow **displaced** — a
    // fourth picture of a different claim.
    //
    // **`window(8, 3)`, and this is the opposite of the row above.** That one grew to a fourth row
    // because a displacement lands the connector lower; a removal lands nothing, so a window of
    // `window(8, 4)` comes back with a fourth row of **nothing** in it. A snapshot that grew a
    // fourth empty row is therefore the signature here of a window widened for no reason.
    let with_the_box_taken_out = Diagram::new();
    let (mut with_the_box_taken_out, the_removed) =
        with_the_box_taken_out.add(small_box(at(0, 0), None));
    with_the_box_taken_out = with_the_box_taken_out.add(hanging_connector(the_removed)).0;
    with_the_box_taken_out = with_the_box_taken_out.remove(the_removed);
    let fourth = block(
        labelled,
        "the box taken out",
        window(8, 3),
        &with_the_box_taken_out,
    );

    snap(
        "an_endpoint_hangs_from_a_side_and_follows_it",
        &format!("{first}\n{second}\n{third}\n{fourth}"),
    );
}

// ----------------------- a symbol terminal attached to a side stands outward by default

/// A connector with an attached symbol terminal: `from` hangs from the first box's right side
/// with `Terminal::Arm` to compose into a junction, and `to` hangs from the second box's left side
/// with `Terminal::Symbol` in `solid` style.
fn symbol_connector(from_id: ShapeId, to_id: ShapeId, out: i16) -> Shape {
    Shape::Connector {
        from: Endpoint::reference(
            Reference::new(from_id, Anchor::Right, Delta { dx: 0, dy: 0 }, 0),
            Terminal::Arm,
        ),
        to: Endpoint::reference(
            Reference::new(to_id, Anchor::Left, Delta { dx: 0, dy: 0 }, out),
            Terminal::Symbol {
                style: SymbolStyleName::from("solid"),
            },
        ),
        stroke: light(),
    }
}

/// A non-arm terminal attached to a side stands one cell outward along the anchor normal by default,
/// so an arrowhead terminates against the target boundary from the outside rather than overwriting
/// the border cell it lands on.
///
/// Five blocks show the five facets of the rule:
/// 1. **As written (`out: 0`)**: the arrow stands at `x = 6`, leaving a one-cell gap before the target
///    box's left border at `x = 7` so that `│` is preserved.
/// 2. **Displaced**: displacing the target box four cells right moves the endpoint along with it,
///    preserving the one-cell gap at the new location.
/// 3. **Removed**: removing the target box freezes the endpoint at its effective resolved position
///    (`x = 6`), preventing visual jumps.
/// 4. **Explicit `out: 1`**: `out` is additive, so `out: 1` yields an effective separation of two cells.
/// 5. **Explicit `out: -1`**: a negative offset pulls the terminal inward, allowing the author to
///    intentionally land the symbol directly on the border cell.
#[test]
fn a_symbol_terminal_attached_to_a_side_stands_one_cell_outward_by_default() {
    let labelled = "[small_box(0,0,no fill), small_box(7,0,no fill), symbol_connector(from = Reference(1, Right), to = Reference(2, Left))]";
    let (diagram, box1_id) = Diagram::new().add(small_box(at(0, 0), None));
    let (mut diagram, box2_id) = diagram.add(small_box(at(7, 0), None));
    diagram = diagram.add(symbol_connector(box1_id, box2_id, 0)).0;
    let first = block(labelled, "as written", window(15, 3), &diagram);

    // Displacing the target box four cells right moves the endpoint along with it,
    // preserving the one-cell outward gap at the new position.
    let moved = diagram
        .get(box2_id)
        .expect("the target box is in the diagram")
        .displaced_by(Delta { dx: 4, dy: 0 });
    diagram = diagram.replace(box2_id, moved);
    let second = block(
        labelled,
        "the target box displaced four cells right",
        window(15, 3),
        &diagram,
    );

    // Removing the target box freezes the arrow at its effective position without visual jump.
    let (diagram3, b1) = Diagram::new().add(small_box(at(0, 0), None));
    let (mut diagram3, b2) = diagram3.add(small_box(at(7, 0), None));
    diagram3 = diagram3.add(symbol_connector(b1, b2, 0)).0;
    diagram3 = diagram3.remove(b2);
    let third = block(
        labelled,
        "the target box taken out",
        window(15, 3),
        &diagram3,
    );

    // Explicit `out: 1` produces an additive gap of two cells.
    let labelled_out1 = "[small_box(0,0,no fill), small_box(7,0,no fill), symbol_connector(from = Reference(1, Right), to = Reference(2, Left, out=1))]";
    let (diagram4, b1) = Diagram::new().add(small_box(at(0, 0), None));
    let (mut diagram4, b2) = diagram4.add(small_box(at(7, 0), None));
    diagram4 = diagram4.add(symbol_connector(b1, b2, 1)).0;
    let fourth = block(
        labelled_out1,
        "the connector with explicit out: 1",
        window(15, 3),
        &diagram4,
    );

    // Explicit `out: -1` pulls the terminal onto the border cell, overwriting it.
    let labelled_out_neg1 = "[small_box(0,0,no fill), small_box(7,0,no fill), symbol_connector(from = Reference(1, Right), to = Reference(2, Left, out=-1))]";
    let (diagram5, b1) = Diagram::new().add(small_box(at(0, 0), None));
    let (mut diagram5, b2) = diagram5.add(small_box(at(7, 0), None));
    diagram5 = diagram5.add(symbol_connector(b1, b2, -1)).0;
    let fifth = block(
        labelled_out_neg1,
        "the connector with explicit out: -1",
        window(15, 3),
        &diagram5,
    );

    snap(
        "a_symbol_terminal_attached_to_a_side_stands_one_cell_outward_by_default",
        &format!("{first}\n{second}\n{third}\n{fourth}\n{fifth}"),
    );
}