Skip to main content

primase_core/
lod.rs

1//! A prototype's levels of detail ([`Lods`], `core.lods`) and the pass that chooses one (`select-lod`). The forms
2//! stay in the world as the source authored them; a writer sees the one form a pipeline's pass leaves it, so no
3//! writer chooses on its own.
4
5use std::collections::BTreeSet;
6
7use serde::Serialize;
8use serde_json::Value;
9
10use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
11use crate::{
12    Collision, Component, Entity, Instance, JsonError, MaterialVariants, Mesh, MeshFile, Name, Source,
13    Transform, World,
14};
15
16/// A prototype's levels of detail (`core.lods`), on the prototype. Its own mesh is one form, at level `detail` (0 the
17/// finest, larger coarser, 0 high to 3 very low in one source); `forms` are its other forms, as the source lists them
18/// (finest first by convention; nothing depends on it).
19///
20/// A form is an entity of its own that holds everything the prototype holds for its mesh: `core.mesh_file` or
21/// `core.mesh`, `core.material_variants` when it has variants, and every dialect component that goes with that mesh
22/// (a reader that attaches per-primitive data to a prototype attaches it to each form too). A form with neither mesh
23/// component draws nothing. Forms are not placed and have no forms of their own. What stays with the prototype
24/// whichever form is drawn (its name, collision, identity) is the prototype's: a reader may give a form its own name
25/// or source too, and [`SelectLod`] leaves those behind.
26///
27/// `default` is the form the source itself takes: 0 the prototype's own mesh, `k` `forms[k - 1]`; `None` when the
28/// source authors none (a source switches between its levels by distance).
29#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
30pub struct Lods {
31    pub detail: u8,
32    pub forms: Vec<LodForm>,
33    pub default: Option<usize>,
34}
35
36/// One other form of a prototype: its level and the entity holding it.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
38pub struct LodForm {
39    pub detail: u8,
40    pub form: Entity,
41}
42
43impl Lods {
44    /// The levels of [own mesh, forms...], in that order.
45    pub fn levels(&self) -> impl Iterator<Item = u8> + '_ {
46        std::iter::once(self.detail).chain(self.forms.iter().map(|f| f.detail))
47    }
48
49    /// The form `choice` takes, as an index into [own mesh, forms...] ([`LodChoice::AtMost`] takes the coarsest form
50    /// whose level is `n` or finer); [`LodChoice::Default`] without an authored default takes the finest.
51    pub fn choose(&self, choice: LodChoice) -> usize {
52        match choice {
53            LodChoice::Finest => 0,
54            LodChoice::Default => self.default.unwrap_or(0),
55            LodChoice::AtMost(n) => {
56                let mut best: Option<(usize, u8)> = None;
57                for (i, d) in self.levels().enumerate() {
58                    if d <= n && best.is_none_or(|(_, b)| d > b) {
59                        best = Some((i, d));
60                    }
61                }
62                best.map_or(0, |(i, _)| i)
63            }
64        }
65    }
66}
67
68impl Component for Lods {
69    const NAME: &'static str = "core.lods";
70    const GEOMETRY: crate::Geometry = crate::Geometry::None;
71    /// The forms are entities of their own (a pass mapping a prototype maps its forms itself).
72    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
73        crate::world::Linearity::Invariant
74    }
75    fn to_json(&self) -> Result<Option<Value>, JsonError> {
76        crate::json::to_value(self).map(Some)
77    }
78    fn references(&self, out: &mut Vec<Entity>) {
79        out.extend(self.forms.iter().map(|f| f.form));
80    }
81    /// `default`, when there is one, one of the forms; with other forms, on an entity with a mesh; each form another
82    /// alive entity, listed once, not placed and without forms of its own. [`SelectLod`] checks it before it changes
83    /// anything, so it checks liveness itself, beside the world's check of every reference.
84    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
85        if let Some(d) = self.default
86            && d > self.forms.len()
87        {
88            return Err(format!("default form {d} of {}", self.forms.len() + 1));
89        }
90        if !self.forms.is_empty() && !(w.has::<Mesh>(e) || w.has::<MeshFile>(e)) {
91            return Err("other forms of an entity without a mesh".into());
92        }
93        let mut seen = BTreeSet::new();
94        for f in &self.forms {
95            if f.form == e || !w.is_alive(f.form) {
96                return Err(format!("form {:?} is not another alive entity", f.form));
97            }
98            if !seen.insert(f.form) {
99                return Err(format!("form {:?} is listed twice", f.form));
100            }
101            if w.has::<Instance>(f.form) || w.has::<Transform>(f.form) || w.has::<Lods>(f.form) {
102                return Err(format!("form {:?} is placed or has forms of its own", f.form));
103            }
104        }
105        Ok(())
106    }
107}
108
109/// Which form [`SelectLod`] takes of each prototype.
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum LodChoice {
112    /// The prototype's own mesh.
113    Finest,
114    /// [`Lods::default`], the source's own choice; the finest where it authored none (a named, counted fallback).
115    Default,
116    /// The coarsest form at level `N` or finer (the first listed of equal levels); the finest when none is.
117    AtMost(u8),
118}
119
120/// `select-lod{form=finest|default|N}` (`form` required: which level of detail is drawn is the program's choice):
121/// every prototype with [`Lods`] is left with the one form [`Lods::choose`] takes (with `default`, the finest where the source authored no default, counted as an
122/// approximation). Another form than its own takes the prototype's own geometry away with everything that describes
123/// it ([`World::remove_mesh`]: `core.mesh`, `core.mesh_file` and every per-primitive component, dialect data
124/// included, so nothing describing the discarded form stays behind when the chosen form lacks it), then every
125/// component the form holds replaces the prototype's of that name ([`World::move_components`]), so the form's
126/// dialect data travels with it without the pass knowing it, except what stays with the prototype (`core.name`,
127/// `core.source`, `core.collision`: the form's own are left behind, counted); a form that draws nothing leaves the
128/// prototype without a mesh. The other forms are despawned and `core.lods` removed. The whole selection is checked before anything
129/// changes: an invalid one (a prototype drawn with another form while it holds an opaque component, which can't be
130/// told to follow, among them) is an error and leaves the world as it was.
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub struct SelectLod {
133    pub choice: LodChoice,
134}
135
136/// `select-lod`'s declaration ([`crate::pass::PassRegistry::add`]).
137pub const SELECT_LOD: PassSpec = PassSpec {
138    name: "select-lod",
139    options: &[OptionSpec::required(
140        "form",
141        "finest",
142        "finest, default (the form the source names, else the finest) or a level 0-255: the form each prototype \
143         keeps (LodChoice)",
144    )],
145    make: select_lod,
146};
147
148fn select_lod(g: &Given) -> Result<Box<dyn Pass>, String> {
149    let choice = match g.required("form") {
150        "finest" => LodChoice::Finest,
151        "default" => LodChoice::Default,
152        n => LodChoice::AtMost(
153            n.parse()
154                .map_err(|_| format!("form={n} is not finest, default or a level 0-255"))?,
155        ),
156    };
157    Ok(Box::new(SelectLod { choice }))
158}
159
160impl Pass for SelectLod {
161    fn name(&self) -> &'static str {
162        "select-lod"
163    }
164    fn options(&self) -> Options {
165        let form = match self.choice {
166            LodChoice::Finest => "finest".to_owned(),
167            LodChoice::Default => "default".to_owned(),
168            LodChoice::AtMost(n) => n.to_string(),
169        };
170        Options::from([("form".to_owned(), form)])
171    }
172    /// A prototype drawn with another form loses its own mesh and variants (and keeps the form's).
173    fn removes(&self) -> &[&'static str] {
174        &[Lods::NAME, Mesh::NAME, MeshFile::NAME, MaterialVariants::NAME]
175    }
176    fn modifies(&self) -> &[&'static str] {
177        &[Mesh::NAME, MeshFile::NAME, MaterialVariants::NAME]
178    }
179    fn despawns_entities(&self) -> bool {
180        true
181    }
182    /// The per-primitive data of a prototype's own form ([`World::remove_mesh`]) is whatever its reader attached; each
183    /// name taken from a prototype the chosen form doesn't give back is recorded.
184    fn removes_by_log(&self) -> bool {
185        true
186    }
187    fn approximates(&self) -> &'static str {
188        match self.choice {
189            LodChoice::Default => "the finest form where the source authored no default",
190            _ => "",
191        }
192    }
193    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
194        let all: Vec<(Entity, Lods)> = w.iter::<Lods>().map(|(e, l)| (e, l.clone())).collect();
195        let mut forms = BTreeSet::new();
196        for (proto, lods) in &all {
197            lods.verify(*proto, w).map_err(|m| format!("{proto:?}: {m}"))?;
198            for f in &lods.forms {
199                if !forms.insert(f.form) {
200                    return Err(format!("form {:?} belongs to more than one prototype", f.form));
201                }
202            }
203            if lods.choose(self.choice) != 0 {
204                w.check_remove_mesh(*proto)?;
205            }
206        }
207        if let Some((name, holder, form)) = w
208            .references_to(&forms)
209            .into_iter()
210            .find(|(name, _, _)| *name != Lods::NAME)
211        {
212            return Err(format!("{name} on {holder:?} refers to form {form:?}"));
213        }
214        for (proto, lods) in all {
215            let chosen = lods.choose(self.choice);
216            if self.choice == LodChoice::Default && lods.default.is_none() {
217                log.approximate("prototypes without an authored default: the finest form", 1);
218            }
219            match chosen.checked_sub(1).map(|k| lods.forms[k]) {
220                None => log.count("prototypes drawn with their own form", 1),
221                Some(f) => {
222                    if let Some(own) = w.get::<Mesh>(proto) {
223                        let n = own.primitives().len() as u64;
224                        log.dropped(
225                            crate::ledger::Measure::Primitives,
226                            "the prototype's own form left out",
227                            n,
228                        );
229                    }
230                    for name in w.remove_mesh(proto)? {
231                        log.record_removed(name);
232                    }
233                    // what stays with the prototype whichever form is drawn is its own: a form's own name, identity or
234                    // collision (a reader that names each form) is left behind
235                    for (left, name) in [
236                        (w.remove::<Name>(f.form).is_some(), Name::NAME),
237                        (w.remove::<Source>(f.form).is_some(), Source::NAME),
238                        (w.remove::<Collision>(f.form).is_some(), Collision::NAME),
239                    ] {
240                        if left {
241                            log.record_removed(name);
242                            log.count(
243                                &format!(
244                                    "{name} of the chosen form left behind: the prototype keeps its own"
245                                ),
246                                1,
247                            );
248                        }
249                    }
250                    let moved = w.move_components(f.form, proto)?;
251                    debug_assert!(moved, "checked alive above");
252                    let draws = w.has::<Mesh>(proto) || w.has::<MeshFile>(proto);
253                    log.count(
254                        if draws {
255                            "prototypes drawn with another form"
256                        } else {
257                            "prototypes whose chosen form draws nothing"
258                        },
259                        1,
260                    );
261                }
262            }
263            for (k, f) in lods.forms.iter().enumerate() {
264                if k + 1 != chosen {
265                    crate::ledger::despawn(w, f.form, log, "forms left out");
266                    log.count("forms left out", 1);
267                }
268            }
269            w.remove::<Lods>(proto);
270        }
271        Ok(())
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::pass::{PassManager, PassRegistry, RunReport};
279    use crate::{Opaque, OpaqueData};
280
281    /// A dialect's per-form data, as a reader attaches it to each form: what a test sees moving with the form taken.
282    #[derive(Clone, Debug, PartialEq)]
283    struct Tag(String);
284    impl Component for Tag {
285        const NAME: &'static str = "game.tag";
286        const GEOMETRY: crate::Geometry = crate::Geometry::None;
287    }
288
289    /// A prototype at level 0 with two other forms (level 1 from a file, level 3 drawing nothing), default the
290    /// level-1 form; each form tags itself and the coarse one carries opaque game data, so a test sees what moved; the
291    /// name is the prototype's.
292    fn world() -> (World, Entity, [Entity; 2]) {
293        let mut w = World::new();
294        let proto = w.spawn();
295        w.insert(proto, Mesh::default());
296        w.insert(proto, Name("the prototype".into()));
297        w.insert(proto, Tag("finest".into()));
298        let coarse = w.spawn();
299        w.insert(
300            coarse,
301            MeshFile {
302                path: "p.glb".into(),
303                mesh: 1,
304                materials: vec![],
305            },
306        );
307        w.insert(coarse, Tag("coarse".into()));
308        w.insert_opaque(
309            coarse,
310            "game.form",
311            Opaque {
312                version: 1,
313                data: OpaqueData::Json(serde_json::json!({"lod": 1})),
314            },
315        )
316        .unwrap();
317        let nothing = w.spawn();
318        w.insert(nothing, Tag("nothing".into()));
319        w.insert(
320            proto,
321            Lods {
322                detail: 0,
323                forms: vec![
324                    LodForm {
325                        detail: 1,
326                        form: coarse,
327                    },
328                    LodForm {
329                        detail: 3,
330                        form: nothing,
331                    },
332                ],
333                default: Some(1),
334            },
335        );
336        (w, proto, [coarse, nothing])
337    }
338
339    fn manager() -> PassManager {
340        let mut r = PassRegistry::new();
341        r.add(&SELECT_LOD);
342        PassManager::new(r)
343    }
344
345    #[test]
346    fn select_lod_refuses_a_dead_form_before_it_changes_anything() {
347        // run, unlike convert, does not verify its input: the pass checks its forms itself
348        let (mut w, proto, forms) = world();
349        w.despawn(forms[0]);
350        let m = manager();
351        let err = m
352            .run(
353                &mut w,
354                &m.registry.parse("select-lod{form=default}").unwrap(),
355                &mut RunReport::default(),
356            )
357            .unwrap_err();
358        assert!(err.to_string().contains("not another alive entity"), "{err}");
359        assert!(w.has::<Mesh>(proto), "the prototype keeps its geometry");
360        assert!(w.has::<Lods>(proto) && w.is_alive(forms[1]));
361    }
362
363    #[test]
364    fn lods_verify_and_keep_their_forms_in_closures_and_subsets() {
365        let (w, proto, forms) = world();
366        assert_eq!(w.verify_components(), vec![]);
367        let keep = w.closure([proto]);
368        assert_eq!(keep, [proto, forms[0], forms[1]].into());
369        assert_eq!(w.subset(&keep).verify_components(), vec![]);
370    }
371
372    /// A reader may name each form and give it its own source; the form drawn leaves them behind and the prototype keeps
373    /// its own, counted.
374    #[test]
375    fn the_prototype_keeps_its_name_and_source_whichever_form_is_drawn() {
376        let (mut w, proto, forms) = world();
377        w.insert(proto, Source("proto".into()));
378        w.insert(forms[0], Name("proto#lod1".into()));
379        w.insert(forms[0], Source("proto/lod1".into()));
380        assert_eq!(w.verify_components(), vec![]);
381        let m = manager().with_pass_checks(true);
382        let mut report = RunReport::default();
383        m.run(
384            &mut w,
385            &m.registry.parse("select-lod{form=default}").unwrap(),
386            &mut report,
387        )
388        .unwrap();
389        assert_eq!(w.get::<Tag>(proto).unwrap().0, "coarse");
390        assert_eq!(w.get::<Name>(proto).unwrap().0, "the prototype");
391        assert_eq!(w.get::<Source>(proto).unwrap().0, "proto");
392        let counts = &report.steps[0].log.counts;
393        assert_eq!(
394            counts["core.name of the chosen form left behind: the prototype keeps its own"],
395            1
396        );
397        assert_eq!(
398            counts["core.source of the chosen form left behind: the prototype keeps its own"],
399            1
400        );
401    }
402
403    #[test]
404    fn choice_follows_the_reference_rule() {
405        let lods = |detail: u8, levels: &[u8], default: Option<usize>| Lods {
406            detail,
407            forms: levels
408                .iter()
409                .enumerate()
410                .map(|(i, &d)| LodForm {
411                    detail: d,
412                    form: Entity::from_index(10 + i as u32),
413                })
414                .collect(),
415            default,
416        };
417        let l = lods(0, &[1, 1, 3], Some(2));
418        assert_eq!(l.choose(LodChoice::Finest), 0);
419        assert_eq!(l.choose(LodChoice::Default), 2);
420        // no authored default: the finest
421        assert_eq!(lods(0, &[1, 3], None).choose(LodChoice::Default), 0);
422        // coarsest at most N; of equal levels the first listed; none at most N: the finest
423        assert_eq!(l.choose(LodChoice::AtMost(0)), 0);
424        assert_eq!(l.choose(LodChoice::AtMost(2)), 1);
425        assert_eq!(l.choose(LodChoice::AtMost(255)), 3);
426        assert_eq!(lods(2, &[3], None).choose(LodChoice::AtMost(1)), 0);
427        // the order does not matter to the levels
428        assert_eq!(lods(0, &[3, 1], None).choose(LodChoice::AtMost(2)), 2);
429    }
430
431    #[test]
432    fn default_without_an_authored_default_takes_the_finest_named_and_counted() {
433        let (mut w, proto, forms) = world();
434        w.get_mut::<Lods>(proto).unwrap().default = None;
435        assert_eq!(w.verify_components(), vec![]);
436        let m = manager();
437        let mut report = RunReport::default();
438        m.run(
439            &mut w,
440            &m.registry.parse("select-lod{form=default}").unwrap(),
441            &mut report,
442        )
443        .unwrap();
444        assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
445        assert!(forms.iter().all(|f| !w.is_alive(*f)));
446        let step = &report.steps[0];
447        assert_eq!(
448            step.log.approximations,
449            [(
450                "prototypes without an authored default: the finest form".to_owned(),
451                1
452            )]
453            .into()
454        );
455        assert_eq!(
456            step.approximates,
457            "the finest form where the source authored no default"
458        );
459        // an authored default is no approximation; neither are the other choices
460        let (mut w, _, _) = world();
461        let mut report = RunReport::default();
462        m.run(
463            &mut w,
464            &m.registry.parse("select-lod{form=default}").unwrap(),
465            &mut report,
466        )
467        .unwrap();
468        assert!(report.steps[0].log.approximations.is_empty());
469        assert_eq!(
470            SelectLod {
471                choice: LodChoice::Finest
472            }
473            .approximates(),
474            ""
475        );
476    }
477
478    #[test]
479    fn select_lod_takes_one_form_with_everything_it_holds() {
480        for (pipeline, name, mesh, file, opaque) in [
481            ("select-lod{form=finest}", "finest", true, false, false),
482            ("select-lod{form=default}", "coarse", false, true, true),
483            ("select-lod{form=2}", "coarse", false, true, true),
484            ("select-lod{form=3}", "nothing", false, false, false),
485        ] {
486            let (mut w, proto, forms) = world();
487            let m = manager();
488            let mut report = RunReport::default();
489            m.run(&mut w, &m.registry.parse(pipeline).unwrap(), &mut report)
490                .unwrap();
491            assert_eq!(w.get::<Tag>(proto).unwrap().0, name, "{pipeline}");
492            assert_eq!(
493                w.get::<Name>(proto).unwrap().0,
494                "the prototype",
495                "{pipeline}: the name stays"
496            );
497            assert_eq!(
498                (w.has::<Mesh>(proto), w.has::<MeshFile>(proto)),
499                (mesh, file),
500                "{pipeline}"
501            );
502            assert_eq!(w.get_opaque(proto, "game.form").is_some(), opaque, "{pipeline}");
503            assert!(!w.has::<Lods>(proto));
504            assert!(forms.iter().all(|f| !w.is_alive(*f)), "{pipeline}: a form left");
505            assert_eq!(w.entity_count(), 1);
506            let step = &report.steps[0];
507            let left_out = if name == "finest" { 2 } else { 1 };
508            assert_eq!(step.log.counts["forms left out"], left_out, "{pipeline}");
509            // the discarded forms' dialect data goes with them, undeclared
510            let mut removed = vec!["core.lods"];
511            removed.extend((!mesh).then_some("core.mesh"));
512            removed.extend((!file).then_some("core.mesh_file"));
513            removed.extend((!opaque).then_some("game.form"));
514            assert_eq!(step.removed, removed, "{pipeline}");
515        }
516        assert_eq!(
517            SELECT_LOD
518                .instance(&Options::from([("form".into(), "2".into())]))
519                .unwrap()
520                .options()["form"],
521            "2"
522        );
523        // no form is refused; every form prints back as it was given
524        assert!(SELECT_LOD.instance(&Options::new()).is_err());
525        for form in ["finest", "default", "0", "255"] {
526            let given = Options::from([("form".into(), form.into())]);
527            assert_eq!(SELECT_LOD.instance(&given).unwrap().options(), given, "{form}");
528        }
529        assert!(
530            SELECT_LOD
531                .instance(&Options::from([("form".into(), "high".into())]))
532                .is_err()
533        );
534        assert!(
535            SELECT_LOD
536                .instance(&Options::from([("lod".into(), "1".into())]))
537                .is_err()
538        );
539    }
540
541    #[test]
542    fn select_lod_preserves_opaque_shader_inputs() {
543        use crate::{Primitive, Semantic, Stream, Topology};
544        use std::collections::BTreeMap;
545        let (mut w, proto, forms) = world();
546        let raw = vec![[-3.0, 0.5, 9.0, -0.0]; 3];
547        w.insert(
548            forms[0],
549            Mesh::new(vec![Primitive {
550                topology: Topology::Triangles,
551                streams: BTreeMap::from([
552                    (Semantic::Position, Stream::F32x3(vec![[0.0; 3]; 3])),
553                    (Semantic::ShaderInput(4), Stream::F32x4(raw.clone())),
554                ]),
555                indices: None,
556                material: None,
557            }]),
558        );
559        let m = manager();
560        m.run(
561            &mut w,
562            &m.registry.parse("select-lod{form=default}").unwrap(),
563            &mut RunReport::default(),
564        )
565        .unwrap();
566        let mesh = w.get::<Mesh>(proto).unwrap();
567        let Stream::F32x4(values) = &mesh.primitives()[0].streams[&Semantic::ShaderInput(4)] else {
568            panic!("opaque stream missing")
569        };
570        assert_eq!(
571            values.iter().map(|v| v.map(f32::to_bits)).collect::<Vec<_>>(),
572            raw.iter().map(|v| v.map(f32::to_bits)).collect::<Vec<_>>()
573        );
574    }
575
576    /// Dialect data per primitive of its entity's mesh, as a reader attaches it.
577    #[derive(Clone, Debug, PartialEq)]
578    struct PerPrimitive(Vec<u8>);
579    impl Component for PerPrimitive {
580        const NAME: &'static str = "game.per_primitive";
581        const GEOMETRY: crate::Geometry = crate::Geometry::PerPrimitive;
582        fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
583            Some(crate::remap_list(&self.0, from).map(PerPrimitive))
584        }
585    }
586
587    /// The prototype's own form takes its per-primitive data with it when another form is drawn, whether its mesh is
588    /// loaded or still only in its file (a world before its load): nothing describing the discarded form stays on the
589    /// prototype when the chosen form has no such data of its own.
590    #[test]
591    fn the_own_forms_per_primitive_data_leaves_with_it() {
592        for loaded in [true, false] {
593            let (mut w, proto, _) = world();
594            if !loaded {
595                w.remove::<Mesh>(proto);
596                w.insert(
597                    proto,
598                    MeshFile {
599                        path: "own.glb".into(),
600                        mesh: 0,
601                        materials: vec![],
602                    },
603                );
604            }
605            w.insert(proto, PerPrimitive(vec![]));
606            w.insert(proto, MaterialVariants(vec![]));
607            let m = manager().with_pass_checks(true);
608            let mut report = RunReport::default();
609            m.run(
610                &mut w,
611                &m.registry.parse("select-lod{form=default}").unwrap(),
612                &mut report,
613            )
614            .unwrap();
615            assert_eq!(w.get::<Tag>(proto).unwrap().0, "coarse");
616            assert!(
617                !w.has::<PerPrimitive>(proto) && !w.has::<MaterialVariants>(proto),
618                "loaded {loaded}: the own form's per-primitive data stayed"
619            );
620            assert_eq!(
621                w.get::<MeshFile>(proto).unwrap().path,
622                std::path::Path::new("p.glb")
623            );
624            let removed = &report.steps[0].removed;
625            for name in [PerPrimitive::NAME, MaterialVariants::NAME] {
626                assert!(removed.iter().any(|n| n == name), "loaded {loaded}: {removed:?}");
627            }
628        }
629        // the own form drawn: its data stays
630        let (mut w, proto, _) = world();
631        w.insert(proto, PerPrimitive(vec![]));
632        let m = manager().with_pass_checks(true);
633        m.run(
634            &mut w,
635            &m.registry.parse("select-lod{form=finest}").unwrap(),
636            &mut RunReport::default(),
637        )
638        .unwrap();
639        assert!(w.has::<PerPrimitive>(proto) && w.has::<Mesh>(proto));
640    }
641
642    /// An opaque component on a prototype drawn with another form can't be told to follow its own form's removal: the
643    /// selection is refused before anything changes. Drawn with its own form, nothing is removed and it passes.
644    #[test]
645    fn an_opaque_component_on_a_prototype_drawn_with_another_form_is_refused() {
646        let opaque = || Opaque {
647            version: 1,
648            data: OpaqueData::Bytes(vec![1]),
649        };
650        let (mut w, proto, forms) = world();
651        w.insert_opaque(proto, "game.thing", opaque()).unwrap();
652        let m = manager().with_pass_checks(true);
653        let err = m
654            .run(
655                &mut w,
656                &m.registry.parse("select-lod{form=default}").unwrap(),
657                &mut RunReport::default(),
658            )
659            .unwrap_err();
660        assert!(err.to_string().contains("game.thing"), "{err}");
661        assert!(w.has::<Mesh>(proto) && w.has::<Lods>(proto));
662        assert!(forms.iter().all(|f| w.is_alive(*f)));
663        assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
664        m.run(
665            &mut w,
666            &m.registry.parse("select-lod{form=finest}").unwrap(),
667            &mut RunReport::default(),
668        )
669        .unwrap();
670        assert!(w.get_opaque(proto, "game.thing").is_some() && w.has::<Mesh>(proto));
671    }
672
673    #[test]
674    fn an_invalid_selection_is_refused_without_mutation() {
675        // an out-of-range default, also on a prototype after a valid one
676        let (mut w, proto, forms) = world();
677        let second = w.spawn();
678        w.insert(second, Mesh::default());
679        w.insert(
680            second,
681            Lods {
682                detail: 0,
683                forms: vec![],
684                default: Some(99),
685            },
686        );
687        let err = SelectLod {
688            choice: LodChoice::Default,
689        }
690        .run(&mut w, &mut PassLog::default())
691        .unwrap_err();
692        assert!(err.contains("default form 99 of 1"), "{err}");
693        assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
694        assert!(forms.iter().all(|e| w.is_alive(*e)) && w.has::<Lods>(second));
695        // a form shared by two prototypes
696        w.insert(second, w.get::<Lods>(proto).unwrap().clone());
697        let err = SelectLod {
698            choice: LodChoice::Default,
699        }
700        .run(&mut w, &mut PassLog::default())
701        .unwrap_err();
702        assert!(err.contains("more than one prototype"), "{err}");
703        // a form something else refers to
704        w.remove::<Lods>(second);
705        let placed = w.spawn();
706        w.insert(
707            placed,
708            Instance {
709                prototype: forms[0],
710                variant: 0,
711            },
712        );
713        let err = SelectLod {
714            choice: LodChoice::Default,
715        }
716        .run(&mut w, &mut PassLog::default())
717        .unwrap_err();
718        assert!(
719            err.contains("core.instance") && err.contains("refers to form"),
720            "{err}"
721        );
722        assert!(forms.iter().all(|e| w.is_alive(*e)));
723        assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
724    }
725
726    #[test]
727    fn malformed_lods_fail_verification() {
728        let failure = |edit: &dyn Fn(&mut World, Entity, [Entity; 2])| {
729            let (mut w, proto, forms) = world();
730            edit(&mut w, proto, forms);
731            w.verify_components()
732                .into_iter()
733                .map(|(_, _, m)| m)
734                .collect::<Vec<_>>()
735        };
736        let twice = failure(&|w, proto, _| {
737            let lods = w.get_mut::<Lods>(proto).unwrap();
738            lods.forms[1] = lods.forms[0];
739        });
740        assert!(twice[0].contains("listed twice"), "{twice:?}");
741        let placed = failure(&|w, _, forms| {
742            w.insert(forms[0], Transform::IDENTITY);
743        });
744        assert!(placed[0].contains("placed"), "{placed:?}");
745        let meshless = failure(&|w, proto, _| {
746            w.remove::<Mesh>(proto);
747        });
748        assert!(meshless[0].contains("without a mesh"), "{meshless:?}");
749        let gone = failure(&|w, _, forms| {
750            w.despawn(forms[1]);
751        });
752        assert!(gone[0].contains("not another alive entity"), "{gone:?}");
753    }
754}