Skip to main content

primase_core/
priority.rs

1//! Authored placement optionality, independent of any consumer's filtering policy, and the explicit pass that
2//! filters by it (`drop-optional`).
3
4use std::collections::BTreeSet;
5
6use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
7use crate::{Component, Entity, Instance, JsonError, Mesh, MeshFile, Transform, World};
8use serde::Serialize;
9use serde_json::Value;
10
11/// How optional a placement is (`core.priority`): 0 is required and higher values
12/// are more optional. Absence uses the required-placement behavior.
13///
14/// The dimensionless range is 0-255. This component does not filter placements;
15/// a consumer with narrower storage must reject values it cannot preserve.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
17pub struct Priority(pub u8);
18
19impl Component for Priority {
20    const NAME: &'static str = "core.priority";
21    const GEOMETRY: crate::Geometry = crate::Geometry::None;
22    /// No local-space geometry.
23    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
24        crate::world::Linearity::Invariant
25    }
26
27    fn to_json(&self) -> Result<Option<Value>, JsonError> {
28        crate::json::to_value(self).map(Some)
29    }
30
31    fn verify(&self, entity: Entity, world: &World) -> Result<(), String> {
32        if world.has::<Instance>(entity)
33            || (world.has::<Transform>(entity)
34                && (world.has::<Mesh>(entity) || world.has::<MeshFile>(entity)))
35        {
36            Ok(())
37        } else {
38            Err("priority on an entity that is not placed".into())
39        }
40    }
41}
42
43/// `drop-optional{max=N}`: every placement more optional than `max` (its [`Priority`] above it) is despawned with
44/// everything it holds, counted per priority; the others are untouched. Without options `max` is 255 and nothing is
45/// dropped: leaving placements out is always a pipeline's explicit choice. A dropped entity that a kept one refers to
46/// (a self-placed mesh other placements use as their prototype) is refused, and nothing is dropped.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct DropOptional {
49    pub max: u8,
50}
51
52impl Default for DropOptional {
53    fn default() -> Self {
54        DropOptional { max: u8::MAX }
55    }
56}
57
58/// `drop-optional`'s declaration ([`crate::pass::PassRegistry::add`]).
59pub const DROP_OPTIONAL: PassSpec = PassSpec {
60    name: "drop-optional",
61    options: &[OptionSpec::optional(
62        "max",
63        "3",
64        "a priority 0-255: placements above it are dropped",
65        "255: nothing is dropped",
66    )],
67    make: drop_optional,
68};
69
70fn drop_optional(g: &Given) -> Result<Box<dyn Pass>, String> {
71    let mut pass = DropOptional::default();
72    if let Some(v) = g.optional("max") {
73        pass.max = v
74            .parse()
75            .map_err(|_| format!("max={v} is not a priority 0-255"))?;
76    }
77    Ok(Box::new(pass))
78}
79
80impl Pass for DropOptional {
81    fn name(&self) -> &'static str {
82        "drop-optional"
83    }
84    fn options(&self) -> Options {
85        if self.max == u8::MAX {
86            Options::new()
87        } else {
88            Options::from([("max".to_owned(), self.max.to_string())])
89        }
90    }
91    fn despawns_entities(&self) -> bool {
92        true
93    }
94    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
95        let dropped: Vec<(Entity, u8)> = w
96            .iter::<Priority>()
97            .filter(|(_, p)| p.0 > self.max)
98            .map(|(e, p)| (e, p.0))
99            .collect();
100        let gone: BTreeSet<Entity> = dropped.iter().map(|(e, _)| *e).collect();
101        if let Some((name, holder, target)) = w
102            .references_to(&gone)
103            .into_iter()
104            .find(|(_, holder, _)| !gone.contains(holder))
105        {
106            return Err(format!(
107                "{name} on {holder:?} refers to {target:?}, a placement above priority {}",
108                self.max
109            ));
110        }
111        for (e, p) in dropped {
112            crate::ledger::despawn(w, e, log, &format!("placements at priority {p} left out"));
113        }
114        Ok(())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::Name;
122    use crate::pass::{PassManager, PassRegistry, RunReport};
123
124    #[test]
125    fn priority_preserves_full_range_and_requires_a_placement() {
126        let mut world = World::new();
127        let entity = world.spawn();
128        world.insert(entity, Priority(255));
129        assert_eq!(world.verify_components().len(), 1);
130        world.insert(entity, Mesh::default());
131        assert_eq!(world.verify_components().len(), 1);
132        world.insert(entity, Transform::IDENTITY);
133        assert!(world.verify_components().is_empty());
134        for value in [0, 15, 16, 255] {
135            assert_eq!(Priority(value).to_json().unwrap(), Some(Value::from(value)));
136        }
137    }
138
139    /// A prototype placed at no priority, 0, 2 and 3 (named "optional"), and maximum 2.
140    fn placed_world() -> (World, Entity, Vec<Entity>) {
141        let mut w = World::new();
142        let proto = w.spawn();
143        w.insert(proto, Mesh::default());
144        let mut placed = Vec::new();
145        for p in [None, Some(0), Some(2), Some(3)] {
146            let e = w.spawn();
147            w.insert(
148                e,
149                Instance {
150                    prototype: proto,
151                    variant: 0,
152                },
153            );
154            w.insert(e, crate::Transform::IDENTITY);
155            if let Some(p) = p {
156                w.insert(e, Priority(p));
157            }
158            if p == Some(3) {
159                w.insert(e, Name("optional".into()));
160            }
161            placed.push(e);
162        }
163        (w, proto, placed)
164    }
165
166    #[test]
167    fn drop_optional_leaves_out_placements_above_the_maximum_through_the_manager() {
168        let (mut w, _, placed) = placed_world();
169        let mut registry = PassRegistry::new();
170        registry.add(&DROP_OPTIONAL);
171        let m = PassManager::new(registry);
172        let pipeline = m.registry.parse("drop-optional{max=2}").unwrap();
173        let mut report = RunReport::default();
174        m.run(&mut w, &pipeline, &mut report).unwrap();
175        let step = &report.steps[0];
176        assert_eq!(step.pass, "drop-optional{max=2}");
177        assert_eq!(
178            step.removed,
179            ["core.name"],
180            "only the dropped placement had a name"
181        );
182        assert!(step.log.counts.is_empty());
183        assert_eq!(
184            step.log.drops,
185            [(
186                (
187                    crate::ledger::Measure::Placements,
188                    "placements at priority 3 left out".to_owned()
189                ),
190                1
191            )]
192            .into()
193        );
194        assert_eq!(
195            placed.iter().map(|e| w.is_alive(*e)).collect::<Vec<_>>(),
196            [true, true, true, false]
197        );
198        // without options nothing goes
199        let (mut w, _, _) = placed_world();
200        let all = w.entity_count();
201        let pipeline = m.registry.parse("drop-optional").unwrap();
202        m.run(&mut w, &pipeline, &mut RunReport::default()).unwrap();
203        assert_eq!(w.entity_count(), all);
204        assert!(
205            DROP_OPTIONAL
206                .instance(&Options::from([("max".into(), "256".into())]))
207                .is_err()
208        );
209        assert!(
210            DROP_OPTIONAL
211                .instance(&Options::from([("level".into(), "2".into())]))
212                .is_err()
213        );
214    }
215
216    #[test]
217    fn a_dropped_placement_something_kept_refers_to_is_refused() {
218        let (mut w, proto, placed) = placed_world();
219        // the prototype is placed as itself too, at priority 5: its placements refer to it
220        w.insert(proto, Transform::IDENTITY);
221        w.insert(proto, Priority(5));
222        let err = DropOptional { max: 2 }
223            .run(&mut w, &mut PassLog::default())
224            .unwrap_err();
225        assert!(
226            err.contains("core.instance") && err.contains("above priority 2"),
227            "{err}"
228        );
229        assert!(
230            w.is_alive(proto) && placed.iter().all(|e| w.is_alive(*e)),
231            "nothing dropped"
232        );
233    }
234}