Skip to main content

primase_core/
asset.rs

1//! One asset read by itself (`core.asset`): a root entity naming what the asset is made of, and the passes that work
2//! by asset: `select-assets` (keep some assets of a read world), `partition-by-asset` (one output part per asset) and
3//! `place-assets` (place an asset a source did not place).
4//!
5//! A reader asked for one asset (a model, a character, a prop and its parts) spawns its entities as it reads any
6//! world and a root holding [`Asset`]: each member with its [`Role`]. The members are references, so the asset's
7//! closure ([`World::closure`]) is everything it needs; the asset's source identity is the root's `core.source`.
8//! Optional members (clips, alternative parts) are members too: a selection pass prunes the ones a program does not
9//! want, rewriting the root's list before it despawns them. An asset as a source authors it has no placement unless
10//! the source places its parts (a prefab's renderers); a program that wants one drawn places it (`place-assets`).
11
12use std::collections::{BTreeMap, BTreeSet};
13
14use serde::Serialize;
15use serde_json::Value;
16
17use crate::convert::{PartitionGroup, PartitionPlan};
18use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
19use crate::{
20    Component, Entity, Instance, JsonError, Mesh, MeshFile, Name, Resource, RigInstance, Skeleton, Skin,
21    SkinInstance, Source, Transform, World,
22};
23
24/// What a member is to its asset.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
26pub enum Role {
27    /// Something it draws: a prototype (with its mesh or mesh file), or a placement the source authors in the asset's
28    /// own space.
29    Part,
30    /// A skeleton its parts bind to (`core.skeleton`).
31    Rig,
32    /// A clip it comes with. A member without `core.clip` is listed, not yet loaded/lowered:
33    /// `core.clip_key` identifies it, `core.name` labels it and optional `core.clip_length`
34    /// states a known length. Its reader descriptor remains on the same entity through load.
35    Clip,
36    /// A part drawn instead of another or not at all (an outfit's alternative), as the source's state says.
37    Alternative,
38}
39
40/// `core.asset` (see the module documentation).
41#[derive(Clone, Debug, Default, PartialEq, Serialize)]
42pub struct Asset {
43    pub members: Vec<(Entity, Role)>,
44}
45
46impl Component for Asset {
47    const NAME: &'static str = "core.asset";
48    const GEOMETRY: crate::Geometry = crate::Geometry::None;
49    fn to_json(&self) -> Result<Option<Value>, JsonError> {
50        crate::json::to_value(self).map(Some)
51    }
52    fn references(&self, out: &mut Vec<Entity>) {
53        out.extend(self.members.iter().map(|(e, _)| *e));
54    }
55    /// Each member once and not the root; each what its role says, in core's form or still in its source's: a rig a
56    /// `core.skeleton`, a clip a `core.clip`, a part or an alternative a prototype (a mesh or a mesh file) or a
57    /// placement; or, still in its source's form, a member holding a component outside core (a reader's model,
58    /// skeleton or clip as the source stores it, which a pass lowers) and no core form of another role (a clip member
59    /// that is in core a mesh is no clip, whatever else it holds).
60    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
61        let names = w.component_names();
62        let in_source_form = |m: Entity| {
63            names
64                .iter()
65                .any(|n| !n.starts_with("core.") && w.entity_has_named(m, n))
66        };
67        let mut seen = BTreeSet::new();
68        for &(m, role) in &self.members {
69            if m == e || !seen.insert(m) {
70                return Err(format!("member {m:?} is the root or listed twice"));
71            }
72            // each role's core form
73            let rig = w.has::<Skeleton>(m);
74            let clip = w.has::<crate::Clip>(m);
75            let part = w.has::<Mesh>(m) || w.has::<MeshFile>(m) || w.has::<Instance>(m);
76            let (own, other) = match role {
77                Role::Rig => (rig, clip || part),
78                Role::Clip => (clip, rig || part),
79                Role::Part | Role::Alternative => (part, rig || clip),
80            };
81            let fits = own || (!other && in_source_form(m));
82            if !fits {
83                return Err(format!("member {m:?} is not what its role {role:?} says"));
84            }
85        }
86        Ok(())
87    }
88}
89
90/// `place-assets` (no options: at the world's origin): every asset whose parts nothing places is placed, once, at the
91/// origin, as its source authors it:
92/// - each `Rig` member gets a rig (`core.rig_instance` and `core.transform`) of its own;
93/// - each `Part` prototype a placement (`core.instance`, `core.transform`), with a `core.skin_instance` when its mesh
94///   is skinned to one of the asset's rigs (its first binding to that rig's skeleton);
95/// - `Alternative` members are not placed (choosing one is a selection: counted), and a part the source already
96///   places (a placement member, or a prototype something places) is left as it is; so is a rig: a skeleton that
97///   already has one keeps it, and parts placed now are skinned to it. Run again, it places nothing.
98///
99/// The placed entities are content of their own ([`PassLog::place`]).
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub struct PlaceAssets;
102
103/// `place-assets`' declaration ([`crate::pass::PassRegistry::add`]): no options.
104pub const PLACE_ASSETS: PassSpec = PassSpec {
105    name: "place-assets",
106    options: &[],
107    make: |_| Ok(Box::new(PlaceAssets)),
108};
109
110impl Pass for PlaceAssets {
111    fn name(&self) -> &'static str {
112        "place-assets"
113    }
114    /// A part of a conversion may hold no asset (the content `partition-by-asset` puts outside them): nothing to place.
115    fn may_lack_inputs(&self) -> bool {
116        true
117    }
118    fn requires(&self) -> &[&'static str] {
119        &[Asset::NAME]
120    }
121    fn provides(&self) -> &[&'static str] {
122        &[
123            Instance::NAME,
124            Transform::NAME,
125            RigInstance::NAME,
126            SkinInstance::NAME,
127        ]
128    }
129    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
130        // what is placed already, and what this run places, as it goes: a part or a rig is placed once
131        let mut placed: BTreeSet<Entity> = w.iter::<Instance>().map(|(_, i)| i.prototype).collect();
132        let mut rigged: BTreeMap<Entity, Entity> = BTreeMap::new();
133        for (rig, r) in w.iter::<RigInstance>() {
134            rigged.entry(r.skeleton).or_insert(rig);
135        }
136        let assets: Vec<(Entity, Asset)> = w.iter::<Asset>().map(|(e, a)| (e, a.clone())).collect();
137        for (root, asset) in assets {
138            let mut rigs: Vec<(Entity, Entity)> = Vec::new();
139            for &(m, role) in &asset.members {
140                if role != Role::Rig {
141                    continue;
142                }
143                if let Some(&rig) = rigged.get(&m) {
144                    log.count("rigs already placed, left as they are", 1);
145                    rigs.push((m, rig));
146                    continue;
147                }
148                let rig = w.spawn();
149                w.insert(rig, RigInstance { skeleton: m });
150                w.insert(rig, Transform::IDENTITY);
151                log.place_from(rig, root);
152                rigged.insert(m, rig);
153                rigs.push((m, rig));
154            }
155            for &(m, role) in &asset.members {
156                match role {
157                    Role::Alternative => log.count("alternative parts not placed (a selection chooses)", 1),
158                    Role::Part if w.has::<Instance>(m) || placed.contains(&m) => {
159                        log.count("parts the source places, left as they are", 1);
160                    }
161                    Role::Part => {
162                        let p = w.spawn();
163                        w.insert(
164                            p,
165                            Instance {
166                                prototype: m,
167                                variant: 0,
168                            },
169                        );
170                        w.insert(p, Transform::IDENTITY);
171                        let binding = w.get::<Skin>(m).and_then(|s| {
172                            s.bindings.iter().find_map(|b| {
173                                rigs.iter()
174                                    .find(|(skel, _)| *skel == b.skeleton)
175                                    .map(|&(_, rig)| (rig, b.id))
176                            })
177                        });
178                        if let Some((rig, binding)) = binding {
179                            w.insert(p, SkinInstance { rig, binding });
180                            log.count("skinned parts placed with their rig", 1);
181                        }
182                        log.place_from(p, root);
183                        log.count("parts placed", 1);
184                        placed.insert(m);
185                    }
186                    Role::Rig | Role::Clip => {}
187                }
188            }
189        }
190        Ok(())
191    }
192}
193
194/// What `select-assets` matches an asset's root by.
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub enum AssetField {
197    /// Its source identity (`core.source`: `chr/c1010`).
198    Key,
199    /// Its name (`core.name`).
200    Name,
201}
202
203impl AssetField {
204    fn option(self) -> &'static str {
205        match self {
206            AssetField::Key => "keys",
207            AssetField::Name => "names",
208        }
209    }
210
211    /// The root's value of this field, if it has one.
212    fn of(self, w: &World, root: Entity) -> Option<&str> {
213        match self {
214            AssetField::Key => w.get::<Source>(root).map(|s| s.0.as_str()),
215            AssetField::Name => w.get::<Name>(root).map(|n| n.0.as_str()),
216        }
217    }
218}
219
220/// `select-assets{keys=K;K...}` or `select-assets{names=N;N...}` (one of the two, required): the assets whose roots'
221/// `core.source` (or `core.name`) is one of the values are kept with everything they reach ([`World::closure`]);
222/// every other entity is despawned, counted as left out (other assets, loose content, and authored placements of a
223/// kept asset's parts, which reference the asset rather than belong to it: a program places what it extracted,
224/// `place-assets`, and recentres it, `translate-world`). A value matching no asset is an error, naming the assets
225/// there are. Every asset a value matches is kept. It runs on a planning world (a selection, before anything loads),
226/// so what it leaves out is never loaded.
227#[derive(Clone, Debug, PartialEq, Eq)]
228pub struct SelectAssets {
229    pub field: AssetField,
230    /// The values, in the order given, each once.
231    pub values: Vec<String>,
232}
233
234/// `select-assets`' declaration ([`crate::pass::PassRegistry::add`]): `keys` or `names`, one of them.
235pub const SELECT_ASSETS: PassSpec = PassSpec {
236    name: "select-assets",
237    options: &[
238        OptionSpec::one_of(
239            "assets",
240            "keys",
241            "example",
242            "the assets' keys, `;` between, each once",
243        ),
244        OptionSpec::one_of(
245            "assets",
246            "names",
247            "example",
248            "the assets' names, `;` between, each once",
249        ),
250    ],
251    make: select_assets,
252};
253
254fn select_assets(g: &Given) -> Result<Box<dyn Pass>, String> {
255    let (k, field, v) = match (g.optional("keys"), g.optional("names")) {
256        (Some(v), _) => ("keys", AssetField::Key, v),
257        (None, Some(v)) => ("names", AssetField::Name, v),
258        (None, None) => unreachable!("the declaration's check gives one of keys and names"),
259    };
260    let mut values: Vec<String> = Vec::new();
261    for value in v.split(';').map(str::trim) {
262        if value.is_empty() {
263            return Err(format!("{k}={v}: an empty value"));
264        }
265        if values.iter().any(|x| x == value) {
266            return Err(format!("{k}={v}: {value} twice"));
267        }
268        values.push(value.to_owned());
269    }
270    Ok(Box::new(SelectAssets { field, values }))
271}
272
273impl Pass for SelectAssets {
274    fn name(&self) -> &'static str {
275        "select-assets"
276    }
277    fn options(&self) -> Options {
278        Options::from([(self.field.option().to_owned(), self.values.join(";"))])
279    }
280    fn requires(&self) -> &[&'static str] {
281        &[Asset::NAME]
282    }
283    fn despawns_entities(&self) -> bool {
284        true
285    }
286    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
287        keep_assets(w, self.field, &self.values, log)
288    }
289}
290
291/// What `select-assets` does, for a pass that keeps assets it finds named elsewhere (what a read was asked for): the
292/// assets whose roots' `field` is one of `values` kept with everything they reach, every other entity despawned and
293/// counted as left out; a value matching no asset refused, naming the assets there are.
294pub fn keep_assets(
295    w: &mut World,
296    field: AssetField,
297    values: &[String],
298    log: &mut PassLog,
299) -> Result<(), String> {
300    let roots: Vec<Entity> = w.iter::<Asset>().map(|(e, _)| e).collect();
301    let mut selected = BTreeSet::new();
302    for value in values {
303        let matched: Vec<Entity> = roots
304            .iter()
305            .copied()
306            .filter(|&r| field.of(w, r) == Some(value.as_str()))
307            .collect();
308        if matched.is_empty() {
309            let mut there: Vec<&str> = roots.iter().filter_map(|&r| field.of(w, r)).collect();
310            there.sort_unstable();
311            let shown = there.iter().take(20).copied().collect::<Vec<_>>().join(", ");
312            let more = there.len().saturating_sub(20);
313            return Err(format!(
314                "no asset whose {} is {value} (the world's: {shown}{})",
315                match field {
316                    AssetField::Key => "core.source",
317                    AssetField::Name => "core.name",
318                },
319                if more > 0 {
320                    format!(" and {more} more")
321                } else {
322                    String::new()
323                }
324            ));
325        }
326        selected.extend(matched);
327    }
328    let keep = w.closure(selected.iter().copied());
329    let gone: Vec<Entity> = w.entities().filter(|e| !keep.contains(e)).collect();
330    log.count("assets selected", selected.len() as u64);
331    log.count("assets left out", (roots.len() - selected.len()) as u64);
332    for e in gone {
333        crate::ledger::despawn(w, e, log, "not in a selected asset");
334    }
335    Ok(())
336}
337
338/// `partition-by-asset` (no options): a [`PartitionPlan`] of one part per asset, so a conversion writes each asset
339/// on its own (`convert_parts` with `PlannedParts`). Each asset root nothing references is a group of its own, in
340/// entity order, keyed by its `core.source` (else its `core.name`, else `asset <index>`), with no context: the part
341/// is the asset's closure, and what its passes place from the root (`place-assets` run in the part's passes) is its
342/// own. Other independent content (loose placements, and placements a selection made, which reference an asset's
343/// parts rather than belong to it: place assets in the part's passes, not the selection) is one more group,
344/// `outside assets`, counted, written with what it reaches. An asset another asset references is written with that
345/// one.
346#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
347pub struct PartitionByAsset;
348
349/// `partition-by-asset`'s declaration ([`crate::pass::PassRegistry::add`]): no options.
350pub const PARTITION_BY_ASSET: PassSpec = PassSpec {
351    name: "partition-by-asset",
352    options: &[],
353    make: |_| Ok(Box::new(PartitionByAsset)),
354};
355
356impl Pass for PartitionByAsset {
357    fn name(&self) -> &'static str {
358        "partition-by-asset"
359    }
360    fn requires(&self) -> &[&'static str] {
361        &[Asset::NAME]
362    }
363    fn provides(&self) -> &[&'static str] {
364        &[PartitionPlan::NAME]
365    }
366    fn modifies(&self) -> &[&'static str] {
367        &[PartitionPlan::NAME]
368    }
369    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
370        // independent content: what nothing refers to (entities with no component are nothing)
371        let names = w.component_names();
372        let independent: Vec<Entity> = w
373            .unreferenced()
374            .into_iter()
375            .filter(|&e| names.iter().any(|n| w.entity_has_named(e, n)))
376            .collect();
377        let (assets, rest): (Vec<Entity>, Vec<Entity>) =
378            independent.into_iter().partition(|&e| w.has::<Asset>(e));
379        let groups: Vec<PartitionGroup> = assets
380            .iter()
381            .map(|&root| PartitionGroup {
382                key: AssetField::Key
383                    .of(w, root)
384                    .or_else(|| AssetField::Name.of(w, root))
385                    .map_or_else(|| format!("asset {}", root.index()), str::to_owned),
386                roots: BTreeSet::from([root]),
387                context: BTreeSet::new(),
388                area: None,
389            })
390            .collect();
391        let mut groups = groups;
392        log.count("assets, each a part", assets.len() as u64);
393        if !rest.is_empty() {
394            log.count("content outside any asset, in its own part", rest.len() as u64);
395            groups.push(PartitionGroup {
396                key: "outside assets".into(),
397                roots: rest.into_iter().collect(),
398                context: BTreeSet::new(),
399                area: None,
400            });
401        }
402        // a part is named by its key: two assets of one source or name (or one named as the rest) can't be told apart
403        let mut keys = BTreeSet::new();
404        if let Some(g) = groups.iter().find(|g| !keys.insert(g.key.as_str())) {
405            return Err(format!(
406                "two parts would be keyed {:?}: assets share a source or a name (or one is named as the content outside \
407                 assets); give them distinct ones",
408                g.key
409            ));
410        }
411        w.insert_resource(PartitionPlan {
412            by: "partition-by-asset".into(),
413            groups,
414        });
415        Ok(())
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use crate::pass::{PassManager, PassRegistry, RunReport};
423    use crate::{
424        Binding, BindingId, Deformation, Influences, Joint, JointId, Primitive, Rest, Semantic, Slot, Stream,
425    };
426    use crate::{Topology, WeightRule};
427
428    /// An asset of a skeleton, a skinned prototype, a static prototype and an alternative: (world, root, [skinned,
429    /// static, alternative], skeleton).
430    fn world() -> (World, Entity, [Entity; 3], Entity) {
431        let mut w = World::new();
432        let skeleton = w.spawn();
433        w.insert(
434            skeleton,
435            Skeleton {
436                joints: vec![Joint {
437                    id: JointId(0),
438                    name: "root".into(),
439                    parent: None,
440                    rest: Rest::Affine(Transform::IDENTITY),
441                }],
442            },
443        );
444        let mesh = || {
445            Mesh::new(vec![Primitive {
446                topology: Topology::Points,
447                streams: [(Semantic::Position, Stream::F32x3(vec![[0.0; 3]]))].into(),
448                indices: None,
449                material: None,
450            }])
451        };
452        let parts = [w.spawn(), w.spawn(), w.spawn()];
453        for &p in &parts {
454            w.insert(p, mesh());
455        }
456        w.insert(
457            parts[0],
458            Skin {
459                influences: vec![Some(Influences::Whole { slot: 0 })],
460                weights: WeightRule::SumsToOne,
461                bindings: vec![Binding {
462                    id: BindingId(2),
463                    skeleton,
464                    palettes: vec![Some(vec![Slot {
465                        joint: JointId(0),
466                        inverse_bind: Transform::IDENTITY,
467                    }])],
468                    deformation: Deformation::Linear,
469                }],
470            },
471        );
472        let root = w.spawn();
473        w.insert(
474            root,
475            Asset {
476                members: vec![
477                    (skeleton, Role::Rig),
478                    (parts[0], Role::Part),
479                    (parts[1], Role::Part),
480                    (parts[2], Role::Alternative),
481                ],
482            },
483        );
484        (w, root, parts, skeleton)
485    }
486
487    #[test]
488    fn an_asset_reaches_its_members_and_verifies() {
489        let (mut w, root, parts, skeleton) = world();
490        assert_eq!(w.verify_components(), vec![]);
491        assert!(
492            w.closure([root])
493                .is_superset(&[skeleton, parts[0], parts[1], parts[2]].into())
494        );
495        w.get_mut::<Asset>(root)
496            .unwrap()
497            .members
498            .push((parts[0], Role::Part));
499        assert!(
500            w.verify_components()
501                .iter()
502                .any(|(_, _, m)| m.contains("listed twice"))
503        );
504        w.get_mut::<Asset>(root).unwrap().members.pop();
505        w.get_mut::<Asset>(root).unwrap().members[0].1 = Role::Clip;
506        assert!(
507            w.verify_components()
508                .iter()
509                .any(|(_, _, m)| m.contains("not what its role"))
510        );
511    }
512
513    /// A member still in its source's form (a reader's model before a pass lowers it to a mesh) fits its role; one
514    /// holding only core components that are not its role's does not.
515    #[test]
516    fn a_member_in_its_sources_form_fits_its_role() {
517        let (mut w, root, parts, _) = world();
518        w.remove::<Mesh>(parts[1]);
519        w.insert(parts[1], Name("model".into()));
520        assert!(
521            w.verify_components()
522                .iter()
523                .any(|(_, _, m)| m.contains("not what its role Part says"))
524        );
525        w.insert_opaque(
526            parts[1],
527            "source.model",
528            crate::Opaque {
529                version: 1,
530                data: crate::OpaqueData::Bytes(vec![1, 2, 3]),
531            },
532        )
533        .unwrap();
534        assert_eq!(w.verify_components(), vec![]);
535        assert!(w.closure([root]).contains(&parts[1]));
536        // in its source's form, but in core another role's: a clip member that is a mesh is no clip
537        let clip_member = parts[0];
538        w.get_mut::<Asset>(root)
539            .unwrap()
540            .members
541            .iter_mut()
542            .find(|(m, _)| *m == clip_member)
543            .unwrap()
544            .1 = Role::Clip;
545        w.insert_opaque(
546            clip_member,
547            "source.clip",
548            crate::Opaque {
549                version: 1,
550                data: crate::OpaqueData::Bytes(vec![4]),
551            },
552        )
553        .unwrap();
554        assert!(
555            w.verify_components()
556                .iter()
557                .any(|(_, _, m)| m.contains("not what its role Clip says")),
558            "{:?}",
559            w.verify_components()
560        );
561    }
562
563    fn registry() -> PassManager {
564        let mut r = PassRegistry::new();
565        crate::register(&mut r);
566        PassManager::new(r).with_pass_checks(true)
567    }
568
569    /// Two assets (`a`/`A` and `b`/`B`, one part each, both drawn with one material) and a loose placement of `b`'s
570    /// part: (world, [root a, part a, root b, part b, material, placement]).
571    fn two_assets() -> (World, [Entity; 6]) {
572        let mut w = World::new();
573        let material = w.spawn();
574        w.insert(material, Name("m".into()));
575        let mut part = || {
576            let p = w.spawn();
577            w.insert(
578                p,
579                Mesh::new(vec![Primitive {
580                    topology: Topology::Points,
581                    streams: [(Semantic::Position, Stream::F32x3(vec![[0.0; 3]]))].into(),
582                    indices: None,
583                    material: Some(material),
584                }]),
585            );
586            p
587        };
588        let (pa, pb) = (part(), part());
589        let mut root = |key: &str, name: &str, p: Entity| {
590            let r = w.spawn();
591            w.insert(r, Source(key.into()));
592            w.insert(r, Name(name.into()));
593            w.insert(
594                r,
595                Asset {
596                    members: vec![(p, Role::Part)],
597                },
598            );
599            r
600        };
601        let (ra, rb) = (root("a", "A", pa), root("b", "B", pb));
602        let placement = w.spawn();
603        w.insert(
604            placement,
605            Instance {
606                prototype: pb,
607                variant: 0,
608            },
609        );
610        w.insert(placement, Transform::IDENTITY);
611        (w, [ra, pa, rb, pb, material, placement])
612    }
613
614    /// Selecting keeps the chosen assets' closure (a material shared with a left-out asset too) and leaves out the
615    /// rest, the placement of a left-out part with it; by key or by name.
616    #[test]
617    fn select_assets_keeps_what_the_chosen_assets_reach() {
618        let m = registry();
619        for (pipeline, kept) in [
620            ("select-assets{keys=a}", [0, 1, 4]),
621            ("select-assets{names=B}", [2, 3, 4]),
622        ] {
623            let (mut w, e) = two_assets();
624            let mut report = RunReport::default();
625            m.run(&mut w, &m.registry.parse(pipeline).unwrap(), &mut report)
626                .unwrap();
627            let alive: BTreeSet<Entity> = w.entities().collect();
628            assert_eq!(alive, kept.map(|i| e[i]).into(), "{pipeline}");
629            assert_eq!(w.verify_components(), vec![], "{pipeline}");
630            let log = &report.steps[0].log;
631            assert_eq!(
632                (log.counts["assets selected"], log.counts["assets left out"]),
633                (1, 1)
634            );
635        }
636        // both, and the placement of b's part goes with it
637        let (mut w, e) = two_assets();
638        let mut report = RunReport::default();
639        m.run(
640            &mut w,
641            &m.registry.parse("select-assets{keys=b;a}").unwrap(),
642            &mut report,
643        )
644        .unwrap();
645        assert_eq!(
646            w.entities().collect::<BTreeSet<_>>(),
647            e[..5].iter().copied().collect()
648        );
649    }
650
651    #[test]
652    fn select_assets_refuses_a_value_no_asset_has_and_bad_options() {
653        let m = registry();
654        let (mut w, _) = two_assets();
655        let err = m
656            .run(
657                &mut w,
658                &m.registry.parse("select-assets{keys=a;c}").unwrap(),
659                &mut RunReport::default(),
660            )
661            .unwrap_err()
662            .to_string();
663        assert!(
664            err.contains("no asset whose core.source is c (the world's: a, b)"),
665            "{err}"
666        );
667        for (bad, why) in [
668            ("select-assets", "one of keys or names is required"),
669            (
670                "select-assets{keys=a,names=A}",
671                "only one of keys or names may be given",
672            ),
673            ("select-assets{keys=a;;b}", "an empty value"),
674            ("select-assets{keys=a;a}", "a twice"),
675            (
676                "select-assets{key=a}",
677                "unknown option key (select-assets takes keys, names)",
678            ),
679        ] {
680            let err = m.registry.parse(bad).unwrap_err().to_string();
681            assert!(err.contains(why), "{bad}: {err}");
682        }
683        // printed as parsed
684        let p = m.registry.parse("select-assets{names=B;A}").unwrap();
685        assert_eq!(p.to_string(), "select-assets{names=B;A}");
686    }
687
688    /// One part per asset, keyed by its source (else its name), and the loose placement in a part of its own.
689    #[test]
690    fn partition_by_asset_plans_a_part_per_asset() {
691        let m = registry();
692        let (mut w, e) = two_assets();
693        w.remove::<Source>(e[2]);
694        let mut report = RunReport::default();
695        m.run(
696            &mut w,
697            &m.registry.parse("partition-by-asset").unwrap(),
698            &mut report,
699        )
700        .unwrap();
701        let plan = w.resource::<PartitionPlan>().unwrap();
702        assert_eq!(plan.by, "partition-by-asset");
703        let groups: Vec<(&str, Vec<Entity>)> = plan
704            .groups
705            .iter()
706            .map(|g| (g.key.as_str(), g.roots.iter().copied().collect()))
707            .collect();
708        assert_eq!(
709            groups,
710            [
711                ("a", vec![e[0]]),
712                ("B", vec![e[2]]),
713                ("outside assets", vec![e[5]])
714            ]
715        );
716        assert!(
717            plan.groups
718                .iter()
719                .all(|g| g.context.is_empty() && g.area.is_none())
720        );
721        let log = &report.steps[0].log;
722        assert_eq!(log.counts["assets, each a part"], 2);
723        assert_eq!(log.counts["content outside any asset, in its own part"], 1);
724    }
725
726    /// Two assets keyed alike (the same source) would be two parts of one name: refused, naming the key.
727    #[test]
728    fn partition_by_asset_refuses_two_parts_of_one_key() {
729        let m = registry();
730        let (mut w, e) = two_assets();
731        w.insert(e[2], Source("a".into()));
732        let err = m
733            .run(
734                &mut w,
735                &m.registry.parse("partition-by-asset").unwrap(),
736                &mut RunReport::default(),
737            )
738            .unwrap_err();
739        assert!(err.to_string().contains("keyed \"a\""), "{err}");
740        assert!(w.resource::<PartitionPlan>().is_none());
741    }
742
743    /// Placing: one rig, a placement per part (the skinned one with its skin instance on that rig), the alternative
744    /// not placed; a part the source already places is left alone.
745    #[test]
746    fn place_assets_places_rigs_and_parts() {
747        let (mut w, _, parts, skeleton) = world();
748        let mut r = PassRegistry::new();
749        r.add(&PLACE_ASSETS);
750        let m = PassManager::new(r).with_pass_checks(true);
751        let mut report = RunReport::default();
752        m.run(&mut w, &m.registry.parse("place-assets").unwrap(), &mut report)
753            .unwrap();
754        let rigs: Vec<_> = w.iter::<RigInstance>().collect();
755        assert_eq!(rigs.len(), 1);
756        assert_eq!(rigs[0].1.skeleton, skeleton);
757        let placed: Vec<Entity> = w.iter::<Instance>().map(|(_, i)| i.prototype).collect();
758        assert_eq!(placed, [parts[0], parts[1]]);
759        let skinned: Vec<_> = w.iter::<SkinInstance>().collect();
760        assert_eq!(skinned.len(), 1);
761        assert_eq!(
762            (skinned[0].1.rig, skinned[0].1.binding),
763            (rigs[0].0, BindingId(2))
764        );
765        assert_eq!(w.verify_components(), vec![]);
766        let log = &report.steps[0].log;
767        assert_eq!(
768            log.counts["alternative parts not placed (a selection chooses)"],
769            1
770        );
771        assert_eq!(log.placed.len(), 3);
772        // placing again: every part already placed
773        let mut again = RunReport::default();
774        m.run(&mut w, &m.registry.parse("place-assets").unwrap(), &mut again)
775            .unwrap();
776        assert_eq!(
777            again.steps[0].log.counts["parts the source places, left as they are"],
778            2
779        );
780        // nor a rig: the skeleton keeps the one it has
781        assert_eq!(w.iter::<RigInstance>().count(), 1);
782        assert_eq!(
783            again.steps[0].log.counts["rigs already placed, left as they are"],
784            1
785        );
786        assert!(again.steps[0].log.placed.is_empty());
787    }
788}