Files
av3-animation-as-crab/rust/src/export.rs
T
2026-09-18 14:01:10 +01:00

326 lines
12 KiB
Rust

use crate::graph::*;
use std::collections::{HashMap, HashSet};
/// Sort keyframes into time order, then validate, then serialize.
pub fn to_json(graph: &ControllerGraph) -> Result<String, String> {
let mut graph = graph.clone();
normalize(&mut graph);
validate(&graph)?;
serde_json::to_string_pretty(&graph).map_err(|e| format!("serialization failed: {e}"))
}
fn normalize(graph: &mut ControllerGraph) {
for clip in &mut graph.clips {
for curve in &mut clip.curves {
curve
.keys
.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
}
}
}
fn validate(graph: &ControllerGraph) -> Result<(), String> {
let mut errors = Vec::new();
if graph.system_name.trim().is_empty() {
errors.push("system_name is not set (call `aac.system_name(\"...\")`)".to_string());
}
if graph.asset_key.trim().is_empty() {
errors.push("asset_key is not set (call `aac.asset_key(\"...\")`)".to_string());
}
let parameters = parameter_types(graph, &mut errors);
validate_clips(graph, &mut errors);
validate_blend_trees(graph, &parameters, &mut errors);
validate_layers(graph, &parameters, &mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(format!(
"graph is invalid ({} problem{}):\n- {}",
errors.len(),
if errors.len() == 1 { "" } else { "s" },
errors.join("\n- ")
))
}
}
#[derive(Clone, Copy, PartialEq)]
enum ParamType {
Float,
Int,
Bool,
}
fn parameter_types(graph: &ControllerGraph, errors: &mut Vec<String>) -> HashMap<String, ParamType> {
let mut seen = HashSet::new();
let mut map = HashMap::new();
for parameter in &graph.parameters {
let name = parameter.name();
if name.trim().is_empty() {
errors.push("a parameter has an empty name".to_string());
continue;
}
if !seen.insert(name.to_string()) {
errors.push(format!("parameter `{name}` is declared more than once"));
}
let ty = match parameter {
Parameter::Float { .. } => ParamType::Float,
Parameter::Int { .. } => ParamType::Int,
Parameter::Bool { .. } => ParamType::Bool,
};
map.insert(name.to_string(), ty);
}
map
}
fn validate_clips(graph: &ControllerGraph, errors: &mut Vec<String>) {
let mut seen = HashSet::new();
for clip in &graph.clips {
if clip.name.trim().is_empty() {
errors.push("a clip has an empty name".to_string());
} else if !seen.insert(clip.name.clone()) {
errors.push(format!("clip `{}` is declared more than once", clip.name));
}
for curve in &clip.curves {
if curve.path.trim().is_empty() {
errors.push(format!("clip `{}` has a curve with an empty path", clip.name));
}
if curve.keys.is_empty() {
errors.push(format!(
"clip `{}` curve `{}` on `{}` has no keyframes",
clip.name, curve.property, curve.path
));
}
for key in &curve.keys {
if !key.time.is_finite() || !key.value.is_finite() {
errors.push(format!(
"clip `{}` curve `{}` on `{}` has a non-finite keyframe",
clip.name, curve.property, curve.path
));
}
}
}
}
}
fn validate_blend_trees(
graph: &ControllerGraph,
parameters: &HashMap<String, ParamType>,
errors: &mut Vec<String>,
) {
let mut seen = HashSet::new();
for tree in &graph.blend_trees {
if tree.name.trim().is_empty() {
errors.push("a blend tree has an empty name".to_string());
} else if !seen.insert(tree.name.clone()) {
errors.push(format!("blend tree `{}` is declared more than once", tree.name));
}
let has_y = tree.param_y.as_ref().is_some_and(|y| !y.trim().is_empty());
if tree.blend_type == BlendType::Direct {
if !tree.param_x.trim().is_empty() || tree.param_y.is_some() {
errors.push(format!(
"blend tree `{}` is Direct and must not have blend parameters",
tree.name
));
}
} else {
if tree.param_x.trim().is_empty() {
errors.push(format!("blend tree `{}` has no x parameter", tree.name));
} else {
expect_param(parameters, &tree.param_x, ParamType::Float, &tree.name, "x", errors);
}
if tree.blend_type.is_2d() && !has_y {
errors.push(format!(
"blend tree `{}` is 2D and needs a y parameter",
tree.name
));
}
if !tree.blend_type.is_2d() && has_y {
errors.push(format!(
"blend tree `{}` is {:?} and must not have a y parameter",
tree.name, tree.blend_type
));
}
if tree.blend_type.is_2d() {
if let Some(y) = &tree.param_y {
expect_param(parameters, y, ParamType::Float, &tree.name, "y", errors);
}
}
}
if tree.children.is_empty() {
errors.push(format!("blend tree `{}` has no children", tree.name));
}
for child in &tree.children {
expect_motion(&child.motion, graph, errors);
if tree.blend_type == BlendType::Direct {
match &child.direct_param {
None => errors.push(format!(
"blend tree `{}` is Direct and a child has no direct parameter",
tree.name
)),
Some(p) => expect_param(parameters, p, ParamType::Float, &tree.name, "direct", errors),
}
} else if child.direct_param.is_some() {
errors.push(format!(
"blend tree `{}` is {:?} and thresholds are used, so a child must not have a direct parameter",
tree.name, tree.blend_type
));
}
}
}
}
fn validate_layers(
graph: &ControllerGraph,
parameters: &HashMap<String, ParamType>,
errors: &mut Vec<String>,
) {
let mut layer_names = HashSet::new();
for layer in &graph.controller.layers {
let label = if layer.name.trim().is_empty() {
errors.push("a layer has an empty name".to_string());
"<unnamed>".to_string()
} else {
if !layer_names.insert(layer.name.clone()) {
errors.push(format!("layer `{}` is declared more than once", layer.name));
}
layer.name.clone()
};
// Transitions cannot cross layers, so resolution is per-layer.
let mut machines = Vec::new();
walk_machines(&layer.state_machine, &label, &mut machines);
let mut state_names = HashSet::new();
for (machine_label, machine) in &machines {
for state in &machine.states {
if state.name.trim().is_empty() {
errors.push(format!("machine `{machine_label}` has a state with an empty name"));
} else if !state_names.insert(state.name.clone()) {
errors.push(format!(
"state `{}` is declared more than once in layer `{label}`",
state.name
));
}
}
}
for (machine_label, machine) in &machines {
for state in &machine.states {
if let Some(motion) = &state.motion {
expect_motion(motion, graph, errors);
}
for transition in &state.transitions {
validate_transition(
transition,
&format!("state `{}`", state.name),
&state_names,
parameters,
errors,
);
}
}
for transition in &machine.any_state_transitions {
validate_transition(
transition,
&format!("machine `{machine_label}` any-state"),
&state_names,
parameters,
errors,
);
}
}
}
}
/// State names are collected in a first pass so that forward references resolve.
fn walk_machines<'a>(machine: &'a StateMachine, label: &str, out: &mut Vec<(String, &'a StateMachine)>) {
out.push((label.to_string(), machine));
for sub in &machine.sub_machines {
let name = sub.name.as_deref().unwrap_or("<unnamed>");
walk_machines(sub, &format!("{label}/{name}"), out);
}
}
fn validate_transition(
transition: &Transition,
origin: &str,
state_names: &HashSet<String>,
parameters: &HashMap<String, ParamType>,
errors: &mut Vec<String>,
) {
if !state_names.contains(&transition.to) {
errors.push(format!(
"{origin} transitions to `{}`, which is not a state in that layer",
transition.to
));
}
if !transition.duration.is_finite() || !transition.exit_time.is_finite() {
errors.push(format!("{origin} has a non-finite duration or exit time"));
}
if !transition.conditions.is_empty() && transition.has_exit_time {
errors.push(format!(
"{origin} has both conditions and an exit time; Unity will ignore the conditions"
));
}
for condition in &transition.conditions {
let Some(ty) = parameters.get(&condition.parameter) else {
errors.push(format!(
"{origin} uses parameter `{}`, which is not declared",
condition.parameter
));
continue;
};
if !condition.threshold.is_finite() {
errors.push(format!("{origin} has a non-finite condition threshold"));
}
let ok = match condition.mode {
CondMode::If | CondMode::IfNot => *ty == ParamType::Bool,
CondMode::Greater | CondMode::Less => *ty != ParamType::Bool,
CondMode::Equals | CondMode::NotEqual => true,
};
if !ok {
errors.push(format!(
"{origin} uses {:?} on `{}`, but that parameter cannot be compared that way",
condition.mode, condition.parameter
));
}
}
}
fn expect_motion(motion: &MotionRef, graph: &ControllerGraph, errors: &mut Vec<String>) {
let (kind, name, exists) = match motion {
MotionRef::Clip { name } => ("clip", name, graph.clips.iter().any(|c| &c.name == name)),
MotionRef::BlendTree { name } => (
"blend tree",
name,
graph.blend_trees.iter().any(|t| &t.name == name),
),
};
if !exists {
errors.push(format!("reference to {kind} `{name}`, which is not declared"));
}
}
fn expect_param(
parameters: &HashMap<String, ParamType>,
name: &str,
expected: ParamType,
owner: &str,
role: &str,
errors: &mut Vec<String>,
) {
match parameters.get(name) {
None => errors.push(format!(
"blend tree `{owner}` uses `{name}` as its {role} parameter, which is not declared"
)),
Some(actual) if *actual != expected => errors.push(format!(
"blend tree `{owner}` uses `{name}` as its {role} parameter, which is not a float parameter"
)),
Some(_) => {}
}
}