use std::collections::{BTreeMap, BTreeSet};
use serde::Serialize;
use serde_json::Value;
use crate::convert::{PartitionGroup, PartitionPlan};
use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
use crate::{
Component, Entity, Instance, JsonError, Mesh, MeshFile, Name, Resource, RigInstance, Skeleton, Skin,
SkinInstance, Source, Transform, World,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub enum Role {
Part,
Rig,
Clip,
Alternative,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Asset {
pub members: Vec<(Entity, Role)>,
}
impl Component for Asset {
const NAME: &'static str = "core.asset";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn references(&self, out: &mut Vec<Entity>) {
out.extend(self.members.iter().map(|(e, _)| *e));
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
let names = w.component_names();
let in_source_form = |m: Entity| {
names
.iter()
.any(|n| !n.starts_with("core.") && w.entity_has_named(m, n))
};
let mut seen = BTreeSet::new();
for &(m, role) in &self.members {
if m == e || !seen.insert(m) {
return Err(format!("member {m:?} is the root or listed twice"));
}
let rig = w.has::<Skeleton>(m);
let clip = w.has::<crate::Clip>(m);
let part = w.has::<Mesh>(m) || w.has::<MeshFile>(m) || w.has::<Instance>(m);
let (own, other) = match role {
Role::Rig => (rig, clip || part),
Role::Clip => (clip, rig || part),
Role::Part | Role::Alternative => (part, rig || clip),
};
let fits = own || (!other && in_source_form(m));
if !fits {
return Err(format!("member {m:?} is not what its role {role:?} says"));
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PlaceAssets;
pub const PLACE_ASSETS: PassSpec = PassSpec {
name: "place-assets",
options: &[],
make: |_| Ok(Box::new(PlaceAssets)),
};
impl Pass for PlaceAssets {
fn name(&self) -> &'static str {
"place-assets"
}
fn may_lack_inputs(&self) -> bool {
true
}
fn requires(&self) -> &[&'static str] {
&[Asset::NAME]
}
fn provides(&self) -> &[&'static str] {
&[
Instance::NAME,
Transform::NAME,
RigInstance::NAME,
SkinInstance::NAME,
]
}
fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
let mut placed: BTreeSet<Entity> = w.iter::<Instance>().map(|(_, i)| i.prototype).collect();
let mut rigged: BTreeMap<Entity, Entity> = BTreeMap::new();
for (rig, r) in w.iter::<RigInstance>() {
rigged.entry(r.skeleton).or_insert(rig);
}
let assets: Vec<(Entity, Asset)> = w.iter::<Asset>().map(|(e, a)| (e, a.clone())).collect();
for (root, asset) in assets {
let mut rigs: Vec<(Entity, Entity)> = Vec::new();
for &(m, role) in &asset.members {
if role != Role::Rig {
continue;
}
if let Some(&rig) = rigged.get(&m) {
log.count("rigs already placed, left as they are", 1);
rigs.push((m, rig));
continue;
}
let rig = w.spawn();
w.insert(rig, RigInstance { skeleton: m });
w.insert(rig, Transform::IDENTITY);
log.place_from(rig, root);
rigged.insert(m, rig);
rigs.push((m, rig));
}
for &(m, role) in &asset.members {
match role {
Role::Alternative => log.count("alternative parts not placed (a selection chooses)", 1),
Role::Part if w.has::<Instance>(m) || placed.contains(&m) => {
log.count("parts the source places, left as they are", 1);
}
Role::Part => {
let p = w.spawn();
w.insert(
p,
Instance {
prototype: m,
variant: 0,
},
);
w.insert(p, Transform::IDENTITY);
let binding = w.get::<Skin>(m).and_then(|s| {
s.bindings.iter().find_map(|b| {
rigs.iter()
.find(|(skel, _)| *skel == b.skeleton)
.map(|&(_, rig)| (rig, b.id))
})
});
if let Some((rig, binding)) = binding {
w.insert(p, SkinInstance { rig, binding });
log.count("skinned parts placed with their rig", 1);
}
log.place_from(p, root);
log.count("parts placed", 1);
placed.insert(m);
}
Role::Rig | Role::Clip => {}
}
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AssetField {
Key,
Name,
}
impl AssetField {
fn option(self) -> &'static str {
match self {
AssetField::Key => "keys",
AssetField::Name => "names",
}
}
fn of(self, w: &World, root: Entity) -> Option<&str> {
match self {
AssetField::Key => w.get::<Source>(root).map(|s| s.0.as_str()),
AssetField::Name => w.get::<Name>(root).map(|n| n.0.as_str()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SelectAssets {
pub field: AssetField,
pub values: Vec<String>,
}
pub const SELECT_ASSETS: PassSpec = PassSpec {
name: "select-assets",
options: &[
OptionSpec::one_of(
"assets",
"keys",
"example",
"the assets' keys, `;` between, each once",
),
OptionSpec::one_of(
"assets",
"names",
"example",
"the assets' names, `;` between, each once",
),
],
make: select_assets,
};
fn select_assets(g: &Given) -> Result<Box<dyn Pass>, String> {
let (k, field, v) = match (g.optional("keys"), g.optional("names")) {
(Some(v), _) => ("keys", AssetField::Key, v),
(None, Some(v)) => ("names", AssetField::Name, v),
(None, None) => unreachable!("the declaration's check gives one of keys and names"),
};
let mut values: Vec<String> = Vec::new();
for value in v.split(';').map(str::trim) {
if value.is_empty() {
return Err(format!("{k}={v}: an empty value"));
}
if values.iter().any(|x| x == value) {
return Err(format!("{k}={v}: {value} twice"));
}
values.push(value.to_owned());
}
Ok(Box::new(SelectAssets { field, values }))
}
impl Pass for SelectAssets {
fn name(&self) -> &'static str {
"select-assets"
}
fn options(&self) -> Options {
Options::from([(self.field.option().to_owned(), self.values.join(";"))])
}
fn requires(&self) -> &[&'static str] {
&[Asset::NAME]
}
fn despawns_entities(&self) -> bool {
true
}
fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
keep_assets(w, self.field, &self.values, log)
}
}
pub fn keep_assets(
w: &mut World,
field: AssetField,
values: &[String],
log: &mut PassLog,
) -> Result<(), String> {
let roots: Vec<Entity> = w.iter::<Asset>().map(|(e, _)| e).collect();
let mut selected = BTreeSet::new();
for value in values {
let matched: Vec<Entity> = roots
.iter()
.copied()
.filter(|&r| field.of(w, r) == Some(value.as_str()))
.collect();
if matched.is_empty() {
let mut there: Vec<&str> = roots.iter().filter_map(|&r| field.of(w, r)).collect();
there.sort_unstable();
let shown = there.iter().take(20).copied().collect::<Vec<_>>().join(", ");
let more = there.len().saturating_sub(20);
return Err(format!(
"no asset whose {} is {value} (the world's: {shown}{})",
match field {
AssetField::Key => "core.source",
AssetField::Name => "core.name",
},
if more > 0 {
format!(" and {more} more")
} else {
String::new()
}
));
}
selected.extend(matched);
}
let keep = w.closure(selected.iter().copied());
let gone: Vec<Entity> = w.entities().filter(|e| !keep.contains(e)).collect();
log.count("assets selected", selected.len() as u64);
log.count("assets left out", (roots.len() - selected.len()) as u64);
for e in gone {
crate::ledger::despawn(w, e, log, "not in a selected asset");
}
Ok(())
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PartitionByAsset;
pub const PARTITION_BY_ASSET: PassSpec = PassSpec {
name: "partition-by-asset",
options: &[],
make: |_| Ok(Box::new(PartitionByAsset)),
};
impl Pass for PartitionByAsset {
fn name(&self) -> &'static str {
"partition-by-asset"
}
fn requires(&self) -> &[&'static str] {
&[Asset::NAME]
}
fn provides(&self) -> &[&'static str] {
&[PartitionPlan::NAME]
}
fn modifies(&self) -> &[&'static str] {
&[PartitionPlan::NAME]
}
fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
let names = w.component_names();
let independent: Vec<Entity> = w
.unreferenced()
.into_iter()
.filter(|&e| names.iter().any(|n| w.entity_has_named(e, n)))
.collect();
let (assets, rest): (Vec<Entity>, Vec<Entity>) =
independent.into_iter().partition(|&e| w.has::<Asset>(e));
let groups: Vec<PartitionGroup> = assets
.iter()
.map(|&root| PartitionGroup {
key: AssetField::Key
.of(w, root)
.or_else(|| AssetField::Name.of(w, root))
.map_or_else(|| format!("asset {}", root.index()), str::to_owned),
roots: BTreeSet::from([root]),
context: BTreeSet::new(),
area: None,
})
.collect();
let mut groups = groups;
log.count("assets, each a part", assets.len() as u64);
if !rest.is_empty() {
log.count("content outside any asset, in its own part", rest.len() as u64);
groups.push(PartitionGroup {
key: "outside assets".into(),
roots: rest.into_iter().collect(),
context: BTreeSet::new(),
area: None,
});
}
let mut keys = BTreeSet::new();
if let Some(g) = groups.iter().find(|g| !keys.insert(g.key.as_str())) {
return Err(format!(
"two parts would be keyed {:?}: assets share a source or a name (or one is named as the content outside \
assets); give them distinct ones",
g.key
));
}
w.insert_resource(PartitionPlan {
by: "partition-by-asset".into(),
groups,
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pass::{PassManager, PassRegistry, RunReport};
use crate::{
Binding, BindingId, Deformation, Influences, Joint, JointId, Primitive, Rest, Semantic, Slot, Stream,
};
use crate::{Topology, WeightRule};
fn world() -> (World, Entity, [Entity; 3], Entity) {
let mut w = World::new();
let skeleton = w.spawn();
w.insert(
skeleton,
Skeleton {
joints: vec![Joint {
id: JointId(0),
name: "root".into(),
parent: None,
rest: Rest::Affine(Transform::IDENTITY),
}],
},
);
let mesh = || {
Mesh::new(vec![Primitive {
topology: Topology::Points,
streams: [(Semantic::Position, Stream::F32x3(vec![[0.0; 3]]))].into(),
indices: None,
material: None,
}])
};
let parts = [w.spawn(), w.spawn(), w.spawn()];
for &p in &parts {
w.insert(p, mesh());
}
w.insert(
parts[0],
Skin {
influences: vec![Some(Influences::Whole { slot: 0 })],
weights: WeightRule::SumsToOne,
bindings: vec![Binding {
id: BindingId(2),
skeleton,
palettes: vec![Some(vec![Slot {
joint: JointId(0),
inverse_bind: Transform::IDENTITY,
}])],
deformation: Deformation::Linear,
}],
},
);
let root = w.spawn();
w.insert(
root,
Asset {
members: vec![
(skeleton, Role::Rig),
(parts[0], Role::Part),
(parts[1], Role::Part),
(parts[2], Role::Alternative),
],
},
);
(w, root, parts, skeleton)
}
#[test]
fn an_asset_reaches_its_members_and_verifies() {
let (mut w, root, parts, skeleton) = world();
assert_eq!(w.verify_components(), vec![]);
assert!(
w.closure([root])
.is_superset(&[skeleton, parts[0], parts[1], parts[2]].into())
);
w.get_mut::<Asset>(root)
.unwrap()
.members
.push((parts[0], Role::Part));
assert!(
w.verify_components()
.iter()
.any(|(_, _, m)| m.contains("listed twice"))
);
w.get_mut::<Asset>(root).unwrap().members.pop();
w.get_mut::<Asset>(root).unwrap().members[0].1 = Role::Clip;
assert!(
w.verify_components()
.iter()
.any(|(_, _, m)| m.contains("not what its role"))
);
}
#[test]
fn a_member_in_its_sources_form_fits_its_role() {
let (mut w, root, parts, _) = world();
w.remove::<Mesh>(parts[1]);
w.insert(parts[1], Name("model".into()));
assert!(
w.verify_components()
.iter()
.any(|(_, _, m)| m.contains("not what its role Part says"))
);
w.insert_opaque(
parts[1],
"source.model",
crate::Opaque {
version: 1,
data: crate::OpaqueData::Bytes(vec![1, 2, 3]),
},
)
.unwrap();
assert_eq!(w.verify_components(), vec![]);
assert!(w.closure([root]).contains(&parts[1]));
let clip_member = parts[0];
w.get_mut::<Asset>(root)
.unwrap()
.members
.iter_mut()
.find(|(m, _)| *m == clip_member)
.unwrap()
.1 = Role::Clip;
w.insert_opaque(
clip_member,
"source.clip",
crate::Opaque {
version: 1,
data: crate::OpaqueData::Bytes(vec![4]),
},
)
.unwrap();
assert!(
w.verify_components()
.iter()
.any(|(_, _, m)| m.contains("not what its role Clip says")),
"{:?}",
w.verify_components()
);
}
fn registry() -> PassManager {
let mut r = PassRegistry::new();
crate::register(&mut r);
PassManager::new(r).with_pass_checks(true)
}
fn two_assets() -> (World, [Entity; 6]) {
let mut w = World::new();
let material = w.spawn();
w.insert(material, Name("m".into()));
let mut part = || {
let p = w.spawn();
w.insert(
p,
Mesh::new(vec![Primitive {
topology: Topology::Points,
streams: [(Semantic::Position, Stream::F32x3(vec![[0.0; 3]]))].into(),
indices: None,
material: Some(material),
}]),
);
p
};
let (pa, pb) = (part(), part());
let mut root = |key: &str, name: &str, p: Entity| {
let r = w.spawn();
w.insert(r, Source(key.into()));
w.insert(r, Name(name.into()));
w.insert(
r,
Asset {
members: vec![(p, Role::Part)],
},
);
r
};
let (ra, rb) = (root("a", "A", pa), root("b", "B", pb));
let placement = w.spawn();
w.insert(
placement,
Instance {
prototype: pb,
variant: 0,
},
);
w.insert(placement, Transform::IDENTITY);
(w, [ra, pa, rb, pb, material, placement])
}
#[test]
fn select_assets_keeps_what_the_chosen_assets_reach() {
let m = registry();
for (pipeline, kept) in [
("select-assets{keys=a}", [0, 1, 4]),
("select-assets{names=B}", [2, 3, 4]),
] {
let (mut w, e) = two_assets();
let mut report = RunReport::default();
m.run(&mut w, &m.registry.parse(pipeline).unwrap(), &mut report)
.unwrap();
let alive: BTreeSet<Entity> = w.entities().collect();
assert_eq!(alive, kept.map(|i| e[i]).into(), "{pipeline}");
assert_eq!(w.verify_components(), vec![], "{pipeline}");
let log = &report.steps[0].log;
assert_eq!(
(log.counts["assets selected"], log.counts["assets left out"]),
(1, 1)
);
}
let (mut w, e) = two_assets();
let mut report = RunReport::default();
m.run(
&mut w,
&m.registry.parse("select-assets{keys=b;a}").unwrap(),
&mut report,
)
.unwrap();
assert_eq!(
w.entities().collect::<BTreeSet<_>>(),
e[..5].iter().copied().collect()
);
}
#[test]
fn select_assets_refuses_a_value_no_asset_has_and_bad_options() {
let m = registry();
let (mut w, _) = two_assets();
let err = m
.run(
&mut w,
&m.registry.parse("select-assets{keys=a;c}").unwrap(),
&mut RunReport::default(),
)
.unwrap_err()
.to_string();
assert!(
err.contains("no asset whose core.source is c (the world's: a, b)"),
"{err}"
);
for (bad, why) in [
("select-assets", "one of keys or names is required"),
(
"select-assets{keys=a,names=A}",
"only one of keys or names may be given",
),
("select-assets{keys=a;;b}", "an empty value"),
("select-assets{keys=a;a}", "a twice"),
(
"select-assets{key=a}",
"unknown option key (select-assets takes keys, names)",
),
] {
let err = m.registry.parse(bad).unwrap_err().to_string();
assert!(err.contains(why), "{bad}: {err}");
}
let p = m.registry.parse("select-assets{names=B;A}").unwrap();
assert_eq!(p.to_string(), "select-assets{names=B;A}");
}
#[test]
fn partition_by_asset_plans_a_part_per_asset() {
let m = registry();
let (mut w, e) = two_assets();
w.remove::<Source>(e[2]);
let mut report = RunReport::default();
m.run(
&mut w,
&m.registry.parse("partition-by-asset").unwrap(),
&mut report,
)
.unwrap();
let plan = w.resource::<PartitionPlan>().unwrap();
assert_eq!(plan.by, "partition-by-asset");
let groups: Vec<(&str, Vec<Entity>)> = plan
.groups
.iter()
.map(|g| (g.key.as_str(), g.roots.iter().copied().collect()))
.collect();
assert_eq!(
groups,
[
("a", vec![e[0]]),
("B", vec![e[2]]),
("outside assets", vec![e[5]])
]
);
assert!(
plan.groups
.iter()
.all(|g| g.context.is_empty() && g.area.is_none())
);
let log = &report.steps[0].log;
assert_eq!(log.counts["assets, each a part"], 2);
assert_eq!(log.counts["content outside any asset, in its own part"], 1);
}
#[test]
fn partition_by_asset_refuses_two_parts_of_one_key() {
let m = registry();
let (mut w, e) = two_assets();
w.insert(e[2], Source("a".into()));
let err = m
.run(
&mut w,
&m.registry.parse("partition-by-asset").unwrap(),
&mut RunReport::default(),
)
.unwrap_err();
assert!(err.to_string().contains("keyed \"a\""), "{err}");
assert!(w.resource::<PartitionPlan>().is_none());
}
#[test]
fn place_assets_places_rigs_and_parts() {
let (mut w, _, parts, skeleton) = world();
let mut r = PassRegistry::new();
r.add(&PLACE_ASSETS);
let m = PassManager::new(r).with_pass_checks(true);
let mut report = RunReport::default();
m.run(&mut w, &m.registry.parse("place-assets").unwrap(), &mut report)
.unwrap();
let rigs: Vec<_> = w.iter::<RigInstance>().collect();
assert_eq!(rigs.len(), 1);
assert_eq!(rigs[0].1.skeleton, skeleton);
let placed: Vec<Entity> = w.iter::<Instance>().map(|(_, i)| i.prototype).collect();
assert_eq!(placed, [parts[0], parts[1]]);
let skinned: Vec<_> = w.iter::<SkinInstance>().collect();
assert_eq!(skinned.len(), 1);
assert_eq!(
(skinned[0].1.rig, skinned[0].1.binding),
(rigs[0].0, BindingId(2))
);
assert_eq!(w.verify_components(), vec![]);
let log = &report.steps[0].log;
assert_eq!(
log.counts["alternative parts not placed (a selection chooses)"],
1
);
assert_eq!(log.placed.len(), 3);
let mut again = RunReport::default();
m.run(&mut w, &m.registry.parse("place-assets").unwrap(), &mut again)
.unwrap();
assert_eq!(
again.steps[0].log.counts["parts the source places, left as they are"],
2
);
assert_eq!(w.iter::<RigInstance>().count(), 1);
assert_eq!(
again.steps[0].log.counts["rigs already placed, left as they are"],
1
);
assert!(again.steps[0].log.placed.is_empty());
}
}