Skip to main content

blockworx_doc/fixtures/
scale.rs

1//! An N×N scale-test scene: a sheet wrapping a square grid of identical
2//! `Add` blocks, fully wired, with no pin left dangling.
3//!
4//! `cargo xtask autogen scale N out.json` serializes what this builds, and
5//! the editor's test fixtures lower the same [`Document`] in-process, so the
6//! file on disk and the scene under test are one scene.
7
8use crate::{
9    block_model::{Block, Label, Pin, Route},
10    commit::CommitBuilder,
11    document::{Document, TitleBlockUpdate},
12    geometry::{GridPoint, GridRect, GridSize, PinSlot},
13    id::{Allocator, BlockId, PinId},
14    opcode::{Crud, OpCodes},
15    values::{LabelSide, PinDir, PinSide},
16};
17
18/// Grid spacing in grid units: each block is [`BLOCK`] wide/tall and columns/rows
19/// are placed on a [`PITCH`] lattice, leaving a gap for wires to route through.
20const BLOCK: i32 = 8;
21const PITCH: i32 = 16;
22const MARGIN: i32 = 12;
23/// A boundary port body, in cells. Wide enough for its name, two cells tall so
24/// the anchor (one cell down from the top) sits inside it.
25const PORT_W: i32 = 4;
26const PORT_H: i32 = 2;
27
28/// The four pins every grid block carries: two west inputs, two east
29/// outputs. Named once so the wiring and the emitter cannot disagree about
30/// which is which.
31struct GridPins {
32    b0: PinId,
33    b1: PinId,
34    s: PinId,
35    c: PinId,
36}
37
38/// Every id the grid uses, minted in the order the file numbers them: the
39/// sheet, then the grid blocks row-major; the sheet's boundary ports, then
40/// each grid block's four.
41struct Scale {
42    ids: Allocator,
43    n: usize,
44    sheet: BlockId,
45    grid: Vec<BlockId>,
46    inputs: Vec<PinId>,
47    outputs: Vec<PinId>,
48    pins: Vec<GridPins>,
49}
50
51impl Scale {
52    fn new(n: usize) -> Self {
53        let mut ids = Allocator::default();
54        let sheet = ids.mint();
55        let grid: Vec<BlockId> = (0..n * n).map(|_| ids.mint()).collect();
56        let inputs: Vec<PinId> = (0..n).map(|_| ids.mint()).collect();
57        let outputs: Vec<PinId> = (0..n).map(|_| ids.mint()).collect();
58        let pins: Vec<GridPins> = (0..n * n)
59            .map(|_| GridPins {
60                b0: ids.mint(),
61                b1: ids.mint(),
62                s: ids.mint(),
63                c: ids.mint(),
64            })
65            .collect();
66        Self {
67            ids,
68            n,
69            sheet,
70            grid,
71            inputs,
72            outputs,
73            pins,
74        }
75    }
76
77    fn at(&self, i: usize, j: usize) -> &GridPins {
78        &self.pins[i * self.n + j]
79    }
80}
81
82/// The N×N scene as a folded document.
83///
84/// # Panics
85/// Never: every op creates an entity under an id this run minted, which is
86/// exactly what the fold accepts.
87#[must_use]
88#[expect(clippy::expect_used)]
89pub fn build_scale(n: usize) -> Document {
90    let mut scale = Scale::new(n);
91    let mut builder = CommitBuilder::new(format!("Generated a {n}×{n} scale grid"));
92    let span = (n as i32 - 1) * PITCH + BLOCK;
93
94    builder.push(OpCodes::Block(
95        scale.sheet,
96        Crud::Create(Block {
97            rect: cells(-MARGIN, -MARGIN, span + 2 * MARGIN, span + 2 * MARGIN),
98            title: title(&format!("scale_{n}x{n}")),
99            ..Block::default()
100        }),
101    ));
102    for i in 0..n {
103        for j in 0..n {
104            builder.push(OpCodes::Block(
105                scale.grid[i * n + j],
106                Crud::Create(Block {
107                    parent: scale.sheet,
108                    rect: cells(j as i32 * PITCH, i as i32 * PITCH, BLOCK, BLOCK),
109                    title: title(&format!("block_{i}_{j}")),
110                    type_label: Label {
111                        name: "Add".to_owned(),
112                        ..Label::default()
113                    },
114                    ..Block::default()
115                }),
116            ));
117        }
118    }
119    builder.push(OpCodes::Document(TitleBlockUpdate::Top(scale.sheet)));
120
121    // The sheet block wraps the whole grid: its west edge carries the
122    // `in_*` ports, its east edge the `out_*` ports, and it owns every wire.
123    for (i, &id) in scale.inputs.iter().enumerate() {
124        builder.push(
125            port(id, scale.sheet, &format!("in_{i}"), west(i), PinDir::Input)
126                .in_body(port_body(-MARGIN + 3, i))
127                .op(),
128        );
129    }
130    for (i, &id) in scale.outputs.iter().enumerate() {
131        builder.push(
132            port(
133                id,
134                scale.sheet,
135                &format!("out_{i}"),
136                east(i),
137                PinDir::Output,
138            )
139            .in_body(port_body(span + MARGIN - 3 - PORT_W, i))
140            .op(),
141        );
142    }
143    for index in 0..n * n {
144        let owner = scale.grid[index];
145        let pins = &scale.pins[index];
146        builder.push(port(pins.b0, owner, "b0", west(0), PinDir::Input).op());
147        builder.push(port(pins.b1, owner, "b1", west(1), PinDir::Input).op());
148        builder.push(port(pins.s, owner, "s", east(0), PinDir::Output).op());
149        builder.push(port(pins.c, owner, "c", east(1), PinDir::Output).op());
150    }
151
152    for (from, to, name) in wiring(&scale) {
153        builder.push(OpCodes::Route(
154            scale.ids.mint(),
155            Crud::Create(Route {
156                owner: scale.sheet,
157                name: name.to_owned(),
158                from,
159                to,
160                ..Route::default()
161            }),
162        ));
163    }
164
165    let commit = builder.seal().expect("the grid emits at least one op");
166    Document::default()
167        .try_apply(&commit)
168        .expect("the generated grid folds")
169}
170
171/// Every wire, as the pins it joins. The scheme leaves no pin dangling:
172///   - `in_i`     → block(i,0).b0  and  block((i+1)%N,0).b1   (column-0 inputs)
173///   - block.s    → right neighbour's b0 and b1               (horizontal chain)
174///   - last col s → `out_i`                                   (sheet outputs)
175///   - block.c    → block-below.b1 (wrapping)                 (vertical carry)
176fn wiring(scale: &Scale) -> Vec<(PinId, PinId, &'static str)> {
177    let n = scale.n;
178    let mut wires = Vec::new();
179    // Sheet inputs feed column 0. `in_i` drives this row's b0 and the next
180    // row's b1, so every column-0 input is covered.
181    for i in 0..n {
182        wires.push((scale.inputs[i], scale.at(i, 0).b0, "in"));
183        wires.push((scale.inputs[i], scale.at((i + 1) % n, 0).b1, "in"));
184    }
185    // Horizontal sum chain: each block's `s` drives both inputs of its right
186    // neighbour.
187    for i in 0..n {
188        for j in 0..n - 1 {
189            let src = scale.at(i, j).s;
190            wires.push((src, scale.at(i, j + 1).b0, "sum"));
191            wires.push((src, scale.at(i, j + 1).b1, "sum"));
192        }
193    }
194    // Last column drains its `s` into the matching sheet output.
195    for i in 0..n {
196        wires.push((scale.at(i, n - 1).s, scale.outputs[i], "out"));
197    }
198    // Vertical carry chain covers every `c` output (and the wrap keeps the
199    // last row's carries wired), giving each `c` a home.
200    for i in 0..n {
201        for j in 0..n {
202            wires.push((scale.at(i, j).c, scale.at((i + 1) % n, j).b1, "carry"));
203        }
204    }
205    wires
206}
207
208/// A pin to create. `slot` places it on its block-as-child; `body` places the
209/// same pin inside the block's own interior view, where the scope draws it as a
210/// free-standing port. The two are independent, and the default body is the
211/// trap: every boundary port left at it stacks at the origin, and the wires
212/// anchored there start from inside whatever block sits over that spot.
213struct PinSpec<'a> {
214    id: PinId,
215    owner: BlockId,
216    name: &'a str,
217    slot: PinSlot,
218    dir: PinDir,
219    body: GridRect,
220}
221
222impl PinSpec<'_> {
223    /// Place the port body, for a pin some scope draws as a boundary port.
224    fn in_body(mut self, body: GridRect) -> Self {
225        self.body = body;
226        self
227    }
228
229    fn op(self) -> OpCodes {
230        OpCodes::Pin(
231            self.id,
232            Crud::Create(Pin {
233                owner: self.owner,
234                name: self.name.to_owned(),
235                type_name: "bit".to_owned(),
236                slot: self.slot,
237                dir: self.dir,
238                rect: self.body,
239                ..Pin::default()
240            }),
241        )
242    }
243}
244
245fn port(id: PinId, owner: BlockId, name: &str, slot: PinSlot, dir: PinDir) -> PinSpec<'_> {
246    PinSpec {
247        id,
248        owner,
249        name,
250        slot,
251        dir,
252        body: GridRect::default(),
253    }
254}
255
256/// A boundary port's body, on the sheet's west or east margin and level with
257/// the grid row it serves, so its wire leaves along that row instead of
258/// crossing the sheet to reach it.
259fn port_body(margin_x: i32, row: usize) -> GridRect {
260    cells(margin_x, row as i32 * PITCH, PORT_W, PORT_H)
261}
262
263fn west(offset: usize) -> PinSlot {
264    PinSlot {
265        side: PinSide::West,
266        offset: offset as u32,
267    }
268}
269
270fn east(offset: usize) -> PinSlot {
271    PinSlot {
272        side: PinSide::East,
273        offset: offset as u32,
274    }
275}
276
277fn title(name: &str) -> Label {
278    Label {
279        name: name.to_owned(),
280        side: LabelSide::Bottom,
281        ..Label::default()
282    }
283}
284
285fn cells(x: i32, y: i32, w: i32, h: i32) -> GridRect {
286    GridRect {
287        top_left: GridPoint { x, y },
288        size: GridSize {
289            w: w.unsigned_abs(),
290            h: h.unsigned_abs(),
291        },
292    }
293}