Skip to main content

primase_core/
components.rs

1//! The core components: what every reader, pass and writer agrees on. Small and versioned; everything game- or
2//! target-specific is a dialect component owned by its crate (`<dialect>.*`, `pbr.*`).
3//!
4//! Entity roles (by convention, not enforced by the store):
5//! - a **prototype** has a [`Mesh`] in its own local space (plus [`Name`], [`Source`]);
6//! - a **placement** has a [`Transform`] and an [`Instance`] pointing at its prototype;
7//! - a **material** has the material components of some dialect (`pbr.material`, a destination's `material`, ...);
8//!   a [`Primitive`] refers to it by entity;
9//! - a **texture** has a [`Texture`] (or only a [`TextureFile`] until a pass loads it); an entity with a
10//!   [`TextureAbsent`] stands for a texture the source names that the read could not give;
11//! - a prototype whose geometry is still in a file has a [`MeshFile`] (a pass loads it into a [`Mesh`]), and
12//!   [`MaterialVariants`] when placements pick materials per variant.
13//!
14//! Geometry is in the world frame ([`crate::frame`]): metres, right-handed, +Y up, -Z north; counter-clockwise front
15//! faces; UV v = 0 at the image's top row (glTF).
16
17use std::collections::BTreeMap;
18use std::path::PathBuf;
19
20use serde::Serialize;
21use serde_json::{Value, json};
22
23use crate::json::JsonError;
24use crate::texformat::TexFormat;
25use crate::vector::{cross, dot};
26use crate::world::{Component, Entity};
27
28/// Display name.
29#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
30pub struct Name(pub String);
31impl Component for Name {
32    const NAME: &'static str = "core.name";
33    const GEOMETRY: crate::Geometry = crate::Geometry::None;
34    /// No local-space geometry.
35    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
36        crate::world::Linearity::Invariant
37    }
38    fn to_json(&self) -> Result<Option<Value>, JsonError> {
39        crate::json::to_value(self).map(Some)
40    }
41}
42
43/// Provenance: a stable key into the source game's data (an archetype hash, a map part's name, an asset path), for
44/// debugging, reports and review links.
45#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
46pub struct Source(pub String);
47impl Component for Source {
48    const NAME: &'static str = "core.source";
49    const GEOMETRY: crate::Geometry = crate::Geometry::None;
50    /// No local-space geometry.
51    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
52        crate::world::Linearity::Invariant
53    }
54    fn to_json(&self) -> Result<Option<Value>, JsonError> {
55        crate::json::to_value(self).map(Some)
56    }
57}
58
59/// An affine 3x4 transform, column by column: the images of local +X, +Y, +Z, then the translation;
60/// `world = M * local`. f32 because every format we read and write stores f32 (byte-identical round trips).
61/// A transform composed of several (a hierarchy, an entity placed in another's space, a tile offset) is computed in
62/// f64 from the stored f32 values and each entry rounded once to f32, never rounded step by step.
63/// Shear and mirroring are allowed; [`Transform::classify`] flags them.
64#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
65pub struct Transform {
66    pub cols: [[f32; 3]; 4],
67}
68impl Component for Transform {
69    const NAME: &'static str = "core.transform";
70    const GEOMETRY: crate::Geometry = crate::Geometry::None;
71    /// The placement itself, which a pass baking its non-rigid part replaces: not mapped.
72    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
73        crate::world::Linearity::Invariant
74    }
75    fn verify(&self, _e: Entity, _w: &crate::World) -> Result<(), String> {
76        finite(self.cols.iter().flatten().copied(), "transform")
77    }
78    fn to_json(&self) -> Result<Option<Value>, JsonError> {
79        crate::json::to_value(self).map(Some)
80    }
81}
82
83/// A basis axis shorter than this, or a determinant under this fraction of the axes' lengths' product, makes a transform
84/// [`TransformKind::Degenerate`]. Fixed, not an option: every consumer that classifies a placement (and
85/// `primase_meshops::transform`, which applies it in f32) means the same by it.
86pub const DEGENERATE_BASIS: f64 = 1e-6;
87/// Two axes whose unit vectors' dot product exceeds this are not orthogonal: the transform is [`TransformKind::Sheared`].
88/// Fixed, as [`DEGENERATE_BASIS`] is.
89pub const SHEAR_TOLERANCE: f64 = 1e-4;
90
91/// What a transform does besides rotating, scaling and translating.
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum TransformKind {
94    /// Rotation with any per-axis scale and a translation.
95    Regular,
96    /// Some axis is (near) zero or the axes are (near) coplanar.
97    Degenerate,
98    /// Axes not orthogonal.
99    Sheared,
100    /// Negative determinant (and not sheared).
101    Mirrored,
102}
103
104impl Transform {
105    pub const IDENTITY: Transform = Transform {
106        cols: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0., 0., 0.]],
107    };
108
109    /// The determinant of the linear part, in f64: negative for a mirroring transform, which reverses the winding of
110    /// triangles mapped through it.
111    pub fn determinant(&self) -> f64 {
112        let c: [[f64; 3]; 3] = std::array::from_fn(|i| self.cols[i].map(f64::from));
113        dot(c[0], cross(c[1], c[2]))
114    }
115
116    /// A rigid transform at `position` whose local -Z points along `direction` (as a light or camera shines) and whose
117    /// +Y is as near world +Y as possible (+Z when `direction` is vertical); `None` for a zero or non-finite
118    /// direction. A point light or round spot cone looks the same under any roll, so no roll is taken from the source.
119    pub fn facing(position: [f32; 3], direction: [f32; 3]) -> Option<Transform> {
120        let unit = |v: [f32; 3]| {
121            let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
122            (n > 0.0 && n.is_finite()).then(|| v.map(|x| x / n))
123        };
124        let cross = |a: [f32; 3], b: [f32; 3]| {
125            [
126                a[1] * b[2] - a[2] * b[1],
127                a[2] * b[0] - a[0] * b[2],
128                a[0] * b[1] - a[1] * b[0],
129            ]
130        };
131        let z = unit(direction.map(|x| -x))?;
132        let x = unit(cross([0.0, 1.0, 0.0], z)).or_else(|| unit(cross([0.0, 0.0, 1.0], z)))?;
133        let y = cross(z, x);
134        Some(Transform {
135            cols: [x, y, z, position],
136        })
137    }
138
139    /// Point `p` through the transform, `M p + t`, computed in f64 and rounded once to f32.
140    pub fn apply(&self, p: [f32; 3]) -> [f32; 3] {
141        std::array::from_fn(|i| {
142            (f64::from(self.cols[3][i])
143                + (0..3)
144                    .map(|j| f64::from(self.cols[j][i]) * f64::from(p[j]))
145                    .sum::<f64>()) as f32
146        })
147    }
148
149    /// A 4x4 matrix given column by column (each column x, y, z, w) as the affine transform it is: refused unless its
150    /// last row (the columns' w) is 0, 0, 0, 1, each within `tolerance`, the one a format states (0: exactly). The
151    /// one rule every reader of a 4x4 matrix holds it to; a format that stores three rows is affine by its storage.
152    pub fn from_columns4(cols: [[f32; 4]; 4], tolerance: f32) -> Result<Transform, String> {
153        let last = cols.map(|c| c[3]);
154        // written so that a NaN entry is refused
155        if !last
156            .iter()
157            .zip([0.0, 0.0, 0.0, 1.0])
158            .all(|(&w, want)| (w - want).abs() <= tolerance)
159        {
160            return Err(format!(
161                "a matrix whose last row is {last:?}, not 0 0 0 1 (not affine)"
162            ));
163        }
164        Ok(Transform {
165            cols: cols.map(|c| [c[0], c[1], c[2]]),
166        })
167    }
168
169    /// `self` after `inner` (`world = self (inner local)`): the transform of an entity placed by `inner` in a space
170    /// `self` places, computed in f64 from the stored values and each entry rounded once to f32. A chain of several
171    /// rounds at each link; a chain whose intermediate rounding matters is composed in f64 by its owner.
172    pub fn compose(&self, inner: &Transform) -> Transform {
173        let o = self.cols.map(|c| c.map(f64::from));
174        let linear = |v: [f32; 3]| -> [f64; 3] {
175            let v = v.map(f64::from);
176            std::array::from_fn(|r| o[0][r] * v[0] + o[1][r] * v[1] + o[2][r] * v[2])
177        };
178        let [x, y, z, t] = inner.cols.map(linear);
179        Transform {
180            cols: [
181                x.map(|v| v as f32),
182                y.map(|v| v as f32),
183                z.map(|v| v as f32),
184                std::array::from_fn(|r| (t[r] + o[3][r]) as f32),
185            ],
186        }
187    }
188
189    /// What the basis does besides rotating, scaling and translating, the one meaning every reader, writer and mesh operation
190    /// that classifies a placement shares: with `u_i = c_i / |c_i|`, degenerate when some `|c_i| <` [`DEGENERATE_BASIS`] or
191    /// `|det| <=` [`DEGENERATE_BASIS`] `|c0||c1||c2|` (or the determinant is not a number); sheared when some `|u_i . u_j| >` [`SHEAR_TOLERANCE`]; mirrored when
192    /// `det < 0`. Computed in f64.
193    pub fn classify(&self) -> TransformKind {
194        let c: [[f64; 3]; 3] = std::array::from_fn(|i| self.cols[i].map(f64::from));
195        let len: [f64; 3] = std::array::from_fn(|i| dot(c[i], c[i]).sqrt());
196        let det = dot(c[0], cross(c[1], c[2]));
197        if degenerate(&c, det) {
198            return TransformKind::Degenerate;
199        }
200        let u: [[f64; 3]; 3] = std::array::from_fn(|i| c[i].map(|x| x / len[i]));
201        if [(0, 1), (0, 2), (1, 2)]
202            .iter()
203            .any(|&(i, j)| dot(u[i], u[j]).abs() > SHEAR_TOLERANCE)
204        {
205            return TransformKind::Sheared;
206        }
207        if det < 0.0 {
208            TransformKind::Mirrored
209        } else {
210            TransformKind::Regular
211        }
212    }
213}
214
215/// Whether the basis with columns `c` and determinant `det` is degenerate ([`Transform::classify`]): some axis
216/// shorter than [`DEGENERATE_BASIS`], or `|det| <=` [`DEGENERATE_BASIS`] `|c0||c1||c2|`, or a determinant that isn't
217/// a number.
218fn degenerate(c: &[[f64; 3]; 3], det: f64) -> bool {
219    let len: [f64; 3] = std::array::from_fn(|i| dot(c[i], c[i]).sqrt());
220    det.is_nan()
221        || len.iter().any(|&l| l < DEGENERATE_BASIS)
222        || det.abs() <= DEGENERATE_BASIS * len[0] * len[1] * len[2]
223}
224
225/// The position of the first non-finite value, as an error.
226fn finite(values: impl Iterator<Item = f32>, what: &str) -> Result<(), String> {
227    match values.enumerate().find(|(_, x)| !x.is_finite()) {
228        Some((i, x)) => Err(format!("{what}: value {i} is {x}, not finite")),
229        None => Ok(()),
230    }
231}
232
233/// A linear map of an entity's local space (a transform's 3x3 part), computed in f64 from the f32 entries and rounded
234/// once per value: what [`crate::Component::apply_linear`] maps local-space geometry by.
235///
236/// Points go through `M`; normals through `M^-T` with the determinant's sign kept (they stay on the side they were
237/// on) and directions through `M`, both then of unit length (zero stays zero); a tangent's w (the bitangent sign) and
238/// a triangle's winding flip when the determinant is negative ([`Linear::mirrors`]). A singular map
239/// ([`Linear::singular`]) turns no normal or tangent.
240#[derive(Clone, Copy, Debug, PartialEq)]
241pub struct Linear {
242    c: [[f64; 3]; 3],
243    /// Columns of the inverse transpose times the determinant: `[c1 x c2, c2 x c0, c0 x c1]`.
244    cof: [[f64; 3]; 3],
245    det: f64,
246}
247
248impl Linear {
249    /// The 3x3 part of `t`.
250    pub fn of(t: &Transform) -> Linear {
251        Linear::new(std::array::from_fn(|i| t.cols[i]))
252    }
253
254    /// The map with these columns (images of local +X, +Y, +Z).
255    pub fn new(cols: [[f32; 3]; 3]) -> Linear {
256        Linear::of_f64(cols.map(|col| col.map(f64::from)))
257    }
258
259    /// The map with these columns held in f64: a part composed in f64 (a chain of bone transforms) taken as it is,
260    /// not rounded to f32 first.
261    pub fn of_f64(c: [[f64; 3]; 3]) -> Linear {
262        let cof = [cross(c[1], c[2]), cross(c[2], c[0]), cross(c[0], c[1])];
263        let det = dot(c[0], cof[0]);
264        Linear { c, cof, det }
265    }
266
267    /// x negated: the mirror a mirroring placement keeps out of its transform, `M = M' * MIRROR_X`.
268    pub fn mirror_x() -> Linear {
269        Linear::new([[-1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])
270    }
271
272    pub fn determinant(&self) -> f64 {
273        self.det
274    }
275
276    /// Whether the map has no inverse to turn normals and tangents by: its basis is degenerate, as
277    /// [`Transform::classify`] says of a transform with this linear part (or its determinant is not finite). The one
278    /// rule [`Linear::normal`], [`Linear::tangent`] and [`Linear::inverse_columns`] answer `None` by.
279    pub fn singular(&self) -> bool {
280        !self.det.is_finite() || degenerate(&self.c, self.det)
281    }
282
283    /// The columns of `M^-1` (the transpose of the cofactors over the determinant); `None` for a singular map
284    /// ([`Linear::singular`]).
285    pub fn inverse_columns(&self) -> Option<[[f64; 3]; 3]> {
286        if self.singular() {
287            return None;
288        }
289        // row i of M^-1 is cof[i] / det (c1 x c2 dotted with c0 is det, with c1 and c2 it is 0): column j holds
290        // entry j of each
291        Some(std::array::from_fn(|j| {
292            std::array::from_fn(|r| self.cof[r][j] / self.det)
293        }))
294    }
295
296    /// Whether it reverses orientation (negative determinant): triangles must be reversed and tangent w negated.
297    pub fn mirrors(&self) -> bool {
298        self.det < 0.0
299    }
300
301    /// The uniform scale `s` when the map is a rotation or mirror times `s` (columns orthogonal and of one length, to
302    /// a relative 1e-6): a sphere stays a sphere of `s` times its radius. `None` for any other map.
303    pub fn similarity(&self) -> Option<f64> {
304        let len: [f64; 3] = self.c.map(|v| dot(v, v).sqrt());
305        let s = len[0];
306        let tol = 1e-6 * s;
307        let orthogonal = [(0, 1), (0, 2), (1, 2)]
308            .iter()
309            .all(|&(i, j)| dot(self.c[i], self.c[j]).abs() <= tol * s);
310        (s > 0.0 && orthogonal && len.iter().all(|l| (l - s).abs() <= tol)).then_some(s)
311    }
312
313    /// For each local axis, the world axis it maps onto, when every column has exactly one nonzero entry (a signed,
314    /// scaled permutation): an axis-aligned box stays one. `None` for any other map.
315    pub fn axis_map(&self) -> Option<[usize; 3]> {
316        let mut out = [0; 3];
317        for (k, col) in self.c.iter().enumerate() {
318            let nonzero: Vec<usize> = (0..3).filter(|&r| col[r] != 0.0).collect();
319            match nonzero[..] {
320                [r] => out[k] = r,
321                _ => return None,
322            }
323        }
324        Some(out)
325    }
326
327    fn apply(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
328        std::array::from_fn(|r| m[0][r] * v[0] + m[1][r] * v[1] + m[2][r] * v[2])
329    }
330
331    /// `M p`.
332    pub fn point(&self, p: [f32; 3]) -> [f32; 3] {
333        Self::apply(&self.c, p.map(f64::from)).map(|x| x as f32)
334    }
335
336    /// `M^-T n` times the determinant's sign, of unit length (zero stays zero); `None` under a singular map
337    /// ([`Linear::singular`]), which turns no normal.
338    pub fn normal(&self, n: [f32; 3]) -> Option<[f32; 3]> {
339        if self.singular() {
340            return None;
341        }
342        let v = Self::apply(&self.cof, n.map(f64::from)).map(|x| x * self.det.signum());
343        Some(unit(v))
344    }
345
346    /// `M t` of unit length (zero stays zero), w negated under a mirror; `None` under a singular map
347    /// ([`Linear::singular`]).
348    pub fn tangent(&self, t: [f32; 4]) -> Option<[f32; 4]> {
349        if self.singular() {
350            return None;
351        }
352        let v = unit(Self::apply(&self.c, [t[0], t[1], t[2]].map(f64::from)));
353        let w = if self.mirrors() { -t[3] } else { t[3] };
354        Some([v[0], v[1], v[2], w])
355    }
356}
357
358/// `v` of unit length as f32 ([`crate::vector::unit`]); a vector with no direction stays zero.
359fn unit(v: [f64; 3]) -> [f32; 3] {
360    crate::vector::unit(v).map_or([0.0; 3], |u| u.map(|x| x as f32))
361}
362
363/// A placement of a prototype.
364#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
365pub struct Instance {
366    pub prototype: Entity,
367    /// Selects per-placement material variants of the prototype's primitives (one source's tint rows, see
368    /// [`MaterialVariants`]); 0 when it has none.
369    pub variant: u32,
370}
371impl Component for Instance {
372    const NAME: &'static str = "core.instance";
373    const GEOMETRY: crate::Geometry = crate::Geometry::None;
374    /// No local-space geometry.
375    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
376        crate::world::Linearity::Invariant
377    }
378    fn to_json(&self) -> Result<Option<Value>, JsonError> {
379        crate::json::to_value(self).map(Some)
380    }
381    fn references(&self, out: &mut Vec<Entity>) {
382        out.push(self.prototype);
383    }
384    /// A placement has its transform ([`Transform`] on the same entity).
385    fn verify(&self, e: Entity, w: &crate::World) -> Result<(), String> {
386        if w.has::<Transform>(e) {
387            Ok(())
388        } else {
389            Err("a placement without a core.transform".into())
390        }
391    }
392}
393
394/// How a placed entity takes part in shadows as a caster.
395#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
396pub enum ShadowCast {
397    Off,
398    On,
399    /// Casts a shadow but is not drawn (a shadow proxy).
400    Only,
401}
402
403/// The shadows of a drawn entity: a placement, a prototype (for each of its placements that states none of its own:
404/// a placement's own overrides it), or an entity drawing its own mesh. The one place shadow casting is stated: a
405/// source's fact where it has one (a reader), else the program's choice (`set-shadow-casting`). `cast: None` casts
406/// (the default); `receive: None` is the renderer's default. Renderer settings (map sizes, bias, cascades) are not
407/// facts of the scene and stay out of core.
408#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize)]
409pub struct Shadows {
410    pub cast: Option<ShadowCast>,
411    pub receive: Option<bool>,
412}
413impl Component for Shadows {
414    const NAME: &'static str = "core.shadows";
415    const GEOMETRY: crate::Geometry = crate::Geometry::None;
416    /// 2: `cast: None` casts (it was the renderer's default); materials no longer state casting.
417    const VERSION: u32 = 2;
418    /// No local-space geometry.
419    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
420        crate::world::Linearity::Invariant
421    }
422    fn to_json(&self) -> Result<Option<Value>, JsonError> {
423        crate::json::to_value(self).map(Some)
424    }
425    /// On something drawn: a placement or an entity with its own mesh.
426    fn verify(&self, e: Entity, w: &crate::World) -> Result<(), String> {
427        if w.has::<Instance>(e) || w.has::<Mesh>(e) {
428            Ok(())
429        } else {
430            Err("shadows on an entity that draws nothing (no core.instance or core.mesh)".into())
431        }
432    }
433}
434
435/// A vertex attribute's meaning.
436#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
437pub enum Semantic {
438    Position,
439    Normal,
440    /// Tangent set: xyz tangent, w the bitangent sign. Set 0 is glTF's standard TANGENT.
441    Tangent(u8),
442    /// u right, v down: (0, 0) is the image's top-left corner (glTF). Readers of bottom-left conventions store 1 - v.
443    TexCoord(u8),
444    Color(u8),
445    Joints(u8),
446    Weights(u8),
447    /// Opaque per-vertex shader data, bound by the game's input index and passed through untransformed.
448    /// Geometry passes must copy it through; a pass that cannot preserve it must refuse ShaderInput by name.
449    /// glTF has no attribute for them: a GLB primase writes leaves them out, and its writer counts them.
450    ShaderInput(u8),
451    /// A point per vertex in the mesh's own space (xyz, three floats), bound by the game's input index: a pivot a
452    /// vertex program turns the vertex about (a billboard card's centre). Whatever moves the positions moves it the same
453    /// way ([`Moves::Point`]). glTF has no attribute for it: a GLB primase writes leaves it out, and its writer counts it.
454    Point(u8),
455}
456
457/// How a map of space moves a vertex stream of a [`Semantic`]: every pass that moves vertices (a [`crate::Linear`]
458/// map, a skin, a frame change) moves each stream by its meaning's rule, so a meaning added here moves everywhere at once.
459#[derive(Clone, Copy, Debug, PartialEq, Eq)]
460pub enum Moves {
461    /// As a point: the map's linear part and its translation (positions, [`Semantic::Point`]).
462    Point,
463    /// As a surface normal: the inverse transpose of the linear part, kept of unit length.
464    Normal,
465    /// As a tangent (xyz, w the bitangent sign): the linear part, of unit length, w negated under a mirror.
466    Tangent,
467    /// Not at all: texture coordinates, colours, skin weights and opaque shader data.
468    Not,
469}
470
471impl Semantic {
472    /// How a map of space moves a stream of this meaning.
473    pub const fn moves(self) -> Moves {
474        match self {
475            Semantic::Position | Semantic::Point(_) => Moves::Point,
476            Semantic::Normal => Moves::Normal,
477            Semantic::Tangent(_) => Moves::Tangent,
478            Semantic::TexCoord(_)
479            | Semantic::Color(_)
480            | Semantic::Joints(_)
481            | Semantic::Weights(_)
482            | Semantic::ShaderInput(_) => Moves::Not,
483        }
484    }
485}
486
487/// A shader input reads a vertex attribute by the same meaning a mesh stream has.
488impl From<Semantic> for crate::shader::Attribute {
489    fn from(s: Semantic) -> Self {
490        use crate::shader::Attribute as A;
491        match s {
492            Semantic::Position => A::Position,
493            Semantic::Normal => A::Normal,
494            Semantic::Tangent(n) => A::Tangent(n),
495            Semantic::TexCoord(n) => A::TexCoord(n),
496            Semantic::Color(n) => A::Color(n),
497            Semantic::Joints(n) => A::Joints(n),
498            Semantic::Weights(n) => A::Weights(n),
499            Semantic::ShaderInput(n) => A::ShaderInput(n),
500            Semantic::Point(n) => A::Point(n),
501        }
502    }
503}
504
505impl From<crate::shader::Attribute> for Semantic {
506    fn from(a: crate::shader::Attribute) -> Self {
507        use crate::shader::Attribute as A;
508        match a {
509            A::Position => Semantic::Position,
510            A::Normal => Semantic::Normal,
511            A::Tangent(n) => Semantic::Tangent(n),
512            A::TexCoord(n) => Semantic::TexCoord(n),
513            A::Color(n) => Semantic::Color(n),
514            A::Joints(n) => Semantic::Joints(n),
515            A::Weights(n) => Semantic::Weights(n),
516            A::ShaderInput(n) => Semantic::ShaderInput(n),
517            A::Point(n) => Semantic::Point(n),
518        }
519    }
520}
521
522/// One vertex stream, decoded to the type a writer can use directly. Readers that need to keep a game's
523/// quantisation (SNORM16 positions with a scale, packed normals) attach it as a dialect component and lower later.
524#[derive(Clone, Debug, PartialEq)]
525pub enum Stream {
526    F32x2(Vec<[f32; 2]>),
527    F32x3(Vec<[f32; 3]>),
528    F32x4(Vec<[f32; 4]>),
529    U8x4(Vec<[u8; 4]>),
530    U16x4(Vec<[u16; 4]>),
531}
532
533impl Stream {
534    pub fn len(&self) -> usize {
535        match self {
536            Stream::F32x2(v) => v.len(),
537            Stream::F32x3(v) => v.len(),
538            Stream::F32x4(v) => v.len(),
539            Stream::U8x4(v) => v.len(),
540            Stream::U16x4(v) => v.len(),
541        }
542    }
543
544    pub fn is_empty(&self) -> bool {
545        self.len() == 0
546    }
547
548    /// The stream's data in bytes: its length times its element size.
549    pub fn byte_len(&self) -> u64 {
550        let element = match self {
551            Stream::F32x2(_) => 8,
552            Stream::F32x3(_) => 12,
553            Stream::F32x4(_) => 16,
554            Stream::U8x4(_) => 4,
555            Stream::U16x4(_) => 8,
556        };
557        self.len() as u64 * element
558    }
559}
560
561/// Primitive topology. Strips and fans are lowered to lists by readers (toolkit vertex helpers).
562#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
563pub enum Topology {
564    Triangles,
565    Lines,
566    Points,
567}
568
569/// One drawable part of a mesh: vertex streams, optional indices, a material. Primitives are kept in the game's
570/// draw order (overlay stacking depends on it).
571#[derive(Clone, Debug, PartialEq)]
572pub struct Primitive {
573    pub topology: Topology,
574    /// Every stream has the same length (the vertex count). `Position` is required.
575    pub streams: BTreeMap<Semantic, Stream>,
576    /// Vertex indices; `None` means sequential.
577    pub indices: Option<Vec<u32>>,
578    /// The material entity, if any.
579    pub material: Option<Entity>,
580}
581
582impl Primitive {
583    pub fn positions(&self) -> Option<&[[f32; 3]]> {
584        match self.streams.get(&Semantic::Position)? {
585            Stream::F32x3(v) => Some(v),
586            _ => None,
587        }
588    }
589
590    pub fn vertex_count(&self) -> usize {
591        self.streams.get(&Semantic::Position).map_or(0, Stream::len)
592    }
593}
594
595/// A map of space a primitive's vertices are moved by, one rule per [`Moves`]: the one way a pass moves vertex streams
596/// in place ([`move_streams`]), so every stream whose meaning moves is moved.
597pub trait VertexMap {
598    /// A point (a position, a [`Semantic::Point`]).
599    fn point(&self, p: [f32; 3]) -> [f32; 3];
600    /// A surface normal. `None` where the map turns no normal (a singular map).
601    fn normal(&self, n: [f32; 3]) -> Option<[f32; 3]>;
602    /// A tangent: xyz and the bitangent sign w. `None` where the map turns no tangent.
603    fn tangent(&self, t: [f32; 4]) -> Option<[f32; 4]>;
604}
605
606impl VertexMap for crate::Linear {
607    fn point(&self, p: [f32; 3]) -> [f32; 3] {
608        crate::Linear::point(self, p)
609    }
610    fn normal(&self, n: [f32; 3]) -> Option<[f32; 3]> {
611        crate::Linear::normal(self, n)
612    }
613    fn tangent(&self, t: [f32; 4]) -> Option<[f32; 4]> {
614        crate::Linear::tangent(self, t)
615    }
616}
617
618/// A move of points by a fixed offset: normals and tangents stay.
619#[derive(Clone, Copy, Debug, PartialEq)]
620pub struct Offset(pub [f32; 3]);
621
622impl VertexMap for Offset {
623    fn point(&self, p: [f32; 3]) -> [f32; 3] {
624        std::array::from_fn(|k| p[k] + self.0[k])
625    }
626    fn normal(&self, n: [f32; 3]) -> Option<[f32; 3]> {
627        Some(n)
628    }
629    fn tangent(&self, t: [f32; 4]) -> Option<[f32; 4]> {
630        Some(t)
631    }
632}
633
634/// `streams` (a primitive's) each moved by `map` by its meaning's rule ([`Semantic::moves`]); streams that do not move
635/// are kept. Refused, naming the stream and changing nothing, where a stream that moves is not of its meaning's element
636/// type (a point three floats, a normal three, a tangent four) or `map` turns no normal or tangent the streams hold.
637pub fn move_streams(streams: &mut BTreeMap<Semantic, Stream>, map: &impl VertexMap) -> Result<(), String> {
638    let mut moved = Vec::new();
639    for (sem, stream) in streams.iter() {
640        let turned = match (sem.moves(), stream) {
641            (Moves::Not, _) => continue,
642            (Moves::Point, Stream::F32x3(v)) => Stream::F32x3(v.iter().map(|x| map.point(*x)).collect()),
643            (Moves::Normal, Stream::F32x3(v)) => Stream::F32x3(
644                v.iter()
645                    .map(|x| map.normal(*x))
646                    .collect::<Option<_>>()
647                    .ok_or_else(|| format!("{sem:?}: the map turns no normal"))?,
648            ),
649            (Moves::Tangent, Stream::F32x4(v)) => Stream::F32x4(
650                v.iter()
651                    .map(|x| map.tangent(*x))
652                    .collect::<Option<_>>()
653                    .ok_or_else(|| format!("{sem:?}: the map turns no tangent"))?,
654            ),
655            (moves, _) => return Err(format!("{sem:?} is not a stream that moves as a {moves:?}")),
656        };
657        moved.push((*sem, turned));
658    }
659    streams.extend(moved);
660    Ok(())
661}
662
663/// One primitive of a [`Mesh`], edited in place: it reads as a [`Primitive`] and changes field by field (its streams,
664/// indices, topology and material), but never as a whole, so a primitive can't be replaced by another or swapped
665/// with one, and the list keeps its count and order. Per-primitive components describe the primitive in its place;
666/// a pass that rebuilds the list uses [`crate::World::rebuild_primitives`] or [`crate::World::edit_mesh`], which
667/// carry them along.
668///
669/// No slice to swap, sort or reverse:
670/// ```compile_fail
671/// let mut mesh = primase_core::Mesh::default();
672/// mesh.primitives_mut().swap(0, 1);
673/// ```
674/// and no primitive replaced by another:
675/// ```compile_fail
676/// let mut mesh = primase_core::Mesh::default();
677/// let other = mesh.primitives()[0].clone();
678/// *mesh.primitive_mut(0).unwrap() = other;
679/// ```
680#[derive(Debug)]
681pub struct PrimitiveMut<'a>(&'a mut Primitive);
682
683impl std::ops::Deref for PrimitiveMut<'_> {
684    type Target = Primitive;
685    fn deref(&self) -> &Primitive {
686        self.0
687    }
688}
689
690impl PrimitiveMut<'_> {
691    pub fn topology_mut(&mut self) -> &mut Topology {
692        &mut self.0.topology
693    }
694    pub fn streams_mut(&mut self) -> &mut BTreeMap<Semantic, Stream> {
695        &mut self.0.streams
696    }
697    pub fn indices_mut(&mut self) -> &mut Option<Vec<u32>> {
698        &mut self.0.indices
699    }
700    pub fn material_mut(&mut self) -> &mut Option<Entity> {
701        &mut self.0.material
702    }
703    /// Every field at once, for an edit that changes several (indices against the streams, say).
704    pub fn fields(&mut self) -> PrimitiveFields<'_> {
705        let Primitive {
706            topology,
707            streams,
708            indices,
709            material,
710        } = self.0;
711        PrimitiveFields {
712            topology,
713            streams,
714            indices,
715            material,
716        }
717    }
718}
719
720/// A primitive's fields, each editable ([`PrimitiveMut::fields`]).
721#[derive(Debug)]
722pub struct PrimitiveFields<'a> {
723    pub topology: &'a mut Topology,
724    pub streams: &'a mut BTreeMap<Semantic, Stream>,
725    pub indices: &'a mut Option<Vec<u32>>,
726    pub material: &'a mut Option<Entity>,
727}
728
729/// A prototype's geometry in its own local space.
730///
731/// Its primitive list changes only through [`crate::World::rebuild_primitives`] and [`crate::World::edit_mesh`], which
732/// keep every per-primitive component (material variants, dialect provenance) in step: a mesh is built whole
733/// ([`Mesh::new`]), read ([`Mesh::primitives`]) and edited one primitive at a time, field by field
734/// ([`Mesh::primitives_mut`], [`Mesh::primitive_mut`]: a [`PrimitiveMut`] edits a primitive's streams, indices,
735/// topology and material, and cannot replace, reorder, add or remove primitives, so per-primitive components stay with
736/// the primitive they describe).
737///
738/// Each mesh built with [`Mesh::new`] has an identity of its own, which a clone and an edit in place keep, so the
739/// conservation diagnostics ([`crate::ledger`]) tell a mesh edited in place from one replaced by another. Equality and
740/// `Debug` ignore it; nothing writes it.
741#[derive(Clone)]
742pub struct Mesh {
743    primitives: Vec<Primitive>,
744    id: u64,
745}
746static NEXT_MESH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
747impl Mesh {
748    pub fn new(primitives: Vec<Primitive>) -> Self {
749        Mesh {
750            primitives,
751            id: NEXT_MESH.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
752        }
753    }
754    /// This mesh's identity (see [`Mesh`]).
755    pub(crate) fn id(&self) -> u64 {
756        self.id
757    }
758    /// Give this mesh `id` (a mesh rebuilt in place stays the mesh it was: [`crate::World::edit_mesh`]).
759    pub(crate) fn keep_id(&mut self, id: u64) {
760        self.id = id;
761    }
762    pub fn primitives(&self) -> &[Primitive] {
763        &self.primitives
764    }
765    /// The primitives, each editable field by field in place (see [`PrimitiveMut`]).
766    pub fn primitives_mut(&mut self) -> impl ExactSizeIterator<Item = PrimitiveMut<'_>> {
767        self.primitives.iter_mut().map(PrimitiveMut)
768    }
769    /// Primitive `i`, editable field by field in place; `None` past the last.
770    pub fn primitive_mut(&mut self, i: usize) -> Option<PrimitiveMut<'_>> {
771        self.primitives.get_mut(i).map(PrimitiveMut)
772    }
773    pub fn into_primitives(self) -> Vec<Primitive> {
774        self.primitives
775    }
776}
777impl Default for Mesh {
778    fn default() -> Self {
779        Mesh::new(Vec::new())
780    }
781}
782impl PartialEq for Mesh {
783    fn eq(&self, other: &Self) -> bool {
784        self.primitives == other.primitives
785    }
786}
787impl std::fmt::Debug for Mesh {
788    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
789        f.debug_struct("Mesh")
790            .field("primitives", &self.primitives)
791            .finish()
792    }
793}
794impl Component for Mesh {
795    const NAME: &'static str = "core.mesh";
796    /// Version 2 gives tangent streams a set index, serialized like TexCoord(n).
797    const VERSION: u32 = 2;
798    const GEOMETRY: crate::Geometry = crate::Geometry::None;
799    /// Every stream mapped by its meaning's rule ([`Semantic::moves`], [`crate::Linear`]); triangles reversed under a
800    /// mirror. A mesh with normals or tangents cannot follow a singular map ([`crate::Linear::singular`]): unsupported.
801    fn apply_linear(&self, l: &crate::Linear) -> crate::world::Linearity<Self> {
802        let mut m = self.clone();
803        for p in &mut m.primitives {
804            // a singular map turns no normal or tangent, and a moving stream of another type cannot follow
805            if move_streams(&mut p.streams, l).is_err() {
806                return crate::world::Linearity::Unsupported;
807            }
808            if l.mirrors() && p.topology == Topology::Triangles {
809                let n = p.vertex_count() as u32;
810                for t in p
811                    .indices
812                    .get_or_insert_with(|| (0..n).collect())
813                    .as_chunks_mut::<3>()
814                    .0
815                {
816                    t.swap(1, 2);
817                }
818            }
819        }
820        crate::world::Linearity::Applied(m)
821    }
822    fn references(&self, out: &mut Vec<Entity>) {
823        out.extend(self.primitives.iter().filter_map(|p| p.material));
824    }
825    /// Every stream's bytes and 4 bytes per index.
826    fn payload_bytes(&self) -> u64 {
827        self.primitives
828            .iter()
829            .map(|p| {
830                p.streams.values().map(Stream::byte_len).sum::<u64>()
831                    + p.indices.as_ref().map_or(0, |i| i.len() as u64 * 4)
832            })
833            .sum()
834    }
835    /// Each primitive whole: a position stream of points (and every other stream that moves as a point, three floats),
836    /// every stream as long as it (one value per vertex), indices
837    /// within it, whole triangles or lines; and float streams finite (glTF and every writer we have refuse NaN and
838    /// infinity). Joint and weight streams mean a skin: only on an entity with a `core.skin`, which checks them.
839    fn verify(&self, e: Entity, w: &crate::World) -> Result<(), String> {
840        if !w.has::<crate::Skin>(e)
841            && let Some(i) = self.primitives.iter().position(crate::skin::has_skin_streams)
842        {
843            return Err(format!(
844                "primitive {i}: joint or weight streams without a core.skin"
845            ));
846        }
847        for (i, p) in self.primitives.iter().enumerate() {
848            let n = match p.streams.get(&Semantic::Position) {
849                Some(Stream::F32x3(v)) => v.len(),
850                Some(_) => return Err(format!("primitive {i}: positions not 3 floats")),
851                None => return Err(format!("primitive {i}: no positions")),
852            };
853            if let Some((sem, _)) = p
854                .streams
855                .iter()
856                .find(|(sem, st)| sem.moves() == Moves::Point && !matches!(st, Stream::F32x3(_)))
857            {
858                return Err(format!("primitive {i}: {sem:?} points not 3 floats"));
859            }
860            if let Some((sem, st)) = p.streams.iter().find(|(_, st)| st.len() != n) {
861                return Err(format!(
862                    "primitive {i}: {sem:?} has {} values for {n} vertices",
863                    st.len()
864                ));
865            }
866            let corners = match &p.indices {
867                Some(idx) => {
868                    if let Some(bad) = idx.iter().find(|&&v| usize::try_from(v).map_or(true, |v| v >= n)) {
869                        return Err(format!("primitive {i}: index {bad} of {n} vertices"));
870                    }
871                    idx.len()
872                }
873                None => n,
874            };
875            let per = match p.topology {
876                Topology::Triangles => 3,
877                Topology::Lines => 2,
878                Topology::Points => 1,
879            };
880            if corners % per != 0 {
881                return Err(format!(
882                    "primitive {i}: {corners} corners are not whole {:?}",
883                    p.topology
884                ));
885            }
886            for (sem, st) in &p.streams {
887                let what = format!("primitive {i} {sem:?}");
888                match st {
889                    Stream::F32x2(v) => finite(v.iter().flatten().copied(), &what)?,
890                    Stream::F32x3(v) => finite(v.iter().flatten().copied(), &what)?,
891                    Stream::F32x4(v) => finite(v.iter().flatten().copied(), &what)?,
892                    Stream::U8x4(_) | Stream::U16x4(_) => {}
893                }
894            }
895        }
896        Ok(())
897    }
898    /// A summary, not the vertex data: per primitive its topology, vertex and index counts, streams and material.
899    fn to_json(&self) -> Result<Option<Value>, JsonError> {
900        let prims: Vec<Value> = self
901            .primitives
902            .iter()
903            .map(|p| {
904                json!({
905                    "topology": p.topology,
906                    "vertices": p.vertex_count(),
907                    "indices": p.indices.as_ref().map(Vec::len),
908                    "streams": p.streams.keys().collect::<Vec<_>>(),
909                    "material": p.material,
910                })
911            })
912            .collect();
913        Ok(Some(json!({ "primitives": prims })))
914    }
915}
916
917/// Colour space of a texture's colour channels.
918#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
919pub enum ColorSpace {
920    Srgb,
921    Linear,
922}
923
924/// What a texture is used as.
925#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
926pub enum TexRole {
927    Color,
928    Normal,
929    /// Packed scalar maps (metal/rough/AO/opacity, specular, ...): always linear.
930    Mask,
931    /// Numbers or indices a shader reads exactly (a palette indexed per vertex, a lookup table): any filtering,
932    /// resizing or lossy coding changes what it means. The reader sets it from how the material binds the texture
933    /// (a point sampler without mips, or the slot's documented use); passes and writers that change texels keep it
934    /// whole and lossless.
935    Data,
936    Other,
937}
938
939/// Normal-map green direction.
940#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
941pub enum NormalGreen {
942    /// OpenGL / glTF convention.
943    Up,
944    /// DirectX convention.
945    Down,
946}
947
948/// Where a normal map's Z comes from.
949#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
950pub enum NormalZ {
951    /// Blue is Z.
952    Stored,
953    /// Z = sqrt(1 - x^2 - y^2); blue is ignored (or holds something else).
954    Rebuild,
955}
956
957/// Which channel holds a normal map's X.
958#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize)]
959pub enum NormalX {
960    /// Red is X.
961    #[default]
962    Red,
963    /// Alpha is X, green Y (DXT5nm, "BC3n"); red and blue are unused.
964    Alpha,
965}
966
967/// Ordering of the two tangent-plane coordinates, relative to a supplied
968/// tangent T and its bitangent B = cross(N,T)*w. This describes the source
969/// frame, not how a consumer regenerates a frame from texture coordinates.
970#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize)]
971pub enum NormalBasis {
972    /// Red/X follows T; green follows B.
973    #[default]
974    TangentBitangent,
975    /// Red/X follows B; green follows T.
976    BitangentTangent,
977}
978
979/// How a normal map's channels encode its tangent-space normal.
980#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
981pub struct NormalConvention {
982    pub green: NormalGreen,
983    pub z: NormalZ,
984    pub x: NormalX,
985    pub basis: NormalBasis,
986}
987
988impl NormalConvention {
989    /// glTF's: X in red, green up, Z stored.
990    pub const GLTF: NormalConvention = NormalConvention {
991        green: NormalGreen::Up,
992        z: NormalZ::Stored,
993        x: NormalX::Red,
994        basis: NormalBasis::TangentBitangent,
995    };
996}
997
998/// Pixels or encoded blocks in memory.
999#[derive(Clone, Debug, PartialEq)]
1000pub struct Texture {
1001    pub width: u32,
1002    pub height: u32,
1003    pub format: TexFormat,
1004    /// Mip levels, largest first; each exactly `format.level_size(w >> i, h >> i)` bytes (axes floor at 1).
1005    pub levels: Vec<Vec<u8>>,
1006    pub color_space: ColorSpace,
1007    pub role: TexRole,
1008    /// For `TexRole::Normal`.
1009    pub normal: Option<NormalConvention>,
1010}
1011impl Component for Texture {
1012    const NAME: &'static str = "core.texture";
1013    const GEOMETRY: crate::Geometry = crate::Geometry::None;
1014    /// 3: the normal convention gained tangent/bitangent basis ordering.
1015    const VERSION: u32 = 3;
1016    /// Every level's bytes.
1017    fn payload_bytes(&self) -> u64 {
1018        self.levels.iter().map(|l| l.len() as u64).sum()
1019    }
1020    /// A size, at least one level and no more than the size has, each exactly its format's size for its dimensions.
1021    fn verify(&self, _e: Entity, _w: &crate::World) -> Result<(), String> {
1022        if self.width == 0 || self.height == 0 {
1023            return Err(format!("{} x {} pixels", self.width, self.height));
1024        }
1025        let most = self.width.max(self.height).ilog2() + 1;
1026        if self.levels.is_empty() || self.levels.len() > most as usize {
1027            return Err(format!(
1028                "{} levels for {} x {} (1 to {most})",
1029                self.levels.len(),
1030                self.width,
1031                self.height
1032            ));
1033        }
1034        for (i, level) in self.levels.iter().enumerate() {
1035            let shift = u32::try_from(i).unwrap_or(u32::MAX);
1036            let (w, h) = (
1037                self.width.checked_shr(shift).unwrap_or(0).max(1),
1038                self.height.checked_shr(shift).unwrap_or(0).max(1),
1039            );
1040            match self.format.checked_level_size(w, h) {
1041                Some(size) if size == level.len() => {}
1042                Some(size) => {
1043                    return Err(format!(
1044                        "level {i} ({w} x {h} {:?}): {} bytes, not {size}",
1045                        self.format,
1046                        level.len()
1047                    ));
1048                }
1049                None => return Err(format!("level {i}: {w} x {h} doesn't fit in memory")),
1050            }
1051        }
1052        Ok(())
1053    }
1054    /// A summary, not the pixels: size, format, level count, colour space, role, normal convention.
1055    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1056        Ok(Some(json!({
1057            "width": self.width,
1058            "height": self.height,
1059            "format": format!("{:?}", self.format),
1060            "levels": self.levels.len(),
1061            "color_space": self.color_space,
1062            "role": self.role,
1063            "normal": self.normal,
1064        })))
1065    }
1066}
1067
1068/// What a [`Light`] is: glTF's `KHR_lights_punctual` types. A spot's cone angles are from its axis, in radians, the
1069/// inner within the outer (full intensity inside the inner, falling to none at the outer).
1070#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
1071pub enum LightKind {
1072    Point,
1073    Spot { inner_cone: f32, outer_cone: f32 },
1074    Directional,
1075}
1076
1077/// A punctual light, exactly `KHR_lights_punctual`'s: placed by its entity's [`Transform`] and shining along that
1078/// transform's local -Z (a point light shines everywhere); `color` linear RGB; `intensity` in candela for point and spot
1079/// lights, in lux for a directional one; `range` the distance where it stops (`None`: unlimited, an inverse-square
1080/// falloff to infinity); `radius` the size of the emitter, the radius of the sphere (or disc, for a spot) the light
1081/// leaves from, in metres (`None`: a point, glTF's punctual light, which has no size); `specular` the colour and
1082/// intensity of the light's highlights where the source states its own (`None`: the light's own colour and intensity,
1083/// as glTF's punctual light, which has one). A game's own light model stays in its dialect; this is what a writer emits.
1084#[derive(Clone, Debug, PartialEq, Serialize)]
1085pub struct Light {
1086    pub kind: LightKind,
1087    pub color: [f32; 3],
1088    pub intensity: f32,
1089    pub range: Option<f32>,
1090    /// Casts shadows; `None`: unknown (the renderer's default).
1091    pub cast_shadows: Option<bool>,
1092    /// The emitter's radius, metres; `None`: a point.
1093    pub radius: Option<f32>,
1094    /// The highlights' own colour and intensity; `None`: the light's [`Light::color`] and [`Light::intensity`].
1095    #[serde(skip_serializing_if = "Option::is_none")]
1096    pub specular: Option<LightSpecular>,
1097}
1098
1099/// The colour and intensity a [`Light`]'s specular highlights take where the source states them apart from its diffuse
1100/// light: linear RGB, and an intensity in the light's own unit (candela for a point or spot light, lux for a directional
1101/// one). A light whose source states no diffuse light at all has intensity 0 and its highlights here.
1102#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
1103pub struct LightSpecular {
1104    pub color: [f32; 3],
1105    pub intensity: f32,
1106}
1107
1108impl Component for Light {
1109    const NAME: &'static str = "core.light";
1110    const GEOMETRY: crate::Geometry = crate::Geometry::None;
1111    /// 2: `cast_shadows`; 3: `radius`; 4: `specular`.
1112    const VERSION: u32 = 4;
1113    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1114        crate::json::to_value(self).map(Some)
1115    }
1116    /// Finite, non-negative colour and intensity (the specular's too), a positive range, a finite non-negative radius,
1117    /// and a spot's cones within 0 <= inner <= outer <= pi / 2.
1118    fn verify(&self, _e: Entity, _w: &crate::World) -> Result<(), String> {
1119        finite(self.color.iter().copied().chain([self.intensity]), "light")?;
1120        if self.color.iter().any(|&c| c < 0.0) || self.intensity < 0.0 {
1121            return Err("a negative colour or intensity".into());
1122        }
1123        if let Some(s) = &self.specular {
1124            finite(s.color.iter().copied().chain([s.intensity]), "light specular")?;
1125            if s.color.iter().any(|&c| c < 0.0) || s.intensity < 0.0 {
1126                return Err("a negative specular colour or intensity".into());
1127            }
1128        }
1129        if self.range.is_some_and(|r| !(r.is_finite() && r > 0.0)) {
1130            return Err("a range that is not positive".into());
1131        }
1132        if self.radius.is_some_and(|r| !(r.is_finite() && r >= 0.0)) {
1133            return Err("a radius that is not finite and non-negative".into());
1134        }
1135        if let LightKind::Spot {
1136            inner_cone,
1137            outer_cone,
1138        } = self.kind
1139            && !(0.0 <= inner_cone && inner_cone <= outer_cone && outer_cone <= std::f32::consts::FRAC_PI_2)
1140        {
1141            return Err(format!(
1142                "spot cones {inner_cone} and {outer_cone} not 0 <= inner <= outer <= pi / 2"
1143            ));
1144        }
1145        Ok(())
1146    }
1147}
1148
1149/// Collision geometry: a triangle soup in the entity's own space, each triangle made of a surface.
1150///
1151/// Placed like a [`Mesh`]: on a prototype, by its [`Instance`]s; on an entity with a [`Transform`] that is not a
1152/// prototype, by that transform. A prototype may collide without drawing (no mesh) and draw without colliding. A
1153/// triangle's front is counter-clockwise (the world's convention, as a mesh's); soups need not be closed or connected.
1154/// `surfaces` holds one index per triangle into the world's [`Surfaces`]. What a collider was (a box, a capsule, a
1155/// heightfield) stays in the reader's dialect; this is the shape a consumer collides with.
1156#[derive(Clone, Debug, PartialEq, Default)]
1157pub struct Collision {
1158    pub positions: Vec<[f32; 3]>,
1159    pub triangles: Vec<[u32; 3]>,
1160    pub surfaces: Vec<u16>,
1161}
1162
1163impl Collision {
1164    /// Append another soup, its positions mapped by `f`, every triangle made of `surface`.
1165    pub fn extend(
1166        &mut self,
1167        positions: &[[f32; 3]],
1168        triangles: &[[u32; 3]],
1169        surface: u16,
1170        f: impl Fn([f32; 3]) -> [f32; 3],
1171    ) {
1172        let base = self.positions.len() as u32;
1173        self.positions.extend(positions.iter().map(|&p| f(p)));
1174        self.triangles
1175            .extend(triangles.iter().map(|t| t.map(|i| i + base)));
1176        self.surfaces
1177            .extend(std::iter::repeat_n(surface, triangles.len()));
1178    }
1179}
1180
1181impl Component for Collision {
1182    const NAME: &'static str = "core.collision";
1183    const GEOMETRY: crate::Geometry = crate::Geometry::None;
1184    /// Positions mapped; triangles reversed under a mirror.
1185    fn apply_linear(&self, l: &crate::Linear) -> crate::world::Linearity<Self> {
1186        let mut c = self.clone();
1187        c.positions.iter_mut().for_each(|p| *p = l.point(*p));
1188        if l.mirrors() {
1189            c.triangles.iter_mut().for_each(|t| t.swap(1, 2));
1190        }
1191        crate::world::Linearity::Applied(c)
1192    }
1193    /// 12 bytes per position, 12 per triangle and 2 per surface index.
1194    fn payload_bytes(&self) -> u64 {
1195        (self.positions.len() as u64 + self.triangles.len() as u64) * 12 + self.surfaces.len() as u64 * 2
1196    }
1197    /// One surface per triangle, indices in range, positions finite, surfaces known to the world's [`Surfaces`].
1198    fn verify(&self, _e: Entity, w: &crate::World) -> Result<(), String> {
1199        if self.surfaces.len() != self.triangles.len() {
1200            return Err(format!(
1201                "{} surfaces for {} triangles",
1202                self.surfaces.len(),
1203                self.triangles.len()
1204            ));
1205        }
1206        let n = self.positions.len() as u32;
1207        if let Some(t) = self.triangles.iter().find(|t| t.iter().any(|&i| i >= n)) {
1208            return Err(format!("triangle {t:?} past the {n} positions"));
1209        }
1210        finite(self.positions.iter().flatten().copied(), "collision positions")?;
1211        let known = w.resource::<Surfaces>().map_or(0, |s| s.0.len());
1212        if let Some(&s) = self.surfaces.iter().find(|&&s| usize::from(s) >= known) {
1213            return Err(format!("surface {s} past the world's {known} surfaces"));
1214        }
1215        Ok(())
1216    }
1217    /// A summary: counts, and triangles per surface index.
1218    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1219        let mut per: BTreeMap<u16, u64> = BTreeMap::new();
1220        for &s in &self.surfaces {
1221            *per.entry(s).or_default() += 1;
1222        }
1223        Ok(Some(json!({
1224            "positions": self.positions.len(),
1225            "triangles": self.triangles.len(),
1226            "surfaces": per,
1227        })))
1228    }
1229}
1230
1231/// A collision surface: what a triangle is made of. `name` is the source's own (a game's physic material, "Snow",
1232/// "CONCRETE_POTHOLE"); `class` what it is generically, set by the source's lowering pass from a named table (a
1233/// reader leaves it `None`). Writers map classes onto their target's surfaces; a game's properties for it (friction,
1234/// sound) stay in its dialect.
1235#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
1236pub struct Surface {
1237    pub name: String,
1238    /// `None`: not classified (a writer that needs a class refuses it or counts it by a named policy).
1239    #[serde(skip_serializing_if = "Option::is_none")]
1240    pub class: Option<SurfaceClass>,
1241}
1242
1243impl Surface {
1244    /// A surface by its source name, not classified.
1245    pub fn new(name: impl Into<String>) -> Surface {
1246        Surface {
1247            name: name.into(),
1248            class: None,
1249        }
1250    }
1251}
1252
1253/// What a surface is made of, generically (its feel, sound and grip), for every source and target: each source's
1254/// lowering pass maps its own names here and each writer maps these to its target, never one game's names to
1255/// another's. `Other`: classified, and no class fits.
1256///
1257/// Closed on purpose: writers match it without a wildcard, so adding a class (with the evidence that a source has
1258/// such surfaces and a target treats them differently, its line in docs/SPEC.md and a `core.surfaces` VERSION bump)
1259/// fails to compile until every writer maps it.
1260#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1261#[serde(rename_all = "snake_case")]
1262pub enum SurfaceClass {
1263    Concrete,
1264    Asphalt,
1265    Stone,
1266    Brick,
1267    Tile,
1268    Metal,
1269    Wood,
1270    Glass,
1271    Plastic,
1272    Rubber,
1273    Fabric,
1274    Grass,
1275    Dirt,
1276    Gravel,
1277    Sand,
1278    Mud,
1279    Snow,
1280    Ice,
1281    /// Swimmable or wading surfaces.
1282    Water,
1283    Other,
1284}
1285
1286impl SurfaceClass {
1287    pub const ALL: [SurfaceClass; 20] = [
1288        SurfaceClass::Concrete,
1289        SurfaceClass::Asphalt,
1290        SurfaceClass::Stone,
1291        SurfaceClass::Brick,
1292        SurfaceClass::Tile,
1293        SurfaceClass::Metal,
1294        SurfaceClass::Wood,
1295        SurfaceClass::Glass,
1296        SurfaceClass::Plastic,
1297        SurfaceClass::Rubber,
1298        SurfaceClass::Fabric,
1299        SurfaceClass::Grass,
1300        SurfaceClass::Dirt,
1301        SurfaceClass::Gravel,
1302        SurfaceClass::Sand,
1303        SurfaceClass::Mud,
1304        SurfaceClass::Snow,
1305        SurfaceClass::Ice,
1306        SurfaceClass::Water,
1307        SurfaceClass::Other,
1308    ];
1309
1310    /// The lower-case word JSON shows.
1311    pub fn as_str(self) -> &'static str {
1312        match self {
1313            SurfaceClass::Concrete => "concrete",
1314            SurfaceClass::Asphalt => "asphalt",
1315            SurfaceClass::Stone => "stone",
1316            SurfaceClass::Brick => "brick",
1317            SurfaceClass::Tile => "tile",
1318            SurfaceClass::Metal => "metal",
1319            SurfaceClass::Wood => "wood",
1320            SurfaceClass::Glass => "glass",
1321            SurfaceClass::Plastic => "plastic",
1322            SurfaceClass::Rubber => "rubber",
1323            SurfaceClass::Fabric => "fabric",
1324            SurfaceClass::Grass => "grass",
1325            SurfaceClass::Dirt => "dirt",
1326            SurfaceClass::Gravel => "gravel",
1327            SurfaceClass::Sand => "sand",
1328            SurfaceClass::Mud => "mud",
1329            SurfaceClass::Snow => "snow",
1330            SurfaceClass::Ice => "ice",
1331            SurfaceClass::Water => "water",
1332            SurfaceClass::Other => "other",
1333        }
1334    }
1335
1336    pub fn parse(word: &str) -> Option<SurfaceClass> {
1337        SurfaceClass::ALL.into_iter().find(|c| c.as_str() == word)
1338    }
1339}
1340
1341/// The world's collision surfaces, indexed by [`Collision::surfaces`].
1342#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)]
1343pub struct Surfaces(pub Vec<Surface>);
1344
1345/// A surface index cannot be represented by collision's u16 surface IDs.
1346#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
1347#[error("surface index {index} exceeds u16")]
1348pub struct SurfaceIndexError {
1349    pub index: usize,
1350}
1351
1352impl Surfaces {
1353    /// The index of the surface named `name`, added when new.
1354    /// An unrepresentable index is an error and leaves the table unchanged.
1355    pub fn index(&mut self, name: &str) -> Result<u16, SurfaceIndexError> {
1356        let index = self.0.iter().position(|s| s.name == name).unwrap_or(self.0.len());
1357        let id = u16::try_from(index).map_err(|_| SurfaceIndexError { index })?;
1358        if index == self.0.len() {
1359            self.0.push(Surface::new(name));
1360        }
1361        Ok(id)
1362    }
1363}
1364
1365impl crate::world::Resource for Surfaces {
1366    const NAME: &'static str = "core.surfaces";
1367    /// 2: each surface's optional `class`.
1368    const VERSION: u32 = 2;
1369    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1370        crate::json::to_value(self).map(Some)
1371    }
1372}
1373
1374/// A texture whose data is still in a file (PNG or DDS); a pass loads it into a [`Texture`]. Lets readers describe
1375/// thousands of textures without holding their pixels.
1376#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
1377pub struct TextureFile {
1378    pub path: PathBuf,
1379}
1380impl Component for TextureFile {
1381    const NAME: &'static str = "core.texture_file";
1382    const GEOMETRY: crate::Geometry = crate::Geometry::None;
1383    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1384        crate::json::to_value(self).map(Some)
1385    }
1386}
1387
1388/// A texture the source names that the read could not give: no file it searched holds it, or its file does not decode.
1389/// The entity stands where the texture would, so what refers to it (a particle's map, a material's texture) still says
1390/// a texture belongs there, and a viewer shows the absence instead of drawing as if the source named none. It holds no
1391/// [`Texture`], [`crate::TextureLayers`] or [`TextureFile`], so no count of textures counts it.
1392#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
1393pub struct TextureAbsent {
1394    /// Why the read could not give it, in the read's words.
1395    pub reason: String,
1396}
1397impl Component for TextureAbsent {
1398    const NAME: &'static str = "core.texture_absent";
1399    const GEOMETRY: crate::Geometry = crate::Geometry::None;
1400    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1401        crate::json::to_value(self).map(Some)
1402    }
1403}
1404
1405/// A prototype whose geometry is still in a file (a GLB); a pass loads it into a [`Mesh`]. Present only while the
1406/// prototype's geometry is exactly that file's mesh: a pass that changes the geometry removes it, so a writer may
1407/// copy the file instead of encoding the mesh again.
1408#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
1409pub struct MeshFile {
1410    pub path: PathBuf,
1411    /// Which mesh of the file (glTF `meshes[mesh]`).
1412    pub mesh: u32,
1413    /// One entry per primitive of that mesh, in order: the material the reader already knows for it (a consumer's
1414    /// `parts[i].material`). The load pass checks the primitive count against it and fills [`Primitive::material`].
1415    pub materials: Vec<Option<Entity>>,
1416}
1417impl Component for MeshFile {
1418    const NAME: &'static str = "core.mesh_file";
1419    const GEOMETRY: crate::Geometry = crate::Geometry::None;
1420    /// The geometry is no longer the file's.
1421    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
1422        crate::world::Linearity::Drop
1423    }
1424    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1425        crate::json::to_value(self).map(Some)
1426    }
1427    fn references(&self, out: &mut Vec<Entity>) {
1428        out.extend(self.materials.iter().flatten());
1429    }
1430}
1431
1432/// Per-placement material choices of a prototype: one map per primitive, variant -> material, overriding the
1433/// primitive's material for placements whose [`Instance::variant`] matches (one source's tint rows).
1434#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)]
1435pub struct MaterialVariants(pub Vec<BTreeMap<u32, Entity>>);
1436impl Component for MaterialVariants {
1437    const NAME: &'static str = "core.material_variants";
1438    const GEOMETRY: crate::Geometry = crate::Geometry::PerPrimitive;
1439    /// No local-space geometry.
1440    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
1441        crate::world::Linearity::Invariant
1442    }
1443    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1444        crate::json::to_value(self).map(Some)
1445    }
1446    fn references(&self, out: &mut Vec<Entity>) {
1447        out.extend(self.0.iter().flat_map(|m| m.values().copied()));
1448    }
1449    fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
1450        Some(crate::world::remap_list(&self.0, from).map(MaterialVariants))
1451    }
1452}
1453
1454/// A streaming or processing tile, `(col, row)` in the ground frame of whoever tiled it.
1455#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1456pub struct Tile {
1457    pub col: i32,
1458    pub row: i32,
1459}
1460
1461/// A point or a tile size that has no tile.
1462#[derive(Clone, Copy, Debug, PartialEq, thiserror::Error)]
1463#[error(
1464    "no tile at ground point {ground:?} on a grid of {size} metres: the point and the size must be finite, the size \
1465     positive and the tile within i32"
1466)]
1467pub struct TileError {
1468    pub ground: [f32; 2],
1469    pub size: f32,
1470}
1471
1472impl Tile {
1473    /// The tile of a `size`-metre grid from the origin holding the ground point `g` (`[east, north]`):
1474    /// `(floor(east / size), floor(north / size))`, in f32. The one rule for every grid that has to agree on where
1475    /// content stands: a writer's tiles, a partition aligned to them. A point or size that is not finite, a size that
1476    /// is not positive, or a tile outside i32 is an error, never a saturated or zero tile.
1477    pub fn of(g: [f32; 2], size: f32) -> Result<Tile, TileError> {
1478        let bad = TileError { ground: g, size };
1479        if !(size.is_finite() && size > 0.0) {
1480            return Err(bad);
1481        }
1482        let axis = |v: f32| {
1483            let t = (v / size).floor();
1484            (t.is_finite() && t >= i32::MIN as f32 && t < i32::MAX as f32).then_some(t as i32)
1485        };
1486        match (axis(g[0]), axis(g[1])) {
1487            (Some(col), Some(row)) => Ok(Tile { col, row }),
1488            _ => Err(bad),
1489        }
1490    }
1491}
1492
1493impl Component for Tile {
1494    const NAME: &'static str = "core.tile";
1495    const GEOMETRY: crate::Geometry = crate::Geometry::None;
1496    /// No local-space geometry.
1497    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
1498        crate::world::Linearity::Invariant
1499    }
1500    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1501        crate::json::to_value(self).map(Some)
1502    }
1503}
1504
1505#[cfg(test)]
1506mod tests {
1507    use super::*;
1508
1509    /// A tile is the floor of the point over the size; a point or size that has none is an error, never a saturated tile.
1510    #[test]
1511    fn a_tile_of_a_point_that_has_none_is_an_error() {
1512        assert_eq!(Tile::of([-0.5, 399.9], 200.0), Ok(Tile { col: -1, row: 1 }));
1513        for (g, size) in [
1514            ([f32::NAN, 0.0], 200.0),
1515            ([0.0, f32::INFINITY], 200.0),
1516            ([1.0, 1.0], 0.0),
1517            ([1.0, 1.0], -5.0),
1518            ([1.0, 1.0], f32::NAN),
1519            ([1e30, 0.0], 200.0),
1520        ] {
1521            let e = Tile::of(g, size).unwrap_err();
1522            assert_eq!(
1523                (e.ground.map(f32::is_nan), e.size.is_nan()),
1524                (g.map(f32::is_nan), size.is_nan())
1525            );
1526        }
1527    }
1528
1529    #[test]
1530    fn payload_bytes_count_streams_indices_levels_and_soups() {
1531        // 3 positions (12 bytes each), 3 normals (12), 3 uvs (8), 3 indices (4): 36 + 36 + 24 + 12 = 108
1532        let streams = BTreeMap::from([
1533            (Semantic::Position, Stream::F32x3(vec![[0.0; 3]; 3])),
1534            (Semantic::Normal, Stream::F32x3(vec![[0.0, 0.0, 1.0]; 3])),
1535            (Semantic::TexCoord(0), Stream::F32x2(vec![[0.0; 2]; 3])),
1536        ]);
1537        let mesh = Mesh::new(vec![Primitive {
1538            topology: Topology::Triangles,
1539            streams,
1540            indices: Some(vec![0, 1, 2]),
1541            material: None,
1542        }]);
1543        assert_eq!(mesh.payload_bytes(), 108);
1544        // a 4 x 4 RGBA8 texture with its 2 x 2 and 1 x 1 levels: 64 + 16 + 4
1545        let texture = Texture {
1546            width: 4,
1547            height: 4,
1548            format: TexFormat::Rgba8Unorm,
1549            levels: vec![vec![0; 64], vec![0; 16], vec![0; 4]],
1550            color_space: ColorSpace::Srgb,
1551            role: TexRole::Color,
1552            normal: None,
1553        };
1554        assert_eq!(texture.payload_bytes(), 84);
1555        // 3 positions and 1 triangle (12 bytes each), 1 surface index (2)
1556        let soup = Collision {
1557            positions: vec![[0.0; 3]; 3],
1558            triangles: vec![[0, 1, 2]],
1559            surfaces: vec![0],
1560        };
1561        assert_eq!(soup.payload_bytes(), 50);
1562        let mut w = crate::World::new();
1563        let (a, b) = (w.spawn(), w.spawn());
1564        w.insert(a, mesh.clone());
1565        w.insert(b, mesh);
1566        w.insert(a, texture);
1567        w.insert(a, Name("small".into()));
1568        w.insert_opaque(
1569            b,
1570            "game.blob",
1571            crate::Opaque {
1572                version: 1,
1573                data: crate::OpaqueData::Bytes(vec![0; 7]),
1574            },
1575        )
1576        .unwrap();
1577        assert_eq!(
1578            w.payload_bytes(),
1579            BTreeMap::from([("core.mesh", 216), ("core.texture", 84), ("game.blob", 7)])
1580        );
1581    }
1582
1583    fn t(cols: [[f32; 3]; 4]) -> Transform {
1584        Transform { cols }
1585    }
1586
1587    #[test]
1588    fn surface_indices_check_the_u16_boundary_without_mutating_on_error() {
1589        let mut surfaces = Surfaces((0..u16::MAX).map(|i| Surface::new(i.to_string())).collect());
1590        assert_eq!(surfaces.index("last"), Ok(u16::MAX));
1591        assert_eq!(surfaces.index("0"), Ok(0));
1592        assert_eq!(surfaces.index("last"), Ok(u16::MAX));
1593        let before = surfaces.clone();
1594        assert_eq!(
1595            surfaces.index("overflow"),
1596            Err(SurfaceIndexError { index: 65536 })
1597        );
1598        assert_eq!(surfaces, before);
1599        // The public vector can also contain an existing out-of-range entry.
1600        surfaces.0.push(Surface::new("outside"));
1601        assert_eq!(surfaces.index("outside"), Err(SurfaceIndexError { index: 65536 }));
1602        assert_eq!(surfaces.index("0"), Ok(0));
1603    }
1604
1605    #[test]
1606    fn surface_classes_are_lower_case_words_shown_only_when_set() {
1607        for c in SurfaceClass::ALL {
1608            assert_eq!(SurfaceClass::parse(c.as_str()), Some(c));
1609            assert_eq!(serde_json::to_value(c).unwrap(), json!(c.as_str()));
1610        }
1611        assert_eq!(SurfaceClass::parse("Concrete"), None);
1612        let mut surfaces = Surfaces::default();
1613        surfaces.index("Snow").unwrap();
1614        surfaces.0.push(Surface {
1615            name: "CONCRETE_POTHOLE".into(),
1616            class: Some(SurfaceClass::Concrete),
1617        });
1618        assert_eq!(
1619            crate::world::Resource::to_json(&surfaces).unwrap(),
1620            Some(json!([{"name": "Snow"}, {"name": "CONCRETE_POTHOLE", "class": "concrete"}]))
1621        );
1622    }
1623
1624    #[test]
1625    fn json_views() {
1626        let mut w = crate::World::new();
1627        let mat = w.spawn();
1628        let p = w.spawn();
1629        w.insert(
1630            p,
1631            MeshFile {
1632                path: "prototypes/a.glb".into(),
1633                mesh: 1,
1634                materials: vec![Some(mat), None],
1635            },
1636        );
1637        w.insert(
1638            p,
1639            MaterialVariants(vec![BTreeMap::new(), BTreeMap::from([(3, mat)])]),
1640        );
1641        w.insert(p, Name("a".into()));
1642        w.insert(
1643            p,
1644            Mesh::new(vec![Primitive {
1645                topology: Topology::Triangles,
1646                streams: BTreeMap::from([(Semantic::Position, Stream::F32x3(vec![[0.; 3]; 3]))]),
1647                indices: None,
1648                material: Some(mat),
1649            }]),
1650        );
1651        let got: Vec<(&str, Option<Value>)> = w
1652            .components_json(p)
1653            .unwrap()
1654            .into_iter()
1655            .map(|c| (c.name, c.value))
1656            .collect();
1657        let names: Vec<&str> = got.iter().map(|(n, _)| *n).collect();
1658        assert_eq!(
1659            names,
1660            [
1661                "core.material_variants",
1662                "core.mesh",
1663                "core.mesh_file",
1664                "core.name"
1665            ]
1666        );
1667        assert_eq!(got[0].1, Some(json!([{}, {"3": 0}])));
1668        assert_eq!(
1669            got[1].1,
1670            Some(
1671                json!({"primitives": [{"topology": "Triangles", "vertices": 3, "indices": null,
1672                "streams": ["Position"], "material": 0}]})
1673            )
1674        );
1675        assert_eq!(
1676            got[2].1,
1677            Some(json!({"path": "prototypes/a.glb", "mesh": 1, "materials": [0, null]}))
1678        );
1679        assert_eq!(got[3].1, Some(json!("a")));
1680    }
1681
1682    /// A mesh, texture or placement that is whole is accepted; one whose parts don't fit together is refused,
1683    /// naming what; an unloaded (file-backed) or collision-only prototype is fine.
1684    #[test]
1685    fn the_verifier_refuses_structurally_broken_data() {
1686        let tri = |streams: BTreeMap<Semantic, Stream>, indices: Option<Vec<u32>>, topology| Primitive {
1687            topology,
1688            streams,
1689            indices,
1690            material: None,
1691        };
1692        let pos = |n: usize| (Semantic::Position, Stream::F32x3(vec![[0.0; 3]; n]));
1693        let mesh_err = |p: Primitive| {
1694            let mut w = crate::World::new();
1695            let e = w.spawn();
1696            w.insert(e, Mesh::new(vec![p]));
1697            w.verify_components()
1698                .into_iter()
1699                .map(|(_, _, m)| m)
1700                .collect::<Vec<_>>()
1701        };
1702        assert!(
1703            mesh_err(tri(
1704                BTreeMap::from([pos(3)]),
1705                Some(vec![0, 1, 2]),
1706                Topology::Triangles
1707            ))
1708            .is_empty()
1709        );
1710        assert!(mesh_err(tri(BTreeMap::from([pos(4)]), Some(vec![0, 1]), Topology::Lines)).is_empty());
1711        for (p, words) in [
1712            (tri(BTreeMap::new(), None, Topology::Triangles), "no positions"),
1713            (
1714                tri(
1715                    BTreeMap::from([(Semantic::Position, Stream::F32x2(vec![[0.0; 2]; 3]))]),
1716                    None,
1717                    Topology::Triangles,
1718                ),
1719                "not 3 floats",
1720            ),
1721            (
1722                tri(
1723                    BTreeMap::from([pos(3), (Semantic::Normal, Stream::F32x3(vec![[0.0; 3]; 2]))]),
1724                    None,
1725                    Topology::Triangles,
1726                ),
1727                "2 values for 3 vertices",
1728            ),
1729            (
1730                tri(BTreeMap::from([pos(3)]), Some(vec![0, 1, 3]), Topology::Triangles),
1731                "index 3 of 3",
1732            ),
1733            (
1734                tri(
1735                    BTreeMap::from([pos(3)]),
1736                    Some(vec![0, 1, 2, 0]),
1737                    Topology::Triangles,
1738                ),
1739                "4 corners",
1740            ),
1741            (tri(BTreeMap::from([pos(3)]), None, Topology::Lines), "3 corners"),
1742        ] {
1743            let got = mesh_err(p);
1744            assert!(got.len() == 1 && got[0].contains(words), "{words}: {got:?}");
1745        }
1746        let tex = |width, height, levels: Vec<usize>| {
1747            let mut w = crate::World::new();
1748            let e = w.spawn();
1749            w.insert(
1750                e,
1751                Texture {
1752                    width,
1753                    height,
1754                    format: TexFormat::Rgba8Unorm,
1755                    levels: levels.into_iter().map(|n| vec![0; n]).collect(),
1756                    color_space: ColorSpace::Srgb,
1757                    role: TexRole::Color,
1758                    normal: None,
1759                },
1760            );
1761            w.verify_components()
1762                .into_iter()
1763                .map(|(_, _, m)| m)
1764                .collect::<Vec<_>>()
1765        };
1766        assert!(tex(4, 2, vec![32, 8, 4]).is_empty());
1767        assert!(tex(0, 2, vec![0])[0].contains("0 x 2"));
1768        assert!(tex(4, 4, vec![])[0].contains("0 levels"));
1769        assert!(tex(4, 4, vec![64, 16, 4, 4])[0].contains("4 levels"));
1770        assert!(tex(4, 4, vec![64, 15])[0].contains("level 1"));
1771        assert!(tex(u32::MAX, u32::MAX, vec![0])[0].contains("doesn't fit"));
1772        // a placement needs its transform; an unloaded and a collision-only prototype are whole as they are
1773        let mut w = crate::World::new();
1774        let (file, collision) = (w.spawn(), w.spawn());
1775        w.insert(
1776            file,
1777            MeshFile {
1778                path: "m.glb".into(),
1779                mesh: 0,
1780                materials: vec![None],
1781            },
1782        );
1783        w.insert(collision, Name("collision only".into()));
1784        let placed = w.spawn();
1785        w.insert(
1786            placed,
1787            Instance {
1788                prototype: file,
1789                variant: 0,
1790            },
1791        );
1792        let bare = w.spawn();
1793        w.insert(
1794            bare,
1795            Instance {
1796                prototype: collision,
1797                variant: 0,
1798            },
1799        );
1800        w.insert(placed, Transform::IDENTITY);
1801        let tf = w.spawn();
1802        w.insert(tf, TextureFile { path: "t.png".into() });
1803        assert_eq!(
1804            w.verify_components(),
1805            [(
1806                "core.instance",
1807                bare,
1808                "a placement without a core.transform".to_owned()
1809            )]
1810        );
1811    }
1812
1813    #[test]
1814    fn the_verifier_refuses_non_finite_geometry() {
1815        let mut w = crate::World::new();
1816        let e = w.spawn();
1817        w.insert(e, Transform::IDENTITY);
1818        let mut m = Transform::IDENTITY;
1819        m.cols[3][2] = f32::NAN;
1820        let e2 = w.spawn();
1821        w.insert(e2, m);
1822        w.insert(
1823            e2,
1824            Mesh::new(vec![Primitive {
1825                topology: Topology::Triangles,
1826                streams: BTreeMap::from([
1827                    (Semantic::Position, Stream::F32x3(vec![[0.0; 3]; 3])),
1828                    (
1829                        Semantic::TexCoord(0),
1830                        Stream::F32x2(vec![[0.0, f32::NEG_INFINITY], [0.0; 2], [0.0; 2]]),
1831                    ),
1832                ]),
1833                indices: None,
1834                material: None,
1835            }]),
1836        );
1837        let got: Vec<(&str, Entity, String)> = w.verify_components();
1838        assert_eq!(
1839            got,
1840            [
1841                (
1842                    "core.mesh",
1843                    e2,
1844                    "primitive 0 TexCoord(0): value 1 is -inf, not finite".to_owned()
1845                ),
1846                (
1847                    "core.transform",
1848                    e2,
1849                    "transform: value 11 is NaN, not finite".to_owned()
1850                ),
1851            ]
1852        );
1853        // and the JSON view refuses to show the NaN rather than writing null
1854        assert!(
1855            w.components_json(e2)
1856                .unwrap_err()
1857                .to_string()
1858                .contains("core.transform on e1")
1859        );
1860    }
1861
1862    #[test]
1863    fn classify_follows_the_scene_interchange_rule() {
1864        assert_eq!(Transform::IDENTITY.classify(), TransformKind::Regular);
1865        assert_eq!(
1866            t([[2., 0., 0.], [0., 0., 3.], [0., -1., 0.], [5., 5., 5.]]).classify(),
1867            TransformKind::Regular
1868        );
1869        assert_eq!(
1870            t([[-1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0.; 3]]).classify(),
1871            TransformKind::Mirrored
1872        );
1873        assert_eq!(
1874            t([[1., 0., 0.], [0.1, 1., 0.], [0., 0., 1.], [0.; 3]]).classify(),
1875            TransformKind::Sheared
1876        );
1877        assert_eq!(
1878            t([[1., 0., 0.], [0., 1., 0.], [0., 0., 0.], [0.; 3]]).classify(),
1879            TransformKind::Degenerate
1880        );
1881    }
1882    /// A 4x4 matrix is affine when its last row is 0 0 0 1 within the format's tolerance (0: exactly); a NaN there
1883    /// is refused whatever the tolerance.
1884    #[test]
1885    fn a_four_by_four_is_affine_by_its_last_row() {
1886        let m = [
1887            [1., 0., 0., 0.],
1888            [0., 2., 0., 0.],
1889            [0., 0., 3., 0.],
1890            [4., 5., 6., 1.],
1891        ];
1892        let t = Transform::from_columns4(m, 0.0).unwrap();
1893        assert_eq!(t.cols, [[1., 0., 0.], [0., 2., 0.], [0., 0., 3.], [4., 5., 6.]]);
1894        let mut noisy = m;
1895        noisy[0][3] = 5e-5;
1896        assert!(Transform::from_columns4(noisy, 0.0).is_err());
1897        assert!(Transform::from_columns4(noisy, 1e-4).is_ok());
1898        let mut projective = m;
1899        projective[3][3] = 2.0;
1900        let e = Transform::from_columns4(projective, 1e-4).unwrap_err();
1901        assert!(e.contains("not affine"), "{e}");
1902        let mut nan = m;
1903        nan[2][3] = f32::NAN;
1904        assert!(Transform::from_columns4(nan, f32::INFINITY).is_err());
1905    }
1906
1907    /// Composition is `outer (inner v)`, each entry rounded once: 1 + 2^-24 + 2^-24 is 1 + 2^-23 (an f32), where
1908    /// summing in f32 would round to 1 at each step.
1909    #[test]
1910    fn composition_rounds_each_entry_once() {
1911        let outer = Transform {
1912            cols: [[1., 0., 0.], [1., 0., -1.], [1., 1., 0.], [10., 20., 30.]],
1913        };
1914        let tiny = 2f32.powi(-24);
1915        let inner = Transform {
1916            cols: [[1., tiny, tiny], [0., 1., 0.], [0., 0., 2.], [1., 2., 3.]],
1917        };
1918        let c = outer.compose(&inner);
1919        assert_eq!(c.cols[0], [1. + 2. * tiny, tiny, -tiny]);
1920        assert_eq!(c.cols[1], [1., 0., -1.]);
1921        assert_eq!(c.cols[2], [2., 2., 0.]);
1922        assert_eq!(c.cols[3], [16., 23., 28.]);
1923        // a point through the composition is the point through inner, then outer
1924        let p = [0., 1., 1.];
1925        assert_eq!(c.apply(p), [19., 25., 27.]);
1926        assert_eq!(c.apply(p), outer.apply(inner.apply(p)));
1927        assert_eq!(Transform::IDENTITY.compose(&inner), inner);
1928        assert_eq!(inner.compose(&Transform::IDENTITY), inner);
1929    }
1930
1931    /// One singular rule: a map is singular when a transform with its part classifies as degenerate (relative to its
1932    /// axes' lengths, so a small uniform scale is not). It then turns no normal or tangent and has no inverse, and a
1933    /// mesh with normals cannot follow it; an invertible map's normals and tangents come out of unit length, the
1934    /// tangent's w flipped under a mirror.
1935    #[test]
1936    fn a_singular_map_turns_no_normal_or_tangent() {
1937        let near_flat = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 1e-7]];
1938        for cols in [
1939            [[0.0; 3]; 3],
1940            [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
1941            near_flat,
1942        ] {
1943            let l = Linear::new(cols);
1944            let t = Transform {
1945                cols: [cols[0], cols[1], cols[2], [0.0; 3]],
1946            };
1947            assert!(l.singular(), "{cols:?}");
1948            assert_eq!(t.classify(), TransformKind::Degenerate, "{cols:?}");
1949            assert_eq!(l.normal([0.0, 0.0, 1.0]), None);
1950            assert_eq!(l.tangent([1.0, 0.0, 0.0, 1.0]), None);
1951            assert_eq!(l.inverse_columns(), None);
1952        }
1953        let flat = Linear::new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]]);
1954        let mut streams = BTreeMap::new();
1955        streams.insert(Semantic::Position, Stream::F32x3(vec![[0.0; 3]; 3]));
1956        streams.insert(Semantic::Normal, Stream::F32x3(vec![[0.0, 0.0, 1.0]; 3]));
1957        let mesh = Mesh::new(vec![Primitive {
1958            topology: Topology::Triangles,
1959            streams,
1960            indices: None,
1961            material: None,
1962        }]);
1963        assert!(matches!(
1964            mesh.apply_linear(&flat),
1965            crate::world::Linearity::Unsupported
1966        ));
1967        // the rule is relative: a small uniform scale (one source's bone of scale 0.00937, det about 8.2e-7) is not
1968        // singular, and turns normals and tangents to unit length; one axis squashed to 1e-7 of the others is
1969        let s = 0.00937f32;
1970        let small = Linear::new([[s, 0.0, 0.0], [0.0, s, 0.0], [0.0, 0.0, s]]);
1971        assert!(!small.singular());
1972        assert_eq!(small.normal([0.0, 1.0, 0.0]), Some([0.0, 1.0, 0.0]));
1973        assert_eq!(small.tangent([1.0, 0.0, 0.0, 1.0]), Some([1.0, 0.0, 0.0, 1.0]));
1974        assert!(small.inverse_columns().is_some());
1975        let squashed = Linear::new([[s, 0.0, 0.0], [0.0, s, 0.0], [0.0, 0.0, s * 1e-7]]);
1976        assert!(squashed.singular());
1977        // scaled by 2 and mirrored in x: unit normals and tangents, w flipped
1978        let l = Linear::of_f64([[-2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]]);
1979        assert!(!l.singular() && l.mirrors());
1980        assert_eq!(l.normal([1.0, 0.0, 0.0]), Some([-1.0, 0.0, 0.0]));
1981        assert_eq!(l.tangent([0.0, 3.0, 0.0, 1.0]), Some([0.0, 1.0, 0.0, -1.0]));
1982    }
1983
1984    /// Every meaning moves by its rule: a point stream (a billboard's pivot) moves as the positions do, also under a
1985    /// mirror; a point stream that is not three floats is refused, by the map and by verify.
1986    #[test]
1987    fn a_point_stream_moves_as_the_positions_do() {
1988        let corners = vec![[1.0, 2.0, 3.0], [-4.0, 5.0, 0.5], [0.0, 0.0, -2.0]];
1989        let pivots = vec![[0.5, 1.0, 1.0]; 3];
1990        let primitive = |pivot: Stream| Primitive {
1991            topology: Topology::Triangles,
1992            streams: BTreeMap::from([
1993                (Semantic::Position, Stream::F32x3(corners.clone())),
1994                (Semantic::Point(3), pivot),
1995                (Semantic::TexCoord(0), Stream::F32x2(vec![[0.25, 0.75]; 3])),
1996            ]),
1997            indices: None,
1998            material: None,
1999        };
2000        let mesh = Mesh::new(vec![primitive(Stream::F32x3(pivots.clone()))]);
2001        let l = Linear::of_f64([[-2.0, 0.0, 0.0], [0.0, 0.0, 3.0], [0.0, -1.0, 0.0]]);
2002        assert!(l.mirrors());
2003        let crate::world::Linearity::Applied(moved) = mesh.apply_linear(&l) else {
2004            panic!("a mesh with a point stream follows an invertible map");
2005        };
2006        let p = &moved.primitives()[0];
2007        let points = |s: Semantic| match &p.streams[&s] {
2008            Stream::F32x3(v) => v.clone(),
2009            other => panic!("{other:?}"),
2010        };
2011        assert_eq!(
2012            points(Semantic::Position),
2013            corners.iter().map(|c| l.point(*c)).collect::<Vec<_>>()
2014        );
2015        assert_eq!(
2016            points(Semantic::Point(3)),
2017            pivots.iter().map(|c| l.point(*c)).collect::<Vec<_>>()
2018        );
2019        assert_eq!(
2020            p.streams[&Semantic::TexCoord(0)],
2021            Stream::F32x2(vec![[0.25, 0.75]; 3])
2022        );
2023        assert_eq!(p.indices, Some(vec![0, 2, 1]), "a mirror reverses the triangles");
2024        let wrong = Mesh::new(vec![primitive(Stream::F32x4(vec![[0.0; 4]; 3]))]);
2025        assert!(matches!(
2026            wrong.apply_linear(&l),
2027            crate::world::Linearity::Unsupported
2028        ));
2029        let mut w = crate::World::new();
2030        let e = w.spawn();
2031        let err = wrong.verify(e, &w).unwrap_err();
2032        assert!(err.contains("Point(3) points not 3 floats"), "{err}");
2033        assert_eq!(Semantic::Point(3).moves(), Moves::Point);
2034        assert_eq!(
2035            crate::shader::Attribute::from(Semantic::Point(3)),
2036            crate::shader::Attribute::Point(3)
2037        );
2038    }
2039
2040    #[test]
2041    fn normal_basis_has_explicit_json_and_default() {
2042        assert_eq!(NormalBasis::default(), NormalConvention::GLTF.basis);
2043        let convention = NormalConvention {
2044            basis: NormalBasis::BitangentTangent,
2045            ..NormalConvention::GLTF
2046        };
2047        let value = crate::json::to_value(&convention).expect("normal convention");
2048        assert_eq!(value["basis"], "BitangentTangent");
2049    }
2050
2051    #[test]
2052    fn collision_soups_and_their_surfaces() {
2053        let mut w = crate::World::new();
2054        let mut surfaces = Surfaces::default();
2055        let (snow, ice) = (surfaces.index("Snow").unwrap(), surfaces.index("Ice").unwrap());
2056        assert_eq!((snow, ice, surfaces.index("Snow").unwrap()), (0, 1, 0));
2057        w.insert_resource(surfaces);
2058        let mut c = Collision::default();
2059        let tri = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]];
2060        c.extend(&tri, &[[0, 1, 2]], snow, |p| p);
2061        c.extend(&tri, &[[0, 1, 2]], ice, |p| [p[0] + 2.0, p[1], p[2]]);
2062        assert_eq!(c.triangles, [[0, 1, 2], [3, 4, 5]]);
2063        assert_eq!(c.surfaces, [0, 1]);
2064        assert_eq!(c.positions[3], [2.0, 0.0, 0.0]);
2065        let e = w.spawn();
2066        assert_eq!(c.verify(e, &w), Ok(()));
2067        let mut bad = c.clone();
2068        bad.surfaces.pop();
2069        assert!(bad.verify(e, &w).is_err(), "a surface per triangle");
2070        let mut bad = c.clone();
2071        bad.triangles[1][2] = 6;
2072        assert!(bad.verify(e, &w).is_err(), "indices in range");
2073        let mut bad = c.clone();
2074        bad.surfaces[0] = 2;
2075        assert!(bad.verify(e, &w).is_err(), "surfaces the world knows");
2076        let mut bad = c.clone();
2077        bad.positions[0][1] = f32::NAN;
2078        assert!(bad.verify(e, &w).is_err(), "finite positions");
2079        assert_eq!(
2080            c.to_json().unwrap().unwrap(),
2081            json!({"positions": 6, "triangles": 2, "surfaces": {"0": 1, "1": 1}})
2082        );
2083    }
2084
2085    #[test]
2086    fn shadows_belong_to_drawn_entities_and_keep_stated_values() {
2087        let mut w = crate::World::new();
2088        let bare = w.spawn();
2089        let proto = w.spawn();
2090        w.insert(proto, Mesh::default());
2091        let place = w.spawn();
2092        w.insert(
2093            place,
2094            Instance {
2095                prototype: proto,
2096                variant: 0,
2097            },
2098        );
2099        let s = Shadows {
2100            cast: Some(ShadowCast::Off),
2101            receive: Some(false),
2102        };
2103        assert_eq!(s.verify(place, &w), Ok(()));
2104        assert_eq!(s.verify(proto, &w), Ok(()), "an entity drawing its own mesh");
2105        assert!(s.verify(bare, &w).is_err());
2106        assert_eq!(
2107            s.to_json().unwrap(),
2108            Some(serde_json::json!({"cast": "Off", "receive": false})),
2109            "stated off and false are kept"
2110        );
2111        assert_eq!(
2112            Shadows::default().to_json().unwrap(),
2113            Some(serde_json::json!({"cast": null, "receive": null}))
2114        );
2115    }
2116    #[test]
2117    fn lights_keep_to_khr_lights_punctual() {
2118        let mut w = crate::World::new();
2119        let e = w.spawn();
2120        let spot = |inner: f32, outer: f32| Light {
2121            kind: LightKind::Spot {
2122                inner_cone: inner,
2123                outer_cone: outer,
2124            },
2125            color: [1.0, 0.5, 0.25],
2126            intensity: 10.0,
2127            range: Some(5.0),
2128            cast_shadows: None,
2129            radius: None,
2130            specular: None,
2131        };
2132        assert_eq!(spot(0.2, 0.6).verify(e, &w), Ok(()));
2133        let sized = |r: f32| Light {
2134            radius: Some(r),
2135            ..spot(0.2, 0.6)
2136        };
2137        assert_eq!(sized(0.0).verify(e, &w), Ok(()), "a radius of 0 is a point");
2138        assert_eq!(sized(0.25).verify(e, &w), Ok(()));
2139        assert!(sized(-0.1).verify(e, &w).is_err(), "a negative radius");
2140        assert!(
2141            sized(f32::NAN).verify(e, &w).is_err(),
2142            "a radius that is not finite"
2143        );
2144        assert!(spot(0.7, 0.6).verify(e, &w).is_err(), "inner past outer");
2145        assert!(spot(0.2, 2.0).verify(e, &w).is_err(), "outer past pi / 2");
2146        let mut l = spot(0.2, 0.6);
2147        l.range = Some(0.0);
2148        assert!(l.verify(e, &w).is_err(), "a range that is not positive");
2149        l.range = None;
2150        l.intensity = -1.0;
2151        assert!(l.verify(e, &w).is_err(), "a negative intensity");
2152        l.intensity = f32::NAN;
2153        assert!(l.verify(e, &w).is_err(), "non-finite");
2154        assert_eq!(
2155            spot(0.25, 0.5).to_json().unwrap().unwrap()["kind"],
2156            serde_json::json!({"Spot": {"inner_cone": 0.25, "outer_cone": 0.5}})
2157        );
2158    }
2159
2160    #[test]
2161    fn closures_follow_declared_references_and_subsets_keep_ids() {
2162        use crate::pbr::{PbrMaterial, TextureRef};
2163        use crate::{Opaque, OpaqueData};
2164        let mut w = crate::World::new();
2165        let texture = w.spawn();
2166        w.insert(texture, Name("albedo".into()));
2167        let other_texture = w.spawn();
2168        let material = w.spawn();
2169        w.insert(
2170            material,
2171            PbrMaterial {
2172                base_color_texture: Some(TextureRef::new(texture)),
2173                ..PbrMaterial::default()
2174            },
2175        );
2176        let variant = w.spawn();
2177        w.insert(
2178            variant,
2179            PbrMaterial {
2180                emissive_texture: Some(TextureRef::new(other_texture)),
2181                ..PbrMaterial::default()
2182            },
2183        );
2184        let prototype = w.spawn();
2185        w.insert(
2186            prototype,
2187            MeshFile {
2188                path: "a.glb".into(),
2189                mesh: 0,
2190                materials: vec![Some(material)],
2191            },
2192        );
2193        w.insert(prototype, MaterialVariants(vec![BTreeMap::from([(1, variant)])]));
2194        let placement = w.spawn();
2195        w.insert(
2196            placement,
2197            Instance {
2198                prototype,
2199                variant: 1,
2200            },
2201        );
2202        w.insert(placement, Transform::IDENTITY);
2203        // an opaque component naming an entity is carried, not followed
2204        let unrelated = w.spawn();
2205        w.insert(unrelated, Name("elsewhere".into()));
2206        w.insert_opaque(
2207            placement,
2208            "game.link",
2209            Opaque {
2210                version: 1,
2211                data: OpaqueData::Json(json!(unrelated.index())),
2212            },
2213        )
2214        .unwrap();
2215        w.insert_resource(crate::Surfaces::default());
2216        let closure = w.closure([placement]);
2217        assert_eq!(
2218            closure,
2219            [texture, other_texture, material, variant, prototype, placement].into()
2220        );
2221        let mut sub = w.subset(&closure);
2222        assert!(!sub.is_alive(unrelated) && sub.get::<Name>(unrelated).is_none());
2223        assert_eq!(sub.get::<Name>(texture), w.get::<Name>(texture));
2224        assert_eq!(sub.get::<Instance>(placement), w.get::<Instance>(placement));
2225        assert!(
2226            sub.opaque_components()
2227                .any(|(n, c)| n == "game.link" && c.contains_key(&placement))
2228        );
2229        assert!(sub.resource::<crate::Surfaces>().is_some());
2230        assert_eq!(sub.verify_components(), Vec::new());
2231        // ids stay unique: a new entity lands above every id of the source
2232        assert!(sub.spawn().index() > unrelated.index());
2233        // retain is the same subset in place
2234        let mut kept = w.clone();
2235        kept.retain(&closure);
2236        assert_eq!(format!("{kept:?}"), format!("{:?}", w.subset(&closure)));
2237        assert!(!kept.is_alive(unrelated));
2238    }
2239
2240    #[test]
2241    fn a_split_off_part_moves_out_and_back() {
2242        let mut w = crate::World::new();
2243        let texture = w.spawn();
2244        w.insert(texture, Name("albedo".into()));
2245        let prototype = w.spawn();
2246        w.insert(prototype, Name("rock".into()));
2247        let placement = w.spawn();
2248        w.insert(
2249            placement,
2250            Instance {
2251                prototype,
2252                variant: 0,
2253            },
2254        );
2255        w.insert(placement, Transform::IDENTITY);
2256        let other = w.spawn();
2257        w.insert(other, Name("elsewhere".into()));
2258        let before = format!("{w:?}");
2259        let next = w.clone().spawn();
2260        let keep = w.closure([placement]);
2261        let mut part = w.split_off(&keep);
2262        assert!(!w.is_alive(placement) && w.get::<Name>(prototype).is_none());
2263        assert_eq!(w.get::<Name>(other), Some(&Name("elsewhere".into())));
2264        assert_eq!(
2265            part.get::<Instance>(placement).map(|i| i.prototype),
2266            Some(prototype)
2267        );
2268        assert!(!part.is_alive(other));
2269        // the part spawns where a copy would, and what it spawns and despawns leaves no trace
2270        let spawned = part.spawn();
2271        assert_eq!(spawned, next);
2272        part.insert(spawned, Name("chunk".into()));
2273        part.despawn(spawned);
2274        w.absorb(part).unwrap();
2275        assert_eq!(format!("{w:?}"), before);
2276        assert_eq!(w.get::<Name>(texture), Some(&Name("albedo".into())));
2277        assert_eq!(w.spawn(), next, "the next id is unchanged");
2278    }
2279}