bevy_grimoire_core 0.5.2

Engine-free world-building core: constraint IR, solvers, WFC, deterministic RNG.
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
//! **A library** — every descriptor a project can place, in one file, validated together.
//!
//! A [`Descriptor`] on its own is a fact about one mesh. Most of the checks worth having are about a
//! *set* of them: a surface class something rests on and nothing offers is only visible when you can
//! see everything at once, and so is a duplicate id. So the unit an editor opens is this.
//!
//! # Why one file and not a directory of them
//!
//! A directory is the obvious shape and it is worse here. Ordering becomes filesystem ordering, which
//! is not stable across machines; a duplicate id becomes two files that both look right in isolation;
//! and the two-sided surface check needs the whole set anyway, so "load them all" is not an
//! optimisation, it is the only mode. One file also means a project's asset set has a single diff.
//!
//! The file is machine-owned — generated from a project's existing manifests by `crate::convert` — so
//! it is written by an ordinary serializer and never text-spliced. Its prose survives because
//! [`Descriptor::note`] is a field; see `crate::map`'s note on why that decision was made.

use serde::{Deserialize, Serialize};

use crate::descriptor::Descriptor;
use crate::vocab::{Masks, Vocabularies};

/// Bumped when the shape below changes. A mismatch is refused, never migrated — `persist.rs`'s rule.
pub const LIBRARY_VERSION: u32 = 1;

/// The most cells one piece's derived lattice may have.
///
/// The old per-descriptor `div` was capped at 9 per axis, which bounded a lattice at 729. Divisions
/// are now **derived** — a piece's span times the project's number — so nothing bounds them from the
/// descriptor's side, and a 10 m corridor at `divisions: 4` would build 80 x 40 x 8. This is the
/// replacement guard: generous enough that a 6 m wall at the shipped setting is nowhere near it, low
/// enough that the editor is never asked to draw a lattice nobody could author.
pub const MAX_LATTICE_CELLS: u32 = 4096;

/// Everything a project can place.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Library {
    pub version: u32,
    /// What this library is and where it came from.
    #[serde(default)]
    pub note: Option<String>,
    pub descriptors: Vec<Descriptor>,
}

/// A declared `extent.height` that the mesh does not bear out.
#[derive(Clone, Debug, PartialEq)]
pub struct HeightDisagreement {
    pub id: String,
    /// `placed_height`: the declared height times `align.stretch_y`.
    pub declared: f32,
    /// The mesh's `ymax − ymin` times `align.stretch_y`, from [`crate::glb::Glb::bounds`] (node
    /// transforms applied).
    pub measured: f32,
    /// `declared − measured`; positive means the lid is declared above the mesh.
    pub error: f32,
}

/// What [`Library::height_disagreements`] found.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct HeightCheck {
    pub disagreements: Vec<HeightDisagreement>,
    /// Descriptors that offer a surface but whose mesh could not be measured, with why. Listed rather
    /// than passed silently.
    pub unmeasured: Vec<(String, String)>,
}

impl Library {
    /// **Does each surface-offering descriptor's declared height match its mesh?**
    ///
    /// A guest `on` a host rests `extent.height` above the host's base, so any difference between that
    /// and the mesh's real `ymax − ymin` is a gap or an interpenetration. Descriptors that offer no
    /// surface are skipped (nothing can rest on them); a surface offerer with no declared height, no
    /// mesh, or an unreadable glb comes back in `unmeasured`. `asset_root` is what `mesh` paths are
    /// relative to; `tolerance_m` is the largest `|error|` accepted.
    pub fn height_disagreements(&self, asset_root: &std::path::Path, tolerance_m: f32) -> HeightCheck {
        let mut out = HeightCheck::default();
        for d in self.descriptors.iter().filter(|d| !d.offers.surfaces.is_empty()) {
            let Some(declared) = crate::descriptor::placed_height(d) else {
                out.unmeasured.push((d.id.clone(), "no extent.height declared".into()));
                continue;
            };
            let Some(mesh) = d.mesh.as_deref() else {
                out.unmeasured.push((d.id.clone(), "no mesh".into()));
                continue;
            };
            let bounds = crate::glb::Glb::open(&asset_root.join(mesh)).and_then(|g| g.bounds());
            match bounds {
                Ok((lo, hi)) => {
                    let measured = (hi[1] - lo[1]) * d.align.stretch_y.unwrap_or(1.0);
                    let error = declared - measured;
                    if error.abs() > tolerance_m {
                        out.disagreements.push(HeightDisagreement {
                            id: d.id.clone(),
                            declared,
                            measured,
                            error,
                        });
                    }
                }
                Err(e) => out.unmeasured.push((d.id.clone(), e)),
            }
        }
        out
    }

    pub fn parse(text: &str) -> Result<Library, String> {
        let lib: Library = ron::from_str(text).map_err(|e| format!("library: {e}"))?;
        lib.validate()?;
        Ok(lib)
    }

    pub fn to_ron(&self) -> Result<String, String> {
        ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
            .map_err(|e| format!("library: serialize: {e}"))
    }

    /// Structural checks that need no vocabulary: the version, and that ids identify.
    pub fn validate(&self) -> Result<(), String> {
        if self.version != LIBRARY_VERSION {
            return Err(format!(
                "library: version {} but this build reads {LIBRARY_VERSION}. Regenerate it from the \
                 project's manifests rather than hand-editing the number — the fields moved.",
                self.version
            ));
        }
        for (i, d) in self.descriptors.iter().enumerate() {
            if d.id.trim().is_empty() {
                return Err(format!("library: descriptor {i} has no id"));
            }
            // **An id is a name, and names have a shape.** Nothing checked this, so whatever a tool or
            // a hand-edit last wrote is what a map ends up referencing. Segments rather than the whole
            // string, because `/` is the kit namespace — see `naming::is_id`.
            if !crate::naming::is_id(&d.id) {
                return Err(format!(
                    "library: `{}` is not a usable id. An id is snake_case — lowercase letters, \
                     digits and single underscores, starting with a letter — and `/` separates a kit \
                     from a piece, as in `site/wall_corner`.",
                    d.id
                ));
            }
            // The lattice is **not** checked here: a cell is in range or not depending on the
            // project's divisions-per-tile, which lives in `project.ron` and is not knowable from
            // this file alone. `policy::layered_library` runs `validate_lattices` once both layers
            // are parsed — it is the one loader, so there is no path on which the check is skipped.
            if let Some(j) = self.descriptors.iter().position(|o| o.id == d.id)
                && j != i
            {
                return Err(format!(
                    "library: `{}` is declared twice (entries {j} and {i}). A map references \
                     descriptors by id, so a duplicate makes every reference to it ambiguous.",
                    d.id
                ));
            }
        }
        Ok(())
    }

    /// **The one namespace this library provides**, or `None` when its ids carry none.
    ///
    /// A kit *implements* a namespace rather than owning one. `assets/grimoire/site/` and
    /// `assets/grimoire/site_greybox/` define the **identical** 45 `site/*` ids — that is what makes
    /// one a re-skin of the other, and what lets a project bind `site` to either. So the namespace
    /// is read off the ids here and never taken from the directory name, which is the skin.
    ///
    /// **Every id, not the first one.** Reading only `descriptors.first()` made the answer depend on
    /// which piece happened to sort first, and that is not a property anybody authored.
    ///
    /// `Ok(None)` is not a failure and must not be treated as one: the 75 pieces in the root library
    /// carry no namespace at all. It is a state the caller answers for itself.
    pub fn namespace(&self) -> Result<Option<&str>, String> {
        let mut found: Option<&str> = None;
        for d in &self.descriptors {
            let Some((ns, _)) = d.id.split_once('/') else {
                continue;
            };
            match found {
                None => found = Some(ns),
                Some(seen) if seen == ns => {}
                Some(seen) => {
                    return Err(format!(
                        "library: `{}` is in namespace `{ns}`, but this library already provides \
                         `{seen}`. One directory implements one namespace — that is what lets a \
                         project bind `{seen}` to this kit or to another kit providing the same \
                         pieces. Split them into two kits.",
                        d.id
                    ));
                }
            }
        }
        Ok(found)
    }

    /// **Every lattice against the project's divisions.**
    ///
    /// Separate from [`Self::validate`] because it needs a number `library.ron` does not carry: a
    /// cell at `(5, 0, 0)` is in range on a 3 m wall and outside a 0.5 m crate, and which it is
    /// depends on `project.ron`'s `divisions`. Called from `policy::layered_library`, the one place
    /// both layers exist at once.
    ///
    /// Refuses on the same grounds the per-lattice check always did — an out-of-range cell is a
    /// value nothing will ever read, a duplicate is two answers to one question — plus a ceiling on
    /// the derived volume, since divisions are multiplied by a piece's span and a 10 m corridor at a
    /// fine setting would otherwise build a lattice nobody can author or draw.
    ///
    /// # The ceiling applies to authored cells only
    ///
    /// It once covered every piece, because a piece gets divisions the moment anything asks for them
    /// and the editor asked as soon as a row was selected: it derived them to draw the grid, and
    /// `rebuild_detail` materialises a button and a text node **per cell**. At the legal maximum
    /// `divisions: 8`, `site/wall` derives 48x40x8 — 15,360 cells, some 33,000 UI entities on every
    /// `ImportState` change.
    ///
    /// That explosion is now stopped where the divisions are derived, not here: the editor's
    /// `Project::divisions_of` refuses a piece past [`MAX_LATTICE_CELLS`], so an oversize piece can
    /// sit in a library, be placed and be drawn, and is only refused edge tokens. Refusing the whole
    /// library for it (every label save failed with "NOT WRITTEN" over a tree nobody meant to give
    /// tokens) punished the wrong thing. A piece that carries authored cells still has to fit: those
    /// cells are indices into a lattice, and one outside it is a value nothing will ever read.
    pub fn validate_lattices(&self, divisions: u32) -> Result<(), String> {
        let ceiling = |d: &Descriptor, div: (u32, u32, u32)| -> Result<(), String> {
            let volume = crate::descriptor::Subgrid::volume(div);
            if volume > MAX_LATTICE_CELLS {
                return Err(format!(
                    "library: `{}` derives a {}x{}x{} lattice of {volume} cells, past the {MAX_LATTICE_CELLS} \
                     a piece may have. Divisions are multiplied by a piece's span, so this is either \
                     an oversized `extent` or a `divisions` set too fine for this kit.",
                    d.id, div.0, div.1, div.2
                ));
            }
            Ok(())
        };
        for d in &self.descriptors {
            let div = crate::descriptor::divisions(d, divisions);
            // No cells, so there is nothing to be wrong about, whatever the lattice would be.
            if let Some(grid) = &d.subgrid {
                // Authored cells. The divisions **must** derive — a cell index means nothing without
                // them — so this is the one arm that propagates that failure.
                let div = div?;
                ceiling(d, div)?;
                grid.validate(&d.id, div)?;
            }
        }
        Ok(())
    }

    /// The full check: structure, every token, and the two-sided surface pass over the whole set.
    ///
    /// Returns the mask per descriptor, in library order, so a caller that is about to build a
    /// palette does not resolve the same tokens a second time.
    pub fn resolve(&self, vocab: &Vocabularies) -> Result<Vec<Masks>, String> {
        self.validate()?;
        vocab.validate_library(&self.descriptors)
    }

    pub fn get(&self, id: &str) -> Option<&Descriptor> {
        self.descriptors.iter().find(|d| d.id == id)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::descriptor::{Descriptor, Mount, Offers};

    fn d(id: &str) -> Descriptor {
        Descriptor {
            id: id.to_owned(),
            ..Descriptor::default()
        }
    }

    fn lib(descriptors: Vec<Descriptor>) -> Library {
        Library {
            version: LIBRARY_VERSION,
            note: None,
            descriptors,
        }
    }

    /// **A kit's namespace is what all of its ids agree on, not what the first one says.**
    ///
    /// The bug this replaces read `descriptors.first()`, so the answer depended on sort order — and
    /// on an empty or unnamespaced library it substituted the literal `"kit"`, which is a namespace
    /// nobody chose and which collides across every kit that ever hit it. `assets/grimoire/compositions.ron`
    /// carries a `kit/tile_1` from exactly that.
    #[test]
    fn a_librarys_namespace_is_what_every_id_agrees_on() {
        assert_eq!(
            lib(vec![d("site/floor"), d("site/wall")])
                .namespace()
                .unwrap_or_else(|e| panic!("{e}")),
            Some("site")
        );

        // **No namespace is a state, not a failure.** The root library's 75 pieces are all flat.
        assert_eq!(
            lib(vec![d("lamp_tall"), d("desk")])
                .namespace()
                .unwrap_or_else(|e| panic!("{e}")),
            None
        );
        assert_eq!(
            lib(Vec::new())
                .namespace()
                .unwrap_or_else(|e| panic!("{e}")),
            None
        );

        // A namespaced piece among flat ones still answers: the flat ones simply say nothing.
        assert_eq!(
            lib(vec![d("lamp_tall"), d("site/floor")])
                .namespace()
                .unwrap_or_else(|e| panic!("{e}")),
            Some("site")
        );
    }

    /// **A library that disagrees with itself is refused, and the refusal names both.**
    ///
    /// One directory implements one namespace — that is what makes binding possible, and it is the
    /// question the old `.first()` answered by accident.
    #[test]
    fn a_library_in_two_namespaces_is_refused() {
        let e = lib(vec![d("site/floor"), d("lab/bench")])
            .namespace()
            .err()
            .unwrap_or_else(|| panic!("two namespaces in one directory must be refused"));
        assert!(e.contains("site") && e.contains("lab"), "{e}");
        assert!(
            e.contains("lab/bench"),
            "and it names the piece that disagreed: {e}"
        );
    }

    #[test]
    fn a_library_round_trips() {
        let l = lib(vec![d("a"), d("b")]);
        assert_eq!(
            Library::parse(&l.to_ron().unwrap_or_else(|e| panic!("{e}"))).unwrap_or_else(|e| panic!("{e}")),
            l
        );
    }

    /// A map references descriptors by id, so two entries answering to one name makes every
    /// reference to it ambiguous — and the wrong mesh appearing is a bug nobody traces to the library.
    #[test]
    fn a_duplicate_id_is_refused() {
        let err = lib(vec![d("crate"), d("crate")]).validate().err().unwrap_or_default();
        assert!(err.contains("declared twice"), "{err}");
        assert!(err.contains("crate"), "must name the id: {err}");
    }

    /// **A piece with no cells is never refused for how big its lattice would be.** The ceiling
    /// used to cover it too, so one tree or landmark in a kit made every library write fail; the
    /// explosion it guarded (a button per cell) is stopped at the editor's `divisions_of` now.
    #[test]
    fn an_unauthored_oversize_piece_is_accepted() {
        let mut wall = d("wall");
        wall.extent = crate::descriptor::Extent {
            footprint: Some((40.0, 40.0)),
            height: Some(30.0),
        };
        assert_eq!(wall.subgrid, None, "the case is a piece nobody has authored");
        let div = crate::descriptor::divisions(&wall, 1).unwrap_or_else(|e| panic!("{e}"));
        assert!(
            crate::descriptor::Subgrid::volume(div) > MAX_LATTICE_CELLS,
            "the fixture has to be past the ceiling for this to mean anything: {div:?}"
        );
        assert!(lib(vec![wall]).validate_lattices(1).is_ok());
    }

    /// **A piece with authored cells still has to fit its ceiling.** Cells are indices into a
    /// lattice; an author who has put edge tokens on an oversize piece has already survived drawing
    /// it, and a library holding it is the one the editor will try to materialise.
    #[test]
    fn an_authored_oversize_piece_is_refused() {
        let mut wall = d("wall");
        wall.extent = crate::descriptor::Extent {
            footprint: Some((3.0, 0.5)),
            height: Some(2.4),
        };
        wall.subgrid = Some(crate::descriptor::Subgrid {
            cells: vec![crate::descriptor::SubCell::default()],
        });
        // At the legal maximum this derives 48x40x8 = 15,360 cells.
        let err = lib(vec![wall.clone()])
            .validate_lattices(8)
            .err()
            .unwrap_or_default();
        assert!(err.contains("15360"), "must name the volume: {err}");
        assert!(err.contains("wall"), "must name the piece: {err}");
        // And the shipped setting is nowhere near it, which is the other half of the guard being
        // useful rather than merely present.
        assert!(lib(vec![wall]).validate_lattices(1).is_ok());
    }

    #[test]
    fn another_schema_version_is_refused_not_migrated() {
        let mut l = lib(vec![d("a")]);
        l.version = LIBRARY_VERSION + 1;
        assert!(l.validate().err().unwrap_or_default().contains("Regenerate"));
    }

    /// The check that needs the whole set. Each descriptor is individually perfect.
    #[test]
    fn resolve_catches_a_surface_nobody_offers() {
        let vocab = Vocabularies {
            surfaces: crate::vocab::Vocabulary::of(&[("worktop", "a desk top")]),
            ..Vocabularies::default()
        };
        let mut mug = d("mug");
        mug.mount = Some(Mount::OnSurface {
            class: "worktop".into(),
        });
        let err = lib(vec![mug]).resolve(&vocab).err().unwrap_or_default();
        assert!(err.contains("no descriptor in this library offers"), "{err}");
    }

    #[test]
    fn resolve_returns_masks_in_library_order() {
        let vocab = Vocabularies {
            surfaces: crate::vocab::Vocabulary::of(&[("worktop", "a desk top")]),
            ..Vocabularies::default()
        };
        let mut table = d("table");
        table.offers = Offers {
            surfaces: vec!["worktop".into()],
            ..Offers::default()
        };
        let mut mug = d("mug");
        mug.mount = Some(Mount::OnSurface {
            class: "worktop".into(),
        });
        let masks = lib(vec![table, mug])
            .resolve(&vocab)
            .unwrap_or_else(|e| panic!("{e}"));
        assert_eq!(masks.len(), 2);
        assert!(masks[1].rests_on(&masks[0]), "the mug should rest on the table");
    }
}