Skip to main content

primase_core/
controller.rs

1//! A layered clip-selection controller and its rig binding: the clips a source's states choose, the layers they play
2//! in, and how a layer applies to the rig's joints (its weight, which a controller parameter may set, and its mask).
3//! The animation toolkit evaluates it.
4//!
5//! Meaning (core's rule for layers; a source whose engine composes otherwise keeps that in its dialect):
6//! - Layers compose in order, the first at the bottom. A layer plays its selected state's clip over the pose below it
7//!   (`core.clip`'s own rule: a joint or channel the clip does not drive keeps the pose below, a delta clip is
8//!   composed onto it), and the result is the pose below mixed toward that one, per joint, by the layer's weight times
9//!   the joint's weight in its [`LayerMask`]. A morph, control, draw-group, material or root-motion channel follows the
10//!   same rule: the layer does not change one its clip does not drive, and mixes one it drives by its weight. A
11//!   discrete value (a draw group), and a value with nothing below it to mix with, take the layer's only at weight 1;
12//!   at a weight between 0 and 1 that is a refusal, never a guess.
13//! - The weight is the numeric parameter's value where [`ControllerLayer::weight_parameter`] names one, else
14//!   [`ControllerLayer::default_weight`]; the toolkit refuses a weight outside 0..=1 instead of clamping it. A layer of
15//!   weight 0 contributes nothing, whatever its states hold.
16//! - [`LayerBlending::Override`] plays the clip as it stands. [`LayerBlending::Additive`] requires the clip to state
17//!   itself a delta (`Clip::additive`): a clip that does not is refused by name, not taken as a delta by assumption.
18//!   [`LayerBlending::Other`] is refused.
19//! - A mask lists the joints the layer drives and their weights; a joint not listed has weight 0. A layer with no mask
20//!   drives every joint at full strength. A mask core has no form for stays named ([`LayerMask::Unsupported`]) and the
21//!   layer is refused by name.
22//!
23//! What stays in the source dialect: transitions between states, the events and fades that turn a layer on or off,
24//! synchronised layers and IK. This projection neither applies them nor replaces them with something else.
25use crate::{Clip, Component, Entity, Geometry, JointId, JsonError, RigInstance, Skeleton, World};
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use std::collections::BTreeSet;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
31pub enum ControllerParameterKind {
32    Float,
33    Int,
34    Bool,
35    Trigger,
36}
37
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct ControllerParameter {
41    pub name: String,
42    pub kind: ControllerParameterKind,
43    /// Finite. Int is integral; Bool/Trigger are exactly 0 or 1. Never rounded or clamped.
44    pub default: f64,
45}
46
47/// The layer's source-stated composition. Unknown values stay explicit, not guessed as override.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
49pub enum LayerBlending {
50    Override,
51    Additive,
52    Other(i64),
53}
54
55/// A selected state's motion. Blend thresholds are finite and strictly increasing, one or more
56/// children. A lowering keeps an unsupported tree as a named refusal instead of selecting a child.
57#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub enum Motion {
60    Clip(Entity),
61    /// A clip the source lists and has not loaded yet (the entity holds no `core.clip`, or not yet the one this state
62    /// plays): the state plays it once it is loaded and the lowering that made this controller has run again, which states
63    /// a [`Motion::Clip`]. Until then a consumer refuses the state by name, like [`Motion::Unsupported`], and may ask the
64    /// source to load the entity. Not verified against the entity's components: it may hold `core.clip` already, between
65    /// the load and that lowering.
66    Pending(Entity),
67    Blend1D {
68        parameter: usize,
69        children: Vec<(f32, Motion)>,
70    },
71    Unsupported(String),
72}
73
74#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct ControllerState {
77    pub name: String,
78    pub motion: Motion,
79    pub speed: f64,
80    /// Numeric controller parameter multiplying speed, or none.
81    pub speed_parameter: Option<usize>,
82    pub cycle_offset: f32,
83    /// Numeric controller parameter supplying the cycle offset, or none.
84    pub cycle_offset_parameter: Option<usize>,
85}
86
87/// One joint's weight in a layer's mask, named as `core.clip` names a joint channel's target: by the skeleton and the
88/// joint's identity in it (never by name). A controller may therefore be bound to any rig whose skeleton an entry names;
89/// the entries of another skeleton do not apply to it.
90#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct MaskedJoint {
93    pub skeleton: Entity,
94    #[serde(deserialize_with = "joint_id")]
95    pub joint: JointId,
96    /// Finite, 0 to 1; a source's weight outside that range is not lowered (never clamped).
97    pub weight: f32,
98}
99
100fn joint_id<'de, D: serde::Deserializer<'de>>(d: D) -> Result<JointId, D::Error> {
101    u32::deserialize(d).map(JointId)
102}
103
104/// An `Option` field that must be present in the JSON (`null` for none): serde fills an absent one in as `None`
105/// otherwise, and for a mask that would read as "unmasked".
106fn stated<'de, D: serde::Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Option<T>, D::Error> {
107    Option::<T>::deserialize(d)
108}
109
110/// Which joints a layer drives, and how strongly (see the module documentation).
111#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub enum LayerMask {
114    /// The listed joints at their weights; a joint not listed has weight 0. Each (skeleton, joint) once.
115    Joints(Vec<MaskedJoint>),
116    /// A mask the source states and core has no form for, or a layer rule it cannot state (the way
117    /// [`Motion::Unsupported`] names a motion): a non-empty reason. The layer is refused by name, never applied unmasked.
118    Unsupported(String),
119}
120
121#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
122#[serde(deny_unknown_fields)]
123pub struct ControllerLayer {
124    pub name: String,
125    pub states: Vec<ControllerState>,
126    /// The state the layer starts in: an index of `states`. `None` exactly when the layer has no states: such a layer
127    /// plays nothing (a source's empty layer, kept so the layers still line up with the source's).
128    pub default_state: Option<usize>,
129    /// As stored; finite, never silently clamped to a consumer's supported range. The weight the layer has when no
130    /// parameter sets one (`weight_parameter`).
131    pub default_weight: f32,
132    /// A numeric controller parameter (an index of `parameters`) whose value is the layer's weight, where the source
133    /// binds the weight to one; `None`: `default_weight`.
134    #[serde(deserialize_with = "stated")]
135    pub weight_parameter: Option<usize>,
136    pub blending: LayerBlending,
137    /// Which joints the layer drives; `None`: every joint at full strength. Stored as a `null` when none: a stored
138    /// controller without the field is refused, so an absent mask never reads as an unmasked layer.
139    #[serde(deserialize_with = "stated")]
140    pub mask: Option<LayerMask>,
141}
142
143/// `core.controller`: states and the clip motions their parameters choose. Transitions and
144/// body/skeleton masks remain in the source dialect until a lowering can represent their semantics.
145/// Layer blending and default weights are retained even where a consumer cannot evaluate them.
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147#[serde(deny_unknown_fields)]
148pub struct Controller {
149    pub name: String,
150    pub parameters: Vec<ControllerParameter>,
151    pub layers: Vec<ControllerLayer>,
152}
153
154/// `core.controller_binding`: a controller driving a rig occurrence, not a skeleton definition.
155#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(deny_unknown_fields)]
157pub struct ControllerBinding {
158    pub controller: Entity,
159    pub rig: Entity,
160}
161
162impl Controller {
163    fn numeric_parameter(&self, index: usize) -> Result<(), String> {
164        let parameter = self
165            .parameters
166            .get(index)
167            .ok_or_else(|| format!("parameter index {index} is out of range"))?;
168        if !matches!(
169            parameter.kind,
170            ControllerParameterKind::Float | ControllerParameterKind::Int
171        ) {
172            return Err(format!(
173                "parameter {} is {:?}, not numeric",
174                parameter.name, parameter.kind
175            ));
176        }
177        Ok(())
178    }
179}
180
181impl Component for Controller {
182    const NAME: &'static str = "core.controller";
183    const GEOMETRY: Geometry = Geometry::None;
184    fn to_json(&self) -> Result<Option<Value>, JsonError> {
185        crate::json::to_value(self).map(Some)
186    }
187    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
188        crate::world::Linearity::Invariant
189    }
190    fn references(&self, out: &mut Vec<Entity>) {
191        let mut pending: Vec<_> = self
192            .layers
193            .iter()
194            .flat_map(|layer| layer.states.iter().map(|state| &state.motion))
195            .collect();
196        while let Some(motion) = pending.pop() {
197            match motion {
198                Motion::Clip(clip) | Motion::Pending(clip) => out.push(*clip),
199                Motion::Blend1D { children, .. } => pending.extend(children.iter().map(|(_, motion)| motion)),
200                Motion::Unsupported(_) => {}
201            }
202        }
203        for layer in &self.layers {
204            if let Some(LayerMask::Joints(joints)) = &layer.mask {
205                out.extend(joints.iter().map(|j| j.skeleton));
206            }
207        }
208    }
209    fn verify(&self, _e: Entity, w: &World) -> Result<(), String> {
210        if self.name.trim().is_empty() {
211            return Err("empty controller name".into());
212        }
213        let mut names = BTreeSet::new();
214        for parameter in &self.parameters {
215            if parameter.name.trim().is_empty() || !names.insert(&parameter.name) {
216                return Err(format!(
217                    "empty or duplicate controller parameter {:?}",
218                    parameter.name
219                ));
220            }
221            if !parameter.default.is_finite() {
222                return Err(format!("{}: non-finite default", parameter.name));
223            }
224            match parameter.kind {
225                ControllerParameterKind::Int if parameter.default.fract() != 0. => {
226                    return Err(format!("{}: fractional Int default", parameter.name));
227                }
228                ControllerParameterKind::Bool | ControllerParameterKind::Trigger
229                    if parameter.default != 0. && parameter.default != 1. =>
230                {
231                    return Err(format!("{}: Bool/Trigger default is not 0 or 1", parameter.name));
232                }
233                _ => {}
234            }
235        }
236        for (layer_index, layer) in self.layers.iter().enumerate() {
237            if layer.name.trim().is_empty() {
238                return Err(format!("layer {layer_index}: empty name"));
239            }
240            if !layer.default_weight.is_finite() {
241                return Err(format!("layer {layer_index}: non-finite weight"));
242            }
243            if let Some(parameter) = layer.weight_parameter {
244                self.numeric_parameter(parameter)
245                    .map_err(|why| format!("layer {layer_index}: weight {why}"))?;
246            }
247            match &layer.mask {
248                None => {}
249                Some(LayerMask::Unsupported(why)) if why.trim().is_empty() => {
250                    return Err(format!("layer {layer_index}: unsupported mask has no reason"));
251                }
252                Some(LayerMask::Unsupported(_)) => {}
253                Some(LayerMask::Joints(joints)) => {
254                    let mut seen = BTreeSet::new();
255                    for masked in joints {
256                        if !masked.weight.is_finite() || !(0.0..=1.0).contains(&masked.weight) {
257                            return Err(format!(
258                                "layer {layer_index}: mask weight {} is not in 0 to 1",
259                                masked.weight
260                            ));
261                        }
262                        if !seen.insert((masked.skeleton, masked.joint)) {
263                            return Err(format!(
264                                "layer {layer_index}: joint {:?} is masked twice",
265                                masked.joint
266                            ));
267                        }
268                        let skeleton = w.get::<Skeleton>(masked.skeleton).ok_or_else(|| {
269                            format!(
270                                "layer {layer_index}: mask skeleton {:?} holds no core.skeleton",
271                                masked.skeleton
272                            )
273                        })?;
274                        if skeleton.index_of(masked.joint).is_none() {
275                            return Err(format!(
276                                "layer {layer_index}: masked joint {:?} is not in skeleton {:?}",
277                                masked.joint, masked.skeleton
278                            ));
279                        }
280                    }
281                }
282            }
283            match layer.default_state {
284                Some(default) if default >= layer.states.len() => {
285                    return Err(format!("layer {layer_index}: default state is out of range"));
286                }
287                None if !layer.states.is_empty() => {
288                    return Err(format!("layer {layer_index}: has states but no default state"));
289                }
290                _ => {}
291            }
292            for (state_index, state) in layer.states.iter().enumerate() {
293                if state.name.trim().is_empty() {
294                    return Err(format!("layer {layer_index} state {state_index}: empty name"));
295                }
296                if !state.speed.is_finite() || !state.cycle_offset.is_finite() {
297                    return Err(format!(
298                        "layer {layer_index} state {state_index}: non-finite speed/offset"
299                    ));
300                }
301                for parameter in [state.speed_parameter, state.cycle_offset_parameter]
302                    .into_iter()
303                    .flatten()
304                {
305                    self.numeric_parameter(parameter)?;
306                }
307                let mut pending = vec![&state.motion];
308                while let Some(motion) = pending.pop() {
309                    match motion {
310                        Motion::Clip(clip) => {
311                            if !w.has::<Clip>(*clip) {
312                                return Err(format!("motion {clip:?} holds no core.clip"));
313                            }
314                        }
315                        Motion::Blend1D { parameter, children } => {
316                            self.numeric_parameter(*parameter)?;
317                            if children.is_empty() {
318                                return Err("1D blend has no children".into());
319                            }
320                            if !children.iter().all(|(threshold, _)| threshold.is_finite())
321                                || !children.windows(2).all(|pair| pair[0].0 < pair[1].0)
322                            {
323                                return Err(
324                                    "1D blend thresholds must be finite and strictly increasing".into()
325                                );
326                            }
327                            pending.extend(children.iter().map(|(_, motion)| motion));
328                        }
329                        Motion::Pending(_) => {}
330                        Motion::Unsupported(why) => {
331                            if why.trim().is_empty() {
332                                return Err("unsupported motion has no reason".into());
333                            }
334                        }
335                    }
336                }
337            }
338        }
339        Ok(())
340    }
341}
342
343impl Component for ControllerBinding {
344    const NAME: &'static str = "core.controller_binding";
345    const GEOMETRY: Geometry = Geometry::None;
346    fn to_json(&self) -> Result<Option<Value>, JsonError> {
347        crate::json::to_value(self).map(Some)
348    }
349    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
350        crate::world::Linearity::Invariant
351    }
352    fn references(&self, out: &mut Vec<Entity>) {
353        out.extend([self.controller, self.rig]);
354    }
355    fn verify(&self, _e: Entity, w: &World) -> Result<(), String> {
356        if !w.has::<Controller>(self.controller) {
357            return Err(format!("{:?} holds no core.controller", self.controller));
358        }
359        if !w.has::<RigInstance>(self.rig) {
360            return Err(format!("{:?} holds no core.rig_instance", self.rig));
361        }
362        Ok(())
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use crate::{Extrapolation, Transform};
370    fn fixture() -> (World, Entity, Controller) {
371        let mut w = World::new();
372        let clip = w.spawn();
373        w.insert(
374            clip,
375            Clip {
376                name: "motion".into(),
377                duration: 1.,
378                extrapolation: Extrapolation::Hold,
379                channels: vec![],
380                root_motion: None,
381                additive: None,
382            },
383        );
384        let controller = w.spawn();
385        let value = Controller {
386            name: "character".into(),
387            parameters: vec![
388                ControllerParameter {
389                    name: "speed".into(),
390                    kind: ControllerParameterKind::Float,
391                    default: 0.,
392                },
393                ControllerParameter {
394                    name: "index".into(),
395                    kind: ControllerParameterKind::Int,
396                    default: 1.,
397                },
398                ControllerParameter {
399                    name: "flag".into(),
400                    kind: ControllerParameterKind::Bool,
401                    default: 0.,
402                },
403                ControllerParameter {
404                    name: "fire".into(),
405                    kind: ControllerParameterKind::Trigger,
406                    default: 1.,
407                },
408            ],
409            layers: vec![ControllerLayer {
410                name: "base".into(),
411                default_state: Some(0),
412                default_weight: 0.75,
413                weight_parameter: None,
414                blending: LayerBlending::Override,
415                mask: None,
416                states: vec![ControllerState {
417                    name: "idle".into(),
418                    speed: -1.,
419                    speed_parameter: Some(0),
420                    cycle_offset: 1.5,
421                    cycle_offset_parameter: Some(1),
422                    motion: Motion::Blend1D {
423                        parameter: 0,
424                        children: vec![
425                            (-1., Motion::Clip(clip)),
426                            (
427                                1.,
428                                Motion::Unsupported("2D tree remains in source dialect".into()),
429                            ),
430                        ],
431                    },
432                }],
433            }],
434        };
435        (w, controller, value)
436    }
437    #[test]
438    fn validates_parameter_defaults_without_rounding_or_clamping() {
439        let (w, e, c) = fixture();
440        assert!(c.verify(e, &w).is_ok());
441        for (index, value) in [(0, f64::NAN), (0, f64::INFINITY), (1, 1.5), (2, 2.), (3, -1.)] {
442            let mut bad = c.clone();
443            bad.parameters[index].default = value;
444            assert!(bad.verify(e, &w).is_err());
445        }
446        let mut bad = c.clone();
447        bad.parameters[1].name = "speed".into();
448        assert!(bad.verify(e, &w).is_err());
449        let mut bad = c.clone();
450        bad.parameters[1].name.clear();
451        assert!(bad.verify(e, &w).is_err());
452    }
453    #[test]
454    fn numeric_refs_blends_and_default_states_are_checked() {
455        let (w, e, c) = fixture();
456        for index in [2, 3, 4] {
457            let mut bad = c.clone();
458            bad.layers[0].states[0].speed_parameter = Some(index);
459            assert!(bad.verify(e, &w).is_err());
460            let mut bad = c.clone();
461            bad.layers[0].states[0].cycle_offset_parameter = Some(index);
462            assert!(bad.verify(e, &w).is_err());
463            let mut bad = c.clone();
464            bad.layers[0].states[0].motion = Motion::Blend1D {
465                parameter: index,
466                children: vec![(0., Motion::Unsupported("not lowered".into()))],
467            };
468            assert!(bad.verify(e, &w).is_err());
469        }
470        for thresholds in [vec![], vec![1., 1.], vec![1., 0.], vec![f32::NAN]] {
471            let mut bad = c.clone();
472            bad.layers[0].states[0].motion = Motion::Blend1D {
473                parameter: 0,
474                children: thresholds
475                    .into_iter()
476                    .map(|t| (t, Motion::Unsupported("not lowered".into())))
477                    .collect(),
478            };
479            assert!(bad.verify(e, &w).is_err());
480        }
481        let mut bad = c.clone();
482        bad.layers[0].default_state = Some(1);
483        assert!(bad.verify(e, &w).is_err());
484        // states without a default state are refused
485        let mut bad = c.clone();
486        bad.layers[0].default_state = None;
487        assert!(
488            bad.verify(e, &w)
489                .unwrap_err()
490                .contains("has states but no default state")
491        );
492        let mut bad = c.clone();
493        bad.layers[0].default_weight = f32::NAN;
494        assert!(bad.verify(e, &w).is_err());
495        let mut bad = c.clone();
496        bad.layers[0].states[0].speed = f64::INFINITY;
497        assert!(bad.verify(e, &w).is_err());
498        let mut bad = c;
499        bad.layers[0].states[0].cycle_offset = f32::INFINITY;
500        assert!(bad.verify(e, &w).is_err());
501    }
502    #[test]
503    fn recursive_motion_references_require_clips_and_keep_named_refusals() {
504        let (mut w, e, mut c) = fixture();
505        let absent = w.spawn();
506        c.layers[0].states[0].motion = Motion::Blend1D {
507            parameter: 0,
508            children: vec![(
509                0.,
510                Motion::Blend1D {
511                    parameter: 1,
512                    children: vec![(0., Motion::Clip(absent))],
513                },
514            )],
515        };
516        let mut refs = Vec::new();
517        c.references(&mut refs);
518        assert_eq!(refs, vec![absent]);
519        assert!(c.verify(e, &w).is_err());
520        c.layers[0].states[0].motion = Motion::Unsupported(String::new());
521        assert!(c.verify(e, &w).is_err());
522        c.layers[0].states[0].motion = Motion::Unsupported("Direct blend is not lowered".into());
523        assert!(c.verify(e, &w).is_ok());
524    }
525    /// A layer with no states plays nothing and has no default state; both halves of that are required, and it survives
526    /// JSON as a null.
527    #[test]
528    fn an_empty_layer_has_no_default_state() {
529        let (w, e, mut c) = fixture();
530        let mut empty = c.layers[0].clone();
531        empty.name = "empty".into();
532        empty.states.clear();
533        empty.default_state = None;
534        c.layers.push(empty);
535        assert!(c.verify(e, &w).is_ok());
536        let json = c.to_json().unwrap().unwrap();
537        assert_eq!(json["layers"][0]["default_state"], 0);
538        assert!(json["layers"][1]["default_state"].is_null());
539        assert_eq!(serde_json::from_value::<Controller>(json).unwrap(), c);
540        // a default state with no states to be in is out of range
541        c.layers[1].default_state = Some(0);
542        assert!(
543            c.verify(e, &w)
544                .unwrap_err()
545                .contains("default state is out of range")
546        );
547    }
548    fn skeleton_of(w: &mut World, joints: u32) -> Entity {
549        let e = w.spawn();
550        w.insert(
551            e,
552            Skeleton {
553                joints: (0..joints)
554                    .map(|i| crate::Joint {
555                        id: JointId(i),
556                        name: format!("j{i}"),
557                        parent: i.checked_sub(1).map(JointId),
558                        rest: crate::Rest::Trs {
559                            translation: [0.; 3],
560                            rotation: [0., 0., 0., 1.],
561                            scale: [1.; 3],
562                        },
563                    })
564                    .collect(),
565            },
566        );
567        e
568    }
569    fn masked(skeleton: Entity, joint: u32, weight: f32) -> MaskedJoint {
570        MaskedJoint {
571            skeleton,
572            joint: JointId(joint),
573            weight,
574        }
575    }
576    /// A mask names joints as a clip does, by skeleton and joint id; it is checked against those skeletons, and an
577    /// invalid weight, a repeated joint, a skeleton that is none, a joint that is not in it, a reasonless refusal or a
578    /// weight parameter that is not numeric is refused.
579    #[test]
580    fn a_layer_mask_is_checked_against_the_skeletons_it_names() {
581        let (mut w, e, mut c) = fixture();
582        let a = skeleton_of(&mut w, 3);
583        let b = skeleton_of(&mut w, 2);
584        c.layers[0].mask = Some(LayerMask::Joints(vec![
585            masked(a, 0, 1.),
586            masked(a, 2, 0.25),
587            masked(b, 0, 0.),
588        ]));
589        c.layers[0].weight_parameter = Some(0);
590        assert!(c.verify(e, &w).is_ok());
591        let mut refs = Vec::new();
592        c.references(&mut refs);
593        assert!(refs.contains(&a) && refs.contains(&b));
594        for (mask, why) in [
595            (vec![masked(a, 0, 1.), masked(a, 0, 0.5)], "masked twice"),
596            (vec![masked(a, 0, 1.0001)], "not in 0 to 1"),
597            (vec![masked(a, 0, -0.5)], "not in 0 to 1"),
598            (vec![masked(a, 0, f32::NAN)], "not in 0 to 1"),
599            (vec![masked(a, 3, 1.)], "is not in skeleton"),
600            (vec![masked(b, 2, 1.)], "is not in skeleton"),
601            (vec![masked(e, 0, 1.)], "holds no core.skeleton"),
602        ] {
603            let mut bad = c.clone();
604            bad.layers[0].mask = Some(LayerMask::Joints(mask));
605            let err = bad.verify(e, &w).unwrap_err();
606            assert!(err.contains(why), "{err}");
607        }
608        let mut bad = c.clone();
609        bad.layers[0].mask = Some(LayerMask::Unsupported(" ".into()));
610        assert!(bad.verify(e, &w).unwrap_err().contains("no reason"));
611        bad.layers[0].mask = Some(LayerMask::Unsupported("a humanoid body mask".into()));
612        assert!(bad.verify(e, &w).is_ok());
613        for index in [2, 3, 4] {
614            let mut bad = c.clone();
615            bad.layers[0].weight_parameter = Some(index);
616            assert!(bad.verify(e, &w).unwrap_err().contains("weight"));
617        }
618    }
619    /// The mask and the weight parameter survive JSON, `null` when none, and a stored controller without either field
620    /// is refused rather than read as an unmasked layer.
621    #[test]
622    fn a_layer_mask_round_trips_and_its_absence_is_not_unmasked() {
623        let (mut w, _, mut c) = fixture();
624        let a = skeleton_of(&mut w, 2);
625        c.layers[0].weight_parameter = Some(0);
626        c.layers[0].mask = Some(LayerMask::Joints(vec![masked(a, 1, 0.5)]));
627        let json = c.to_json().unwrap().unwrap();
628        assert_eq!(serde_json::from_value::<Controller>(json.clone()).unwrap(), c);
629        c.layers[0].mask = Some(LayerMask::Unsupported("why".into()));
630        let json = c.to_json().unwrap().unwrap();
631        assert_eq!(
632            json["layers"][0]["mask"],
633            serde_json::json!({"Unsupported": "why"})
634        );
635        assert_eq!(serde_json::from_value::<Controller>(json).unwrap(), c);
636        c.layers[0].mask = None;
637        c.layers[0].weight_parameter = None;
638        let json = c.to_json().unwrap().unwrap();
639        assert!(json["layers"][0]["mask"].is_null() && json["layers"][0]["weight_parameter"].is_null());
640        assert_eq!(serde_json::from_value::<Controller>(json.clone()).unwrap(), c);
641        for field in ["mask", "weight_parameter"] {
642            let mut stripped = json.clone();
643            stripped["layers"][0].as_object_mut().unwrap().remove(field);
644            assert!(serde_json::from_value::<Controller>(stripped).is_err(), "{field}");
645        }
646    }
647    #[test]
648    fn controller_and_binding_json_round_trip_keep_entity_ids_and_layer_modes() {
649        let (mut w, e, mut c) = fixture();
650        for blending in [
651            LayerBlending::Override,
652            LayerBlending::Additive,
653            LayerBlending::Other(17),
654        ] {
655            c.layers[0].blending = blending;
656            assert_eq!(
657                serde_json::from_value::<Controller>(c.to_json().unwrap().unwrap()).unwrap(),
658                c
659            );
660        }
661        w.insert(e, c.clone());
662        let rig = w.spawn();
663        let binding = ControllerBinding { controller: e, rig };
664        assert!(binding.verify(e, &w).is_err());
665        let skeleton = w.spawn();
666        w.insert(rig, RigInstance { skeleton });
667        w.insert(rig, Transform::IDENTITY);
668        assert!(binding.verify(e, &w).is_ok());
669        let mut refs = Vec::new();
670        binding.references(&mut refs);
671        assert_eq!(refs, vec![e, rig]);
672        assert_eq!(
673            serde_json::from_value::<ControllerBinding>(binding.to_json().unwrap().unwrap()).unwrap(),
674            binding
675        );
676        assert!(c.remap_primitives(&[0]).is_none());
677        assert!(binding.remap_primitives(&[0]).is_none());
678        let mut json = c.to_json().unwrap().unwrap();
679        json["forgotten_source_field"] = serde_json::json!(true);
680        assert!(serde_json::from_value::<Controller>(json).is_err());
681        let mut invalid = binding;
682        invalid.controller = skeleton;
683        assert!(invalid.verify(e, &w).is_err());
684    }
685}