use crate::components::{CharacterCapsule, CharacterShape};
use crate::ecs::{SkinnedMeshHandle, World};
use crate::gfx::shape_preview;
use serde_json::{Map, Value};
use super::Apply;
const KEYS: [&str; 2] = ["sliders", "proportions"];
pub(super) fn plan(
world: &World,
entries: &[Value],
args: &Map<String, Value>,
keys: &[String],
) -> Option<Apply> {
if !keys.iter().all(|k| KEYS.contains(&k.as_str())) {
return None;
}
let mesh = args.get("target")?.as_str()?;
let target = shape_preview::mesh_handle(world, crate::ecs::asset_id::lookup(mesh)?)?;
let shape = shape_of(args, target)?;
Some(Apply::Shape {
shape,
capsule: capsule_of(entries, mesh),
})
}
fn shape_of(args: &Map<String, Value>, target: SkinnedMeshHandle) -> Option<CharacterShape> {
fn list<T: serde::de::DeserializeOwned + Default>(
args: &Map<String, Value>,
key: &str,
) -> Option<T> {
match args.get(key) {
None | Some(Value::Null) => Some(T::default()),
Some(v) => serde_json::from_value(v.clone()).ok(),
}
}
Some(CharacterShape {
target: Some(target),
sliders: list(args, "sliders")?,
proportions: list(args, "proportions")?,
..Default::default()
})
}
fn capsule_of(entries: &[Value], mesh: &str) -> Option<CharacterCapsule> {
let entry = entries
.iter()
.find(|e| e.get("name").and_then(|v| v.as_str()) == Some(mesh))?;
serde_json::from_value(entry.pointer("/args/capsule")?.clone()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn args(v: Value) -> Map<String, Value> {
v.as_object().cloned().unwrap()
}
#[test]
fn shape_values_build_the_component_against_its_target() {
let shape = shape_of(
&args(json!({
"target": "hero_mesh",
"sliders": [{ "name": "weight", "value": 0.4 }],
"proportions": [{ "joint": "spine", "scale": 1.1, "length": 0.0 }],
})),
SkinnedMeshHandle(3),
)
.expect("shape values build");
assert_eq!(shape.target, Some(SkinnedMeshHandle(3)));
assert_eq!(shape.sliders.len(), 1);
assert_eq!(shape.proportions[0].scale, 1.1);
assert!(!shape.bake, "a live re-seed never flattens");
}
#[test]
fn absent_lists_build_an_empty_shape() {
let shape = shape_of(
&args(json!({ "target": "hero_mesh" })),
SkinnedMeshHandle(0),
)
.expect("an empty shape builds");
assert!(shape.sliders.is_empty() && shape.proportions.is_empty());
}
#[test]
fn a_malformed_list_declines() {
assert!(
shape_of(
&args(json!({ "sliders": "not a list" })),
SkinnedMeshHandle(0)
)
.is_none()
);
}
#[test]
fn the_capsule_comes_from_the_target_mesh() {
let entries = vec![
json!({ "name": "plain_mesh", "type": "SkinnedMesh", "args": {} }),
json!({
"name": "hero_mesh",
"type": "SkinnedMesh",
"args": { "capsule": { "half_height": 0.8, "radius": 0.3 } },
}),
];
assert_eq!(
capsule_of(&entries, "hero_mesh").expect("declared").radius,
0.3
);
assert!(
capsule_of(&entries, "plain_mesh").is_none(),
"a mesh may declare none"
);
assert!(capsule_of(&entries, "absent").is_none());
}
#[test]
fn retargeting_and_baking_decline() {
let world = World::new();
let shape = args(json!({ "target": "hero_mesh", "sliders": [] }));
assert!(plan(&world, &[], &shape, &["target".to_string()]).is_none());
assert!(plan(&world, &[], &shape, &["bake".to_string()]).is_none());
}
#[test]
fn an_absent_target_declines() {
let world = World::new();
assert!(
plan(
&world,
&[],
&args(json!({ "target": "hero_mesh", "sliders": [] })),
&["sliders".to_string()],
)
.is_none()
);
}
}