1use std::collections::{BTreeMap, BTreeSet};
31
32use serde::{Deserialize, Serialize};
33
34use crate::Facing;
35
36pub const RIG_VERSION: u32 = 1;
38
39pub const RIGS_DIR: &str = "rigs";
41
42pub const RIG_FILE: &str = "rig.json";
44
45pub const MAX_TICKS_PER_FRAME: u32 = 20;
50
51pub const MIN_TICKS_PER_FRAME: u32 = 1;
53
54#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56#[serde(deny_unknown_fields)]
57pub struct Rig {
58 pub rig_version: u32,
60 pub parts: Vec<RigPart>,
62 pub clips: BTreeMap<String, Clip>,
66 pub provenance: RigProvenance,
69}
70
71#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct RigPart {
75 pub id: String,
77 pub kind: PartKind,
79 pub block: String,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub rest: Option<Transform>,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "kebab-case")]
92pub enum PartKind {
93 Block,
95}
96
97#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct Clip {
101 pub ticks_per_frame: u32,
104 #[serde(rename = "loop")]
107 pub looping: bool,
108 pub frames: Vec<Vec<Transform>>,
110}
111
112impl Clip {
113 pub fn length_ticks(&self) -> u32 {
119 1 + (self.frames.len().saturating_sub(1) as u32) * self.ticks_per_frame
120 }
121
122 pub fn landing_ticks(&self) -> u32 {
128 self.length_ticks() + self.ticks_per_frame
129 }
130}
131
132#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
136#[serde(deny_unknown_fields)]
137pub struct Transform {
138 pub translation: [f64; 3],
140 pub left_rotation: [f64; 4],
142 pub scale: [f64; 3],
144 pub right_rotation: [f64; 4],
146}
147
148#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
150#[serde(deny_unknown_fields)]
151pub struct RigProvenance {
152 pub generator: String,
155 pub source: String,
157 pub spdx: String,
159}
160
161#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct RigIssue {
164 pub field: String,
166 pub message: String,
168}
169
170pub fn parse(raw: &str) -> Result<Rig, String> {
172 serde_json::from_str::<Rig>(raw).map_err(|e| e.to_string())
173}
174
175pub fn is_clip_name(name: &str) -> bool {
178 crate::ids::is_kebab(name)
179}
180
181pub fn check(rig: &Rig) -> Vec<RigIssue> {
184 let mut out = Vec::new();
185 let mut push = |field: String, message: String| out.push(RigIssue { field, message });
186 if rig.rig_version != RIG_VERSION {
187 push(
188 "/rig_version".into(),
189 format!(
190 "`rig_version` is {}; this engine reads version {RIG_VERSION}",
191 rig.rig_version
192 ),
193 );
194 }
195 if rig.parts.is_empty() {
196 push(
197 "/parts".into(),
198 "the rig declares no part, so the assembly would show nothing".into(),
199 );
200 }
201 let mut seen: BTreeSet<&str> = BTreeSet::new();
202 for (i, p) in rig.parts.iter().enumerate() {
203 if p.id.is_empty() {
204 push(format!("/parts/{i}/id"), "a part's `id` is empty".into());
205 } else if !seen.insert(p.id.as_str()) {
206 push(
207 format!("/parts/{i}/id"),
208 format!("part id `{}` is declared twice", p.id),
209 );
210 }
211 if let Err(e) = crate::blocks::BlockRegistry::v1_21_11().validate_state_string(&p.block) {
212 push(
213 format!("/parts/{i}/block"),
214 format!(
215 "part `{}` shows `{}`, which is not a block state of Minecraft Java \
216 1.21.11 ({e:?})",
217 p.id, p.block
218 ),
219 );
220 }
221 if let Some(t) = &p.rest {
222 for m in transform_issues(t) {
223 push(format!("/parts/{i}/rest"), m);
224 }
225 }
226 }
227 if rig.clips.is_empty() {
228 push(
229 "/clips".into(),
230 "the rig declares no clip, so the assembly has nothing to play and no pose to \
231 stand in"
232 .into(),
233 );
234 }
235 for (name, clip) in &rig.clips {
236 let at = format!("/clips/{name}");
237 if !is_clip_name(name) {
238 push(
239 at.clone(),
240 format!(
241 "clip name `{name}` is not lowercase kebab-case (`[a-z0-9]` segments joined by `-`)"
242 ),
243 );
244 }
245 if !(MIN_TICKS_PER_FRAME..=MAX_TICKS_PER_FRAME).contains(&clip.ticks_per_frame) {
246 push(
247 format!("{at}/ticks_per_frame"),
248 format!(
249 "clip `{name}` declares `ticks_per_frame` {}; the cadence is \
250 {MIN_TICKS_PER_FRAME}..={MAX_TICKS_PER_FRAME} ticks per frame",
251 clip.ticks_per_frame
252 ),
253 );
254 }
255 if clip.frames.is_empty() {
256 push(
257 format!("{at}/frames"),
258 format!("clip `{name}` has no frame"),
259 );
260 }
261 for (f, frame) in clip.frames.iter().enumerate() {
262 if frame.len() != rig.parts.len() {
263 push(
264 format!("{at}/frames/{f}"),
265 format!(
266 "clip `{name}` frame {f} carries {} transform(s) and the rig has {} \
267 part(s); a frame is one transform per part",
268 frame.len(),
269 rig.parts.len()
270 ),
271 );
272 }
273 for (p, t) in frame.iter().enumerate() {
274 for m in transform_issues(t) {
275 push(format!("{at}/frames/{f}/{p}"), m);
276 }
277 }
278 }
279 }
280 out
281}
282
283fn transform_issues(t: &Transform) -> Vec<String> {
287 let mut out = Vec::new();
288 let all = t
289 .translation
290 .iter()
291 .chain(t.left_rotation.iter())
292 .chain(t.scale.iter())
293 .chain(t.right_rotation.iter());
294 if all.clone().any(|v| !v.is_finite()) {
295 out.push("a transform carries a value that is not a finite number".to_string());
296 return out;
297 }
298 for (axis, v) in ["x", "y", "z"].iter().zip(t.scale) {
299 if v == 0.0 {
300 out.push(format!(
301 "`scale` is 0 on {axis}: the part collapses to a plane and shows nothing"
302 ));
303 }
304 }
305 for (name, q) in [
306 ("left_rotation", t.left_rotation),
307 ("right_rotation", t.right_rotation),
308 ] {
309 if q.iter().map(|v| v * v).sum::<f64>() < 1e-12 {
310 out.push(format!("`{name}` is a quaternion of zero length"));
311 }
312 }
313 out
314}
315
316impl Rig {
317 pub fn clip_names(&self) -> Vec<&str> {
319 self.clips.keys().map(String::as_str).collect()
320 }
321
322 pub fn clip_index(&self, name: &str) -> Option<usize> {
324 self.clips.keys().position(|k| k == name)
325 }
326
327 pub fn rest_pose(&self) -> Option<Vec<Transform>> {
330 let first = self.clips.values().next().and_then(|c| c.frames.first());
331 self.parts
332 .iter()
333 .enumerate()
334 .map(|(i, p)| {
335 p.rest
336 .clone()
337 .or_else(|| first.and_then(|f| f.get(i).cloned()))
338 })
339 .collect()
340 }
341
342 pub fn frame_count(&self) -> usize {
345 self.clips.values().map(|c| c.frames.len()).sum()
346 }
347}
348
349impl RigPart {
350 pub fn block_state_snbt(&self) -> String {
353 let (name, props) = crate::blocks::parse_state(&self.block);
354 let name = if name.contains(':') {
355 name.to_string()
356 } else {
357 format!("minecraft:{name}")
358 };
359 if props.is_empty() {
360 format!("{{Name:\"{name}\"}}")
361 } else {
362 let p: Vec<String> = props.iter().map(|(k, v)| format!("{k}:\"{v}\"")).collect();
363 format!("{{Name:\"{name}\",Properties:{{{}}}}}", p.join(","))
364 }
365 }
366}
367
368pub fn facing_angle(f: Facing) -> f64 {
375 match f {
376 Facing::South => 0.0,
377 Facing::East => std::f64::consts::FRAC_PI_2,
378 Facing::North => std::f64::consts::PI,
379 Facing::West => -std::f64::consts::FRAC_PI_2,
380 }
381}
382
383fn qmul(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
385 let [ax, ay, az, aw] = a;
386 let [bx, by, bz, bw] = b;
387 [
388 aw * bx + ax * bw + ay * bz - az * by,
389 aw * by - ax * bz + ay * bw + az * bx,
390 aw * bz + ax * by - ay * bx + az * bw,
391 aw * bw - ax * bx - ay * by - az * bz,
392 ]
393}
394
395fn qrot(q: [f64; 4], v: [f64; 3]) -> [f64; 3] {
397 let n = q.iter().map(|c| c * c).sum::<f64>().sqrt();
398 let q = [q[0] / n, q[1] / n, q[2] / n, q[3] / n];
399 let p = qmul(
400 qmul(q, [v[0], v[1], v[2], 0.0]),
401 [-q[0], -q[1], -q[2], q[3]],
402 );
403 [p[0], p[1], p[2]]
404}
405
406fn tidy(v: f64) -> f64 {
410 let r = v.round();
411 let v = if (v - r).abs() < 1e-9 { r } else { v };
412 if v == 0.0 { 0.0 } else { v }
413}
414
415impl Transform {
416 pub fn faced(&self, facing: Facing) -> Transform {
423 self.turned(facing_angle(facing))
424 }
425
426 pub fn turned(&self, a: f64) -> Transform {
431 if a == 0.0 {
432 return self.clone();
433 }
434 let yaw = [0.0, (a / 2.0).sin(), 0.0, (a / 2.0).cos()];
435 let t = qrot(yaw, self.translation);
436 let l = qmul(yaw, self.left_rotation);
437 Transform {
438 translation: t.map(tidy),
439 left_rotation: l.map(tidy),
440 scale: self.scale,
441 right_rotation: self.right_rotation,
442 }
443 }
444
445 fn corners(&self) -> [[f64; 3]; 8] {
447 let mut out = [[0.0; 3]; 8];
448 for (k, c) in out.iter_mut().enumerate() {
449 let unit = [(k & 1) as f64, ((k >> 1) & 1) as f64, ((k >> 2) & 1) as f64];
450 let r = qrot(self.right_rotation, unit);
451 let s = [
452 r[0] * self.scale[0],
453 r[1] * self.scale[1],
454 r[2] * self.scale[2],
455 ];
456 let l = qrot(self.left_rotation, s);
457 *c = [
458 l[0] + self.translation[0],
459 l[1] + self.translation[1],
460 l[2] + self.translation[2],
461 ];
462 }
463 out
464 }
465
466 pub fn cells(&self) -> BTreeSet<[i32; 3]> {
473 let origin = [0.5, 0.0, 0.5];
475 let corners = self
476 .corners()
477 .map(|c| [c[0] + origin[0], c[1] + origin[1], c[2] + origin[2]]);
478 let mut lo = [f64::INFINITY; 3];
479 let mut hi = [f64::NEG_INFINITY; 3];
480 for c in corners {
481 for i in 0..3 {
482 lo[i] = lo[i].min(c[i]);
483 hi[i] = hi[i].max(c[i]);
484 }
485 }
486 let span = |i: usize| -> (i32, i32) {
487 let from = (lo[i] + CELL_EPS).floor() as i32;
488 let to = (hi[i] - CELL_EPS).ceil() as i32 - 1;
489 (from, to.max(from))
490 };
491 let (x0, x1) = span(0);
492 let (y0, y1) = span(1);
493 let (z0, z1) = span(2);
494 let axes = separating_axes(&corners);
495 let mut out = BTreeSet::new();
496 for x in x0..=x1 {
497 for y in y0..=y1 {
498 for z in z0..=z1 {
499 let lo = [f64::from(x), f64::from(y), f64::from(z)];
500 if box_meets(&corners, &axes, lo, [lo[0] + 1.0, lo[1] + 1.0, lo[2] + 1.0]) {
501 out.insert([x, y, z]);
502 }
503 }
504 }
505 }
506 out
507 }
508
509 pub fn meets_box(&self, lo: [f64; 3], hi: [f64; 3]) -> bool {
514 let corners = self.corners().map(|c| [c[0] + 0.5, c[1], c[2] + 0.5]);
515 box_meets(&corners, &separating_axes(&corners), lo, hi)
516 }
517}
518
519const CELL_EPS: f64 = 1e-9;
523
524fn separating_axes(c: &[[f64; 3]; 8]) -> Vec<[f64; 3]> {
528 let sub = |a: [f64; 3], b: [f64; 3]| [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
529 let edges = [sub(c[1], c[0]), sub(c[2], c[0]), sub(c[4], c[0])];
530 let world = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
531 let mut axes: Vec<[f64; 3]> = world.to_vec();
532 let mut push = |v: [f64; 3]| {
533 let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
534 if n > 1e-12 {
535 axes.push([v[0] / n, v[1] / n, v[2] / n]);
536 }
537 };
538 for e in edges {
539 push(e);
540 }
541 for a in world {
542 for e in edges {
543 push([
544 a[1] * e[2] - a[2] * e[1],
545 a[2] * e[0] - a[0] * e[2],
546 a[0] * e[1] - a[1] * e[0],
547 ]);
548 }
549 }
550 axes
551}
552
553fn box_meets(c: &[[f64; 3]; 8], axes: &[[f64; 3]], lo: [f64; 3], hi: [f64; 3]) -> bool {
557 let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
558 axes.iter().all(|a| {
559 let (mut p0, mut p1) = (f64::INFINITY, f64::NEG_INFINITY);
560 for v in c {
561 let d = dot(*v, *a);
562 p0 = p0.min(d);
563 p1 = p1.max(d);
564 }
565 let (mut q0, mut q1) = (f64::INFINITY, f64::NEG_INFINITY);
566 for k in 0..8 {
567 let pick = |i: usize, bit: usize| if (k >> bit) & 1 == 0 { lo[i] } else { hi[i] };
568 let v = [pick(0, 0), pick(1, 1), pick(2, 2)];
569 let d = dot(v, *a);
570 q0 = q0.min(d);
571 q1 = q1.max(d);
572 }
573 p1 > q0 + CELL_EPS && q1 > p0 + CELL_EPS
574 })
575}
576
577pub fn frame_footprint(frame: &[Transform], facing: Facing) -> BTreeSet<[i32; 3]> {
584 frame_footprint_turned(frame, facing_angle(facing))
585}
586
587pub fn frame_footprint_turned(frame: &[Transform], a: f64) -> BTreeSet<[i32; 3]> {
591 let mut out = BTreeSet::new();
592 for t in frame {
593 out.extend(t.turned(a).cells());
594 }
595 out
596}
597
598pub fn clip_footprint(clip: &Clip, facing: Facing) -> BTreeSet<[i32; 3]> {
600 let mut out = BTreeSet::new();
601 for f in &clip.frames {
602 out.extend(frame_footprint(f, facing));
603 }
604 out
605}
606
607pub fn last_frame_footprint(clip: &Clip, facing: Facing) -> BTreeSet<[i32; 3]> {
610 clip.frames
611 .last()
612 .map(|f| frame_footprint(f, facing))
613 .unwrap_or_default()
614}
615
616pub fn cells_line(cells: &BTreeSet<[i32; 3]>) -> String {
619 cells
620 .iter()
621 .map(|c| format!("[{}, {}, {}]", c[0], c[1], c[2]))
622 .collect::<Vec<_>>()
623 .join(" ")
624}
625
626pub fn describe(id: &str, rig: &Rig, facing: Facing) -> String {
631 let mut out = String::new();
632 out.push_str(&format!(
633 "rig {id}: {} part(s), {} clip(s), {} frame(s) in all; facing {}; provenance: {} ({}, {})\n",
634 rig.parts.len(),
635 rig.clips.len(),
636 rig.frame_count(),
637 facing.token(),
638 rig.provenance.generator,
639 rig.provenance.source,
640 rig.provenance.spdx,
641 ));
642 for (name, clip) in &rig.clips {
643 let last = last_frame_footprint(clip, facing);
644 out.push_str(&format!(
645 "clip {name}: {} frame(s) every {} tick(s), {} tick(s) from switch to last frame \
646 applied, {} until it is drawn whole (a blow from it lands then), {}\n",
647 clip.frames.len(),
648 clip.ticks_per_frame,
649 clip.length_ticks(),
650 clip.landing_ticks(),
651 if clip.looping {
652 "loops"
653 } else {
654 "holds its last frame"
655 },
656 ));
657 out.push_str(&format!(
658 " last-frame footprint, {} cell(s) relative to the mark: {}\n",
659 last.len(),
660 cells_line(&last)
661 ));
662 }
663 out
664}
665
666#[derive(Clone, Copy, Debug)]
668pub enum RigLookup<'a> {
669 Unknown,
673 Missing,
675 Malformed(&'a str),
677 Found(&'a Rig),
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684
685 fn unit() -> Transform {
686 Transform {
687 translation: [0.0, 0.0, 0.0],
688 left_rotation: [0.0, 0.0, 0.0, 1.0],
689 scale: [1.0, 1.0, 1.0],
690 right_rotation: [0.0, 0.0, 0.0, 1.0],
691 }
692 }
693
694 fn rig(frames: Vec<Vec<Transform>>, tpf: u32) -> Rig {
695 let mut clips = BTreeMap::new();
696 clips.insert(
697 "idle".to_string(),
698 Clip {
699 ticks_per_frame: tpf,
700 looping: true,
701 frames,
702 },
703 );
704 Rig {
705 rig_version: RIG_VERSION,
706 parts: vec![RigPart {
707 id: "a".into(),
708 kind: PartKind::Block,
709 block: "minecraft:stone".into(),
710 rest: None,
711 }],
712 clips,
713 provenance: RigProvenance {
714 generator: "test".into(),
715 source: "original".into(),
716 spdx: "GPL-3.0-or-later".into(),
717 },
718 }
719 }
720
721 #[test]
724 fn the_unit_cube_at_the_origin_meets_four_cells() {
725 let cells = unit().cells();
726 assert_eq!(
727 cells,
728 [[0, 0, 0], [0, 0, 1], [1, 0, 0], [1, 0, 1]]
729 .into_iter()
730 .collect()
731 );
732 }
733
734 #[test]
737 fn a_centred_cube_is_the_mark_cell() {
738 let mut t = unit();
739 t.translation = [-0.5, 0.0, -0.5];
740 assert_eq!(t.cells(), [[0, 0, 0]].into_iter().collect());
741 }
742
743 #[test]
747 fn a_diagonal_part_meets_the_cells_along_it_not_its_hull() {
748 let a = std::f64::consts::FRAC_PI_4;
753 let t = Transform {
754 translation: [0.0, 0.0, 0.0],
755 left_rotation: [0.0, (a / 2.0).sin(), 0.0, (a / 2.0).cos()],
756 scale: [0.2, 1.0, 4.24],
757 right_rotation: [0.0, 0.0, 0.0, 1.0],
758 };
759 let cells = t.cells();
760 assert!(
761 cells.contains(&[0, 0, 0]) && cells.contains(&[2, 0, 2]),
762 "{cells:?}"
763 );
764 assert!(
765 !cells.contains(&[0, 0, 3]) && !cells.contains(&[3, 0, 0]),
766 "{cells:?}"
767 );
768 assert!(cells.len() < 16, "{} cells: {cells:?}", cells.len());
769 }
770
771 #[test]
772 fn rotation_and_scale_move_the_cell_set() {
773 let s = std::f64::consts::FRAC_1_SQRT_2;
774 let t = Transform {
775 translation: [-0.5, 0.0, -0.5],
776 left_rotation: [0.0, 0.0, s, s],
778 scale: [1.0, 3.0, 1.0],
779 right_rotation: [0.0, 0.0, 0.0, 1.0],
780 };
781 assert_eq!(
782 t.cells(),
783 [[-3, 0, 0], [-2, 0, 0], [-1, 0, 0]].into_iter().collect()
784 );
785 }
786
787 #[test]
790 fn facing_turns_the_footprint_about_the_mark() {
791 let mut t = unit();
792 t.translation = [-0.5, 0.0, 1.5];
793 assert_eq!(
794 frame_footprint(std::slice::from_ref(&t), Facing::South),
795 [[0, 0, 2]].into_iter().collect()
796 );
797 assert_eq!(
798 frame_footprint(std::slice::from_ref(&t), Facing::North),
799 [[0, 0, -2]].into_iter().collect()
800 );
801 assert_eq!(
802 frame_footprint(std::slice::from_ref(&t), Facing::East),
803 [[2, 0, 0]].into_iter().collect()
804 );
805 assert_eq!(
806 frame_footprint(std::slice::from_ref(&t), Facing::West),
807 [[-2, 0, 0]].into_iter().collect()
808 );
809 }
810
811 #[test]
812 fn a_well_formed_rig_has_no_issue() {
813 assert!(check(&rig(vec![vec![unit()]], 5)).is_empty());
814 }
815
816 #[test]
817 fn each_structural_defect_names_its_field() {
818 let short = rig(vec![vec![]], 5);
819 assert!(
820 check(&short)
821 .iter()
822 .any(|i| i.field == "/clips/idle/frames/0")
823 );
824 for tpf in [0, 21] {
825 let r = rig(vec![vec![unit()]], tpf);
826 assert!(
827 check(&r)
828 .iter()
829 .any(|i| i.field == "/clips/idle/ticks_per_frame"),
830 "{tpf}"
831 );
832 }
833 let mut z = unit();
834 z.scale = [1.0, 0.0, 1.0];
835 assert!(
836 check(&rig(vec![vec![z]], 5))
837 .iter()
838 .any(|i| i.field == "/clips/idle/frames/0/0" && i.message.contains("`scale` is 0"))
839 );
840 let mut bad = rig(vec![vec![unit()]], 5);
841 bad.parts[0].block = "minecraft:no_such_block".into();
842 assert!(check(&bad).iter().any(|i| i.field == "/parts/0/block"));
843 }
844
845 #[test]
846 fn block_state_snbt_carries_properties() {
847 let mut r = rig(vec![vec![unit()]], 5);
848 assert_eq!(r.parts[0].block_state_snbt(), "{Name:\"minecraft:stone\"}");
849 r.parts[0].block = "minecraft:oak_log[axis=x]".into();
850 assert_eq!(
851 r.parts[0].block_state_snbt(),
852 "{Name:\"minecraft:oak_log\",Properties:{axis:\"x\"}}"
853 );
854 }
855
856 #[test]
857 fn describe_is_deterministic() {
858 let r = rig(vec![vec![unit()], vec![unit()]], 5);
859 assert_eq!(
860 describe("rig/x", &r, Facing::South),
861 describe("rig/x", &r, Facing::South)
862 );
863 assert!(describe("rig/x", &r, Facing::South).contains("6 tick(s)"));
864 }
865}