1use std::collections::BTreeSet;
33
34use serde::{Deserialize, Serialize};
35use serde_json::Value;
36
37use crate::morphs::{MorphId, Morphs};
38use crate::rig::{RigInstance, RigPose};
39use crate::skeleton::{JointId, Skeleton};
40use crate::{Component, Entity, Instance, JsonError, World};
41
42#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
44pub enum ClipTarget {
45 Joint {
47 skeleton: Entity,
48 #[serde(deserialize_with = "joint_id")]
49 joint: JointId,
50 },
51 Morph {
53 geometry: Entity,
54 #[serde(deserialize_with = "morph_id")]
55 morph: MorphId,
56 },
57 Control { name: String },
59 DrawGroup { scope: String, group: u32 },
62 MaterialParameter { materials: Vec<Entity>, name: String },
65}
66
67fn joint_id<'de, D: serde::Deserializer<'de>>(d: D) -> Result<JointId, D::Error> {
68 u32::deserialize(d).map(JointId)
69}
70fn morph_id<'de, D: serde::Deserializer<'de>>(d: D) -> Result<MorphId, D::Error> {
71 u32::deserialize(d).map(MorphId)
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
76pub enum Property {
77 Translation,
79 Rotation,
81 Scale,
83 Scalar,
85 Visibility,
88 Values(u8),
90}
91
92impl Property {
93 pub fn width(self) -> usize {
96 match self {
97 Property::Translation | Property::Scale => 3,
98 Property::Rotation => 4,
99 Property::Scalar | Property::Visibility => 1,
100 Property::Values(n) => usize::from(n),
101 }
102 }
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
107pub enum Interpolation {
108 Step,
109 Linear,
110 Hermite,
111}
112
113#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
116pub struct Channel {
117 pub target: ClipTarget,
118 pub property: Property,
119 pub interpolation: Interpolation,
120 pub times: Vec<f32>,
121 pub values: Vec<f32>,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
126pub enum Extrapolation {
127 Hold,
128 Loop,
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
133pub enum RootSpace {
134 Rig,
136}
137
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
141pub struct RootMotion {
142 pub space: RootSpace,
143 pub translation: Option<(Interpolation, Vec<f32>, Vec<f32>)>,
144 pub rotation: Option<(Interpolation, Vec<f32>, Vec<f32>)>,
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
149pub enum AdditiveReference {
150 Identity,
154 IdentityParent,
159 Pose(Entity),
161 ClipFrame { clip: Entity, time: f32 },
163}
164
165#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
167pub struct Clip {
168 pub name: String,
170 pub duration: f32,
172 pub extrapolation: Extrapolation,
173 pub channels: Vec<Channel>,
174 pub root_motion: Option<RootMotion>,
175 pub additive: Option<AdditiveReference>,
176}
177
178#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
182pub struct ClipBinding {
183 pub clip: Entity,
184 pub rig: Option<Entity>,
185 pub meshes: Vec<Entity>,
186}
187
188const UNIT_TOLERANCE: f32 = 1e-3;
189
190fn verify_keys(
192 what: &str,
193 interpolation: Interpolation,
194 width: usize,
195 times: &[f32],
196 values: &[f32],
197 duration: f32,
198 unit: bool,
199) -> Result<(), String> {
200 if times.is_empty() {
201 return Err(format!("{what}: no keys"));
202 }
203 if !times
204 .iter()
205 .all(|t| t.is_finite() && (0.0..=duration).contains(t))
206 || !times.windows(2).all(|p| p[0] < p[1])
207 {
208 return Err(format!("{what}: key times not increasing within [0, {duration}]"));
209 }
210 let per_key = if interpolation == Interpolation::Hermite {
211 3 * width
212 } else {
213 width
214 };
215 if values.len() != times.len() * per_key {
216 return Err(format!(
217 "{what}: {} values for {} keys of {per_key}",
218 values.len(),
219 times.len()
220 ));
221 }
222 if !values.iter().all(|x| x.is_finite()) {
223 return Err(format!("{what}: a value not finite"));
224 }
225 if unit {
226 let offset = if interpolation == Interpolation::Hermite {
228 width
229 } else {
230 0
231 };
232 for (k, key) in values.chunks(per_key).enumerate() {
233 let q = &key[offset..offset + width];
234 let len = q.iter().map(|x| x * x).sum::<f32>().sqrt();
235 if (len - 1.0).abs() > UNIT_TOLERANCE {
236 return Err(format!("{what}: key {k}'s rotation of length {len}"));
237 }
238 }
239 }
240 Ok(())
241}
242
243impl Component for Clip {
244 const NAME: &'static str = "core.clip";
245 const VERSION: u32 = 2;
247 const GEOMETRY: crate::Geometry = crate::Geometry::None;
248 fn to_json(&self) -> Result<Option<Value>, JsonError> {
250 let channels: Vec<Value> = self
251 .channels
252 .iter()
253 .map(|c| {
254 serde_json::json!({
255 "target": c.target,
256 "property": c.property,
257 "interpolation": c.interpolation,
258 "keys": c.times.len(),
259 })
260 })
261 .collect();
262 Ok(Some(serde_json::json!({
263 "name": self.name,
264 "duration": crate::json::to_value(&self.duration)?,
265 "extrapolation": self.extrapolation,
266 "channels": channels,
267 "root_motion": self.root_motion.is_some(),
268 "additive": crate::json::to_value(&self.additive)?,
269 })))
270 }
271 fn references(&self, out: &mut Vec<Entity>) {
272 for c in &self.channels {
273 match &c.target {
274 ClipTarget::Joint { skeleton, .. } => out.push(*skeleton),
275 ClipTarget::Morph { geometry, .. } => out.push(*geometry),
276 ClipTarget::MaterialParameter { materials, .. } => out.extend(materials),
277 ClipTarget::DrawGroup { .. } | ClipTarget::Control { .. } => {}
278 }
279 }
280 match self.additive {
281 Some(AdditiveReference::Pose(e)) => out.push(e),
282 Some(AdditiveReference::ClipFrame { clip, .. }) => out.push(clip),
283 Some(AdditiveReference::Identity | AdditiveReference::IdentityParent) | None => {}
284 }
285 }
286 fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
292 if !self.duration.is_finite() || self.duration < 0.0 {
293 return Err(format!("duration {} not finite and non-negative", self.duration));
294 }
295 let mut seen = BTreeSet::new();
296 let mut material_channels = BTreeSet::new();
297 for (k, c) in self.channels.iter().enumerate() {
298 let what = format!("channel {k}");
299 match (&c.target, c.property) {
300 (
301 ClipTarget::Joint { skeleton, joint },
302 Property::Translation | Property::Rotation | Property::Scale,
303 ) => {
304 let s = w
305 .get::<Skeleton>(*skeleton)
306 .ok_or_else(|| format!("{what}: {skeleton:?} holds no core.skeleton"))?;
307 if s.index_of(*joint).is_none() {
308 return Err(format!("{what}: joint {joint:?} is not in its skeleton"));
309 }
310 }
311 (ClipTarget::Morph { geometry, morph }, Property::Scalar) => {
312 let m = w
313 .get::<Morphs>(*geometry)
314 .ok_or_else(|| format!("{what}: {geometry:?} holds no core.morphs"))?;
315 if m.target(*morph).is_none() {
316 return Err(format!("{what}: morph {morph:?} is not in its set"));
317 }
318 }
319 (ClipTarget::Control { .. }, Property::Scalar) => {}
320 (ClipTarget::DrawGroup { scope, group }, Property::Visibility) => {
321 if scope.trim().is_empty() || *group >= 64 {
322 return Err(format!(
323 "{what}: draw-group scope must be nonempty and group below core's limit of 64"
324 ));
325 }
326 if c.interpolation != Interpolation::Step {
327 return Err(format!("{what}: visibility requires Step interpolation"));
328 }
329 if c.values.iter().any(|v| ![-1.0, 0.0, 1.0].contains(v)) {
330 return Err(format!(
331 "{what}: visibility values must be -1 (default), 0 (hidden) or 1 (shown)"
332 ));
333 }
334 }
335 (ClipTarget::MaterialParameter { materials, name }, Property::Values(width)) => {
336 if !(1..=4).contains(&width) || name.trim().is_empty() || materials.is_empty() {
337 return Err(format!(
338 "{what}: material parameter needs a name, targets and Values(1..=4)"
339 ));
340 }
341 if c.interpolation == Interpolation::Hermite {
342 return Err(format!(
343 "{what}: material values require Step or Linear interpolation"
344 ));
345 }
346 let mut targets = BTreeSet::new();
347 for &material in materials {
348 if !targets.insert(material) {
349 return Err(format!("{what}: repeated material target {material:?}"));
350 }
351 let defaults = w.get::<crate::MaterialParameters>(material).ok_or_else(|| {
352 format!("{what}: {material:?} holds no core.material_parameters default")
353 })?;
354 let value = defaults.0.get(name).ok_or_else(|| {
355 format!("{what}: {material:?} has no default for material parameter {name:?}")
356 })?;
357 if value.len() != usize::from(width) || !value.iter().all(|v| v.is_finite()) {
358 return Err(format!(
359 "{what}: {material:?} default for {name:?} is not {width} finite values"
360 ));
361 }
362 if !material_channels.insert((material, name)) {
363 return Err(format!(
364 "{what}: a second channel for material {material:?} parameter {name:?}"
365 ));
366 }
367 }
368 }
369 (t, p) => return Err(format!("{what}: {p:?} does not fit {t:?}")),
370 }
371 if !seen.insert((&c.target, c.property)) {
372 return Err(format!(
373 "{what}: a second channel for {:?} {:?}",
374 c.target, c.property
375 ));
376 }
377 verify_keys(
378 &what,
379 c.interpolation,
380 c.property.width(),
381 &c.times,
382 &c.values,
383 self.duration,
384 c.property == Property::Rotation,
385 )?;
386 }
387 if let Some(r) = &self.root_motion {
388 for (what, curve, width) in [
389 ("root translation", &r.translation, 3),
390 ("root rotation", &r.rotation, 4),
391 ] {
392 if let Some((i, times, values)) = curve {
393 verify_keys(what, *i, width, times, values, self.duration, width == 4)?;
394 }
395 }
396 }
397 match self.additive {
398 Some(AdditiveReference::Pose(e)) if !w.has::<RigPose>(e) => {
399 return Err(format!("additive reference {e:?} holds no core.rig_pose"));
400 }
401 Some(AdditiveReference::ClipFrame { clip, time }) => {
402 let c = w
403 .get::<Clip>(clip)
404 .ok_or_else(|| format!("additive reference {clip:?} holds no core.clip"))?;
405 if !(time.is_finite() && (0.0..=c.duration).contains(&time)) {
406 return Err(format!(
407 "additive reference time {time} outside its clip's [0, {}]",
408 c.duration
409 ));
410 }
411 let mut seen = BTreeSet::from([e]);
414 let mut next = Some(clip);
415 while let Some(at) = next {
416 if !seen.insert(at) {
417 return Err(format!(
418 "additive references from {e:?} come back to {at:?}: a cycle"
419 ));
420 }
421 next = match w.get::<Clip>(at).and_then(|c| c.additive) {
422 Some(AdditiveReference::ClipFrame { clip, .. }) => Some(clip),
423 _ => None,
424 };
425 }
426 }
427 _ => {}
428 }
429 Ok(())
430 }
431}
432
433impl Component for ClipBinding {
434 const NAME: &'static str = "core.clip_binding";
435 const GEOMETRY: crate::Geometry = crate::Geometry::None;
436 fn to_json(&self) -> Result<Option<Value>, JsonError> {
437 crate::json::to_value(self).map(Some)
438 }
439 fn references(&self, out: &mut Vec<Entity>) {
440 out.push(self.clip);
441 out.extend(self.rig);
442 out.extend(&self.meshes);
443 }
444 fn verify(&self, _e: Entity, w: &World) -> Result<(), String> {
449 let clip = w
450 .get::<Clip>(self.clip)
451 .ok_or_else(|| format!("{:?} holds no core.clip", self.clip))?;
452 let skeleton = match self.rig {
453 Some(r) => Some(
454 w.get::<RigInstance>(r)
455 .ok_or_else(|| format!("{r:?} is not a rig"))?
456 .skeleton,
457 ),
458 None => None,
459 };
460 let mut driven = BTreeSet::new();
461 for c in &clip.channels {
462 match c.target {
463 ClipTarget::Joint { skeleton: s, .. } if Some(s) != skeleton => {
464 return Err(format!("a joint channel on {s:?}, the rig poses {skeleton:?}"));
465 }
466 ClipTarget::Morph { geometry, .. } => {
467 driven.insert(geometry);
468 }
469 ClipTarget::Joint { .. }
470 | ClipTarget::Control { .. }
471 | ClipTarget::DrawGroup { .. }
472 | ClipTarget::MaterialParameter { .. } => {}
473 }
474 }
475 for &m in &self.meshes {
476 let drawn = w.get::<Instance>(m).map_or(m, |i| i.prototype);
477 if !driven.contains(&drawn) {
478 return Err(format!(
479 "mesh {m:?} draws {drawn:?}, whose morph set the clip does not drive"
480 ));
481 }
482 }
483 Ok(())
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::morphs::{Deltas, Frame, Target};
491 use crate::skeleton::{Joint, Rest};
492 use crate::{Mesh, Primitive, Semantic, Stream, Topology, Transform};
493
494 const S: f32 = std::f32::consts::FRAC_1_SQRT_2;
495
496 fn world() -> (World, Entity, Entity, Entity, Entity, Entity) {
500 let mut w = World::new();
501 let skeleton = w.spawn();
502 w.insert(
503 skeleton,
504 Skeleton {
505 joints: [(1, None), (2, Some(1))]
506 .map(|(id, parent): (u32, Option<u32>)| Joint {
507 id: JointId(id),
508 name: format!("j{id}"),
509 parent: parent.map(JointId),
510 rest: Rest::Affine(Transform::IDENTITY),
511 })
512 .into(),
513 },
514 );
515 let proto = w.spawn();
516 w.insert(
517 proto,
518 Mesh::new(vec![Primitive {
519 topology: Topology::Points,
520 streams: [(Semantic::Position, Stream::F32x3(vec![[0.0; 3]]))].into(),
521 indices: None,
522 material: None,
523 }]),
524 );
525 w.insert(
526 proto,
527 Morphs {
528 targets: vec![Target {
529 id: MorphId(9),
530 name: "blink".into(),
531 default_weight: 0.0,
532 frames: vec![Frame {
533 weight: 1.0,
534 primitives: vec![Some(Deltas {
535 positions: Some(vec![[0.0, 0.1, 0.0]]),
536 ..Deltas::default()
537 })],
538 }],
539 }],
540 },
541 );
542 let rig = w.spawn();
543 w.insert(rig, RigInstance { skeleton });
544 w.insert(rig, Transform::IDENTITY);
545 let placed = w.spawn();
546 w.insert(
547 placed,
548 Instance {
549 prototype: proto,
550 variant: 0,
551 },
552 );
553 w.insert(placed, Transform::IDENTITY);
554 let joint2 = ClipTarget::Joint {
555 skeleton,
556 joint: JointId(2),
557 };
558 let clip = w.spawn();
559 w.insert(
560 clip,
561 Clip {
562 name: "wave".into(),
563 duration: 1.0,
564 extrapolation: Extrapolation::Loop,
565 channels: vec![
566 Channel {
567 target: joint2.clone(),
568 property: Property::Rotation,
569 interpolation: Interpolation::Linear,
570 times: vec![0.0, 1.0],
571 values: vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, S, S],
572 },
573 Channel {
574 target: joint2,
575 property: Property::Translation,
576 interpolation: Interpolation::Hermite,
577 times: vec![0.0, 1.0],
578 values: vec![0.0; 3]
579 .into_iter()
580 .chain([0.0, 0.0, 0.0])
581 .chain([0.0; 3])
582 .chain([0.0; 3])
583 .chain([0.0, 1.0, 0.0])
584 .chain([0.0; 3])
585 .collect(),
586 },
587 Channel {
588 target: ClipTarget::Morph {
589 geometry: proto,
590 morph: MorphId(9),
591 },
592 property: Property::Scalar,
593 interpolation: Interpolation::Step,
594 times: vec![0.0, 0.5],
595 values: vec![0.0, 1.0],
596 },
597 Channel {
598 target: ClipTarget::Control {
599 name: "board_flip".into(),
600 },
601 property: Property::Scalar,
602 interpolation: Interpolation::Linear,
603 times: vec![0.25],
604 values: vec![0.5],
605 },
606 ],
607 root_motion: Some(RootMotion {
608 space: RootSpace::Rig,
609 translation: Some((
610 Interpolation::Linear,
611 vec![0.0, 1.0],
612 vec![0.0, 0.0, 0.0, 0.0, 0.0, -2.0],
613 )),
614 rotation: None,
615 }),
616 additive: None,
617 },
618 );
619 let binding = w.spawn();
620 w.insert(
621 binding,
622 ClipBinding {
623 clip,
624 rig: Some(rig),
625 meshes: vec![placed],
626 },
627 );
628 (w, clip, binding, skeleton, proto, rig)
629 }
630
631 fn errors(w: &World) -> Vec<String> {
632 w.verify_components().into_iter().map(|(_, _, m)| m).collect()
633 }
634
635 #[test]
638 fn a_clip_drives_joints_morphs_and_controls_together() {
639 let (w, clip, binding, skeleton, proto, rig) = world();
640 assert_eq!(errors(&w), Vec::<String>::new());
641 assert_eq!(w.closure([clip]), [clip, skeleton, proto].into());
642 assert!(w.closure([binding]).contains(&rig));
643 }
644
645 fn with_display_channels() -> (World, Entity, Entity) {
646 let (mut w, clip, ..) = world();
647 let material = w.spawn();
648 w.insert(
649 material,
650 crate::MaterialParameters(std::collections::BTreeMap::from([(
651 "tint".into(),
652 vec![0.25, 0.5],
653 )])),
654 );
655 w.get_mut::<Clip>(clip).unwrap().channels.extend([
656 Channel {
657 target: ClipTarget::DrawGroup {
658 scope: "character".into(),
659 group: 63,
660 },
661 property: Property::Visibility,
662 interpolation: Interpolation::Step,
663 times: vec![0.0, 0.5, 1.0],
664 values: vec![-1.0, 0.0, 1.0],
665 },
666 Channel {
667 target: ClipTarget::MaterialParameter {
668 materials: vec![material],
669 name: "tint".into(),
670 },
671 property: Property::Values(2),
672 interpolation: Interpolation::Linear,
673 times: vec![0.0, 1.0],
674 values: vec![0.25, 0.5, 0.75, 1.0],
675 },
676 ]);
677 (w, clip, material)
678 }
679
680 #[test]
681 fn display_channels_roundtrip_and_keep_material_references_in_the_closure() {
682 let (w, clip, material) = with_display_channels();
683 assert!(errors(&w).is_empty(), "{:?}", errors(&w));
684 let c = w.get::<Clip>(clip).unwrap();
685 let copy: Clip = serde_json::from_value(serde_json::to_value(c).unwrap()).unwrap();
686 assert_eq!(
687 ©, c,
688 "includes old joint/morph ids and new targets/properties"
689 );
690 assert_eq!(copy.channels.last().unwrap().property.width(), 2);
691 let closure = w.closure([clip]);
692 assert!(closure.contains(&material));
693 let part = w.subset(&closure);
694 assert_eq!(part.get::<Clip>(clip), Some(c));
695 assert!(
696 c.remap_primitives(&[1, 0]).is_none(),
697 "clip channels are not mesh-primitive arrays"
698 );
699 assert!(errors(&part).is_empty());
700 }
701
702 #[test]
703 fn visibility_is_step_only_three_valued_and_scope_bounded() {
704 for interpolation in [Interpolation::Linear, Interpolation::Hermite] {
705 let (mut w, c, _) = with_display_channels();
706 w.get_mut::<Clip>(c).unwrap().channels[4].interpolation = interpolation;
707 assert!(errors(&w).iter().any(|e| e.contains("visibility requires Step")));
708 }
709 for value in [-2.0, 0.5, 2.0, f32::NAN] {
710 let (mut w, c, _) = with_display_channels();
711 w.get_mut::<Clip>(c).unwrap().channels[4].values[1] = value;
712 assert!(errors(&w).iter().any(|e| e.contains("visibility values")));
713 }
714 for (scope, group) in [(" ", 0), ("character", 64)] {
715 let (mut w, c, _) = with_display_channels();
716 w.get_mut::<Clip>(c).unwrap().channels[4].target = ClipTarget::DrawGroup {
717 scope: scope.into(),
718 group,
719 };
720 assert!(errors(&w).iter().any(|e| e.contains("scope")));
721 }
722 }
723
724 #[test]
725 fn material_channels_require_defaults_widths_unique_targets_and_no_overlap() {
726 for width in [0, 1, 3, 4, 5] {
727 let (mut w, c, _) = with_display_channels();
728 w.get_mut::<Clip>(c).unwrap().channels[5].property = Property::Values(width);
729 assert!(!errors(&w).is_empty(), "default width 2 does not match {width}");
730 }
731 let (mut w, _c, m) = with_display_channels();
732 w.remove::<crate::MaterialParameters>(m);
733 assert!(
734 errors(&w)
735 .iter()
736 .any(|e| e.contains("no core.material_parameters"))
737 );
738 let (mut w, c, m) = with_display_channels();
739 w.get_mut::<Clip>(c).unwrap().channels[5].target = ClipTarget::MaterialParameter {
740 materials: vec![m, m],
741 name: "tint".into(),
742 };
743 assert!(errors(&w).iter().any(|e| e.contains("repeated material")));
744 let (mut w, c, _) = with_display_channels();
745 let duplicate = w.get::<Clip>(c).unwrap().channels[5].clone();
746 w.get_mut::<Clip>(c).unwrap().channels.push(duplicate);
747 assert!(errors(&w).iter().any(|e| e.contains("second channel")));
748 let (mut w, c, _) = with_display_channels();
749 w.get_mut::<Clip>(c).unwrap().channels[5].interpolation = Interpolation::Hermite;
750 assert!(errors(&w).iter().any(|e| e.contains("Step or Linear")));
751 }
752
753 #[test]
754 fn malformed_clips_and_bindings_fail_verification() {
755 let fails = |edit: &dyn Fn(&mut World, Entity, Entity), what: &str| {
756 let (mut w, clip, binding, ..) = world();
757 edit(&mut w, clip, binding);
758 let e = errors(&w);
759 assert!(e.iter().any(|m| m.contains(what)), "{what}: {e:?}");
760 };
761 fails(
762 &|w, c, _| w.get_mut::<Clip>(c).unwrap().channels[0].times = vec![1.0, 0.0],
763 "not increasing",
764 );
765 fails(
766 &|w, c, _| w.get_mut::<Clip>(c).unwrap().channels[0].times = vec![0.0, 2.0],
767 "within [0, 1]",
768 );
769 fails(
770 &|w, c, _| {
771 w.get_mut::<Clip>(c).unwrap().channels[0].values.pop();
772 },
773 "7 values for 2 keys of 4",
774 );
775 fails(
776 &|w, c, _| w.get_mut::<Clip>(c).unwrap().channels[0].values[3] = 2.0,
777 "rotation of length",
778 );
779 fails(
780 &|w, c, _| {
781 w.get_mut::<Clip>(c).unwrap().channels[1].values.pop();
782 },
783 "values for 2 keys of 9",
784 );
785 fails(
786 &|w, c, _| w.get_mut::<Clip>(c).unwrap().channels[2].property = Property::Rotation,
787 "does not fit",
788 );
789 fails(
790 &|w, c, _| {
791 let clip = w.get_mut::<Clip>(c).unwrap();
792 let again = clip.channels[2].clone();
793 clip.channels.push(again);
794 },
795 "a second channel",
796 );
797 fails(
798 &|w, c, _| {
799 if let ClipTarget::Joint { joint, .. } = &mut w.get_mut::<Clip>(c).unwrap().channels[0].target
800 {
801 *joint = JointId(7);
802 }
803 },
804 "not in its skeleton",
805 );
806 fails(
807 &|w, c, _| {
808 if let ClipTarget::Morph { morph, .. } = &mut w.get_mut::<Clip>(c).unwrap().channels[2].target
809 {
810 *morph = MorphId(1);
811 }
812 },
813 "not in its set",
814 );
815 fails(
816 &|w, c, _| w.get_mut::<Clip>(c).unwrap().duration = f32::NAN,
817 "duration",
818 );
819 fails(
821 &|w, c, _| w.get_mut::<Clip>(c).unwrap().additive = Some(AdditiveReference::Pose(c)),
822 "no core.rig_pose",
823 );
824 fails(
825 &|w, c, _| {
826 w.get_mut::<Clip>(c).unwrap().additive =
827 Some(AdditiveReference::ClipFrame { clip: c, time: 3.0 })
828 },
829 "outside its clip",
830 );
831 fails(
833 &|w, c, _| {
834 w.get_mut::<Clip>(c).unwrap().additive =
835 Some(AdditiveReference::ClipFrame { clip: c, time: 0.0 })
836 },
837 "a cycle",
838 );
839 fails(
840 &|w, c, _| {
841 let mut other = w.get::<Clip>(c).unwrap().clone();
842 other.additive = Some(AdditiveReference::ClipFrame { clip: c, time: 0.0 });
843 let o = w.spawn();
844 w.insert(o, other);
845 w.get_mut::<Clip>(c).unwrap().additive =
846 Some(AdditiveReference::ClipFrame { clip: o, time: 0.0 });
847 },
848 "a cycle",
849 );
850 fails(
852 &|w, _, b| w.get_mut::<ClipBinding>(b).unwrap().rig = None,
853 "the rig poses None",
854 );
855 fails(
856 &|w, c, b| w.get_mut::<ClipBinding>(b).unwrap().meshes.push(c),
857 "whose morph set the clip does not drive",
858 );
859 }
860}