Skip to main content

mittens_engine/engine/ecs/rx/
intent_executor.rs

1use crate::engine::ecs::{ComponentId, IntentValue, Signal, SignalEmitter, World};
2use crate::engine::graphics::RenderAssets;
3
4/// Built-in executor for **intent** signals.
5///
6/// This is intentionally minimal scaffolding for the ongoing refactor described in:
7/// - docs/signals.md
8///
9/// The goal is to keep handlers observers-only, and execute side effects via intent signals.
10#[derive(Debug, Default)]
11pub struct RxIntentExecutor;
12
13impl RxIntentExecutor {
14    pub fn new() -> Self {
15        Self::default()
16    }
17
18    /// Returns whether this executor is expected to handle the given signal value.
19    ///
20    /// Note: during migration, this is intentionally conservative; it is not yet wired into the
21    /// drain loop.
22    pub fn handles_value(value: &IntentValue) -> bool {
23        // These are the “intent interpretation” values that expand into follow-up mutations.
24        //
25        // Note: `SetText` is currently executed by the default executor for text rebuilds.
26        matches!(
27            value,
28            IntentValue::Noop
29                | IntentValue::SpawnComponentTree { .. }
30                | IntentValue::Print { .. }
31                | IntentValue::SetColor { .. }
32                | IntentValue::SetPosition { .. }
33                | IntentValue::LookAt { .. }
34                | IntentValue::GLTFArmatureVisible { .. }
35                | IntentValue::SelectionSet { .. }
36                | IntentValue::Attach { .. }
37                | IntentValue::QueryFindComponent { .. }
38                | IntentValue::QueryFindAllComponents { .. }
39                | IntentValue::AttachClone { .. }
40                | IntentValue::Detach { .. }
41                | IntentValue::RemoveChild { .. }
42                | IntentValue::RemoveChildren { .. }
43                | IntentValue::AudioGraphRebuild { .. }
44                | IntentValue::RequestRaycast { .. }
45                | IntentValue::AudioLowPassSetCutoffHz { .. }
46                | IntentValue::AudioBandPassSetCenterHz { .. }
47                | IntentValue::OscillatorSetEnabled { .. }
48                | IntentValue::OscillatorSetPitch { .. }
49                | IntentValue::OscillatorScheduleSetPitch { .. }
50                | IntentValue::AudioSchedulePlay { .. }
51        )
52    }
53
54    /// Execute an intent signal, emitting follow-up mutation signals via `emit`.
55    ///
56    pub fn execute(
57        &mut self,
58        world: &mut World,
59        render_assets: &mut RenderAssets,
60        emit: &mut dyn SignalEmitter,
61        env: &Signal,
62    ) {
63        handle_intent_signal(world, render_assets, emit, env);
64    }
65}
66
67fn handle_intent_signal(
68    world: &mut World,
69    render_assets: &mut RenderAssets,
70    emit: &mut dyn SignalEmitter,
71    env: &Signal,
72) {
73    use crate::engine::ecs::component::{
74        AudioBandPassFilterComponent, AudioClipComponent, AudioClipLoadState,
75        AudioLowPassFilterComponent, AudioOscillatorComponent, ColorComponent, MusicNoteComponent,
76        RayCastComponent, TransformComponent,
77    };
78    use crate::engine::ecs::system::MusicSystem;
79    use crate::engine::ecs::system::audio_system::AudioOp;
80    use crate::engine::ecs::{ComponentId, EventSignal, IntentValue};
81
82    let beat_now = 0.0;
83
84    let Some(intent) = env.intent.as_ref() else {
85        return;
86    };
87
88    match &intent.value {
89        IntentValue::Noop => {}
90
91        IntentValue::SpawnComponentTree { root, parent } => {
92            match crate::scripting::component_registry::with_live_render_assets(
93                render_assets,
94                || crate::scripting::component_registry::spawn_tree(root, *parent, world, emit),
95            ) {
96                Ok(id) => println!("[SpawnComponentTree] spawned root {id:?}"),
97                Err(e) => println!("[SpawnComponentTree] error: {e}"),
98            }
99        }
100
101        IntentValue::Print { .. } => {}
102
103        IntentValue::SetColor {
104            component_ids,
105            rgba,
106        } => {
107            let mut color_cids = Vec::new();
108            for &t in component_ids.iter() {
109                collect_color_targets(world, t, &mut color_cids);
110            }
111            color_cids.sort();
112            for color_cid in color_cids {
113                if let Some(c) = world.get_component_by_id_as_mut::<ColorComponent>(color_cid) {
114                    c.rgba = *rgba;
115                    emit.push_intent_now(
116                        color_cid,
117                        IntentValue::RegisterColor {
118                            component_ids: vec![color_cid],
119                        },
120                    );
121                }
122            }
123        }
124
125        IntentValue::SetText { .. } => {
126            // Executed by the mutation executor.
127        }
128
129        IntentValue::SetPosition {
130            component_ids,
131            position,
132        } => {
133            let mut transform_cids = Vec::new();
134            for &t in component_ids.iter() {
135                collect_transform_targets(world, t, &mut transform_cids);
136            }
137            transform_cids.sort();
138            transform_cids.dedup();
139            for transform_cid in transform_cids {
140                if let Some(t) =
141                    world.get_component_by_id_as_mut::<TransformComponent>(transform_cid)
142                {
143                    t.set_position(emit, position[0], position[1], position[2]);
144                }
145            }
146        }
147
148        IntentValue::LookAt {
149            component_ids,
150            target_world,
151        } => {
152            let mut transform_cids = Vec::new();
153            for &t in component_ids.iter() {
154                collect_transform_targets(world, t, &mut transform_cids);
155            }
156            transform_cids.sort();
157            transform_cids.dedup();
158            for transform_cid in transform_cids {
159                let Some(world_position) =
160                    crate::engine::ecs::system::TransformSystem::world_position(
161                        world,
162                        transform_cid,
163                    )
164                else {
165                    continue;
166                };
167
168                let Some(desired_world_rotation) =
169                    TransformComponent::look_at_world_rotation(world_position, *target_world)
170                else {
171                    continue;
172                };
173
174                let parent_world_rotation = world
175                    .parent_of(transform_cid)
176                    .and_then(|parent| {
177                        crate::engine::ecs::system::TransformSystem::world_model(world, parent)
178                    })
179                    .map(crate::utils::math::mat_to_quat)
180                    .unwrap_or([0.0, 0.0, 0.0, 1.0]);
181                let local_rotation =
182                    crate::utils::math::quat_normalize(crate::utils::math::quat_mul(
183                        crate::utils::math::quat_conjugate(parent_world_rotation),
184                        desired_world_rotation,
185                    ));
186
187                let Some(transform) = world
188                    .get_component_by_id_as::<TransformComponent>(transform_cid)
189                    .map(|t| t.transform)
190                else {
191                    continue;
192                };
193
194                emit.push_intent_now(
195                    transform_cid,
196                    IntentValue::UpdateTransform {
197                        component_ids: vec![transform_cid],
198                        translation: transform.translation,
199                        rotation_quat_xyzw: local_rotation,
200                        scale: transform.scale,
201                    },
202                );
203            }
204        }
205
206        IntentValue::GLTFArmatureVisible {
207            component_ids,
208            visible,
209        } => {
210            for &component_id in component_ids {
211                if let Some(gltf) = world
212                    .get_component_by_id_as_mut::<crate::engine::ecs::component::GLTFComponent>(
213                        component_id,
214                    )
215                {
216                    gltf.armature_visible = *visible;
217                }
218            }
219        }
220
221        IntentValue::SelectionSet {
222            component_ids,
223            entries,
224            primary,
225        } => {
226            for &selection_root in component_ids.iter() {
227                crate::engine::ecs::system::selection_system::apply_selection_set(
228                    world,
229                    emit,
230                    selection_root,
231                    entries.clone(),
232                    *primary,
233                );
234            }
235        }
236
237        IntentValue::Attach { parents, child } => {
238            for &parent in parents.iter() {
239                let old_parent = world.parent_of(*child);
240                if let Err(e) = world.add_child(parent, *child) {
241                    println!("[IntentExecutor] attach failed: {e}");
242                    continue;
243                }
244
245                emit.push_event(
246                    *child,
247                    EventSignal::ParentChanged {
248                        child: *child,
249                        old_parent,
250                        new_parent: Some(parent),
251                    },
252                );
253
254                if world.is_initialized(parent) {
255                    world.init_component_tree(*child, emit);
256                }
257
258                emit_topology_transform_refresh(world, emit, *child);
259                emit_topology_transform_refresh(world, emit, parent);
260
261                emit.push_intent_now(
262                    parent,
263                    IntentValue::AudioGraphDirtyImmediate {
264                        component_ids: vec![parent],
265                    },
266                );
267                emit.push_intent_now(
268                    *child,
269                    IntentValue::AudioGraphDirtyImmediate {
270                        component_ids: vec![*child],
271                    },
272                );
273            }
274        }
275
276        IntentValue::QueryFindComponent {
277            root,
278            selector,
279            reply,
280        } => {
281            let _ = reply.send(world.find_component(*root, selector));
282        }
283
284        IntentValue::QueryFindAllComponents {
285            root,
286            selector,
287            reply,
288        } => {
289            let _ = reply.send(world.find_all_components(*root, selector));
290        }
291
292        IntentValue::AttachClone {
293            parents,
294            prefab_root,
295        } => {
296            // Encode prefab subtree → MMS ComponentExpression AST → MaterializedCE.
297            // Cloning then drops into the same `spawn_tree` path that MMS source uses.
298            let ce_ast = match crate::scripting::component_registry::subtree_to_ce_ast(
299                &*world,
300                *prefab_root,
301            ) {
302                Ok(ce) => ce,
303                Err(e) => {
304                    println!("[IntentExecutor] attach_clone encode failed: {e}");
305                    return;
306                }
307            };
308            let materialized =
309                match crate::scripting::component_registry::ce_ast_to_materialized(&ce_ast) {
310                    Ok(m) => m,
311                    Err(e) => {
312                        println!("[IntentExecutor] attach_clone materialize failed: {e}");
313                        return;
314                    }
315                };
316
317            for &parent in parents.iter() {
318                let new_root = match crate::scripting::component_registry::with_live_render_assets(
319                    render_assets,
320                    || {
321                        crate::scripting::component_registry::spawn_tree(
322                            &materialized,
323                            Some(parent),
324                            world,
325                            emit,
326                        )
327                    },
328                ) {
329                    Ok(id) => id,
330                    Err(e) => {
331                        println!("[IntentExecutor] attach_clone spawn failed: {e}");
332                        continue;
333                    }
334                };
335
336                if world.get_component_record(new_root).is_none() {
337                    println!("[IntentExecutor] attach_clone: new root missing after spawn");
338                    continue;
339                }
340
341                // spawn_tree already attached + initialized the subtree if
342                // the parent was initialized. Just emit the topology signals.
343
344                emit.push_event(
345                    new_root,
346                    EventSignal::ParentChanged {
347                        child: new_root,
348                        old_parent: None,
349                        new_parent: Some(parent),
350                    },
351                );
352
353                emit_topology_transform_refresh(world, emit, new_root);
354                emit_topology_transform_refresh(world, emit, parent);
355
356                emit.push_intent_now(
357                    parent,
358                    IntentValue::AudioGraphDirtyImmediate {
359                        component_ids: vec![parent],
360                    },
361                );
362                emit.push_intent_now(
363                    new_root,
364                    IntentValue::AudioGraphDirtyImmediate {
365                        component_ids: vec![new_root],
366                    },
367                );
368            }
369        }
370
371        IntentValue::Detach { component_ids } => {
372            for &child in component_ids.iter() {
373                let old_parent = world.parent_of(child);
374                world.detach_from_parent(child);
375
376                emit.push_event(
377                    child,
378                    EventSignal::ParentChanged {
379                        child,
380                        old_parent,
381                        new_parent: None,
382                    },
383                );
384
385                if let Some(p) = old_parent {
386                    emit.push_intent_now(
387                        p,
388                        IntentValue::AudioGraphDirtyImmediate {
389                            component_ids: vec![p],
390                        },
391                    );
392                }
393
394                emit_topology_transform_refresh(world, emit, child);
395                if let Some(p) = old_parent {
396                    emit_topology_transform_refresh(world, emit, p);
397                }
398            }
399        }
400
401        IntentValue::RemoveChild { parents, index } => {
402            for &parent in parents.iter() {
403                let child = world.children_of(parent).get(*index).copied();
404                let Some(child) = child else {
405                    continue;
406                };
407
408                emit.push_intent_now(
409                    parent,
410                    IntentValue::AudioGraphDirtyImmediate {
411                        component_ids: vec![parent],
412                    },
413                );
414                emit.push_intent_now(
415                    child,
416                    IntentValue::AudioGraphDirtyImmediate {
417                        component_ids: vec![child],
418                    },
419                );
420
421                world.detach_from_parent(child);
422
423                emit.push_event(
424                    child,
425                    EventSignal::ParentChanged {
426                        child,
427                        old_parent: Some(parent),
428                        new_parent: None,
429                    },
430                );
431
432                emit.push_intent_now(
433                    child,
434                    IntentValue::RemoveSubtree {
435                        component_ids: vec![child],
436                    },
437                );
438
439                emit_topology_transform_refresh(world, emit, parent);
440            }
441        }
442
443        IntentValue::RemoveChildren { parents } => {
444            for &parent in parents.iter() {
445                let children: Vec<ComponentId> = world.children_of(parent).to_vec();
446                if children.is_empty() {
447                    continue;
448                }
449
450                emit.push_intent_now(
451                    parent,
452                    IntentValue::AudioGraphDirtyImmediate {
453                        component_ids: vec![parent],
454                    },
455                );
456                for child in children {
457                    emit.push_intent_now(
458                        child,
459                        IntentValue::AudioGraphDirtyImmediate {
460                            component_ids: vec![child],
461                        },
462                    );
463
464                    world.detach_from_parent(child);
465                    emit.push_event(
466                        child,
467                        EventSignal::ParentChanged {
468                            child,
469                            old_parent: Some(parent),
470                            new_parent: None,
471                        },
472                    );
473
474                    emit.push_intent_now(
475                        child,
476                        IntentValue::RemoveSubtree {
477                            component_ids: vec![child],
478                        },
479                    );
480                }
481
482                emit_topology_transform_refresh(world, emit, parent);
483            }
484        }
485
486        IntentValue::AudioGraphRebuild { component_ids } => {
487            for &t in component_ids.iter() {
488                emit.push_intent_now(
489                    t,
490                    IntentValue::AudioGraphDirtyImmediate {
491                        component_ids: vec![t],
492                    },
493                );
494            }
495        }
496
497        IntentValue::RequestRaycast { component_ids } => {
498            let mut raycast_cids = Vec::new();
499            for &t in component_ids.iter() {
500                collect_raycast_targets(world, t, &mut raycast_cids);
501            }
502            raycast_cids.sort();
503            raycast_cids.dedup();
504
505            for rcid in raycast_cids {
506                if let Some(rc) = world.get_component_by_id_as_mut::<RayCastComponent>(rcid) {
507                    rc.cast_requests = rc.cast_requests.saturating_add(1);
508                }
509            }
510        }
511
512        IntentValue::AudioLowPassSetCutoffHz {
513            component_ids,
514            cutoff_hz,
515        } => {
516            for &t in component_ids.iter() {
517                if let Some(c) = world.get_component_by_id_as_mut::<AudioLowPassFilterComponent>(t)
518                {
519                    c.cutoff_hz = if cutoff_hz.is_finite() {
520                        cutoff_hz.max(0.0)
521                    } else {
522                        c.cutoff_hz
523                    };
524                    emit.push_intent_now(
525                        t,
526                        IntentValue::ScheduleAudioOp {
527                            component_ids: vec![t],
528                            beat: beat_now,
529                            op: AudioOp::SetLowPassCutoffHz(c.cutoff_hz),
530                        },
531                    );
532                }
533            }
534        }
535
536        IntentValue::AudioBandPassSetCenterHz {
537            component_ids,
538            center_hz,
539        } => {
540            for &t in component_ids.iter() {
541                if let Some(c) = world.get_component_by_id_as_mut::<AudioBandPassFilterComponent>(t)
542                {
543                    c.center_hz = if center_hz.is_finite() {
544                        center_hz.max(0.0)
545                    } else {
546                        c.center_hz
547                    };
548                    emit.push_intent_now(
549                        t,
550                        IntentValue::ScheduleAudioOp {
551                            component_ids: vec![t],
552                            beat: beat_now,
553                            op: AudioOp::SetBandPassCenterHz(c.center_hz),
554                        },
555                    );
556                }
557            }
558        }
559
560        IntentValue::OscillatorSetEnabled {
561            component_ids,
562            enabled,
563        } => {
564            let mut osc_cids = Vec::new();
565            for &t in component_ids.iter() {
566                collect_oscillator_targets(world, t, &mut osc_cids);
567            }
568            osc_cids.sort();
569            osc_cids.dedup();
570            for osc_cid in osc_cids {
571                if let Some(c) =
572                    world.get_component_by_id_as_mut::<AudioOscillatorComponent>(osc_cid)
573                {
574                    for osc in c.oscillators.iter_mut() {
575                        osc.enabled = *enabled;
576                    }
577                    emit.push_intent_now(
578                        osc_cid,
579                        IntentValue::RegisterAudioOscillator {
580                            component_ids: vec![osc_cid],
581                        },
582                    );
583                }
584            }
585        }
586
587        IntentValue::OscillatorSetPitch {
588            component_ids,
589            frequency_hz,
590        } => {
591            if !frequency_hz.is_finite() {
592                println!(
593                    "[IntentExecutor] oscillator_set_pitch: non-finite frequency_hz={frequency_hz}"
594                );
595                return;
596            }
597
598            let mut osc_cids = Vec::new();
599            for &t in component_ids.iter() {
600                collect_oscillator_targets(world, t, &mut osc_cids);
601            }
602            osc_cids.sort();
603            osc_cids.dedup();
604            for osc_cid in osc_cids {
605                if let Some(c) =
606                    world.get_component_by_id_as_mut::<AudioOscillatorComponent>(osc_cid)
607                {
608                    for osc in c.oscillators.iter_mut() {
609                        osc.frequency = *frequency_hz;
610                    }
611                    emit.push_intent_now(
612                        osc_cid,
613                        IntentValue::RegisterAudioOscillator {
614                            component_ids: vec![osc_cid],
615                        },
616                    );
617                }
618            }
619        }
620
621        IntentValue::OscillatorScheduleSetPitch {
622            component_ids,
623            beat_offset,
624            beat_context,
625            frequency_hz,
626        } => {
627            let beat = beat_context.unwrap_or(beat_now) + *beat_offset;
628
629            let mut osc_cids = Vec::new();
630            for &t in component_ids.iter() {
631                collect_oscillator_targets(world, t, &mut osc_cids);
632            }
633            osc_cids.sort();
634            osc_cids.dedup();
635            for osc_cid in osc_cids {
636                emit.push_intent_now(
637                    osc_cid,
638                    IntentValue::ScheduleAudioPitchSetHz {
639                        component_ids: vec![osc_cid],
640                        beat,
641                        frequency_hz: *frequency_hz,
642                    },
643                );
644            }
645        }
646
647        IntentValue::AudioSchedulePlay {
648            component_ids,
649            beat_offset,
650            beat_context,
651            note,
652            gain,
653            rate: _,
654            duration,
655        } => {
656            let beat = beat_context.unwrap_or(beat_now) + *beat_offset;
657
658            // Per-source semantics (oscillator path): set pitch from note (if any),
659            // gate on, set gain, gate off after duration. See docs/spec/audio-sources.md §4.
660            let frequency_hz = note
661                .as_ref()
662                .map(|n| MusicSystem::frequency_hz_for_note(*n));
663
664            let velocity_gain = gain.or_else(|| note.as_ref().map(|n| n.velocity()));
665            let final_gain = velocity_gain.map(|g| if g.is_finite() { g.max(0.0) } else { 1.0 });
666
667            let note_duration = note.as_ref().map(|n| n.duration_beats() as f64);
668            let stop_after = duration.or(note_duration);
669
670            // Resolve every input id to a concrete audio source id (osc or clip).
671            let mut source_cids = Vec::new();
672            for &t in component_ids.iter() {
673                // MusicNoteComponent: full resolution chain (cache →
674                // target_source → context voice). See §6.6.
675                let mut resolved_via_note: Option<ComponentId> = None;
676                if world
677                    .get_component_by_id_as::<MusicNoteComponent>(t)
678                    .is_some()
679                {
680                    let mut taken: MusicNoteComponent = world
681                        .get_component_by_id_as::<MusicNoteComponent>(t)
682                        .cloned()
683                        .unwrap();
684                    resolved_via_note = taken.resolve_target(world);
685                    if let Some(slot) = world.get_component_by_id_as_mut::<MusicNoteComponent>(t) {
686                        slot.target_resolved = taken.target_resolved;
687                    }
688                }
689                if let Some(via_note) = resolved_via_note {
690                    source_cids.push(via_note);
691                    continue;
692                }
693
694                let before = source_cids.len();
695                collect_audio_source_targets(world, t, &mut source_cids);
696                // Cache the ancestor-walk result back into the
697                // MusicNoteComponent so subsequent fires skip the walk.
698                if let Some(&found) = source_cids[before..].first() {
699                    if let Some(mn) = world.get_component_by_id_as_mut::<MusicNoteComponent>(t) {
700                        if mn.target_resolved.is_none() {
701                            mn.target_resolved = Some(found);
702                        }
703                    }
704                }
705            }
706            source_cids.sort();
707            source_cids.dedup();
708
709            for src_cid in source_cids {
710                // Dispatch by source kind. Oscillator → gate+pitch+gain schedule;
711                // Clip → cursor reset (stub log in phase 4).
712                if world
713                    .get_component_by_id_as::<AudioOscillatorComponent>(src_cid)
714                    .is_some()
715                {
716                    emit.push_intent_now(
717                        src_cid,
718                        IntentValue::ScheduleAudioOscillatorEnabled {
719                            component_ids: vec![src_cid],
720                            beat,
721                            enabled: true,
722                        },
723                    );
724                    if let Some(frequency_hz) = frequency_hz {
725                        emit.push_intent_now(
726                            src_cid,
727                            IntentValue::ScheduleAudioPitchSetHz {
728                                component_ids: vec![src_cid],
729                                beat,
730                                frequency_hz,
731                            },
732                        );
733                    }
734                    if let Some(g) = final_gain {
735                        emit.push_intent_now(
736                            src_cid,
737                            IntentValue::ScheduleAudioGainSet {
738                                component_ids: vec![src_cid],
739                                beat,
740                                gain: g,
741                            },
742                        );
743                    }
744
745                    if let Some(dur) = stop_after {
746                        if dur.is_finite() && dur > 0.0 {
747                            emit.push_intent_now(
748                                src_cid,
749                                IntentValue::ScheduleAudioOscillatorEnabled {
750                                    component_ids: vec![src_cid],
751                                    beat: beat + dur,
752                                    enabled: false,
753                                },
754                            );
755                            if final_gain.is_some() {
756                                emit.push_intent_now(
757                                    src_cid,
758                                    IntentValue::ScheduleAudioGainSet {
759                                        component_ids: vec![src_cid],
760                                        beat: beat + dur,
761                                        gain: 1.0,
762                                    },
763                                );
764                            }
765                        }
766                    }
767                } else if let Some(clip) =
768                    world.get_component_by_id_as::<AudioClipComponent>(src_cid)
769                {
770                    match &clip.load_state {
771                        AudioClipLoadState::Failed(reason) => {
772                            eprintln!(
773                                "[AudioSchedulePlay] skip clip {:?} ({}): {}",
774                                src_cid, clip.uri, reason
775                            );
776                        }
777                        AudioClipLoadState::Pending | AudioClipLoadState::Loaded => {
778                            // Schedule playback via the same SetEnabled
779                            // ops the oscillator path uses. The RT thread
780                            // resets the clip's cursor on SetEnabled(true)
781                            // (docs/spec/audio-sources.md §4).
782                            emit.push_intent_now(
783                                src_cid,
784                                IntentValue::ScheduleAudioOscillatorEnabled {
785                                    component_ids: vec![src_cid],
786                                    beat,
787                                    enabled: true,
788                                },
789                            );
790                            if let Some(g) = final_gain {
791                                emit.push_intent_now(
792                                    src_cid,
793                                    IntentValue::ScheduleAudioGainSet {
794                                        component_ids: vec![src_cid],
795                                        beat,
796                                        gain: g,
797                                    },
798                                );
799                            }
800                            if let Some(dur) = stop_after {
801                                if dur.is_finite() && dur > 0.0 {
802                                    emit.push_intent_now(
803                                        src_cid,
804                                        IntentValue::ScheduleAudioOscillatorEnabled {
805                                            component_ids: vec![src_cid],
806                                            beat: beat + dur,
807                                            enabled: false,
808                                        },
809                                    );
810                                }
811                            }
812                        }
813                    }
814                }
815            }
816        }
817
818        _ => {}
819    }
820}
821
822fn collect_raycast_targets(world: &World, target: ComponentId, out: &mut Vec<ComponentId>) {
823    use crate::engine::ecs::component::RayCastComponent;
824
825    if world
826        .get_component_by_id_as::<RayCastComponent>(target)
827        .is_some()
828    {
829        out.push(target);
830        return;
831    }
832
833    // Subtree search: collect all RayCastComponents under this target.
834    let mut stack = vec![target];
835    while let Some(node) = stack.pop() {
836        if world
837            .get_component_by_id_as::<RayCastComponent>(node)
838            .is_some()
839        {
840            out.push(node);
841            continue;
842        }
843        for &ch in world.children_of(node).iter() {
844            stack.push(ch);
845        }
846    }
847}
848
849fn collect_transform_targets(world: &World, target: ComponentId, out: &mut Vec<ComponentId>) {
850    use crate::engine::ecs::component::TransformComponent;
851
852    if world
853        .get_component_by_id_as::<TransformComponent>(target)
854        .is_some()
855    {
856        out.push(target);
857        return;
858    }
859
860    // Subtree search: pick the first TransformComponent encountered per branch.
861    let mut stack = vec![target];
862    while let Some(node) = stack.pop() {
863        if world
864            .get_component_by_id_as::<TransformComponent>(node)
865            .is_some()
866        {
867            out.push(node);
868            continue;
869        }
870        for &ch in world.children_of(node).iter() {
871            stack.push(ch);
872        }
873    }
874}
875
876fn emit_topology_transform_refresh(world: &World, emit: &mut dyn SignalEmitter, cid: ComponentId) {
877    use crate::engine::ecs::IntentValue;
878    use crate::engine::ecs::component::{TransformComponent, TransformParentComponent};
879
880    // If this node is a TransformComponent, refreshing it updates cached world matrices
881    // for its whole subtree.
882    if let Some(t) = world.get_component_by_id_as::<TransformComponent>(cid) {
883        let _ = t;
884        emit.push_intent_now(
885            cid,
886            IntentValue::UpdateTransformWorld {
887                component_ids: vec![cid],
888            },
889        );
890        return;
891    }
892
893    if world
894        .get_component_by_id_as::<TransformParentComponent>(cid)
895        .is_some()
896    {
897        emit.push_intent_now(
898            cid,
899            IntentValue::UpdateTransformWorld {
900                component_ids: vec![cid],
901            },
902        );
903        return;
904    }
905
906    // Otherwise, refresh the nearest ancestor transform (if any).
907    let mut cur = cid;
908    while let Some(p) = world.parent_of(cur) {
909        if world
910            .get_component_by_id_as::<TransformParentComponent>(p)
911            .is_some()
912        {
913            emit.push_intent_now(
914                p,
915                IntentValue::UpdateTransformWorld {
916                    component_ids: vec![p],
917                },
918            );
919            return;
920        }
921        if let Some(t) = world.get_component_by_id_as::<TransformComponent>(p) {
922            let _ = t;
923            emit.push_intent_now(
924                p,
925                IntentValue::UpdateTransformWorld {
926                    component_ids: vec![p],
927                },
928            );
929            return;
930        }
931        cur = p;
932    }
933}
934
935fn collect_color_targets(world: &World, target: ComponentId, out: &mut Vec<ComponentId>) {
936    use crate::engine::ecs::component::{ColorComponent, RenderableComponent};
937
938    // 1) Direct ColorComponent target.
939    if world
940        .get_component_by_id_as::<ColorComponent>(target)
941        .is_some()
942    {
943        out.push(target);
944        return;
945    }
946
947    // 2) RenderableComponent target -> find immediate ColorComponent child.
948    if world
949        .get_component_by_id_as::<RenderableComponent>(target)
950        .is_some()
951    {
952        for &ch in world.children_of(target) {
953            if world.get_component_by_id_as::<ColorComponent>(ch).is_some() {
954                out.push(ch);
955                return;
956            }
957        }
958        return;
959    }
960
961    // 3) Generic subtree target (e.g. TransformComponent): search for renderables and their color children.
962    let mut stack = vec![target];
963    while let Some(node) = stack.pop() {
964        for &ch in world.children_of(node) {
965            stack.push(ch);
966        }
967
968        if world
969            .get_component_by_id_as::<RenderableComponent>(node)
970            .is_some()
971        {
972            for &ch in world.children_of(node) {
973                if world.get_component_by_id_as::<ColorComponent>(ch).is_some() {
974                    out.push(ch);
975                }
976            }
977        }
978    }
979}
980
981fn is_audio_source(world: &World, id: ComponentId) -> bool {
982    use crate::engine::ecs::component::{AudioClipComponent, AudioOscillatorComponent};
983    world
984        .get_component_by_id_as::<AudioOscillatorComponent>(id)
985        .is_some()
986        || world
987            .get_component_by_id_as::<AudioClipComponent>(id)
988            .is_some()
989}
990
991/// Collect ids of `AudioSource`s reachable from `target`.
992///
993/// Treats `AudioOscillatorComponent` and `AudioClipComponent` as peer
994/// source variants (§5 of docs/spec/audio-sources.md). Search order:
995/// the target itself, then the subtree, then fall back to walking
996/// ancestors for the nearest source (§6.6 rank 5).
997fn collect_audio_source_targets(world: &World, target: ComponentId, out: &mut Vec<ComponentId>) {
998    if is_audio_source(world, target) {
999        out.push(target);
1000        return;
1001    }
1002
1003    let before = out.len();
1004    let mut stack = vec![target];
1005    while let Some(node) = stack.pop() {
1006        for &ch in world.children_of(node) {
1007            stack.push(ch);
1008        }
1009        if is_audio_source(world, node) {
1010            out.push(node);
1011        }
1012    }
1013
1014    if out.len() == before {
1015        let mut cur = target;
1016        while let Some(p) = world.parent_of(cur) {
1017            if is_audio_source(world, p) {
1018                out.push(p);
1019                return;
1020            }
1021            cur = p;
1022        }
1023    }
1024}
1025
1026fn collect_oscillator_targets(world: &World, target: ComponentId, out: &mut Vec<ComponentId>) {
1027    use crate::engine::ecs::component::AudioOscillatorComponent;
1028
1029    if world
1030        .get_component_by_id_as::<AudioOscillatorComponent>(target)
1031        .is_some()
1032    {
1033        out.push(target);
1034        return;
1035    }
1036
1037    let before = out.len();
1038    let mut stack = vec![target];
1039    while let Some(node) = stack.pop() {
1040        for &ch in world.children_of(node) {
1041            stack.push(ch);
1042        }
1043
1044        if world
1045            .get_component_by_id_as::<AudioOscillatorComponent>(node)
1046            .is_some()
1047        {
1048            out.push(node);
1049        }
1050    }
1051
1052    if out.len() == before {
1053        let mut cur = target;
1054        while let Some(p) = world.parent_of(cur) {
1055            if world
1056                .get_component_by_id_as::<AudioOscillatorComponent>(p)
1057                .is_some()
1058            {
1059                out.push(p);
1060                return;
1061            }
1062            cur = p;
1063        }
1064    }
1065}