Skip to main content

mittens_engine/engine/ecs/rx/
signal.rs

1use crate::engine::ecs::component::style::SizeDimension;
2use crate::engine::ecs::component::{
3    AnimationState, ControllerHand, SelectionEntry, SelectionMode, XrAxisControl, XrButtonControl,
4};
5use crate::engine::ecs::{ComponentId, World};
6use std::sync::mpsc::Sender;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TextInputCaretDirection {
10    Left,
11    Right,
12}
13
14pub type HttpHeaders = Vec<(String, String)>;
15
16/// Signal envelope.
17///
18/// Exactly one of `event` or `intent` should be `Some`.
19#[derive(Debug, Clone)]
20pub struct Signal {
21    pub scope: ComponentId,
22    pub event: Option<EventSignal>,
23    pub intent: Option<IntentSignal>,
24}
25
26impl Signal {
27    pub fn event(scope: ComponentId, event: EventSignal) -> Self {
28        Self {
29            scope,
30            event: Some(event),
31            intent: None,
32        }
33    }
34
35    pub fn intent(scope: ComponentId, intent: IntentSignal) -> Self {
36        Self {
37            scope,
38            event: None,
39            intent: Some(intent),
40        }
41    }
42
43    pub fn kind(&self) -> Option<SignalKind> {
44        self.event.as_ref().map(|e| e.kind())
45    }
46}
47
48/// Event signal: a fact/observation.
49#[derive(Debug, Clone)]
50pub enum EventSignal {
51    /// Topology changed.
52    ParentChanged {
53        child: ComponentId,
54        old_parent: Option<ComponentId>,
55        new_parent: Option<ComponentId>,
56    },
57
58    /// A raycast intersected a renderable.
59    RayIntersected {
60        raycaster: ComponentId,
61        renderable: ComponentId,
62        t: f32,
63        origin: [f32; 3],
64        dir: [f32; 3],
65    },
66
67    /// Two collision objects began overlapping this tick.
68    ///
69    /// `delta` is the vector from `a` to `b` in world space: `pos(b) - pos(a)`.
70    CollisionStarted {
71        a: ComponentId,
72        b: ComponentId,
73        delta: [f32; 3],
74    },
75
76    /// Two collision objects stopped overlapping this tick.
77    ///
78    /// `delta` is the last known vector from `a` to `b` in world space: `pos(b) - pos(a)`.
79    CollisionEnded {
80        a: ComponentId,
81        b: ComponentId,
82        delta: [f32; 3],
83    },
84
85    /// A drag gesture started.
86    DragStart {
87        raycaster: ComponentId,
88        renderable: ComponentId,
89        hit_point: [f32; 3],
90
91        /// World-space ray direction at the time the drag started.
92        ///
93        /// This makes DragStart self-contained for consumers that need a stable plane normal
94        /// (e.g. gizmo debug drag plane / plane-projection drags) without also observing
95        /// RayIntersected events.
96        ray_dir_world: [f32; 3],
97
98        /// Optional screen-space cursor/pointer position in pixels.
99        ///
100        /// Present for screen-space pointers (mouse/touch). Absent for non-screen pointers.
101        screen_pos_px: Option<(f32, f32)>,
102    },
103
104    /// A drag gesture moved this tick.
105    ///
106    /// `delta_world` is the world-space movement since the last DragMove for this gesture.
107    DragMove {
108        raycaster: ComponentId,
109        renderable: ComponentId,
110        hit_point: [f32; 3],
111        delta_world: [f32; 3],
112
113        /// Optional screen-space cursor/pointer position in pixels.
114        screen_pos_px: Option<(f32, f32)>,
115
116        /// Optional pixel delta since the previous DragMove for this drag.
117        ///
118        /// Present for screen-space pointers (mouse/touch) when previous screen position is
119        /// known. Absent for non-screen pointers.
120        screen_delta_px: Option<(f32, f32)>,
121    },
122
123    /// A drag gesture ended.
124    DragEnd {
125        raycaster: ComponentId,
126        renderable: ComponentId,
127        hit_point: Option<[f32; 3]>,
128    },
129
130    /// A click: a drag gesture that ended close to where it started.
131    ///
132    /// Emitted by `GestureSystem` at `DragEnd` time when the net pointer displacement is below
133    /// the click threshold (8 px screen-space, or 0.02 world units for non-screen pointers).
134    ///
135    /// All intermediate `DragMove` events are still emitted; `Click` fires *in addition to*
136    /// `DragEnd`, on the same scope (the hit renderable at press time).
137    ///
138    /// Payload mirrors `DragStart` — the thing clicked is what was under the pointer at press.
139    Click {
140        raycaster: ComponentId,
141        renderable: ComponentId,
142        hit_point: [f32; 3],
143        screen_pos_px: Option<(f32, f32)>,
144    },
145
146    /// A selection scope changed.
147    SelectionChanged {
148        selection_root: ComponentId,
149        mode: SelectionMode,
150        selected_entries: Vec<SelectionEntry>,
151        selected_component: Option<ComponentId>,
152        selected_payload: Option<ComponentId>,
153    },
154
155    /// An entry was added to a selection scope.
156    SelectionAdded {
157        selection_root: ComponentId,
158        entry: SelectionEntry,
159    },
160
161    /// An entry was removed from a selection scope.
162    SelectionRemoved {
163        selection_root: ComponentId,
164        entry: SelectionEntry,
165    },
166
167    /// A selection scope was cleared.
168    SelectionCleared { selection_root: ComponentId },
169
170    /// A scrolling component consumed drag motion and updated its offset.
171    ///
172    /// Scoped to the `ScrollingComponent` so downstream systems can subscribe to scroll state
173    /// changes through the scroll owner rather than the ancestor drag-capture surface.
174    Scrolling {
175        scroll_component: ComponentId,
176        drag_scope: ComponentId,
177        delta_world: [f32; 3],
178        scroll_offset: f32,
179        max_scroll: f32,
180        viewport_height: f32,
181        content_height: f32,
182    },
183
184    TextInputFocusChanged {
185        old: Option<ComponentId>,
186        new: Option<ComponentId>,
187    },
188
189    TextInputChanged {
190        component_id: ComponentId,
191        text: String,
192        caret: usize,
193    },
194
195    /// A `LayoutComponent` subtree finished layout and its computed size is now
196    /// available in world units. Scoped to the `LayoutComponent`'s `ComponentId`.
197    LayoutRootSizeAvailable {
198        layout_id: ComponentId,
199        width_wu: f32,
200        height_wu: f32,
201    },
202
203    /// A named data event for cross-subtree communication.
204    ///
205    /// The `name` identifies the event kind (e.g. "asset_selected", "tool_selected").
206    /// The `scope` on the `Signal` envelope identifies the shared ancestor on which
207    /// handlers are registered. `payload` is an optional `ComponentId` reference.
208    DataEvent {
209        name: String,
210        payload: Option<ComponentId>,
211    },
212
213    XrButtonDown {
214        source_component: ComponentId,
215        hand: ControllerHand,
216        control: XrButtonControl,
217        value: f32,
218    },
219    XrButtonUp {
220        source_component: ComponentId,
221        hand: ControllerHand,
222        control: XrButtonControl,
223        value: f32,
224    },
225    XrButtonChanged {
226        source_component: ComponentId,
227        hand: ControllerHand,
228        control: XrButtonControl,
229        value: f32,
230    },
231    XrAxisChanged {
232        source_component: ComponentId,
233        hand: ControllerHand,
234        control: XrAxisControl,
235        value: [f32; 2],
236    },
237
238    HttpRequest {
239        request_id: u64,
240        method: String,
241        path: String,
242        query: Option<String>,
243        url: String,
244        headers: HttpHeaders,
245        body_text: String,
246        remote_addr: Option<String>,
247    },
248    HttpResponse {
249        request_id: u64,
250        status: u16,
251        ok: bool,
252        headers: HttpHeaders,
253        body_text: String,
254        url: String,
255    },
256    HttpError {
257        request_id: Option<u64>,
258        phase: String,
259        message: String,
260        url: Option<String>,
261        bind_addr: Option<String>,
262    },
263}
264
265impl EventSignal {
266    pub fn kind(&self) -> SignalKind {
267        match self {
268            EventSignal::ParentChanged { .. } => SignalKind::ParentChanged,
269            EventSignal::RayIntersected { .. } => SignalKind::RayIntersected,
270            EventSignal::CollisionStarted { .. } => SignalKind::CollisionStarted,
271            EventSignal::CollisionEnded { .. } => SignalKind::CollisionEnded,
272            EventSignal::DragStart { .. } => SignalKind::DragStart,
273            EventSignal::DragMove { .. } => SignalKind::DragMove,
274            EventSignal::DragEnd { .. } => SignalKind::DragEnd,
275            EventSignal::Click { .. } => SignalKind::Click,
276            EventSignal::SelectionChanged { .. } => SignalKind::SelectionChanged,
277            EventSignal::SelectionAdded { .. } => SignalKind::SelectionAdded,
278            EventSignal::SelectionRemoved { .. } => SignalKind::SelectionRemoved,
279            EventSignal::SelectionCleared { .. } => SignalKind::SelectionCleared,
280            EventSignal::Scrolling { .. } => SignalKind::Scrolling,
281            EventSignal::TextInputFocusChanged { .. } => SignalKind::TextInputFocusChanged,
282            EventSignal::TextInputChanged { .. } => SignalKind::TextInputChanged,
283            EventSignal::LayoutRootSizeAvailable { .. } => SignalKind::LayoutRootSizeAvailable,
284            EventSignal::DataEvent { .. } => SignalKind::DataEvent,
285            EventSignal::XrButtonDown { .. } => SignalKind::XrButtonDown,
286            EventSignal::XrButtonUp { .. } => SignalKind::XrButtonUp,
287            EventSignal::XrButtonChanged { .. } => SignalKind::XrButtonChanged,
288            EventSignal::XrAxisChanged { .. } => SignalKind::XrAxisChanged,
289            EventSignal::HttpRequest { .. } => SignalKind::HttpRequest,
290            EventSignal::HttpResponse { .. } => SignalKind::HttpResponse,
291            EventSignal::HttpError { .. } => SignalKind::HttpError,
292        }
293    }
294}
295
296/// Intent signal: requests side effects.
297#[derive(Debug, Clone)]
298pub struct IntentSignal {
299    pub when: SignalWhen,
300    pub value: IntentValue,
301}
302
303impl IntentSignal {
304    pub fn now(value: IntentValue) -> Self {
305        Self {
306            when: SignalWhen::Now,
307            value,
308        }
309    }
310
311    pub fn at_beat(beat: f64, value: IntentValue) -> Self {
312        if !beat.is_finite() {
313            return Self::now(value);
314        }
315        Self {
316            when: SignalWhen::AtBeat(beat),
317            value,
318        }
319    }
320}
321
322/// Intent payload: both user-facing intent and internal mutation commands live here for now.
323#[derive(Debug, Clone)]
324pub enum IntentValue {
325    Noop,
326    RetryXrRuntime,
327
328    /// Spawn a component tree described by a fully-evaluated `MaterializedCE` and optionally
329    /// attach it to a parent. If `parent` is `None` the root becomes a world root.
330    SpawnComponentTree {
331        root: Box<crate::scripting::object::MaterializedCE>,
332        parent: Option<ComponentId>,
333    },
334    Print {
335        message: String,
336    },
337
338    /// Queue a REPL command to be executed on the main thread (if the REPL is enabled).
339    ///
340    /// This is used for editor integration (e.g. jump to the clicked component).
341    ReplExec {
342        command: String,
343    },
344
345    SetColor {
346        component_ids: Vec<ComponentId>,
347        rgba: [f32; 4],
348    },
349    SetText {
350        component_ids: Vec<ComponentId>,
351        text: String,
352    },
353    SetEmissiveIntensity {
354        component_ids: Vec<ComponentId>,
355        intensity: f32,
356    },
357    SetPosition {
358        component_ids: Vec<ComponentId>,
359        position: [f32; 3],
360    },
361    LookAt {
362        component_ids: Vec<ComponentId>,
363        target_world: [f32; 3],
364    },
365    GLTFArmatureVisible {
366        component_ids: Vec<ComponentId>,
367        visible: bool,
368    },
369    SetLayoutAvailableWidth {
370        component_ids: Vec<ComponentId>,
371        width: SizeDimension,
372    },
373    SetLayoutAvailableHeight {
374        component_ids: Vec<ComponentId>,
375        height: SizeDimension,
376    },
377    SetLayoutInspect {
378        component_ids: Vec<ComponentId>,
379        enabled: bool,
380    },
381    SelectionSet {
382        component_ids: Vec<ComponentId>,
383        entries: Vec<SelectionEntry>,
384        primary: Option<ComponentId>,
385    },
386
387    Attach {
388        parents: Vec<ComponentId>,
389        child: ComponentId,
390    },
391    QueryFindComponent {
392        root: ComponentId,
393        selector: String,
394        reply: Sender<Option<ComponentId>>,
395    },
396    QueryFindAllComponents {
397        root: ComponentId,
398        selector: String,
399        reply: Sender<Vec<ComponentId>>,
400    },
401    AttachClone {
402        parents: Vec<ComponentId>,
403        prefab_root: ComponentId,
404    },
405    Detach {
406        component_ids: Vec<ComponentId>,
407    },
408    RemoveChild {
409        parents: Vec<ComponentId>,
410        index: usize,
411    },
412    RemoveChildren {
413        parents: Vec<ComponentId>,
414    },
415    RemoveSubtree {
416        component_ids: Vec<ComponentId>,
417    },
418
419    AudioGraphRebuild {
420        component_ids: Vec<ComponentId>,
421    },
422    RequestRaycast {
423        component_ids: Vec<ComponentId>,
424    },
425
426    AudioLowPassSetCutoffHz {
427        component_ids: Vec<ComponentId>,
428        cutoff_hz: f32,
429    },
430    AudioBandPassSetCenterHz {
431        component_ids: Vec<ComponentId>,
432        center_hz: f32,
433    },
434    OscillatorSetEnabled {
435        component_ids: Vec<ComponentId>,
436        enabled: bool,
437    },
438    OscillatorSetPitch {
439        component_ids: Vec<ComponentId>,
440        frequency_hz: f32,
441    },
442
443    /// Schedule a pitch set at beat = beat_context + beat_offset.
444    OscillatorScheduleSetPitch {
445        component_ids: Vec<ComponentId>,
446        beat_offset: f64,
447        beat_context: Option<f64>,
448        frequency_hz: f32,
449    },
450
451    /// Unified play/trigger intent for any `AudioSource` (oscillator or clip).
452    /// Fires at beat = beat_context + beat_offset.
453    ///
454    /// `note` carries pitch/velocity/duration semantics when meaningful.
455    /// `gain` / `rate` / `duration` are generic playback overrides:
456    /// - oscillator: `rate` ignored, `gain` overrides note velocity, `duration` overrides note.duration
457    /// - clip: `rate` sets playback rate, `gain` overrides note velocity, `duration` overrides note.duration
458    /// See docs/spec/audio-sources.md §3 and §4.
459    AudioSchedulePlay {
460        component_ids: Vec<ComponentId>,
461        beat_offset: f64,
462        beat_context: Option<f64>,
463        note: Option<crate::engine::ecs::component::MusicNote>,
464        gain: Option<f32>,
465        rate: Option<f32>,
466        duration: Option<f64>,
467    },
468
469    RegisterRenderable {
470        component_ids: Vec<ComponentId>,
471    },
472    RemoveRenderable {
473        component_ids: Vec<ComponentId>,
474    },
475    RegisterStencilClip {
476        component_ids: Vec<ComponentId>,
477    },
478    UnregisterStencilClip {
479        component_ids: Vec<ComponentId>,
480    },
481
482    PoseCapture {
483        target: ComponentId,
484        pose_name: Option<String>,
485    },
486    PoseApply {
487        target: ComponentId,
488        pose: ComponentId,
489    },
490
491    RegisterRouter {
492        component_ids: Vec<ComponentId>,
493    },
494    RegisterHttpServer {
495        component_ids: Vec<ComponentId>,
496    },
497    RegisterHttpClient {
498        component_ids: Vec<ComponentId>,
499    },
500    HttpClientRequest {
501        component_id: ComponentId,
502        method: String,
503        url: String,
504        headers: HttpHeaders,
505        body_text: Option<String>,
506    },
507    HttpServerReply {
508        component_id: ComponentId,
509        request_id: u64,
510        status: u16,
511        headers: HttpHeaders,
512        body_text: String,
513    },
514    RegisterScrolling {
515        component_ids: Vec<ComponentId>,
516    },
517    RegisterTransform {
518        component_ids: Vec<ComponentId>,
519    },
520    /// Recompute transform-derived caches (world matrices, skinning, BVH) without modifying the transform value.
521    ///
522    /// Intended for topology changes (e.g. Attach/Detach) where world matrices need recomputation.
523    UpdateTransformWorld {
524        component_ids: Vec<ComponentId>,
525    },
526    UpdateTransform {
527        component_ids: Vec<ComponentId>,
528        translation: [f32; 3],
529        rotation_quat_xyzw: [f32; 4],
530        scale: [f32; 3],
531    },
532    RemoveTransform {
533        component_ids: Vec<ComponentId>,
534    },
535
536    RegisterCamera3d {
537        component_ids: Vec<ComponentId>,
538    },
539    RegisterCamera2d {
540        component_ids: Vec<ComponentId>,
541    },
542    MakeActiveCamera {
543        component_ids: Vec<ComponentId>,
544    },
545
546    RegisterInput {
547        component_ids: Vec<ComponentId>,
548    },
549    RegisterUv {
550        component_ids: Vec<ComponentId>,
551    },
552
553    RegisterLight {
554        component_ids: Vec<ComponentId>,
555    },
556    RegisterColor {
557        component_ids: Vec<ComponentId>,
558    },
559    RegisterOpacity {
560        component_ids: Vec<ComponentId>,
561    },
562    RegisterTransparentCutout {
563        component_ids: Vec<ComponentId>,
564    },
565    RegisterBackgroundColor {
566        component_ids: Vec<ComponentId>,
567    },
568    RegisterRendererSettings {
569        component_ids: Vec<ComponentId>,
570    },
571    RegisterRenderGraph {
572        component_ids: Vec<ComponentId>,
573    },
574    RegisterAmbientLight {
575        component_ids: Vec<ComponentId>,
576    },
577    RegisterEmissive {
578        component_ids: Vec<ComponentId>,
579    },
580    RegisterLightQuantization {
581        component_ids: Vec<ComponentId>,
582    },
583
584    RegisterTexture {
585        component_ids: Vec<ComponentId>,
586    },
587    RegisterTextureFiltering {
588        component_ids: Vec<ComponentId>,
589    },
590
591    RegisterText {
592        component_ids: Vec<ComponentId>,
593    },
594    RegisterGLTF {
595        component_ids: Vec<ComponentId>,
596    },
597    RegisterTextInput {
598        component_ids: Vec<ComponentId>,
599    },
600
601    TextInputSetFocus {
602        component_id: ComponentId,
603    },
604    TextInputClearFocus,
605    TextInputInsertText {
606        text: String,
607    },
608    TextInputBackspace,
609    TextInputDeleteForward,
610    TextInputMoveCaret {
611        direction: TextInputCaretDirection,
612        amount: usize,
613    },
614    TextInputMoveCaretTo {
615        index: usize,
616    },
617
618    RegisterCollision {
619        component_ids: Vec<ComponentId>,
620    },
621    RemoveCollision {
622        component_ids: Vec<ComponentId>,
623    },
624    RegisterKineticResponse {
625        component_ids: Vec<ComponentId>,
626    },
627    RemoveKineticResponse {
628        component_ids: Vec<ComponentId>,
629    },
630    RegisterAvatarControl {
631        component_ids: Vec<ComponentId>,
632    },
633    RegisterAvatarBodyYaw {
634        component_ids: Vec<ComponentId>,
635    },
636    RegisterIkChain {
637        component_ids: Vec<ComponentId>,
638    },
639
640    RegisterXr {
641        component_ids: Vec<ComponentId>,
642    },
643    RegisterInputXr {
644        component_ids: Vec<ComponentId>,
645    },
646    RegisterControllerXr {
647        component_ids: Vec<ComponentId>,
648    },
649    RegisterInputXrGamepad {
650        component_ids: Vec<ComponentId>,
651    },
652    RemoveInputXr {
653        component_ids: Vec<ComponentId>,
654    },
655    RemoveControllerXr {
656        component_ids: Vec<ComponentId>,
657    },
658    RemoveInputXrGamepad {
659        component_ids: Vec<ComponentId>,
660    },
661
662    RegisterRaycast {
663        component_ids: Vec<ComponentId>,
664    },
665    RegisterRaycastable {
666        component_ids: Vec<ComponentId>,
667    },
668    RegisterPointer {
669        component_ids: Vec<ComponentId>,
670    },
671    RemoveRaycast {
672        component_ids: Vec<ComponentId>,
673    },
674    RemoveRaycastable {
675        component_ids: Vec<ComponentId>,
676    },
677
678    RegisterAnimation {
679        component_ids: Vec<ComponentId>,
680    },
681    SetAnimationState {
682        component_ids: Vec<ComponentId>,
683        state: AnimationState,
684    },
685    RegisterKeyframe {
686        component_ids: Vec<ComponentId>,
687    },
688
689    RegisterAudioOutput {
690        component_ids: Vec<ComponentId>,
691    },
692    AudioGraphDirtyImmediate {
693        component_ids: Vec<ComponentId>,
694    },
695    RegisterAudioOscillator {
696        component_ids: Vec<ComponentId>,
697    },
698    RegisterAudioClip {
699        component_ids: Vec<ComponentId>,
700    },
701    RegisterAudioBufferSize {
702        component_ids: Vec<ComponentId>,
703    },
704    RegisterClock {
705        component_ids: Vec<ComponentId>,
706    },
707    RegisterTransformGizmo {
708        component_ids: Vec<ComponentId>,
709    },
710    RegisterNormalVis {
711        component_ids: Vec<ComponentId>,
712    },
713
714    RegisterEditor {
715        component_ids: Vec<ComponentId>,
716    },
717
718    RegisterAction {
719        component_ids: Vec<ComponentId>,
720    },
721
722    /// Register/unregister routing operators.
723    ///
724    /// These are internal mutation-style intents executed by the pipeline system.
725    RegisterSignalRouteUpward {
726        component_ids: Vec<ComponentId>,
727    },
728    RemoveSignalRouteUpward {
729        component_ids: Vec<ComponentId>,
730    },
731
732    ScheduleAudioOp {
733        component_ids: Vec<ComponentId>,
734        beat: f64,
735        op: crate::engine::ecs::system::audio_system::AudioOp,
736    },
737    ScheduleAudioGraphSwap {
738        component_ids: Vec<ComponentId>,
739        beat: f64,
740    },
741    ScheduleAudioPitchSetHz {
742        component_ids: Vec<ComponentId>,
743        beat: f64,
744        frequency_hz: f32,
745    },
746    ScheduleAudioOscillatorEnabled {
747        component_ids: Vec<ComponentId>,
748        beat: f64,
749        enabled: bool,
750    },
751    ScheduleAudioGainSet {
752        component_ids: Vec<ComponentId>,
753        beat: f64,
754        gain: f32,
755    },
756}
757
758impl IntentValue {
759    /// Stable, human-readable kind name for routing/filtering.
760    ///
761    /// Convention: snake_case.
762    pub fn kind_name(&self) -> &'static str {
763        match self {
764            IntentValue::Noop => "noop",
765            IntentValue::RetryXrRuntime => "retry_xr_runtime",
766            IntentValue::SpawnComponentTree { .. } => "spawn_component_tree",
767            IntentValue::Print { .. } => "print",
768            IntentValue::ReplExec { .. } => "repl_exec",
769
770            IntentValue::SetColor { .. } => "set_color",
771            IntentValue::SetText { .. } => "set_text",
772            IntentValue::SetEmissiveIntensity { .. } => "set_emissive_intensity",
773            IntentValue::SetPosition { .. } => "set_position",
774            IntentValue::LookAt { .. } => "look_at",
775            IntentValue::SetLayoutAvailableWidth { .. } => "set_layout_available_width",
776            IntentValue::SetLayoutAvailableHeight { .. } => "set_layout_available_height",
777            IntentValue::SetLayoutInspect { .. } => "set_layout_inspect",
778            IntentValue::GLTFArmatureVisible { .. } => "gltf_armature_visible",
779            IntentValue::SelectionSet { .. } => "selection_set",
780
781            IntentValue::Attach { .. } => "attach",
782            IntentValue::QueryFindComponent { .. } => "query_find_component",
783            IntentValue::QueryFindAllComponents { .. } => "query_find_all_components",
784            IntentValue::AttachClone { .. } => "attach_clone",
785            IntentValue::Detach { .. } => "detach",
786            IntentValue::RemoveChild { .. } => "remove_child",
787            IntentValue::RemoveChildren { .. } => "remove_children",
788            IntentValue::RemoveSubtree { .. } => "remove_subtree",
789
790            IntentValue::AudioGraphRebuild { .. } => "audio_graph_rebuild",
791            IntentValue::RequestRaycast { .. } => "request_raycast",
792
793            IntentValue::AudioLowPassSetCutoffHz { .. } => "audio_low_pass_set_cutoff_hz",
794            IntentValue::AudioBandPassSetCenterHz { .. } => "audio_band_pass_set_center_hz",
795            IntentValue::OscillatorSetEnabled { .. } => "oscillator_set_enabled",
796            IntentValue::OscillatorSetPitch { .. } => "oscillator_set_pitch",
797            IntentValue::OscillatorScheduleSetPitch { .. } => "oscillator_schedule_set_pitch",
798            IntentValue::AudioSchedulePlay { .. } => "audio_schedule_play",
799
800            IntentValue::RegisterRenderable { .. } => "register_renderable",
801            IntentValue::RemoveRenderable { .. } => "remove_renderable",
802            IntentValue::RegisterStencilClip { .. } => "register_stencil_clip",
803            IntentValue::UnregisterStencilClip { .. } => "unregister_stencil_clip",
804            IntentValue::RegisterRouter { .. } => "register_router",
805            IntentValue::RegisterHttpServer { .. } => "register_http_server",
806            IntentValue::RegisterHttpClient { .. } => "register_http_client",
807            IntentValue::HttpClientRequest { .. } => "http_client_request",
808            IntentValue::HttpServerReply { .. } => "http_server_reply",
809            IntentValue::RegisterScrolling { .. } => "register_scrolling",
810            IntentValue::RegisterTransform { .. } => "register_transform",
811            IntentValue::UpdateTransformWorld { .. } => "update_transform_world",
812            IntentValue::UpdateTransform { .. } => "update_transform",
813            IntentValue::RemoveTransform { .. } => "remove_transform",
814
815            IntentValue::RegisterCamera3d { .. } => "register_camera3d",
816            IntentValue::RegisterCamera2d { .. } => "register_camera2d",
817            IntentValue::MakeActiveCamera { .. } => "make_active_camera",
818
819            IntentValue::RegisterInput { .. } => "register_input",
820            IntentValue::RegisterUv { .. } => "register_uv",
821
822            IntentValue::RegisterLight { .. } => "register_light",
823            IntentValue::RegisterColor { .. } => "register_color",
824            IntentValue::RegisterOpacity { .. } => "register_opacity",
825            IntentValue::RegisterTransparentCutout { .. } => "register_transparent_cutout",
826            IntentValue::RegisterBackgroundColor { .. } => "register_background_color",
827            IntentValue::RegisterRendererSettings { .. } => "register_renderer_settings",
828            IntentValue::RegisterRenderGraph { .. } => "register_render_graph",
829            IntentValue::RegisterAmbientLight { .. } => "register_ambient_light",
830            IntentValue::RegisterEmissive { .. } => "register_emissive",
831            IntentValue::RegisterLightQuantization { .. } => "register_light_quantization",
832
833            IntentValue::RegisterTexture { .. } => "register_texture",
834            IntentValue::RegisterTextureFiltering { .. } => "register_texture_filtering",
835
836            IntentValue::RegisterText { .. } => "register_text",
837            IntentValue::RegisterGLTF { .. } => "register_gltf",
838            IntentValue::RegisterTextInput { .. } => "register_text_input",
839            IntentValue::TextInputSetFocus { .. } => "text_input_set_focus",
840            IntentValue::TextInputClearFocus => "text_input_clear_focus",
841            IntentValue::TextInputInsertText { .. } => "text_input_insert_text",
842            IntentValue::TextInputBackspace => "text_input_backspace",
843            IntentValue::TextInputDeleteForward => "text_input_delete_forward",
844            IntentValue::TextInputMoveCaret { .. } => "text_input_move_caret",
845            IntentValue::TextInputMoveCaretTo { .. } => "text_input_move_caret_to",
846
847            IntentValue::RegisterCollision { .. } => "register_collision",
848            IntentValue::RemoveCollision { .. } => "remove_collision",
849            IntentValue::RegisterKineticResponse { .. } => "register_kinetic_response",
850            IntentValue::RemoveKineticResponse { .. } => "remove_kinetic_response",
851            IntentValue::RegisterAvatarControl { .. } => "register_avatar_control",
852            IntentValue::RegisterAvatarBodyYaw { .. } => "register_avatar_body_yaw",
853            IntentValue::RegisterIkChain { .. } => "register_ik_chain",
854
855            IntentValue::RegisterXr { .. } => "register_xr",
856            IntentValue::RegisterInputXr { .. } => "register_input_xr",
857            IntentValue::RegisterControllerXr { .. } => "register_controller_xr",
858            IntentValue::RegisterInputXrGamepad { .. } => "register_input_xr_gamepad",
859            IntentValue::RemoveInputXr { .. } => "remove_input_xr",
860            IntentValue::RemoveControllerXr { .. } => "remove_controller_xr",
861            IntentValue::RemoveInputXrGamepad { .. } => "remove_input_xr_gamepad",
862
863            IntentValue::RegisterRaycast { .. } => "register_raycast",
864            IntentValue::RegisterRaycastable { .. } => "register_raycastable",
865            IntentValue::RegisterPointer { .. } => "register_pointer",
866            IntentValue::RemoveRaycast { .. } => "remove_raycast",
867            IntentValue::RemoveRaycastable { .. } => "remove_raycastable",
868
869            IntentValue::RegisterAnimation { .. } => "register_animation",
870            IntentValue::SetAnimationState { .. } => "set_animation_state",
871            IntentValue::RegisterKeyframe { .. } => "register_keyframe",
872
873            IntentValue::RegisterAudioOutput { .. } => "register_audio_output",
874            IntentValue::AudioGraphDirtyImmediate { .. } => "audio_graph_dirty_immediate",
875            IntentValue::RegisterAudioOscillator { .. } => "register_audio_oscillator",
876            IntentValue::RegisterAudioClip { .. } => "register_audio_clip",
877            IntentValue::RegisterAudioBufferSize { .. } => "register_audio_buffer_size",
878            IntentValue::RegisterClock { .. } => "register_clock",
879            IntentValue::RegisterTransformGizmo { .. } => "register_transform_gizmo",
880            IntentValue::RegisterNormalVis { .. } => "register_normal_vis",
881            IntentValue::RegisterEditor { .. } => "register_editor",
882            IntentValue::RegisterAction { .. } => "register_action",
883
884            IntentValue::PoseCapture { .. } => "pose_capture",
885            IntentValue::PoseApply { .. } => "pose_apply",
886
887            IntentValue::RegisterSignalRouteUpward { .. } => "register_signal_route_upward",
888            IntentValue::RemoveSignalRouteUpward { .. } => "remove_signal_route_upward",
889
890            IntentValue::ScheduleAudioOp { .. } => "schedule_audio_op",
891            IntentValue::ScheduleAudioGraphSwap { .. } => "schedule_audio_graph_swap",
892            IntentValue::ScheduleAudioPitchSetHz { .. } => "schedule_audio_pitch_set_hz",
893            IntentValue::ScheduleAudioOscillatorEnabled { .. } => {
894                "schedule_audio_oscillator_enabled"
895            }
896            IntentValue::ScheduleAudioGainSet { .. } => "schedule_audio_gain_set",
897        }
898    }
899}
900
901/// Event kinds used for handler routing.
902///
903/// Note: `SignalKind::Action` intentionally does not exist.
904#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
905pub enum SignalKind {
906    Any,
907    ParentChanged,
908    RayIntersected,
909    CollisionStarted,
910    CollisionEnded,
911    DragStart,
912    DragMove,
913    DragEnd,
914    Click,
915    SelectionChanged,
916    SelectionAdded,
917    SelectionRemoved,
918    SelectionCleared,
919    Scrolling,
920    TextInputFocusChanged,
921    TextInputChanged,
922    LayoutRootSizeAvailable,
923
924    /// A named data event for cross-subtree communication.
925    ///
926    /// Handlers must filter by name inside the closure body since the kind is
927    /// a unit variant (no payload).
928    DataEvent,
929    XrButtonDown,
930    XrButtonUp,
931    XrButtonChanged,
932    XrAxisChanged,
933    HttpRequest,
934    HttpResponse,
935    HttpError,
936}
937
938/// Optional timing metadata on the signal envelope.
939///
940/// Semantics:
941/// - `Now`: signal is eligible for execution/dispatch immediately at the next drain point.
942/// - `AtBeat(b)`: signal is held in a pending queue until the transport beat is >= `b`.
943#[derive(Debug, Clone, Copy, PartialEq)]
944pub enum SignalWhen {
945    Now,
946    AtBeat(f64),
947}
948
949impl Default for SignalWhen {
950    fn default() -> Self {
951        Self::Now
952    }
953}
954
955impl SignalWhen {
956    pub fn at_beat(beat: f64) -> Self {
957        Self::AtBeat(beat)
958    }
959
960    pub fn beat(&self) -> Option<f64> {
961        match *self {
962            Self::Now => None,
963            Self::AtBeat(b) => Some(b),
964        }
965    }
966}
967
968pub trait SignalEmitter {
969    fn push_event(&mut self, scope: ComponentId, event: EventSignal);
970    fn push_intent(&mut self, scope: ComponentId, intent: IntentSignal);
971
972    fn push_intent_now(&mut self, scope: ComponentId, value: IntentValue) {
973        self.push_intent(scope, IntentSignal::now(value));
974    }
975
976    fn push_intent_at_beat(&mut self, scope: ComponentId, beat: f64, value: IntentValue) {
977        self.push_intent(scope, IntentSignal::at_beat(beat, value));
978    }
979}
980
981pub type SignalHandler = fn(&mut World, &mut dyn SignalEmitter, &Signal);