1use anyhow::Result;
2use clap::{Subcommand, ValueEnum};
3use ranim::{
4 App, AppExt, SceneConstructor,
5 core::{
6 animation::node::{AnimationInfo, AnimationInfoKind},
7 frame::{EvaluatedFrame, FrameItemMetadata},
8 utils::rate_functions,
9 },
10};
11#[cfg(feature = "items")]
12use ranim::{
13 color::{AlphaColor, Srgb},
14 core::{
15 anchor::Aabb,
16 components::{rgba::Rgba, vpoint::VPointVec},
17 utils::bezier::get_subpath_closed_flag,
18 },
19 glam::{DAffine3, DMat4, DVec3, Mat3},
20 items::{
21 camera::CameraFrame,
22 hierarchy::PlacedLeaves,
23 mesh::{self, render::RenderMeshItem},
24 vitem::{
25 self,
26 render::{RenderVItem, vitem_normal_from_points},
27 },
28 },
29};
30use serde::Serialize;
31
32use super::cli::select_scene;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
36pub enum FormatArg {
37 #[default]
39 Text,
40 Json,
42}
43
44#[derive(Debug, Subcommand)]
47pub enum InspectCommand {
48 Scenes {
50 #[arg(long, value_enum, default_value_t = FormatArg::Text)]
52 format: FormatArg,
53 },
54 Tree {
56 scene: Option<String>,
58 #[arg(long, value_enum, default_value_t = FormatArg::Text)]
60 format: FormatArg,
61 },
62 Frame {
64 scene: String,
66 #[arg(long)]
68 at: f64,
69 #[arg(long, value_enum, default_value_t = FormatArg::Text)]
71 format: FormatArg,
72 #[arg(long)]
74 verbose: bool,
75 },
76}
77
78pub fn inspect_command(app: &App, command: InspectCommand) -> Result<()> {
80 match command {
81 InspectCommand::Scenes { format } => scenes_command(app, format),
82 InspectCommand::Tree { scene, format } => tree_command(app, scene.as_deref(), format),
83 InspectCommand::Frame {
84 scene,
85 at,
86 format,
87 verbose,
88 } => frame_command(app, &scene, at, format, verbose),
89 }
90}
91
92#[derive(Debug, Serialize)]
95struct ScenesOutput {
96 schema_version: u32,
97 scenes: Vec<SceneDto>,
98}
99
100#[derive(Debug, Serialize)]
101struct SceneDto {
102 name: String,
103 clear_color: String,
104 outputs: Vec<OutputDto>,
105}
106
107#[derive(Debug, Serialize)]
108struct OutputDto {
109 width: u32,
110 height: u32,
111 fps: u32,
112 format: String,
113 dir: String,
114 name_template: String,
115 save_frames: bool,
116}
117
118const DEFAULT_NAME_TEMPLATE: &str = "{name}_{width}x{height}_{fps}";
119
120fn scenes_output(app: &App) -> ScenesOutput {
121 let scenes = app
122 .scenes()
123 .iter()
124 .map(|scene| SceneDto {
125 name: scene.name.clone(),
126 clear_color: scene.config.clear_color.clone(),
127 outputs: scene
128 .outputs
129 .iter()
130 .map(|output| OutputDto {
131 width: output.width,
132 height: output.height,
133 fps: output.fps,
134 format: output.format.to_string(),
135 dir: output.dir.clone(),
136 name_template: output
137 .name_template
138 .clone()
139 .unwrap_or_else(|| DEFAULT_NAME_TEMPLATE.to_string()),
140 save_frames: output.save_frames,
141 })
142 .collect(),
143 })
144 .collect();
145 ScenesOutput {
146 schema_version: 1,
147 scenes,
148 }
149}
150
151fn scenes_command(app: &App, format: FormatArg) -> Result<()> {
152 let output = scenes_output(app);
153 match format {
154 FormatArg::Json => print_json(&output),
155 FormatArg::Text => {
156 for scene in &output.scenes {
157 println!("scene {}", scene.name);
158 println!(" clear_color: {}", scene.clear_color);
159 println!(" outputs:");
160 if scene.outputs.is_empty() {
161 println!(" (none)");
162 }
163 for output in &scene.outputs {
164 println!(
165 " - {}x{} @{}fps {} -> {} save_frames={} name_template=\"{}\"",
166 output.width,
167 output.height,
168 output.fps,
169 output.format,
170 output.dir,
171 output.save_frames,
172 output.name_template
173 );
174 }
175 }
176 Ok(())
177 }
178 }
179}
180
181#[derive(Debug, Serialize)]
184struct TreeOutput {
185 schema_version: u32,
186 scene: String,
187 total_secs: f64,
188 time_marks: Vec<TimeMarkDto>,
189 tree: Vec<TreeNodeDto>,
190}
191
192#[derive(Debug, Serialize)]
193struct TimeMarkDto {
194 sec: f64,
195 kind: String,
196 label: String,
197}
198
199#[derive(Debug, Serialize)]
200struct TreeNodeDto {
201 path: Vec<usize>,
202 kind: String,
203 anim_name: String,
204 range: [f64; 2],
205 content_duration_secs: f64,
206 rate_func: String,
207 enabled: bool,
208 #[serde(skip_serializing_if = "Option::is_none")]
211 sim_step: Option<f64>,
212 children: Vec<TreeNodeDto>,
213}
214
215fn tree_output(
216 scene_name: String,
217 total_secs: f64,
218 time_marks: Vec<TimeMarkDto>,
219 infos: Vec<AnimationInfo>,
220) -> TreeOutput {
221 TreeOutput {
222 schema_version: 1,
223 scene: scene_name,
224 total_secs,
225 time_marks,
226 tree: infos
227 .iter()
228 .enumerate()
229 .map(|(idx, info)| animation_info_to_tree_node(info, vec![idx]))
230 .collect(),
231 }
232}
233
234fn animation_info_to_tree_node(info: &AnimationInfo, path: Vec<usize>) -> TreeNodeDto {
235 TreeNodeDto {
236 path: path.clone(),
237 kind: animation_kind_str(info.kind).to_string(),
238 anim_name: info.anim_name.clone(),
239 range: range_to_array(&info.range),
240 content_duration_secs: info.content_duration_secs,
241 rate_func: rate_func_name(info.rate_func).to_string(),
242 enabled: info.enabled,
243 sim_step: info.sim_step,
244 children: info
245 .children
246 .iter()
247 .enumerate()
248 .map(|(idx, child)| {
249 let mut child_path = path.clone();
250 child_path.push(idx);
251 animation_info_to_tree_node(child, child_path)
252 })
253 .collect(),
254 }
255}
256
257fn range_to_array(range: &std::ops::Range<f64>) -> [f64; 2] {
258 [range.start, range.end]
259}
260
261fn animation_kind_str(kind: AnimationInfoKind) -> &'static str {
262 match kind {
263 AnimationInfoKind::Anim => "anim",
264 AnimationInfoKind::Children => "children",
265 AnimationInfoKind::Audio => "audio",
266 }
267}
268
269fn rate_func_name(rate_func: fn(f64) -> f64) -> &'static str {
270 const PROBE_POINTS: [f64; 6] = [0.0, 0.1, 0.25, 0.5, 0.75, 1.0];
274 type KnownRateFunc = (&'static str, fn(f64) -> f64);
275 const KNOWN: &[KnownRateFunc] = &[
276 ("linear", rate_functions::linear),
277 ("smooth", rate_functions::smooth),
278 ("ease_in_quad", rate_functions::ease_in_quad),
279 ("ease_out_quad", rate_functions::ease_out_quad),
280 ("ease_in_out_quad", rate_functions::ease_in_out_quad),
281 ("ease_in_cubic", rate_functions::ease_in_cubic),
282 ("ease_out_cubic", rate_functions::ease_out_cubic),
283 ("ease_in_out_cubic", rate_functions::ease_in_out_cubic),
284 ];
285
286 KNOWN
287 .iter()
288 .find(|(_, known)| {
289 PROBE_POINTS
290 .iter()
291 .all(|&point| (known(point) - rate_func(point)).abs() < 1e-12)
292 })
293 .map(|(name, _)| *name)
294 .unwrap_or("custom")
295}
296
297fn short_anim_name(anim_name: &str) -> &str {
298 let without_generics = anim_name.split('<').next().unwrap_or(anim_name);
299 without_generics
300 .rsplit("::")
301 .next()
302 .unwrap_or(without_generics)
303}
304
305fn tree_command(app: &App, scene_name: Option<&str>, format: FormatArg) -> Result<()> {
306 let scene = select_scene(app, scene_name)?;
307 let sealed = scene.build_scene();
308 let time_marks = sealed
309 .time_marks()
310 .iter()
311 .map(|(sec, mark)| TimeMarkDto {
312 sec: *sec,
313 kind: match mark {
314 ranim::core::TimeMark::Capture(_) => "capture".to_string(),
315 },
316 label: match mark {
317 ranim::core::TimeMark::Capture(label) => label.clone(),
318 },
319 })
320 .collect();
321 let output = tree_output(
322 scene.name.clone(),
323 sealed.total_secs(),
324 time_marks,
325 sealed.get_animation_infos(),
326 );
327 match format {
328 FormatArg::Json => print_json(&output),
329 FormatArg::Text => {
330 println!("scene {}", output.scene);
331 println!("total_secs: {}", output.total_secs);
332 println!("time_marks:");
333 for mark in &output.time_marks {
334 println!(" - {} {} {:?}", mark.sec, mark.kind, mark.label);
335 }
336 println!("tree:");
337 for node in &output.tree {
338 print_tree_node(node, 0);
339 }
340 Ok(())
341 }
342 }
343}
344
345fn print_tree_node(node: &TreeNodeDto, depth: usize) {
346 let path = node
347 .path
348 .iter()
349 .map(|idx| idx.to_string())
350 .collect::<Vec<_>>()
351 .join(", ");
352 let sim_step = node
353 .sim_step
354 .map(|step| format!(" sim_step={step}"))
355 .unwrap_or_default();
356 println!(
357 "{}[{}] {} {} [{}..{}] content={} rate_func={} enabled={}{}",
358 " ".repeat(depth),
359 path,
360 short_anim_name(&node.anim_name),
361 node.kind,
362 node.range[0],
363 node.range[1],
364 node.content_duration_secs,
365 node.rate_func,
366 node.enabled,
367 sim_step
368 );
369 for child in &node.children {
370 print_tree_node(child, depth + 1);
371 }
372}
373
374#[derive(Debug, Serialize)]
377struct FrameOutput {
378 schema_version: u32,
379 scene: String,
380 at: f64,
381 total_secs: f64,
382 items: Vec<FrameItemDto>,
383}
384
385#[derive(Debug, Serialize)]
386struct FrameItemDto {
387 order: usize,
388 input_type: String,
389 id: [usize; 2],
390 animation_id: usize,
391 part: usize,
392 kind: String,
393 source: SourceDto,
394 data: FrameItemDataDto,
395}
396
397#[derive(Debug, Serialize)]
398struct SourceDto {
399 path: Vec<usize>,
400 kind: String,
401 anim_name: String,
402 range: [f64; 2],
403}
404
405#[derive(Debug, Serialize)]
406#[serde(untagged)]
407enum FrameItemDataDto {
408 #[cfg(feature = "items")]
409 VItem(VItemDataDto),
410 #[cfg(feature = "items")]
411 Mesh(MeshDataDto),
412 #[cfg(feature = "items")]
413 Camera(CameraDataDto),
414 #[cfg(feature = "items")]
415 Group {
416 items: Vec<FrameItemDataDto>,
417 },
418 Opaque {},
419}
420
421#[cfg(feature = "items")]
422#[derive(Debug, Serialize)]
423struct BoundsDto {
424 min: [f64; 3],
425 max: [f64; 3],
426}
427
428#[cfg(feature = "items")]
429#[derive(Debug, Serialize)]
430struct VItemDataDto {
431 point_count: usize,
432 subpath_count: usize,
433 closed: Vec<bool>,
434 fill_colors: Vec<String>,
435 stroke_colors: Vec<String>,
436 stroke_widths: Vec<f32>,
437 normal: [f32; 3],
438 bounds: BoundsDto,
439 #[serde(skip_serializing_if = "Option::is_none")]
440 points: Option<Vec<[f32; 4]>>,
441}
442
443#[cfg(feature = "items")]
444#[derive(Debug, Serialize)]
445struct MeshDataDto {
446 point_count: usize,
447 triangle_count: usize,
448 transform: [f32; 16],
449 bounds: BoundsDto,
450 #[serde(skip_serializing_if = "Option::is_none")]
451 points: Option<Vec<[f32; 3]>>,
452 #[serde(skip_serializing_if = "Option::is_none")]
453 triangle_indices: Option<Vec<u32>>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 vertex_colors: Option<Vec<String>>,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 vertex_normals: Option<Vec<[f32; 3]>>,
458}
459
460#[cfg(feature = "items")]
461#[derive(Debug, Serialize)]
462struct CameraDataDto {
463 pos: [f64; 3],
464 facing: [f64; 3],
465 up: [f64; 3],
466 near: f64,
467 far: f64,
468 perspective_blend: f64,
469 frame_height: f64,
470 scale: f64,
471 fovy: f64,
472}
473
474fn frame_command(
475 app: &App,
476 scene_name: &str,
477 at: f64,
478 format: FormatArg,
479 verbose: bool,
480) -> Result<()> {
481 let scene = select_scene(app, Some(scene_name))?;
482 let sealed = scene.build_scene();
483 let total_secs = sealed.total_secs();
484 let mut evaluator = sealed.into_evaluator(120.0);
485 let infos = evaluator.animation_infos();
486 let mut frame = EvaluatedFrame::new();
487 evaluator.sample_at(at, &mut frame);
488
489 let items = inspect_frame(&frame, &infos, verbose);
490
491 let output = FrameOutput {
492 schema_version: 2,
493 scene: scene.name.clone(),
494 at,
495 total_secs,
496 items,
497 };
498
499 match format {
500 FormatArg::Json => print_json(&output),
501 FormatArg::Text => {
502 println!(
503 "scene {} @{} total_secs={}",
504 output.scene, output.at, output.total_secs
505 );
506 for item in &output.items {
507 print_frame_item(item);
508 }
509 Ok(())
510 }
511 }
512}
513
514fn inspect_frame(
515 frame: &EvaluatedFrame,
516 infos: &[AnimationInfo],
517 _verbose: bool,
518) -> Vec<FrameItemDto> {
519 let mut data = std::collections::HashMap::<usize, FrameItemDataDto>::new();
520 #[cfg(feature = "items")]
521 {
522 let verbose = _verbose;
523 for item in frame.items::<CameraFrame>() {
524 data.insert(item.order, FrameItemDataDto::from_camera(&item.value));
525 }
526 macro_rules! vector_inputs {
527 ($($input:ty),* $(,)?) => {$({
528 for item in frame.items::<$input>() {
529 let value = RenderVItem::from(vitem::VItem::from(item.value.clone()));
530 data.insert(item.order, inspect_vitem(value, DAffine3::IDENTITY, verbose));
531 }
532 })*};
533 }
534 vector_inputs!(
535 vitem::VItem,
536 vitem::geometry::Arc,
537 vitem::geometry::ArcBetweenPoints,
538 vitem::geometry::Circle,
539 vitem::geometry::Ellipse,
540 vitem::geometry::EllipticArc,
541 vitem::geometry::Line,
542 vitem::geometry::Parallelogram,
543 vitem::geometry::Polygon,
544 vitem::geometry::Rectangle,
545 vitem::geometry::Square,
546 );
547 for item in frame.items::<vitem::geometry::RegularPolygon>() {
548 let value = RenderVItem::from(vitem::VItem::from(vitem::geometry::Polygon::from(
549 item.value.clone(),
550 )));
551 data.insert(
552 item.order,
553 inspect_vitem(value, DAffine3::IDENTITY, verbose),
554 );
555 }
556 for item in frame.items::<vitem::svg::SvgItem>() {
557 let items = item
558 .value
559 .tree()
560 .leaves()
561 .map(|(placement, value)| {
562 inspect_vitem(RenderVItem::from(value.clone()), placement, verbose)
563 })
564 .collect();
565 data.insert(item.order, FrameItemDataDto::Group { items });
566 }
567 macro_rules! mesh_inputs {
568 ($($input:ty),* $(,)?) => {$({
569 for item in frame.items::<$input>() {
570 let value = RenderMeshItem::from(mesh::MeshItem::from(item.value.clone()));
571 data.insert(item.order, inspect_mesh(value, DAffine3::IDENTITY, verbose));
572 }
573 })*};
574 }
575 mesh_inputs!(mesh::MeshItem, mesh::Sphere, mesh::Surface);
576 #[cfg(feature = "typst")]
577 {
578 macro_rules! text_inputs {
579 ($($input:ty),* $(,)?) => {$({
580 for item in frame.items::<$input>() {
581 let items = item.value.vitems().iter().cloned()
582 .map(|value| inspect_vitem(value.into(), DAffine3::IDENTITY, verbose))
583 .collect();
584 data.insert(item.order, FrameItemDataDto::Group { items });
585 }
586 })*};
587 }
588 text_inputs!(vitem::text::TextItem, vitem::typst::TypstText);
589 }
590 }
591 let mut items = frame
592 .item_metadata()
593 .map(|metadata| {
594 let item_data = data
595 .remove(&metadata.order)
596 .unwrap_or(FrameItemDataDto::Opaque {});
597 FrameItemDto::from_metadata(metadata, item_data, infos)
598 })
599 .collect::<Vec<_>>();
600 items.sort_by_key(|item| item.order);
601 items
602}
603
604#[cfg(feature = "items")]
605fn inspect_vitem(mut value: RenderVItem, placement: DAffine3, verbose: bool) -> FrameItemDataDto {
606 value.transform = DMat4::from(placement).as_mat4() * value.transform;
607 FrameItemDataDto::from_vitem(&value, verbose)
608}
609
610#[cfg(feature = "items")]
611fn inspect_mesh(mut value: RenderMeshItem, placement: DAffine3, verbose: bool) -> FrameItemDataDto {
612 value.transform = DMat4::from(placement).as_mat4() * value.transform;
613 FrameItemDataDto::from_mesh(&value, verbose)
614}
615
616impl FrameItemDto {
617 fn from_metadata(
618 metadata: FrameItemMetadata<'_>,
619 data: FrameItemDataDto,
620 infos: &[AnimationInfo],
621 ) -> Self {
622 let (animation_id, part) = metadata.id;
623 let kind = match &data {
624 #[cfg(feature = "items")]
625 FrameItemDataDto::VItem(_) => "vitem",
626 #[cfg(feature = "items")]
627 FrameItemDataDto::Mesh(_) => "mesh",
628 #[cfg(feature = "items")]
629 FrameItemDataDto::Camera(_) => "camera",
630 #[cfg(feature = "items")]
631 FrameItemDataDto::Group { .. } => "group",
632 FrameItemDataDto::Opaque {} => "opaque",
633 };
634 Self {
635 order: metadata.order,
636 input_type: metadata.type_name.to_owned(),
637 id: [animation_id, part],
638 animation_id,
639 part,
640 kind: kind.to_string(),
641 source: source_from_infos(infos, animation_id),
642 data,
643 }
644 }
645}
646
647#[cfg(feature = "items")]
648impl FrameItemDataDto {
649 fn from_vitem(item: &RenderVItem, verbose: bool) -> Self {
650 let normal_matrix = Mat3::from_mat4(item.transform).inverse().transpose();
654 let vpoints = VPointVec(
655 item.points
656 .iter()
657 .map(|point| item.transform.transform_point3(point.truncate()).as_dvec3())
658 .collect(),
659 );
660 let subpaths = if vpoints.is_empty() {
661 Vec::new()
662 } else {
663 vpoints.get_subpaths()
664 };
665 let closed = subpaths
666 .iter()
667 .map(|subpath| {
668 get_subpath_closed_flag(subpath)
669 .map(|(_, closed)| closed)
670 .unwrap_or(false)
671 })
672 .collect();
673 let normal_local = item
674 .normal
675 .unwrap_or_else(|| vitem_normal_from_points(&item.points));
676 let normal = (normal_matrix * normal_local).normalize_or_zero();
677 let bounds = dvec3_bounds(vpoints.aabb());
678
679 Self::VItem(VItemDataDto {
680 point_count: item.points.len(),
681 subpath_count: subpaths.len(),
682 closed,
683 fill_colors: item.fill_rgbas.iter().map(rgba_to_hex).collect(),
684 stroke_colors: item.stroke_rgbas.iter().map(rgba_to_hex).collect(),
685 stroke_widths: item.stroke_widths.iter().map(|width| width.0).collect(),
686 normal: normal.to_array(),
687 bounds,
688 points: verbose.then(|| {
689 item.points
690 .iter()
691 .map(|point| {
692 let world = item.transform.transform_point3(point.truncate());
693 [world.x, world.y, world.z, point.w]
694 })
695 .collect()
696 }),
697 })
698 }
699
700 fn from_mesh(item: &RenderMeshItem, verbose: bool) -> Self {
701 let transformed_points = item
702 .points
703 .iter()
704 .map(|point| item.transform.transform_point3(*point))
705 .collect::<Vec<_>>();
706 let bounds = vec3_bounds(&transformed_points);
707
708 Self::Mesh(MeshDataDto {
709 point_count: item.points.len(),
710 triangle_count: item.triangle_indices.len() / 3,
711 transform: item.transform.to_cols_array(),
712 bounds,
713 points: verbose.then(|| {
714 transformed_points
715 .iter()
716 .map(|point| point.to_array())
717 .collect()
718 }),
719 triangle_indices: verbose.then(|| item.triangle_indices.clone()),
720 vertex_colors: verbose.then(|| item.vertex_colors.iter().map(rgba_to_hex).collect()),
721 vertex_normals: verbose.then(|| {
722 item.vertex_normals
723 .iter()
724 .map(|normal| normal.to_array())
725 .collect()
726 }),
727 })
728 }
729
730 fn from_camera(item: &CameraFrame) -> Self {
731 Self::Camera(CameraDataDto {
732 pos: dvec3_to_array(item.pos),
733 facing: dvec3_to_array(item.facing),
734 up: dvec3_to_array(item.up),
735 near: item.near,
736 far: item.far,
737 perspective_blend: item.perspective_blend,
738 frame_height: item.frame_height,
739 scale: item.scale,
740 fovy: item.fovy,
741 })
742 }
743}
744
745fn source_from_infos(infos: &[AnimationInfo], animation_id: usize) -> SourceDto {
746 infos
747 .get(animation_id)
748 .map(|info| SourceDto {
749 path: vec![animation_id],
750 kind: animation_kind_str(info.kind).to_string(),
751 anim_name: info.anim_name.clone(),
752 range: range_to_array(&info.range),
753 })
754 .unwrap_or(SourceDto {
755 path: vec![animation_id],
756 kind: "unknown".to_string(),
757 anim_name: "<unknown>".to_string(),
758 range: [0.0, 0.0],
759 })
760}
761
762#[cfg(feature = "items")]
763fn dvec3_bounds(bounds: [DVec3; 2]) -> BoundsDto {
764 BoundsDto {
765 min: dvec3_to_array(bounds[0]),
766 max: dvec3_to_array(bounds[1]),
767 }
768}
769
770#[cfg(feature = "items")]
771fn vec3_bounds(points: &[ranim::glam::Vec3]) -> BoundsDto {
772 if points.is_empty() {
773 return BoundsDto {
774 min: [0.0; 3],
775 max: [0.0; 3],
776 };
777 }
778 let mut min = points[0];
779 let mut max = points[0];
780 for point in &points[1..] {
781 min = min.min(*point);
782 max = max.max(*point);
783 }
784 BoundsDto {
785 min: [min.x as f64, min.y as f64, min.z as f64],
786 max: [max.x as f64, max.y as f64, max.z as f64],
787 }
788}
789
790#[cfg(feature = "items")]
791fn dvec3_to_array(v: DVec3) -> [f64; 3] {
792 [v.x, v.y, v.z]
793}
794
795#[cfg(feature = "items")]
796fn rgba_to_hex(rgba: &Rgba) -> String {
797 let srgb: AlphaColor<Srgb> = (*rgba).into();
798 let [r, g, b, a] = srgb.to_rgba8().to_u8_array();
799 format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
800}
801
802fn print_frame_item(item: &FrameItemDto) {
803 println!(
804 "[{}] id=[{}, {}] {} input_type={} source=[{}] {} {} range=[{}..{}]",
805 item.order,
806 item.id[0],
807 item.id[1],
808 item.kind,
809 item.input_type,
810 item.source
811 .path
812 .iter()
813 .map(|idx| idx.to_string())
814 .collect::<Vec<_>>()
815 .join(", "),
816 item.source.kind,
817 short_anim_name(&item.source.anim_name),
818 item.source.range[0],
819 item.source.range[1],
820 );
821 print_frame_item_data(&item.data);
822}
823
824fn print_frame_item_data(data: &FrameItemDataDto) {
825 match data {
826 #[cfg(feature = "items")]
827 FrameItemDataDto::VItem(data) => {
828 println!(
829 " point_count={} subpath_count={} closed={:?}",
830 data.point_count, data.subpath_count, data.closed
831 );
832 println!(
833 " fill={:?} stroke={:?} stroke_widths={:?}",
834 data.fill_colors, data.stroke_colors, data.stroke_widths
835 );
836 println!(
837 " normal={:?} bounds=min:{:?} max:{:?}",
838 data.normal, data.bounds.min, data.bounds.max
839 );
840 if let Some(points) = &data.points {
841 println!(" points={points:?}");
842 }
843 }
844 #[cfg(feature = "items")]
845 FrameItemDataDto::Mesh(data) => {
846 println!(
847 " point_count={} triangle_count={} bounds=min:{:?} max:{:?}",
848 data.point_count, data.triangle_count, data.bounds.min, data.bounds.max
849 );
850 if let Some(points) = &data.points {
851 println!(" points={points:?}");
852 }
853 if let Some(triangle_indices) = &data.triangle_indices {
854 println!(" triangle_indices={triangle_indices:?}");
855 }
856 if let Some(vertex_colors) = &data.vertex_colors {
857 println!(" vertex_colors={vertex_colors:?}");
858 }
859 if let Some(vertex_normals) = &data.vertex_normals {
860 println!(" vertex_normals={vertex_normals:?}");
861 }
862 }
863 #[cfg(feature = "items")]
864 FrameItemDataDto::Camera(data) => {
865 println!(
866 " pos={:?} facing={:?} up={:?} near={} far={} perspective_blend={} frame_height={} scale={} fovy={}",
867 data.pos,
868 data.facing,
869 data.up,
870 data.near,
871 data.far,
872 data.perspective_blend,
873 data.frame_height,
874 data.scale,
875 data.fovy
876 );
877 }
878 #[cfg(feature = "items")]
879 FrameItemDataDto::Group { items } => {
880 println!(" rendered_parts={}", items.len());
881 for item in items {
882 print_frame_item_data(item);
883 }
884 }
885 FrameItemDataDto::Opaque {} => println!(" no built-in inspector for this input type"),
886 }
887}
888
889fn print_json<T: Serialize>(output: &T) -> Result<()> {
890 println!("{}", serde_json::to_string_pretty(output)?);
891 Ok(())
892}
893
894#[cfg(test)]
895mod tests {
896 use ranim::core::{animation::node::AnimationInfo, utils::rate_functions};
897
898 use super::*;
899 #[cfg(feature = "items")]
900 use ranim::glam::{DVec3, Vec4};
901
902 fn sample_animation_info() -> AnimationInfo {
903 AnimationInfo {
904 anim_name: "AnimSequence".to_string(),
905 kind: AnimationInfoKind::Children,
906 range: 0.0..8.0,
907 content_duration_secs: 8.0,
908 rate_func: rate_functions::linear,
909 enabled: true,
910 sim_step: None,
911 children: vec![AnimationInfo {
912 anim_name: "FadeIn<VItem>".to_string(),
913 kind: AnimationInfoKind::Anim,
914 range: 0.0..3.0,
915 content_duration_secs: 3.0,
916 rate_func: rate_functions::smooth,
917 enabled: true,
918 sim_step: None,
919 children: vec![],
920 }],
921 }
922 }
923
924 #[test]
925 fn maps_animation_info_to_tree_dto() {
926 let node = animation_info_to_tree_node(&sample_animation_info(), vec![0]);
927 assert_eq!(node.path, [0]);
928 assert_eq!(node.kind, "children");
929 assert_eq!(node.anim_name, "AnimSequence");
930 assert_eq!(node.range, [0.0, 8.0]);
931 assert_eq!(node.rate_func, "linear");
932 assert!(node.enabled);
933 assert_eq!(node.children.len(), 1);
934 assert_eq!(node.children[0].path, [0, 0]);
935 assert_eq!(node.children[0].kind, "anim");
936 assert_eq!(node.children[0].rate_func, "smooth");
937
938 fn custom(_t: f64) -> f64 {
939 0.0
940 }
941 assert_eq!(rate_func_name(custom as fn(f64) -> f64), "custom");
942 }
943
944 #[test]
945 fn custom_inputs_keep_metadata_without_builtin_inspectors() {
946 struct CustomInput;
947 let mut frame = EvaluatedFrame::new();
948 frame.push((0, 3), CustomInput);
949 let items = inspect_frame(&frame, &[sample_animation_info()], true);
950 assert_eq!(items.len(), 1);
951 assert_eq!(items[0].id, [0, 3]);
952 assert_eq!(items[0].kind, "opaque");
953 assert_eq!(items[0].input_type, std::any::type_name::<CustomInput>());
954 assert_eq!(items[0].source.anim_name, "AnimSequence");
955 assert_eq!(
956 serde_json::to_value(&items[0].data).unwrap(),
957 serde_json::json!({})
958 );
959 }
960
961 #[cfg(feature = "items")]
962 #[test]
963 fn maps_vitem_to_frame_dto() {
964 let vitem = vitem::VItem {
965 vpoints: VPointVec(vec![
966 DVec3::new(-1.0, -1.0, 0.0),
967 DVec3::new(1.0, 0.0, 0.0),
968 DVec3::new(-1.0, -1.0, 0.0),
969 ]),
970 fill_rgbas: vec![Vec4::new(1.0, 1.0, 1.0, 1.0).into()].into(),
971 stroke_rgbas: vec![Vec4::new(0.0, 0.0, 0.0, 1.0).into()].into(),
972 stroke_widths: vec![0.0.into()].into(),
973 normal: None,
974 };
975
976 let mut frame = EvaluatedFrame::new();
977 frame.push((0, 0), vitem);
978 let dto = inspect_frame(&frame, &[sample_animation_info()], false).remove(0);
979
980 assert_eq!(dto.id, [0, 0]);
981 assert_eq!(dto.kind, "vitem");
982 assert_eq!(dto.source.path, [0]);
983 assert_eq!(dto.source.anim_name, "AnimSequence");
984
985 let FrameItemDataDto::VItem(data) = &dto.data else {
986 panic!("expected vitem data");
987 };
988 assert_eq!(data.point_count, 3);
989 assert_eq!(data.subpath_count, 1);
990 assert_eq!(data.closed, [true]);
991 assert_eq!(data.fill_colors, ["#ffffffff"]);
992 assert_eq!(data.stroke_colors, ["#000000ff"]);
993 assert!(data.points.is_none());
994
995 let bounds = dvec3_bounds([DVec3::new(-1.0, -2.0, -3.0), DVec3::new(1.0, 2.0, 3.0)]);
996 assert_eq!(bounds.min, [-1.0, -2.0, -3.0]);
997 assert_eq!(bounds.max, [1.0, 2.0, 3.0]);
998 }
999
1000 #[cfg(feature = "items")]
1001 #[test]
1002 fn typed_inputs_keep_order_and_unknown_payloads() {
1003 struct CustomInput;
1004
1005 let mut frame = EvaluatedFrame::new();
1006 frame.push((0, 0), CustomInput);
1007 frame.push((0, 1), vitem::geometry::Square::new(2.0));
1008 frame.push((0, 2), CameraFrame::default());
1009
1010 let items = inspect_frame(&frame, &[sample_animation_info()], true);
1011 assert_eq!(
1012 items.iter().map(|item| item.order).collect::<Vec<_>>(),
1013 [0, 1, 2]
1014 );
1015 assert_eq!(
1016 items.iter().map(|item| item.id).collect::<Vec<_>>(),
1017 [[0, 0], [0, 1], [0, 2]]
1018 );
1019 assert_eq!(items[0].input_type, std::any::type_name::<CustomInput>());
1020 assert!(matches!(items[0].data, FrameItemDataDto::Opaque {}));
1021 assert_eq!(
1022 items[1].input_type,
1023 std::any::type_name::<vitem::geometry::Square>()
1024 );
1025 let FrameItemDataDto::VItem(data) = &items[1].data else {
1026 panic!("expected projected square geometry");
1027 };
1028 assert_eq!(data.bounds.min, [-1.0, -1.0, 0.0]);
1029 assert_eq!(data.bounds.max, [1.0, 1.0, 0.0]);
1030 assert!(matches!(items[2].data, FrameItemDataDto::Camera(_)));
1031 assert!(
1032 serde_json::to_value(&items[1])
1033 .unwrap()
1034 .get("placement")
1035 .is_none()
1036 );
1037 }
1038
1039 #[cfg(feature = "items")]
1040 #[test]
1041 fn whole_transformed_vectors_keep_their_type_and_identity_as_opaque_inputs() {
1042 use ranim::core::transformed::Transformed;
1043
1044 type Input = Transformed<Vec<vitem::VItem>, DAffine3>;
1045 let mut frame = EvaluatedFrame::new();
1046 frame.push((0, 0), CameraFrame::default());
1047 frame.push(
1048 (4, 9),
1049 Input::new(
1050 vec![vitem::VItem::from_vpoints(Vec::new()); 2],
1051 DAffine3::from_translation(DVec3::new(3.0, 4.0, 0.0)),
1052 ),
1053 );
1054
1055 let items = inspect_frame(&frame, &[sample_animation_info()], true);
1056 assert_eq!(items.len(), 2);
1057 let item = &items[1];
1058 assert_eq!(item.order, 1);
1059 assert_eq!(item.id, [4, 9]);
1060 assert_eq!(item.input_type, std::any::type_name::<Input>());
1061 assert_eq!(item.kind, "opaque");
1062 assert!(matches!(item.data, FrameItemDataDto::Opaque {}));
1063 assert_eq!(frame.items::<Input>()[0].value.inner.len(), 2);
1064 assert!(frame.items::<vitem::VItem>().is_empty());
1065 }
1066}