Skip to main content

mittens_engine/engine/ecs/rx/
mutation_executor.rs

1use crate::engine::ecs::component::{
2    AnimationState, RenderableComponent, TextComponent, TransformComponent,
3};
4use crate::engine::ecs::system::SystemWorld;
5use crate::engine::ecs::{ComponentId, EventSignal, IntentValue, Signal, SignalEmitter, World};
6use crate::engine::graphics::VisualWorld;
7
8/// Built-in executor for low-level engine mutations expressed as intents.
9///
10/// This is the “mutation executor” layer: it applies canonical engine side effects
11/// like register/remove/update and other internal operations.
12#[derive(Debug, Default)]
13pub struct RxMutationExecutor;
14
15impl RxMutationExecutor {
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    pub fn execute(
21        &mut self,
22        systems: &mut SystemWorld,
23        world: &mut World,
24        visuals: &mut VisualWorld,
25        render_assets: &mut crate::engine::graphics::RenderAssets,
26        emit: &mut dyn SignalEmitter,
27        env: &Signal,
28    ) {
29        use crate::engine::ecs::system::audio_system::AudioOp;
30        use crate::engine::graphics::primitives::Transform;
31
32        fn collect_text_targets(world: &World, target: ComponentId, out: &mut Vec<ComponentId>) {
33            if world
34                .get_component_by_id_as::<TextComponent>(target)
35                .is_some()
36            {
37                out.push(target);
38                return;
39            }
40
41            let mut stack = vec![target];
42            while let Some(node) = stack.pop() {
43                for &ch in world.children_of(node) {
44                    stack.push(ch);
45                }
46
47                if world
48                    .get_component_by_id_as::<TextComponent>(node)
49                    .is_some()
50                {
51                    out.push(node);
52                }
53            }
54        }
55
56        fn apply_set_text_to_component(
57            systems: &mut SystemWorld,
58            world: &mut World,
59            visuals: &mut VisualWorld,
60            emit: &mut dyn SignalEmitter,
61            component: ComponentId,
62            text: &String,
63        ) {
64            // Update text payload and force a rebuild.
65            if let Some(tc) = world.get_component_by_id_as_mut::<TextComponent>(component) {
66                tc.text = text.clone();
67                tc.mark_unbuilt();
68
69                // Best-effort: delete glyph transform children (keep style components).
70                let children: Vec<ComponentId> = world.children_of(component).to_vec();
71                for ch in children {
72                    if world
73                        .get_component_by_id_as::<TransformComponent>(ch)
74                        .is_none()
75                    {
76                        continue;
77                    }
78
79                    let has_renderable_child = world.children_of(ch).iter().any(|&gch| {
80                        world
81                            .get_component_by_id_as::<RenderableComponent>(gch)
82                            .is_some()
83                    });
84                    if has_renderable_child {
85                        // This is very likely a glyph root.
86                        systems.remove_subtree_immediate(world, visuals, ch);
87                    }
88                }
89            }
90
91            systems.register_text(world, visuals, component, emit);
92        }
93
94        let Some(intent) = env.intent.as_ref() else {
95            return;
96        };
97
98        match &intent.value {
99            IntentValue::RegisterRenderable { component_ids } => {
100                for &component in component_ids.iter() {
101                    systems.register_renderable(world, visuals, component);
102                }
103            }
104            IntentValue::RemoveRenderable { component_ids } => {
105                for &component in component_ids.iter() {
106                    systems.remove_renderable(world, visuals, component);
107                }
108            }
109            IntentValue::RegisterStencilClip { component_ids } => {
110                for &component in component_ids.iter() {
111                    systems.register_stencil_clip(world, visuals, component);
112                }
113            }
114            IntentValue::UnregisterStencilClip { component_ids } => {
115                for &component in component_ids.iter() {
116                    systems.unregister_stencil_clip(world, visuals, component);
117                }
118            }
119            IntentValue::RegisterRouter { component_ids } => {
120                for &component in component_ids.iter() {
121                    systems.register_router(world, emit, component);
122                }
123            }
124            IntentValue::RegisterHttpServer { component_ids } => {
125                for &component in component_ids.iter() {
126                    systems.register_http_server(world, emit, component);
127                }
128            }
129            IntentValue::RegisterHttpClient { component_ids } => {
130                for &component in component_ids.iter() {
131                    systems.register_http_client(world, emit, component);
132                }
133            }
134            IntentValue::HttpClientRequest {
135                component_id,
136                method,
137                url,
138                headers,
139                body_text,
140            } => {
141                systems.http_client.issue_request(
142                    *component_id,
143                    method.clone(),
144                    url.clone(),
145                    headers.clone(),
146                    body_text.clone(),
147                );
148            }
149            IntentValue::HttpServerReply {
150                component_id,
151                request_id,
152                status,
153                headers,
154                body_text,
155            } => {
156                systems.http_server.deliver_reply(
157                    *component_id,
158                    *request_id,
159                    *status,
160                    headers.clone(),
161                    body_text.clone(),
162                );
163            }
164            IntentValue::RegisterScrolling { component_ids } => {
165                for &component in component_ids.iter() {
166                    systems.register_scrolling(world, emit, component);
167                }
168            }
169
170            IntentValue::RegisterTransform { component_ids } => {
171                for &component in component_ids.iter() {
172                    systems.transform_changed(world, visuals, component);
173                }
174            }
175            IntentValue::UpdateTransformWorld { component_ids } => {
176                for &component in component_ids.iter() {
177                    systems.transform_changed(world, visuals, component);
178                }
179            }
180            IntentValue::UpdateTransform {
181                component_ids,
182                translation,
183                rotation_quat_xyzw,
184                scale,
185            } => {
186                let mut t = Transform::default();
187                t.translation = *translation;
188                t.rotation = *rotation_quat_xyzw;
189                t.scale = *scale;
190                t.recompute_model();
191
192                for &component in component_ids.iter() {
193                    systems.update_transform(world, visuals, component, t);
194                }
195            }
196            IntentValue::RemoveTransform { component_ids } => {
197                for &component in component_ids.iter() {
198                    systems.remove_transform(world, visuals, component);
199                }
200            }
201
202            IntentValue::RegisterCamera3d { component_ids } => {
203                for &component in component_ids.iter() {
204                    systems.register_camera(world, visuals, component);
205                }
206            }
207            IntentValue::RegisterCamera2d { component_ids } => {
208                for &component in component_ids.iter() {
209                    systems.register_camera2d(world, visuals, component);
210                }
211            }
212            IntentValue::MakeActiveCamera { component_ids } => {
213                for &component in component_ids.iter() {
214                    systems.make_active_camera(world, visuals, component);
215                }
216            }
217
218            IntentValue::RegisterInput { component_ids } => {
219                for &component in component_ids.iter() {
220                    systems.register_input(component);
221                }
222            }
223            IntentValue::RegisterUv { component_ids } => {
224                for &component in component_ids.iter() {
225                    systems.register_uv(world, visuals, component);
226                }
227            }
228
229            IntentValue::RegisterLight { component_ids } => {
230                for &component in component_ids.iter() {
231                    systems.register_light(world, visuals, component);
232                }
233            }
234            IntentValue::RegisterColor { component_ids } => {
235                for &component in component_ids.iter() {
236                    systems.register_color(world, visuals, component);
237                }
238            }
239            IntentValue::RegisterOpacity { component_ids } => {
240                for &component in component_ids.iter() {
241                    systems.register_opacity(world, visuals, component);
242                }
243            }
244            IntentValue::RegisterTransparentCutout { component_ids } => {
245                for &component in component_ids.iter() {
246                    systems.register_transparent_cutout(world, visuals, component);
247                }
248            }
249            IntentValue::RegisterBackgroundColor { component_ids } => {
250                for &component in component_ids.iter() {
251                    systems.register_background_color(world, visuals, component);
252                }
253            }
254            IntentValue::RegisterRendererSettings { component_ids } => {
255                for &component in component_ids.iter() {
256                    systems.register_renderer_settings(world, visuals, component);
257                }
258            }
259            IntentValue::RegisterRenderGraph { component_ids } => {
260                for &component in component_ids.iter() {
261                    systems.register_render_graph(world, visuals, component);
262                }
263            }
264            IntentValue::RegisterAmbientLight { component_ids } => {
265                for &component in component_ids.iter() {
266                    systems.register_ambient_light(world, visuals, component);
267                }
268            }
269            IntentValue::RegisterEmissive { component_ids } => {
270                for &component in component_ids.iter() {
271                    systems.register_emissive(world, visuals, component);
272                }
273            }
274            IntentValue::RegisterLightQuantization { component_ids } => {
275                for &component in component_ids.iter() {
276                    systems.register_light_quantization(world, visuals, component);
277                }
278            }
279
280            IntentValue::RegisterTexture { component_ids } => {
281                for &component in component_ids.iter() {
282                    systems.register_texture(world, visuals, component);
283                }
284            }
285            IntentValue::RegisterTextureFiltering { component_ids } => {
286                for &component in component_ids.iter() {
287                    systems.register_texture_filtering(world, visuals, component);
288                }
289            }
290
291            IntentValue::RegisterText { component_ids } => {
292                for &component in component_ids.iter() {
293                    systems.register_text(world, visuals, component, emit);
294                }
295            }
296            IntentValue::RegisterGLTF { component_ids } => {
297                for &component in component_ids.iter() {
298                    systems.gltf.register_component(component);
299                }
300            }
301            IntentValue::RegisterTextInput { component_ids } => {
302                for &component in component_ids.iter() {
303                    systems.register_text_input(world, component, emit);
304                }
305            }
306            IntentValue::SetText {
307                component_ids,
308                text,
309            } => {
310                let mut text_cids = Vec::new();
311                for &t in component_ids.iter() {
312                    collect_text_targets(world, t, &mut text_cids);
313                }
314                text_cids.sort();
315                text_cids.dedup();
316
317                for text_cid in text_cids {
318                    apply_set_text_to_component(systems, world, visuals, emit, text_cid, text);
319                }
320            }
321
322            IntentValue::SetEmissiveIntensity {
323                component_ids,
324                intensity,
325            } => {
326                let intensity = (*intensity).max(0.0);
327                for &cid in component_ids.iter() {
328                    systems.update_emissive_intensity(world, visuals, cid, intensity);
329                }
330            }
331
332            IntentValue::SetLayoutAvailableWidth {
333                component_ids,
334                width,
335            } => {
336                use crate::engine::ecs::component::LayoutComponent;
337                let width = *width;
338                for &cid in component_ids.iter() {
339                    if let Some(lo) = world.get_component_by_id_as_mut::<LayoutComponent>(cid) {
340                        lo.set_available_width_dimension(width);
341                    }
342                }
343            }
344
345            IntentValue::SetLayoutAvailableHeight {
346                component_ids,
347                height,
348            } => {
349                use crate::engine::ecs::component::LayoutComponent;
350                let height = *height;
351                for &cid in component_ids.iter() {
352                    if let Some(lo) = world.get_component_by_id_as_mut::<LayoutComponent>(cid) {
353                        lo.set_available_height_dimension(height);
354                    }
355                }
356            }
357
358            IntentValue::SetLayoutInspect {
359                component_ids,
360                enabled,
361            } => {
362                use crate::engine::ecs::component::LayoutComponent;
363                let enabled = *enabled;
364                for &cid in component_ids.iter() {
365                    if let Some(lo) = world.get_component_by_id_as_mut::<LayoutComponent>(cid) {
366                        if lo.inspect != enabled {
367                            lo.inspect = enabled;
368                            lo.dirty = true;
369                        }
370                    }
371                }
372            }
373
374            IntentValue::TextInputSetFocus { .. }
375            | IntentValue::TextInputClearFocus
376            | IntentValue::TextInputInsertText { .. }
377            | IntentValue::TextInputBackspace
378            | IntentValue::TextInputDeleteForward
379            | IntentValue::TextInputMoveCaret { .. }
380            | IntentValue::TextInputMoveCaretTo { .. } => {
381                systems.text_input.execute_intent(world, emit, env);
382            }
383
384            IntentValue::RegisterCollision { component_ids } => {
385                for &component in component_ids.iter() {
386                    systems.register_collision(world, visuals, component);
387                }
388            }
389            IntentValue::RemoveCollision { component_ids } => {
390                for &component in component_ids.iter() {
391                    systems.remove_collision(world, visuals, component);
392                }
393            }
394            IntentValue::RegisterKineticResponse { component_ids } => {
395                for &component in component_ids.iter() {
396                    systems.register_kinetic_response(world, visuals, component);
397                }
398            }
399            IntentValue::RemoveKineticResponse { component_ids } => {
400                for &component in component_ids.iter() {
401                    systems.remove_kinetic_response(world, visuals, component);
402                }
403            }
404            IntentValue::RegisterAvatarControl { component_ids } => {
405                for &component in component_ids {
406                    systems.register_avatar_control(component);
407                }
408            }
409            IntentValue::RegisterAvatarBodyYaw { component_ids } => {
410                for &component in component_ids {
411                    systems.avatar_body_yaw.register(component);
412                }
413            }
414            IntentValue::RegisterIkChain { component_ids } => {
415                for &component in component_ids {
416                    systems.ik.register(component);
417                }
418            }
419
420            IntentValue::RemoveSubtree { component_ids } => {
421                let mut roots: Vec<ComponentId> = component_ids.iter().copied().collect();
422                roots.sort();
423                roots.dedup();
424                for root in roots {
425                    emit.push_intent_now(
426                        root,
427                        IntentValue::AudioGraphDirtyImmediate {
428                            component_ids: vec![root],
429                        },
430                    );
431                    // Best-effort: if the root is still attached, detach it first and publish
432                    // a topology fact before deletion.
433                    if let Some(old_parent) = world.parent_of(root) {
434                        world.detach_from_parent(root);
435                        emit.push_event(
436                            root,
437                            EventSignal::ParentChanged {
438                                child: root,
439                                old_parent: Some(old_parent),
440                                new_parent: None,
441                            },
442                        );
443                    }
444                    systems.remove_subtree_immediate(world, visuals, root);
445                }
446            }
447
448            IntentValue::RegisterXr { component_ids } => {
449                for &component in component_ids.iter() {
450                    systems.register_xr(world, visuals, component);
451                }
452            }
453            IntentValue::RegisterInputXr { component_ids } => {
454                for &component in component_ids.iter() {
455                    systems.register_input_xr(world, visuals, component);
456                }
457            }
458            IntentValue::RegisterControllerXr { component_ids } => {
459                for &component in component_ids.iter() {
460                    systems.register_controller_xr(world, visuals, component);
461                }
462            }
463            IntentValue::RegisterInputXrGamepad { component_ids } => {
464                for &component in component_ids.iter() {
465                    systems.register_input_xr_gamepad(world, visuals, component);
466                }
467            }
468            IntentValue::RemoveInputXr { component_ids } => {
469                for &component in component_ids.iter() {
470                    systems.remove_input_xr(world, visuals, component);
471                }
472            }
473            IntentValue::RemoveControllerXr { component_ids } => {
474                for &component in component_ids.iter() {
475                    systems.remove_controller_xr(world, visuals, component);
476                }
477            }
478            IntentValue::RemoveInputXrGamepad { component_ids } => {
479                for &component in component_ids.iter() {
480                    systems.remove_input_xr_gamepad(world, visuals, component);
481                }
482            }
483
484            IntentValue::RegisterRaycast { component_ids } => {
485                for &component in component_ids.iter() {
486                    systems.register_raycast(world, visuals, component);
487                }
488            }
489            IntentValue::RegisterPointer { component_ids } => {
490                for &component in component_ids.iter() {
491                    systems.register_pointer(world, visuals, component, emit);
492                }
493            }
494            IntentValue::RemoveRaycast { component_ids } => {
495                for &component in component_ids.iter() {
496                    systems.remove_raycast(world, visuals, component);
497                }
498            }
499
500            IntentValue::RegisterAnimation { component_ids } => {
501                for &component in component_ids.iter() {
502                    systems.register_animation(world, visuals, component);
503                }
504            }
505            IntentValue::SetAnimationState {
506                component_ids,
507                state,
508            } => {
509                let state: AnimationState = state.clone();
510                for &component in component_ids.iter() {
511                    systems.set_animation_state(component, state.clone());
512                }
513            }
514            IntentValue::RegisterKeyframe { component_ids } => {
515                for &component in component_ids.iter() {
516                    systems.register_keyframe(world, visuals, component);
517                }
518            }
519
520            IntentValue::RegisterAudioOutput { component_ids } => {
521                for &component in component_ids.iter() {
522                    systems.register_audio_output(world, visuals, component);
523                }
524            }
525            IntentValue::AudioGraphDirtyImmediate { component_ids } => {
526                for &component in component_ids.iter() {
527                    systems.audio_graph_dirty(world, visuals, component);
528                }
529            }
530            IntentValue::RegisterAudioOscillator { component_ids } => {
531                for &component in component_ids.iter() {
532                    systems.register_audio_oscillator(world, visuals, component);
533                }
534            }
535            IntentValue::RegisterAudioClip { component_ids } => {
536                for &component in component_ids.iter() {
537                    systems.register_audio_clip(world, visuals, component);
538                }
539            }
540            IntentValue::RegisterAudioBufferSize { component_ids } => {
541                for &component in component_ids.iter() {
542                    systems.register_audio_buffer_size(world, visuals, component);
543                }
544            }
545
546            IntentValue::RegisterClock { component_ids } => {
547                for &component in component_ids.iter() {
548                    systems.register_clock(world, visuals, component);
549                }
550            }
551
552            IntentValue::RegisterTransformGizmo { component_ids } => {
553                for &component in component_ids.iter() {
554                    systems.register_transform_gizmo(world, visuals, component, emit);
555                }
556            }
557
558            IntentValue::RegisterNormalVis { component_ids } => {
559                for &component in component_ids.iter() {
560                    systems.register_normal_vis(world, component);
561                }
562            }
563
564            IntentValue::ReplExec { command } => {
565                systems.queue_repl_command(command.clone());
566            }
567
568            IntentValue::RegisterEditor { component_ids } => {
569                for &component in component_ids.iter() {
570                    systems.register_editor(world, visuals, render_assets, component, emit);
571                }
572            }
573
574            IntentValue::RegisterAction { component_ids } => {
575                for &component in component_ids.iter() {
576                    crate::engine::ecs::system::action_system::register_action(
577                        world, emit, component,
578                    );
579                }
580            }
581
582            IntentValue::RegisterSignalRouteUpward { component_ids } => {
583                for &component in component_ids.iter() {
584                    systems.pipeline.register_signal_route_upward(
585                        world,
586                        &mut systems.rx,
587                        component,
588                    );
589                }
590            }
591            IntentValue::RemoveSignalRouteUpward { component_ids } => {
592                for &component in component_ids.iter() {
593                    systems
594                        .pipeline
595                        .remove_signal_route_upward(&mut systems.rx, component);
596                }
597            }
598
599            IntentValue::ScheduleAudioOp {
600                component_ids,
601                beat,
602                op,
603            } => {
604                for &component in component_ids.iter() {
605                    systems.audio.schedule_audio_op(component, *beat, *op);
606                }
607            }
608            IntentValue::ScheduleAudioGraphSwap {
609                component_ids,
610                beat,
611            } => {
612                for &component in component_ids.iter() {
613                    systems.audio.schedule_graph_swap(&*world, component, *beat);
614                }
615            }
616            IntentValue::ScheduleAudioPitchSetHz {
617                component_ids,
618                beat,
619                frequency_hz,
620            } => {
621                for &component in component_ids.iter() {
622                    systems.audio.schedule_audio_op(
623                        component,
624                        *beat,
625                        AudioOp::SetHz(*frequency_hz),
626                    );
627                }
628            }
629            IntentValue::ScheduleAudioOscillatorEnabled {
630                component_ids,
631                beat,
632                enabled,
633            } => {
634                for &component in component_ids.iter() {
635                    systems.audio.schedule_audio_op(
636                        component,
637                        *beat,
638                        AudioOp::SetEnabled(*enabled),
639                    );
640                }
641            }
642            IntentValue::ScheduleAudioGainSet {
643                component_ids,
644                beat,
645                gain,
646            } => {
647                for &component in component_ids.iter() {
648                    systems
649                        .audio
650                        .schedule_audio_op(component, *beat, AudioOp::SetGain(*gain));
651                }
652            }
653
654            IntentValue::PoseCapture { target, pose_name } => {
655                systems
656                    .pose_capture
657                    .handle_capture(world, emit, *target, pose_name.clone());
658            }
659            IntentValue::PoseApply { target, pose } => {
660                systems
661                    .pose_capture
662                    .handle_apply(world, emit, *target, *pose);
663            }
664
665            // Not executed by the mutation executor.
666            _ => {}
667        }
668    }
669}