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
//! **Every count, computed from the catalog, in one place.**
//!
//! A count is derived data. The build-systems and architecture literature is unanimous that derived
//! quantities are recomputed from one authoritative store rather than cached in parallel copies, and
//! this project has the receipt: three module notes each stated the size of the same library — *"360
//! meshes and 41 are in the library"*, *"131 library entries and ~319 unlabeled candidates"* — against
//! a file holding 75. Nothing was broken by that, because the panels had always computed their numbers
//! at runtime; what drifted was the prose. But prose is what a reader trusts when deciding whether a
//! number on screen looks right.
//!
//! # Why a module rather than a habit
//!
//! Because the pressure is about to increase. One library gave one count. A library plus a set of
//! compositions plus a map that stamps them gives *described*, *composed*, *stamped* and *expanded* —
//! and every panel that wants to say "N of M" is a place to compute one slightly differently. The
//! remedy is the one the key census already uses for keys: one table, everything derived from it, so
//! disagreement is unrepresentable rather than unlikely.
//!
//! # What is deliberately not here
//!
//! Anything that has to *solve* to answer. `stamps` counts the references a map holds; it does not
//! expand them, because expansion can legitimately fail (a member with nowhere to rest) and a counter
//! that can fail is a counter every caller has to branch on. What a map expands to is
//! [`crate::composition::expand`]'s answer, and it is returned with the rows rather than as a number.
use crate::composition::{Composition, Envelope};
use crate::library::Library;
use crate::map::Map;
/// What a project can place, counted.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Catalog {
/// Every descriptor the library defines.
pub descriptors: usize,
/// Those carrying at least one `kind` token — the judgement half, which measurement cannot supply
/// and a VLM only proposes. This is the number "N of M described" is about.
pub described: usize,
/// Those saying anything at all about their edges. The kit contract, counted.
pub with_edges: usize,
/// Every composition.
pub compositions: usize,
/// Those claiming a tile, and therefore able to have a derived interface.
pub bounded: usize,
/// Members across every composition — before nesting is flattened, so a group inside a group
/// counts as the one member it is written as.
pub members: usize,
}
/// What one map holds, counted.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MapCensus {
/// Rows authored by hand. **Not** what the map draws — stamps add to that.
pub placements: usize,
/// Rows an author has taken responsibility for.
pub owned: usize,
/// References to compositions. What each expands to is `expand`'s answer, not a count.
pub stamps: usize,
/// Locations authored directly on the map, excluding any a stamp brings with it.
pub locations: usize,
/// **Rows the stamps brought** — what a consumer of this map will have beyond the
/// hand-authored ones.
///
/// This is a count of *rows*, not of references, and that is the distinction the module note
/// used to draw the other way. It said the counter deliberately does not expand, "because
/// expansion can legitimately fail (a member with nowhere to rest) and a counter that can fail
/// is a counter every caller has to branch on". That reasoning was right for a counter of
/// *references* — `map.stamps.len()` cannot fail — and wrong for a counter of *rows*, because
/// the row count is the number the author must read to know what the game will have. A panel
/// saying "746 placed" beside a game holding 728 is a panel lying.
///
/// So the two are separate numbers, and the one that can fail is a different counter from the
/// one that cannot: [`of_map`] returns a `Result`, and the reference count stays a plain
/// `len()`.
pub stamped: usize,
}
impl MapCensus {
/// **The rows the map has** — hand-authored plus what its stamps brought.
///
/// The number a consumer of this map will spawn, and the number the editor's panel must
/// show. It is a method rather than a field because it is derived from two that are stored,
/// and a stored third could disagree with them.
pub fn total(&self) -> usize {
self.placements + self.stamped
}
}
/// Count a catalog. Pure arithmetic over the two files a project's palette comes from.
pub fn of_catalog(library: &Library, compositions: &[Composition]) -> Catalog {
Catalog {
descriptors: library.descriptors.len(),
described: library.descriptors.iter().filter(|d| !d.kind.is_empty()).count(),
with_edges: library
.descriptors
.iter()
.filter(|d| {
d.subgrid
.as_ref()
.is_some_and(|g| g.cells.iter().any(|c| c.edge.is_some()))
})
.count(),
compositions: compositions.len(),
bounded: compositions
.iter()
.filter(|c| matches!(c.envelope, Envelope::Bounded { .. }))
.count(),
members: compositions.iter().map(|c| c.members.len()).sum(),
}
}
/// Count a map — the rows it has **and** the rows its stamps bring.
///
/// **This expands**, and so it can fail. It returns a `Result` rather than swallowing the
/// failure, because a map whose stamps do not expand has no row count to report: the honest
/// answer is the refusal, not a number that quietly omits a house.
pub fn of_map(
map: &Map,
compositions: &[Composition],
library: &Library,
) -> Result<MapCensus, String> {
// **The refusal is the answer, not a smaller number.** A map whose stamps do not expand has
// no row count to report, and a count that quietly omitted the rows would be a number
// nobody could check — which is the thing this module exists to prevent.
let stamped = crate::composition::expand_map(map, compositions, library)?
.placements
.len();
Ok(MapCensus {
placements: map.placements.len(),
owned: map.placements.iter().filter(|p| p.owned).count(),
stamps: map.stamps.len(),
locations: map.locations.len(),
stamped,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::{Descriptor, Extent, SubCell, Subgrid};
use crate::library::LIBRARY_VERSION;
use crate::vocab::{Vocabularies, Vocabulary};
fn lib() -> Library {
let plain = Descriptor {
id: "crate_a".to_owned(),
extent: Extent { footprint: Some((0.5, 0.5)), height: Some(0.5) },
kind: vec!["container".to_owned()],
..Default::default()
};
let tokened = Descriptor {
id: "wall".to_owned(),
extent: Extent { footprint: Some((0.5, 0.5)), height: Some(0.5) },
subgrid: Some(Subgrid {
cells: vec![SubCell {
at: (0, 0, 0),
solid: true,
edge: Some("wall".to_owned()),
}],
}),
..Default::default()
};
Library { version: LIBRARY_VERSION, note: None, descriptors: vec![plain, tokened] }
}
/// **Two independent ways of counting the same thing agree.**
///
/// The collision test's analogue for metrics, and the whole reason this module is not just a
/// habit. `of_catalog` walks the lattice cells directly; `Vocabularies::masks` resolves the same
/// tokens into a bitfield through an entirely separate path. If those two ever disagree, one of
/// them is lying about the corpus and no panel could tell which.
#[test]
fn the_edge_count_agrees_with_what_the_vocabulary_resolved() {
let library = lib();
let vocab = Vocabularies {
kind: Vocabulary::of(&[("container", "a thing that holds things")]),
edge: Vocabulary::of(&[("wall", "a solid run-face")]),
..Default::default()
};
let by_scan = of_catalog(&library, &[]).with_edges;
let by_mask = library
.descriptors
.iter()
.filter(|d| vocab.masks(d).expect("resolves").edges != 0)
.count();
assert_eq!(by_scan, by_mask, "two ways of counting tokened pieces must agree");
assert_eq!(by_scan, 1);
}
/// A count is about the catalog, not about how the file happens to be ordered.
#[test]
fn counting_does_not_depend_on_order() {
let mut library = lib();
let one = of_catalog(&library, &[]);
library.descriptors.reverse();
assert_eq!(one, of_catalog(&library, &[]));
}
/// `described` is the judgement half — measurement never supplies a `kind`.
#[test]
fn described_counts_only_the_pieces_somebody_judged() {
let c = of_catalog(&lib(), &[]);
assert_eq!(c.descriptors, 2);
assert_eq!(c.described, 1);
}
/// A map's own rows and the stamps it holds are separate numbers, because they are separate
/// things — a stamp is one reference however many rows it stands for.
///
/// And the third number: the rows the stamps bring, which is what the consumer will have
/// beyond the hand-authored ones. A stamp naming a group nothing defines cannot be counted —
/// there is no row count for it — so the counter refuses rather than under-reporting.
#[test]
fn a_maps_rows_and_its_stamps_are_counted_apart() {
let mut map = Map::default();
map.placements.push(crate::map::Placed {
id: "a".to_owned(),
descriptor: "crate_a".to_owned(),
owned: true,
owned_because: Some("the only way out".to_owned()),
..Default::default()
});
map.stamps.push(crate::composition::Stamped {
id: "s1".to_owned(),
of: "station".to_owned(),
..Default::default()
});
let err = of_map(&map, &[], &lib()).expect_err("the group does not exist");
assert!(
err.contains("s1"),
"the refusal must name the stamp: {err}"
);
}
/// **A map whose stamps expand is counted in all three numbers.**
///
/// The hand-authored rows, the references, and the rows the references stand for — and the
/// total, which is the number the consumer will have and the number the editor's panel must
/// show. A panel that said "746" beside a game holding 728 would be a panel lying.
#[test]
fn a_maps_stamps_are_counted_in_the_rows_they_bring() {
let mut map = Map::default();
map.placements.push(crate::map::Placed {
id: "a".to_owned(),
descriptor: "crate_a".to_owned(),
..Default::default()
});
let comp = crate::composition::Composition {
id: "station".to_owned(),
envelope: crate::composition::Envelope::Anchored,
members: vec![crate::composition::Member {
id: "m0".to_owned(),
body: crate::composition::Body::Descriptor {
id: "crate_a".to_owned(),
tip: (0, 0),
on: None,
patch: None,
},
at: (0.0, 0.0),
yaw: 0.0,
lift: 0.0,
paint: 0,
of_fingerprint: None,
note: None,
}],
locations: Vec::new(),
note: None,
};
map.stamps.push(crate::composition::Stamped {
id: "s1".to_owned(),
of: "station".to_owned(),
..Default::default()
});
let c = of_map(&map, &[comp], &lib()).unwrap_or_else(|e| panic!("{e}"));
assert_eq!((c.placements, c.stamps, c.stamped), (1, 1, 1));
assert_eq!(c.total(), 2, "the consumer will have two rows");
}
}