Skip to main content

mittens_engine/engine/ecs/system/
transform_stream_system.rs

1use crate::engine::ecs::component::{
2    QuatExtractYawComponent, QuatTemporalFilterComponent, QuatYawFollowComponent,
3    TransformCameraSpecificComponent, TransformCameraSpecificMode, TransformComponent,
4    TransformDropComponent, TransformForkTRSComponent, TransformMapRotationComponent,
5    TransformMapScaleComponent, TransformMapTranslationComponent, TransformMergeTRSComponent,
6    TransformParentComponent, TransformSampleAncestorComponent, Vector3TemporalFilterComponent,
7};
8use crate::engine::ecs::system::System;
9use crate::engine::ecs::{ComponentId, World};
10use crate::engine::graphics::VisualWorld;
11use crate::engine::graphics::primitives::TransformMatrix;
12use crate::engine::user_input::InputState;
13use crate::utils::math;
14use std::collections::{HashMap, VecDeque};
15use std::sync::OnceLock;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TransformPipelineInput {
19    ParentWorld,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum TransformPipelineVec3Op {
24    Pass,
25    Drop,
26    TemporalSmooth {
27        smoothing_factor: f32,
28    },
29    /// Replace this channel's value with the world translation of an ancestor
30    /// TransformComponent. `skip` counts TransformComponent ancestors upward from the
31    /// pipeline owner: 0 = the driven T directly above the pipeline, 1 = the next T above
32    /// that (e.g. the armature bone above an InputXR splice), etc.
33    SampleAncestorTranslation {
34        skip: usize,
35    },
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum TransformPipelineQuatOp {
40    Pass,
41    Drop,
42    TemporalFilter {
43        smoothing_factor: f32,
44    },
45    /// Replace this channel's value with the world rotation of an ancestor
46    /// TransformComponent. Same `skip` semantics as `SampleAncestorTranslation`.
47    SampleAncestorRotation {
48        skip: usize,
49    },
50    /// Project the rotation onto the Y-rotation subspace: `normalize([0, q.y, 0, q.w])`.
51    /// Strips pitch and roll, keeping only the Y-axis component. Convention-independent.
52    ExtractYaw,
53    /// Stateful body-yaw follow. Extracts world-Y yaw from the input quaternion using
54    /// the specified forward convention, then advances a running `body_yaw` toward the
55    /// extracted head yaw when the delta exceeds `threshold`, at `rate` rad/s.
56    /// Outputs a pure-Y quaternion for `body_yaw`.
57    YawFollow {
58        threshold: f32,
59        rate: f32,
60        initial_yaw: f32,
61        forward_plus_z: bool,
62    },
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub struct TransformForkTrsStage {
67    pub translation_ops: Vec<TransformPipelineVec3Op>,
68    pub rotation_ops: Vec<TransformPipelineQuatOp>,
69    pub scale_ops: Vec<TransformPipelineVec3Op>,
70}
71
72#[derive(Debug, Clone, PartialEq)]
73pub enum TransformPipelineStage {
74    ForkTrs(TransformForkTrsStage),
75}
76
77#[derive(Debug, Clone, PartialEq)]
78pub struct TransformPipelinePlan {
79    pub owner_component: Option<ComponentId>,
80    pub input: TransformPipelineInput,
81    pub stages: Vec<TransformPipelineStage>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq)]
85pub struct TransformPipelineChannels {
86    pub translation: [f32; 3],
87    pub rotation_quat_xyzw: [f32; 4],
88    pub scale: [f32; 3],
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
92struct TransformPipelineStageKey {
93    owner_component: Option<ComponentId>,
94    stage_path: Vec<usize>,
95}
96
97#[derive(Debug, Clone, PartialEq)]
98struct QuatTemporalState {
99    output_quat_xyzw: [f32; 4],
100    last_input_quat_xyzw: [f32; 4],
101    debug_window: QuatTemporalDebugWindow,
102}
103
104#[derive(Debug, Clone, PartialEq, Default)]
105struct QuatTemporalDebugWindow {
106    raw_step_deg: VecDeque<f32>,
107    filtered_step_deg: VecDeque<f32>,
108    lag_deg: VecDeque<f32>,
109    sample_count: u64,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq)]
113struct Vec3TemporalState {
114    output_vec3: [f32; 3],
115}
116
117#[derive(Debug, Default)]
118pub struct TransformStreamSystem {
119    last_dt_sec: Option<f32>,
120    vec3_temporal_state: HashMap<TransformPipelineStageKey, Vec3TemporalState>,
121    quat_temporal_state: HashMap<TransformPipelineStageKey, QuatTemporalState>,
122    yaw_follow_state: HashMap<TransformPipelineStageKey, f32>,
123    stereoscopic_active: bool,
124    /// Pre-camera and effective matrices for camera-specific anchors. A direct refresh of an
125    /// anchor starts from its cached `matrix_world`, which is the previous effective matrix;
126    /// retaining the unmodified basis prevents the selected settings transform compounding.
127    camera_specific_basis: HashMap<ComponentId, (TransformMatrix, TransformMatrix)>,
128}
129
130impl TransformStreamSystem {
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    pub fn is_transform_stream_boundary(&self, world: &World, cid: ComponentId) -> bool {
136        world
137            .get_component_by_id_as::<TransformForkTRSComponent>(cid)
138            .is_some()
139            || world
140                .get_component_by_id_as::<TransformParentComponent>(cid)
141                .is_some()
142            || Self::camera_specific_settings(world, cid).is_some()
143    }
144
145    pub fn set_stereoscopic_active(&mut self, active: bool) {
146        self.stereoscopic_active = active;
147    }
148
149    fn camera_specific_settings(
150        world: &World,
151        anchor: ComponentId,
152    ) -> Option<(Option<ComponentId>, Option<ComponentId>)> {
153        let mut mono = None;
154        let mut stereo = None;
155        for &child in world.children_of(anchor) {
156            let Some(c) = world.get_component_by_id_as::<TransformCameraSpecificComponent>(child)
157            else {
158                continue;
159            };
160            let settings = world.children_of(child).iter().copied().find(|&id| {
161                world
162                    .get_component_by_id_as::<TransformComponent>(id)
163                    .is_some()
164            });
165            match c.mode {
166                TransformCameraSpecificMode::Monoscopic => mono = settings,
167                TransformCameraSpecificMode::Stereoscopic => stereo = settings,
168            }
169        }
170        (mono.is_some() || stereo.is_some()).then_some((mono, stereo))
171    }
172
173    pub fn parse_component_tree(
174        &self,
175        world: &World,
176        root: ComponentId,
177    ) -> Option<TransformPipelinePlan> {
178        if world
179            .get_component_by_id_as::<TransformForkTRSComponent>(root)
180            .is_some()
181        {
182            return Some(TransformPipelinePlan {
183                owner_component: Some(root),
184                input: TransformPipelineInput::ParentWorld,
185                stages: vec![TransformPipelineStage::ForkTrs(
186                    self.parse_fork_trs(world, root),
187                )],
188            });
189        }
190        None
191    }
192
193    pub fn evaluate_stream_node(
194        &mut self,
195        world: &World,
196        root: ComponentId,
197        input_world: TransformMatrix,
198    ) -> Option<(TransformMatrix, Vec<ComponentId>)> {
199        let rebased_world = Self::apply_transform_parent_basis(world, root, input_world);
200
201        if let Some((mono, stereo)) = Self::camera_specific_settings(world, root) {
202            let basis = match self.camera_specific_basis.get(&root).copied() {
203                Some((basis, previous_effective)) if input_world == previous_effective => basis,
204                _ => rebased_world,
205            };
206            let selected = if self.stereoscopic_active {
207                stereo
208            } else {
209                mono
210            };
211            let effective = selected
212                .and_then(|id| world.get_component_by_id_as::<TransformComponent>(id))
213                .map(|t| math::mat4_mul(basis, t.transform.model))
214                .unwrap_or(basis);
215            self.camera_specific_basis.insert(root, (basis, effective));
216            let outputs = world
217                .children_of(root)
218                .iter()
219                .copied()
220                .filter(|&id| {
221                    world
222                        .get_component_by_id_as::<TransformCameraSpecificComponent>(id)
223                        .is_none()
224                })
225                .collect();
226            return Some((effective, outputs));
227        }
228
229        if let Some(block) = self.parse_component_tree(world, root) {
230            let outputs = self.downstream_children(world, root, &block);
231            let world_matrix = self.evaluate_block(&block, rebased_world, world, self.last_dt_sec);
232            return Some((world_matrix, outputs));
233        }
234
235        if world
236            .get_component_by_id_as::<TransformParentComponent>(root)
237            .is_some()
238        {
239            return Some((rebased_world, world.children_of(root).to_vec()));
240        }
241
242        None
243    }
244
245    fn apply_transform_parent_basis(
246        world: &World,
247        node: ComponentId,
248        current_world: TransformMatrix,
249    ) -> TransformMatrix {
250        world
251            .get_component_by_id_as::<TransformParentComponent>(node)
252            .and_then(|tp| tp.resolve_target_component(world))
253            .and_then(|target| Self::world_model(world, target))
254            .unwrap_or(current_world)
255    }
256
257    fn world_model(world: &World, cid: ComponentId) -> Option<TransformMatrix> {
258        if let Some(t) = world.get_component_by_id_as::<TransformComponent>(cid) {
259            return Some(t.transform.matrix_world);
260        }
261
262        let mut cur = cid;
263        while let Some(parent) = world.parent_of(cur) {
264            if let Some(t) = world.get_component_by_id_as::<TransformComponent>(parent) {
265                return Some(t.transform.matrix_world);
266            }
267            cur = parent;
268        }
269        None
270    }
271
272    fn downstream_children(
273        &self,
274        world: &World,
275        root: ComponentId,
276        _block: &TransformPipelinePlan,
277    ) -> Vec<ComponentId> {
278        world
279            .children_of(root)
280            .iter()
281            .copied()
282            .filter(|&child| {
283                world
284                    .get_component_by_id_as::<TransformMapTranslationComponent>(child)
285                    .is_none()
286                    && world
287                        .get_component_by_id_as::<TransformMapRotationComponent>(child)
288                        .is_none()
289                    && world
290                        .get_component_by_id_as::<TransformMapScaleComponent>(child)
291                        .is_none()
292                    && world
293                        .get_component_by_id_as::<TransformMergeTRSComponent>(child)
294                        .is_none()
295            })
296            .collect()
297    }
298
299    pub fn pipeline_for_controller_rotation_smoothing(
300        owner_component: Option<ComponentId>,
301        smoothing_factor: f32,
302    ) -> TransformPipelinePlan {
303        TransformPipelinePlan {
304            owner_component,
305            input: TransformPipelineInput::ParentWorld,
306            stages: vec![TransformPipelineStage::ForkTrs(TransformForkTrsStage {
307                translation_ops: vec![TransformPipelineVec3Op::Pass],
308                rotation_ops: vec![TransformPipelineQuatOp::TemporalFilter { smoothing_factor }],
309                scale_ops: vec![TransformPipelineVec3Op::Pass],
310            })],
311        }
312    }
313
314    fn parse_fork_trs(&self, world: &World, node: ComponentId) -> TransformForkTrsStage {
315        let mut translation_ops = vec![TransformPipelineVec3Op::Pass];
316        let mut rotation_ops = vec![TransformPipelineQuatOp::Pass];
317        let mut scale_ops = vec![TransformPipelineVec3Op::Pass];
318
319        for &child in world.children_of(node) {
320            if world
321                .get_component_by_id_as::<TransformMapTranslationComponent>(child)
322                .is_some()
323            {
324                translation_ops = self.parse_vec3_ops(world, child);
325                continue;
326            }
327
328            if world
329                .get_component_by_id_as::<TransformMapRotationComponent>(child)
330                .is_some()
331            {
332                rotation_ops = self.parse_quat_ops(world, child);
333                continue;
334            }
335
336            if world
337                .get_component_by_id_as::<TransformMapScaleComponent>(child)
338                .is_some()
339            {
340                scale_ops = self.parse_vec3_ops(world, child);
341                continue;
342            }
343
344            if world
345                .get_component_by_id_as::<TransformMergeTRSComponent>(child)
346                .is_some()
347            {
348                debug_assert!(
349                    world.children_of(child).is_empty(),
350                    "TransformMergeTRS should not contain child stages; attach driven children directly under the TransformForkTRS root"
351                );
352                continue;
353            }
354        }
355
356        TransformForkTrsStage {
357            translation_ops,
358            rotation_ops,
359            scale_ops,
360        }
361    }
362
363    fn parse_vec3_ops(&self, world: &World, node: ComponentId) -> Vec<TransformPipelineVec3Op> {
364        let mut ops = Vec::new();
365        for &child in world.children_of(node) {
366            if world
367                .get_component_by_id_as::<TransformDropComponent>(child)
368                .is_some()
369            {
370                ops.push(TransformPipelineVec3Op::Drop);
371                continue;
372            }
373            if let Some(s) = world.get_component_by_id_as::<TransformSampleAncestorComponent>(child)
374            {
375                ops.push(TransformPipelineVec3Op::SampleAncestorTranslation { skip: s.skip });
376                continue;
377            }
378            if let Some(filter) =
379                world.get_component_by_id_as::<Vector3TemporalFilterComponent>(child)
380            {
381                ops.push(TransformPipelineVec3Op::TemporalSmooth {
382                    smoothing_factor: filter.smoothing_factor,
383                });
384            }
385        }
386        if ops.is_empty() {
387            ops.push(TransformPipelineVec3Op::Pass);
388        }
389        ops
390    }
391
392    fn parse_quat_ops(&self, world: &World, node: ComponentId) -> Vec<TransformPipelineQuatOp> {
393        let mut ops = Vec::new();
394        for &child in world.children_of(node) {
395            if world
396                .get_component_by_id_as::<TransformDropComponent>(child)
397                .is_some()
398            {
399                ops.push(TransformPipelineQuatOp::Drop);
400                continue;
401            }
402            if let Some(s) = world.get_component_by_id_as::<TransformSampleAncestorComponent>(child)
403            {
404                ops.push(TransformPipelineQuatOp::SampleAncestorRotation { skip: s.skip });
405                continue;
406            }
407            if let Some(filter) = world.get_component_by_id_as::<QuatTemporalFilterComponent>(child)
408            {
409                ops.push(TransformPipelineQuatOp::TemporalFilter {
410                    smoothing_factor: filter.smoothing_factor,
411                });
412                continue;
413            }
414            if world
415                .get_component_by_id_as::<QuatExtractYawComponent>(child)
416                .is_some()
417            {
418                ops.push(TransformPipelineQuatOp::ExtractYaw);
419                continue;
420            }
421            if let Some(c) = world.get_component_by_id_as::<QuatYawFollowComponent>(child) {
422                ops.push(TransformPipelineQuatOp::YawFollow {
423                    threshold: c.threshold,
424                    rate: c.rate,
425                    initial_yaw: c.initial_yaw,
426                    forward_plus_z: c.forward_plus_z,
427                });
428            }
429        }
430        if ops.is_empty() {
431            ops.push(TransformPipelineQuatOp::Pass);
432        }
433        ops
434    }
435
436    pub fn evaluate_block(
437        &mut self,
438        pipeline: &TransformPipelinePlan,
439        input_world: TransformMatrix,
440        world: &World,
441        dt_sec: Option<f32>,
442    ) -> TransformMatrix {
443        let mut channels = Self::decompose_matrix(input_world);
444        for (stage_index, stage) in pipeline.stages.iter().enumerate() {
445            let mut stage_path = vec![stage_index];
446            channels = self.evaluate_stage(
447                pipeline.owner_component,
448                stage,
449                channels,
450                &mut stage_path,
451                world,
452                dt_sec,
453            );
454        }
455        Self::recompose_matrix(channels)
456    }
457
458    fn evaluate_stage(
459        &mut self,
460        owner_component: Option<ComponentId>,
461        stage: &TransformPipelineStage,
462        input: TransformPipelineChannels,
463        stage_path: &mut Vec<usize>,
464        world: &World,
465        dt_sec: Option<f32>,
466    ) -> TransformPipelineChannels {
467        match stage {
468            TransformPipelineStage::ForkTrs(fork) => {
469                self.evaluate_fork_trs(owner_component, fork, input, stage_path, world, dt_sec)
470            }
471        }
472    }
473
474    fn evaluate_fork_trs(
475        &mut self,
476        owner_component: Option<ComponentId>,
477        fork: &TransformForkTrsStage,
478        input: TransformPipelineChannels,
479        stage_path: &[usize],
480        world: &World,
481        dt_sec: Option<f32>,
482    ) -> TransformPipelineChannels {
483        let translation = self.apply_vec3_ops(
484            owner_component,
485            &fork.translation_ops,
486            input.translation,
487            [0.0, 0.0, 0.0],
488            stage_path,
489            world,
490            dt_sec,
491        );
492        let rotation_quat_xyzw = self.apply_quat_ops(
493            owner_component,
494            &fork.rotation_ops,
495            input.rotation_quat_xyzw,
496            [0.0, 0.0, 0.0, 1.0],
497            stage_path,
498            world,
499            dt_sec,
500        );
501        let scale = self.apply_vec3_ops(
502            owner_component,
503            &fork.scale_ops,
504            input.scale,
505            [1.0, 1.0, 1.0],
506            stage_path,
507            world,
508            dt_sec,
509        );
510
511        TransformPipelineChannels {
512            translation,
513            rotation_quat_xyzw,
514            scale,
515        }
516    }
517
518    /// Walk ancestor TransformComponents from `owner` upward. Returns the world matrix of the
519    /// `skip`-th TransformComponent found (0 = first / nearest ancestor, 1 = second, etc.).
520    fn sample_ancestor_world(
521        world: &World,
522        owner: ComponentId,
523        skip: usize,
524    ) -> Option<TransformMatrix> {
525        let mut found = 0usize;
526        let mut cur = owner;
527        while let Some(parent) = world.parent_of(cur) {
528            if let Some(t) = world.get_component_by_id_as::<TransformComponent>(parent) {
529                if found == skip {
530                    return Some(t.transform.matrix_world);
531                }
532                found += 1;
533            }
534            cur = parent;
535        }
536        None
537    }
538
539    fn apply_vec3_ops(
540        &mut self,
541        owner_component: Option<ComponentId>,
542        ops: &[TransformPipelineVec3Op],
543        input: [f32; 3],
544        dropped_value: [f32; 3],
545        stage_path: &[usize],
546        world: &World,
547        dt_sec: Option<f32>,
548    ) -> [f32; 3] {
549        let mut current = input;
550        for (op_index, op) in ops.iter().enumerate() {
551            current = match *op {
552                TransformPipelineVec3Op::Pass => current,
553                TransformPipelineVec3Op::Drop => dropped_value,
554                TransformPipelineVec3Op::SampleAncestorTranslation { skip } => owner_component
555                    .and_then(|owner| Self::sample_ancestor_world(world, owner, skip))
556                    .map(|m| [m[3][0], m[3][1], m[3][2]])
557                    .unwrap_or(current),
558                TransformPipelineVec3Op::TemporalSmooth { smoothing_factor } => {
559                    let mut full_path = stage_path.to_vec();
560                    full_path.push(op_index);
561                    let key = TransformPipelineStageKey {
562                        owner_component,
563                        stage_path: full_path,
564                    };
565                    let alpha = Self::alpha_from_smoothing_factor(smoothing_factor, dt_sec);
566                    let previous = self
567                        .vec3_temporal_state
568                        .get(&key)
569                        .map(|state| state.output_vec3)
570                        .unwrap_or(current);
571                    let filtered = Self::vec3_lerp(previous, current, alpha);
572                    self.vec3_temporal_state.insert(
573                        key,
574                        Vec3TemporalState {
575                            output_vec3: filtered,
576                        },
577                    );
578                    filtered
579                }
580            };
581        }
582        current
583    }
584
585    fn apply_quat_ops(
586        &mut self,
587        owner_component: Option<ComponentId>,
588        ops: &[TransformPipelineQuatOp],
589        input: [f32; 4],
590        dropped_value: [f32; 4],
591        stage_path: &[usize],
592        world: &World,
593        dt_sec: Option<f32>,
594    ) -> [f32; 4] {
595        let mut current = Self::quat_normalize(input);
596        for (op_index, op) in ops.iter().enumerate() {
597            current = match *op {
598                TransformPipelineQuatOp::Pass => current,
599                TransformPipelineQuatOp::Drop => dropped_value,
600                TransformPipelineQuatOp::SampleAncestorRotation { skip } => owner_component
601                    .and_then(|owner| Self::sample_ancestor_world(world, owner, skip))
602                    .map(|m| Self::decompose_matrix(m).rotation_quat_xyzw)
603                    .unwrap_or(current),
604                TransformPipelineQuatOp::ExtractYaw => {
605                    // Project onto Y-rotation subspace: normalize([0, q.y, 0, q.w])
606                    let (qy, qw) = (current[1], current[3]);
607                    let len = (qy * qy + qw * qw).sqrt().max(1e-8);
608                    [0.0, qy / len, 0.0, qw / len]
609                }
610                TransformPipelineQuatOp::YawFollow {
611                    threshold,
612                    rate,
613                    initial_yaw,
614                    forward_plus_z,
615                } => {
616                    let mut full_path = stage_path.to_vec();
617                    full_path.push(op_index);
618                    let key = TransformPipelineStageKey {
619                        owner_component,
620                        stage_path: full_path,
621                    };
622                    let head_yaw = Self::extract_yaw_from_quat(current, forward_plus_z);
623                    let body_yaw = self
624                        .yaw_follow_state
625                        .get(&key)
626                        .copied()
627                        .unwrap_or(initial_yaw);
628                    let new_body_yaw = if let Some(dt) = dt_sec {
629                        let delta = Self::signed_yaw_diff(head_yaw, body_yaw);
630                        if delta.abs() > threshold {
631                            let target = head_yaw - delta.signum() * threshold;
632                            let step = rate * dt;
633                            Self::lerp_angle(
634                                body_yaw,
635                                target,
636                                step.min(delta.abs()) / delta.abs().max(1e-9),
637                            )
638                        } else {
639                            body_yaw
640                        }
641                    } else {
642                        body_yaw
643                    };
644                    self.yaw_follow_state.insert(key, new_body_yaw);
645                    Self::quat_rotation_y(new_body_yaw)
646                }
647                TransformPipelineQuatOp::TemporalFilter { smoothing_factor } => {
648                    let mut full_path = stage_path.to_vec();
649                    full_path.push(op_index);
650                    let key = TransformPipelineStageKey {
651                        owner_component,
652                        stage_path: full_path.clone(),
653                    };
654                    let alpha = Self::alpha_from_smoothing_factor(smoothing_factor, dt_sec);
655                    let previous_state = self.quat_temporal_state.get(&key).cloned();
656                    let previous_output = previous_state
657                        .as_ref()
658                        .map(|state| state.output_quat_xyzw)
659                        .unwrap_or(current);
660                    let previous_input = previous_state
661                        .as_ref()
662                        .map(|state| state.last_input_quat_xyzw)
663                        .unwrap_or(current);
664                    let filtered = previous_state
665                        .as_ref()
666                        .map(|state| state.output_quat_xyzw)
667                        .unwrap_or(current);
668
669                    let mut next_state = previous_state.unwrap_or(QuatTemporalState {
670                        output_quat_xyzw: filtered,
671                        last_input_quat_xyzw: current,
672                        debug_window: QuatTemporalDebugWindow::default(),
673                    });
674                    next_state.output_quat_xyzw = filtered;
675                    next_state.last_input_quat_xyzw = current;
676
677                    if Self::debug_quat_filter_enabled() {
678                        let window_len = Self::debug_quat_filter_window_len();
679                        let raw_step_deg = Self::quat_angle_degrees(previous_input, current);
680                        let filtered_step_deg = Self::quat_angle_degrees(previous_output, filtered);
681                        let lag_deg = Self::quat_angle_degrees(filtered, current);
682
683                        Self::push_rolling_sample(
684                            &mut next_state.debug_window.raw_step_deg,
685                            raw_step_deg,
686                            window_len,
687                        );
688                        Self::push_rolling_sample(
689                            &mut next_state.debug_window.filtered_step_deg,
690                            filtered_step_deg,
691                            window_len,
692                        );
693                        Self::push_rolling_sample(
694                            &mut next_state.debug_window.lag_deg,
695                            lag_deg,
696                            window_len,
697                        );
698                        next_state.debug_window.sample_count += 1;
699
700                        if next_state.debug_window.sample_count % window_len as u64 == 0 {
701                            let avg_raw = Self::rolling_avg(&next_state.debug_window.raw_step_deg);
702                            let avg_filtered =
703                                Self::rolling_avg(&next_state.debug_window.filtered_step_deg);
704                            let avg_lag = Self::rolling_avg(&next_state.debug_window.lag_deg);
705                            let max_raw = Self::rolling_max(&next_state.debug_window.raw_step_deg);
706                            let max_filtered =
707                                Self::rolling_max(&next_state.debug_window.filtered_step_deg);
708                            let attenuation_pct = if avg_raw > 1e-4 {
709                                (1.0 - (avg_filtered / avg_raw)).clamp(-10.0, 1.0) * 100.0
710                            } else {
711                                0.0
712                            };
713
714                            eprintln!(
715                                "[TransformPipeline][QuatFilter] owner={owner_component:?} stage_path={full_path:?} smoothing_factor={smoothing_factor:.3} dt={:.5} alpha={alpha:.5} raw_avg_deg={avg_raw:.3} filtered_avg_deg={avg_filtered:.3} lag_avg_deg={avg_lag:.3} raw_max_deg={max_raw:.3} filtered_max_deg={max_filtered:.3} attenuation_pct={attenuation_pct:.1} window={} samples={}",
716                                dt_sec.unwrap_or(0.0),
717                                next_state.debug_window.raw_step_deg.len(),
718                                next_state.debug_window.sample_count,
719                            );
720                        }
721                    }
722
723                    self.quat_temporal_state.insert(key, next_state);
724                    filtered
725                }
726            };
727        }
728        Self::quat_normalize(current)
729    }
730
731    fn extract_yaw_from_quat(q: [f32; 4], forward_plus_z: bool) -> f32 {
732        let z = math::quat_rotate_vec3(q, [0.0, 0.0, 1.0]);
733        if forward_plus_z {
734            z[0].atan2(z[2])
735        } else {
736            (-z[0]).atan2(-z[2])
737        }
738    }
739
740    fn quat_rotation_y(yaw: f32) -> [f32; 4] {
741        let h = yaw * 0.5;
742        [0.0, h.sin(), 0.0, h.cos()]
743    }
744
745    fn signed_yaw_diff(a: f32, b: f32) -> f32 {
746        let pi = std::f32::consts::PI;
747        let mut d = (a - b) % (2.0 * pi);
748        if d > pi {
749            d -= 2.0 * pi;
750        }
751        if d < -pi {
752            d += 2.0 * pi;
753        }
754        d
755    }
756
757    fn lerp_angle(from: f32, to: f32, t: f32) -> f32 {
758        from + Self::signed_yaw_diff(to, from) * t.clamp(0.0, 1.0)
759    }
760
761    fn decompose_matrix(m: TransformMatrix) -> TransformPipelineChannels {
762        fn col3(m: TransformMatrix, c: usize) -> [f32; 3] {
763            [m[c][0], m[c][1], m[c][2]]
764        }
765
766        let translation = [m[3][0], m[3][1], m[3][2]];
767        let b0 = col3(m, 0);
768        let b1 = col3(m, 1);
769        let b2 = col3(m, 2);
770
771        let scale = [
772            math::vec3_len(b0).max(1e-8),
773            math::vec3_len(b1).max(1e-8),
774            math::vec3_len(b2).max(1e-8),
775        ];
776
777        let [x, y, z] = Self::orthonormalize_basis(b0, b1, b2);
778        let rotation_quat_xyzw = Self::quat_from_basis_columns(x, y, z);
779
780        TransformPipelineChannels {
781            translation,
782            rotation_quat_xyzw,
783            scale,
784        }
785    }
786
787    fn recompose_matrix(channels: TransformPipelineChannels) -> TransformMatrix {
788        let x = math::quat_rotate_vec3(channels.rotation_quat_xyzw, [1.0, 0.0, 0.0]);
789        let y = math::quat_rotate_vec3(channels.rotation_quat_xyzw, [0.0, 1.0, 0.0]);
790        let z = math::quat_rotate_vec3(channels.rotation_quat_xyzw, [0.0, 0.0, 1.0]);
791
792        let mut out = math::mat4_identity();
793        out[0][0] = x[0] * channels.scale[0];
794        out[0][1] = x[1] * channels.scale[0];
795        out[0][2] = x[2] * channels.scale[0];
796
797        out[1][0] = y[0] * channels.scale[1];
798        out[1][1] = y[1] * channels.scale[1];
799        out[1][2] = y[2] * channels.scale[1];
800
801        out[2][0] = z[0] * channels.scale[2];
802        out[2][1] = z[1] * channels.scale[2];
803        out[2][2] = z[2] * channels.scale[2];
804
805        out[3][0] = channels.translation[0];
806        out[3][1] = channels.translation[1];
807        out[3][2] = channels.translation[2];
808        out
809    }
810
811    fn orthonormalize_basis(b0: [f32; 3], b1: [f32; 3], b2: [f32; 3]) -> [[f32; 3]; 3] {
812        let x = math::vec3_normalize(b0);
813        let y_proj = math::vec3_sub(b1, math::vec3_scale(x, math::vec3_dot(b1, x)));
814        let mut y = math::vec3_normalize(y_proj);
815        let mut z = math::vec3_cross(x, y);
816        if math::vec3_len(z) < 1e-6 {
817            z = math::vec3_normalize(b2);
818            y = math::vec3_normalize(math::vec3_cross(z, x));
819        } else {
820            z = math::vec3_normalize(z);
821            y = math::vec3_normalize(math::vec3_cross(z, x));
822        }
823        [x, y, z]
824    }
825
826    fn quat_from_basis_columns(x: [f32; 3], y: [f32; 3], z: [f32; 3]) -> [f32; 4] {
827        let r00 = x[0];
828        let r01 = y[0];
829        let r02 = z[0];
830        let r10 = x[1];
831        let r11 = y[1];
832        let r12 = z[1];
833        let r20 = x[2];
834        let r21 = y[2];
835        let r22 = z[2];
836
837        let trace = r00 + r11 + r22;
838        let (qx, qy, qz, qw) = if trace > 0.0 {
839            let s = (trace + 1.0).sqrt() * 2.0;
840            ((r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s, 0.25 * s)
841        } else if r00 > r11 && r00 > r22 {
842            let s = (1.0 + r00 - r11 - r22).sqrt() * 2.0;
843            (0.25 * s, (r01 + r10) / s, (r02 + r20) / s, (r21 - r12) / s)
844        } else if r11 > r22 {
845            let s = (1.0 + r11 - r00 - r22).sqrt() * 2.0;
846            ((r01 + r10) / s, 0.25 * s, (r12 + r21) / s, (r02 - r20) / s)
847        } else {
848            let s = (1.0 + r22 - r00 - r11).sqrt() * 2.0;
849            ((r02 + r20) / s, (r12 + r21) / s, 0.25 * s, (r10 - r01) / s)
850        };
851
852        Self::quat_normalize([qx, qy, qz, qw])
853    }
854
855    fn quat_normalize(q: [f32; 4]) -> [f32; 4] {
856        let len = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
857        if len > 0.0 {
858            [q[0] / len, q[1] / len, q[2] / len, q[3] / len]
859        } else {
860            [0.0, 0.0, 0.0, 1.0]
861        }
862    }
863
864    fn vec3_lerp(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
865        let one_minus_t = 1.0 - t;
866        [
867            a[0] * one_minus_t + b[0] * t,
868            a[1] * one_minus_t + b[1] * t,
869            a[2] * one_minus_t + b[2] * t,
870        ]
871    }
872
873    fn alpha_from_smoothing_factor(smoothing_factor: f32, dt_sec: Option<f32>) -> f32 {
874        match dt_sec {
875            Some(dt) if dt > 0.0 => 1.0 - (-smoothing_factor.max(0.0) * dt).exp(),
876            _ => smoothing_factor.clamp(0.0, 1.0),
877        }
878    }
879
880    fn debug_quat_filter_enabled() -> bool {
881        static ENABLED: OnceLock<bool> = OnceLock::new();
882        *ENABLED.get_or_init(|| {
883            std::env::var("CAT_DEBUG_QUAT_FILTER")
884                .ok()
885                .map(|value| {
886                    let value = value.trim().to_ascii_lowercase();
887                    matches!(value.as_str(), "1" | "true" | "yes" | "on")
888                })
889                .unwrap_or(false)
890        })
891    }
892
893    fn debug_quat_filter_window_len() -> usize {
894        static WINDOW_LEN: OnceLock<usize> = OnceLock::new();
895        *WINDOW_LEN.get_or_init(|| {
896            std::env::var("CAT_DEBUG_QUAT_FILTER_WINDOW")
897                .ok()
898                .and_then(|value| value.parse::<usize>().ok())
899                .map(|value| value.clamp(1, 600))
900                .unwrap_or(60)
901        })
902    }
903
904    fn push_rolling_sample(window: &mut VecDeque<f32>, value: f32, max_len: usize) {
905        if window.len() >= max_len {
906            let _ = window.pop_front();
907        }
908        window.push_back(value);
909    }
910
911    fn rolling_avg(window: &VecDeque<f32>) -> f32 {
912        if window.is_empty() {
913            0.0
914        } else {
915            window.iter().copied().sum::<f32>() / window.len() as f32
916        }
917    }
918
919    fn rolling_max(window: &VecDeque<f32>) -> f32 {
920        window.iter().copied().fold(0.0, f32::max)
921    }
922
923    fn quat_angle_degrees(a: [f32; 4], b: [f32; 4]) -> f32 {
924        let a = Self::quat_normalize(a);
925        let b = Self::quat_normalize(b);
926        let dot = (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3])
927            .abs()
928            .clamp(0.0, 1.0);
929        (2.0 * dot.acos()).to_degrees()
930    }
931}
932
933impl From<TransformMatrix> for TransformPipelineChannels {
934    fn from(value: TransformMatrix) -> Self {
935        TransformStreamSystem::decompose_matrix(value)
936    }
937}
938
939impl System for TransformStreamSystem {
940    fn tick(
941        &mut self,
942        _world: &mut World,
943        _visuals: &mut VisualWorld,
944        _input: &InputState,
945        dt_sec: f32,
946    ) {
947        self.last_dt_sec = Some(dt_sec);
948    }
949}
950
951#[cfg(test)]
952mod tests {
953    use super::*;
954    use crate::engine::ecs::World;
955    use crate::engine::ecs::component::{
956        QuatTemporalFilterComponent, TransformCameraSpecificComponent, TransformComponent,
957        TransformForkTRSComponent, TransformMapRotationComponent, TransformMapScaleComponent,
958        TransformMapTranslationComponent, Vector3TemporalFilterComponent,
959    };
960
961    #[test]
962    fn camera_specific_selects_modes_and_excludes_configuration_children() {
963        let mut world = World::default();
964        let anchor = world.add_component(TransformComponent::new());
965        let mono = world.add_component(TransformCameraSpecificComponent::active_monoscopic());
966        let mono_settings =
967            world.add_component(TransformComponent::new().with_position(0.0, 2.0, 0.0));
968        let stereo = world.add_component(TransformCameraSpecificComponent::active_stereoscopic());
969        let stereo_settings =
970            world.add_component(TransformComponent::new().with_position(0.0, 0.0, 3.0));
971        let output = world.add_component(TransformComponent::new());
972        world.add_child(anchor, mono).unwrap();
973        world.add_child(mono, mono_settings).unwrap();
974        world.add_child(anchor, stereo).unwrap();
975        world.add_child(stereo, stereo_settings).unwrap();
976        world.add_child(anchor, output).unwrap();
977        let input = TransformComponent::new()
978            .with_position(1.0, 0.0, 0.0)
979            .transform
980            .model;
981        let mut system = TransformStreamSystem::new();
982        let (m, outputs) = system.evaluate_stream_node(&world, anchor, input).unwrap();
983        assert_eq!([m[3][0], m[3][1], m[3][2]], [1.0, 2.0, 0.0]);
984        assert_eq!(outputs, vec![output]);
985        system.set_stereoscopic_active(true);
986        let (m, outputs) = system.evaluate_stream_node(&world, anchor, input).unwrap();
987        assert_eq!([m[3][0], m[3][1], m[3][2]], [1.0, 0.0, 3.0]);
988        assert_eq!(outputs, vec![output]);
989    }
990
991    #[test]
992    fn camera_specific_uses_generic_anchor_when_mode_has_no_settings() {
993        let mut world = World::default();
994        let anchor = world.add_component(TransformComponent::new());
995        let mono = world.add_component(TransformCameraSpecificComponent::active_monoscopic());
996        let settings = world.add_component(TransformComponent::new().with_scale(2.0, 2.0, 2.0));
997        world.add_child(anchor, mono).unwrap();
998        world.add_child(mono, settings).unwrap();
999        let input = TransformComponent::new()
1000            .with_position(4.0, 5.0, 6.0)
1001            .transform
1002            .model;
1003        let mut system = TransformStreamSystem::new();
1004        system.set_stereoscopic_active(true);
1005        assert_eq!(
1006            system
1007                .evaluate_stream_node(&world, anchor, input)
1008                .unwrap()
1009                .0,
1010            input
1011        );
1012    }
1013
1014    #[test]
1015    fn repeated_camera_specific_refresh_does_not_compound_scale() {
1016        let mut world = World::default();
1017        let anchor = world.add_component(TransformComponent::new());
1018        let mono = world.add_component(TransformCameraSpecificComponent::active_monoscopic());
1019        let settings = world.add_component(TransformComponent::new().with_scale(2.0, 2.0, 2.0));
1020        world.add_child(anchor, mono).unwrap();
1021        world.add_child(mono, settings).unwrap();
1022
1023        let mut system = TransformStreamSystem::new();
1024        let identity = TransformComponent::new().transform.model;
1025        let first = system
1026            .evaluate_stream_node(&world, anchor, identity)
1027            .unwrap()
1028            .0;
1029        let second = system
1030            .evaluate_stream_node(&world, anchor, first)
1031            .unwrap()
1032            .0;
1033        let third = system
1034            .evaluate_stream_node(&world, anchor, second)
1035            .unwrap()
1036            .0;
1037
1038        assert_eq!(first, second);
1039        assert_eq!(second, third);
1040        assert_eq!(third[0][0], 2.0);
1041        assert_eq!(third[1][1], 2.0);
1042        assert_eq!(third[2][2], 2.0);
1043    }
1044
1045    #[test]
1046    fn controller_rotation_pipeline_contains_temporal_quat_op() {
1047        let block = TransformStreamSystem::pipeline_for_controller_rotation_smoothing(None, 1.0);
1048        let stage = match &block.stages[0] {
1049            TransformPipelineStage::ForkTrs(stage) => stage,
1050        };
1051        assert_eq!(
1052            stage.rotation_ops,
1053            vec![TransformPipelineQuatOp::TemporalFilter {
1054                smoothing_factor: 1.0
1055            }]
1056        );
1057    }
1058
1059    #[test]
1060    fn parses_fork_root_component_tree() {
1061        let mut world = World::default();
1062        let fork = world.add_component(TransformForkTRSComponent::new());
1063        let map_translation = world.add_component(TransformMapTranslationComponent::new());
1064        let vec_filter =
1065            world.add_component(Vector3TemporalFilterComponent::new().with_smoothing_factor(12.0));
1066        let map_rotation = world.add_component(TransformMapRotationComponent::new());
1067        let quat_filter =
1068            world.add_component(QuatTemporalFilterComponent::new().with_smoothing_factor(18.0));
1069
1070        world.set_parent(map_translation, Some(fork)).unwrap();
1071        world.set_parent(vec_filter, Some(map_translation)).unwrap();
1072        world.set_parent(map_rotation, Some(fork)).unwrap();
1073        world.set_parent(quat_filter, Some(map_rotation)).unwrap();
1074
1075        let parser = TransformStreamSystem::new();
1076        let block = parser
1077            .parse_component_tree(&world, fork)
1078            .expect("pipeline block");
1079        assert_eq!(block.owner_component, Some(fork));
1080        assert_eq!(block.stages.len(), 1);
1081        let stage = match &block.stages[0] {
1082            TransformPipelineStage::ForkTrs(stage) => stage,
1083        };
1084        assert_eq!(
1085            stage.translation_ops,
1086            vec![TransformPipelineVec3Op::TemporalSmooth {
1087                smoothing_factor: 12.0
1088            }]
1089        );
1090        assert_eq!(
1091            stage.rotation_ops,
1092            vec![TransformPipelineQuatOp::TemporalFilter {
1093                smoothing_factor: 18.0
1094            }]
1095        );
1096        assert_eq!(stage.scale_ops, vec![TransformPipelineVec3Op::Pass]);
1097    }
1098
1099    #[test]
1100    fn fork_trs_defaults_missing_streams_to_pass() {
1101        let mut world = World::default();
1102        let fork = world.add_component(TransformForkTRSComponent::new());
1103        let map_scale = world.add_component(TransformMapScaleComponent::new());
1104
1105        world.set_parent(map_scale, Some(fork)).unwrap();
1106
1107        let parser = TransformStreamSystem::new();
1108        let block = parser
1109            .parse_component_tree(&world, fork)
1110            .expect("pipeline block");
1111        let stage = match &block.stages[0] {
1112            TransformPipelineStage::ForkTrs(stage) => stage,
1113        };
1114
1115        assert_eq!(stage.translation_ops, vec![TransformPipelineVec3Op::Pass]);
1116        assert_eq!(stage.rotation_ops, vec![TransformPipelineQuatOp::Pass]);
1117        assert_eq!(stage.scale_ops, vec![TransformPipelineVec3Op::Pass]);
1118    }
1119
1120    #[test]
1121    fn parses_fork_trs_as_root_pipeline() {
1122        let mut world = World::default();
1123        let fork = world.add_component(TransformForkTRSComponent::new());
1124        let map_rotation = world.add_component(TransformMapRotationComponent::new());
1125        let quat_filter =
1126            world.add_component(QuatTemporalFilterComponent::new().with_smoothing_factor(9.0));
1127
1128        world.set_parent(map_rotation, Some(fork)).unwrap();
1129        world.set_parent(quat_filter, Some(map_rotation)).unwrap();
1130
1131        let parser = TransformStreamSystem::new();
1132        let block = parser
1133            .parse_component_tree(&world, fork)
1134            .expect("fork root block");
1135        assert_eq!(block.owner_component, Some(fork));
1136        assert_eq!(block.stages.len(), 1);
1137        let stage = match &block.stages[0] {
1138            TransformPipelineStage::ForkTrs(stage) => stage,
1139        };
1140        assert_eq!(
1141            stage.rotation_ops,
1142            vec![TransformPipelineQuatOp::TemporalFilter {
1143                smoothing_factor: 9.0
1144            }]
1145        );
1146    }
1147
1148    #[test]
1149    fn fork_root_returns_non_map_children_as_downstream_children() {
1150        let mut world = World::default();
1151        let fork = world.add_component(TransformForkTRSComponent::new());
1152        let map_rotation = world.add_component(TransformMapRotationComponent::new());
1153        let quat_filter =
1154            world.add_component(QuatTemporalFilterComponent::new().with_smoothing_factor(9.0));
1155        let downstream = world.add_component(TransformComponent::new());
1156
1157        world.set_parent(map_rotation, Some(fork)).unwrap();
1158        world.set_parent(quat_filter, Some(map_rotation)).unwrap();
1159        world.set_parent(downstream, Some(fork)).unwrap();
1160
1161        let mut system = TransformStreamSystem::new();
1162        let ident = [
1163            [1.0, 0.0, 0.0, 0.0],
1164            [0.0, 1.0, 0.0, 0.0],
1165            [0.0, 0.0, 1.0, 0.0],
1166            [0.0, 0.0, 0.0, 1.0],
1167        ];
1168        let (_, children) = system
1169            .evaluate_stream_node(&world, fork, ident)
1170            .expect("fork root eval");
1171        assert_eq!(children, vec![downstream]);
1172    }
1173}