Skip to main content

ClockComponent

Struct ClockComponent 

Source
pub struct ClockComponent {
    pub bpm: f64,
    /* private fields */
}
Expand description

Controls the global beat clock tempo (BPM).

Intended to be singleton-like: the most recently registered ClockComponent wins.

Fields§

§bpm: f64

Implementations§

Source§

impl ClockComponent

Source

pub fn new() -> Self

Examples found in repository?
examples/text-animation.rs (line 97)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    let world = engine::ecs::World::default();
11    let mut universe = engine::Universe::new(world);
12
13    // Input-driven camera rig.
14    // Topology: I { T { C3D } }
15    let input = universe
16        .world
17        .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
18    let camera3d = universe
19        .world
20        .add_component(engine::ecs::component::Camera3DComponent::new());
21    let rig_transform = universe.world.add_component(
22        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.5),
23    );
24    let input_mode = universe.world.add_component(
25        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
26    );
27    let _ = universe.attach(input, input_mode);
28    let _ = universe.attach(input, rig_transform);
29    let _ = universe.attach(rig_transform, camera3d);
30
31    // Small on-screen help.
32    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
33    universe.add(input);
34
35    // Light so we can see non-emissive materials.
36    let light_transform = universe.world.add_component(
37        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.0),
38    );
39    let light = universe.world.add_component(
40        engine::ecs::component::PointLightComponent::new()
41            .with_distance(25.0)
42            .with_color(1.0, 1.0, 1.0),
43    );
44    let _ = universe.attach(light_transform, light);
45    universe.add(light_transform);
46
47    use engine::ecs::component::{
48        ColorComponent, TextComponent, TextShadowComponent, TextureComponent,
49        TextureFilteringComponent, TransformComponent, TransparentCutoutComponent,
50    };
51
52    // Styled text root.
53    let text_root = universe.world.add_component(
54        TransformComponent::new()
55            .with_position(0.0, 0.0, 0.0)
56            .with_scale(0.25, 0.25, 1.0),
57    );
58
59    // Color must be an ancestor of the glyph renderables.
60    let color_id = universe
61        .world
62        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
63    let _ = universe.attach(text_root, color_id);
64
65    // This is the component we animate via SetText.
66    let text_id = universe.world.add_component(TextComponent::new(">w<"));
67    let _ = universe.attach(color_id, text_id);
68
69    // Route into cutout pass for cleaner edges.
70    let cutout = universe
71        .world
72        .add_component(TransparentCutoutComponent::new());
73    let _ = universe.attach(text_id, cutout);
74
75    // Font atlas.
76    let tex = universe.world.add_component(TextureComponent::with_uri(
77        "assets/textures/font_system.dds",
78    ));
79    let _ = universe.attach(text_id, tex);
80
81    let shadow = universe.world.add_component(
82        TextShadowComponent::new()
83            .with_scale(1.35)
84            .with_offset([0.06, -0.06, 0.0015]),
85    );
86    let filtering = universe
87        .world
88        .add_component(TextureFilteringComponent::nearest_magnification());
89
90    let _ = universe.attach(text_id, shadow);
91    let _ = universe.attach(text_id, filtering);
92
93    universe.add(text_root);
94
95    let clock_component = universe
96        .world
97        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
98    let _ = universe.add(clock_component);
99
100    // Looping animation: 4 beats long, 4 keyframes.
101    let anim = universe
102        .world
103        .add_component(engine::ecs::component::AnimationComponent::new());
104
105    let faces = [">w<", "^w^", "-_-", "o_o"];
106    for (i, &face) in faces.iter().enumerate() {
107        let kf = universe
108            .world
109            .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
110
111        // Each keyframe emits a SetText intent.
112        let action = universe
113            .world
114            .add_component(engine::ecs::component::ActionComponent::new(
115                engine::ecs::IntentValue::SetText {
116                    component_ids: vec![text_id],
117                    text: face.to_string(),
118                },
119            ));
120
121        let _ = universe.attach(anim, kf);
122        let _ = universe.attach(kf, action);
123    }
124
125    universe.add(anim);
126
127    // Process init-time registrations (Text expands into glyph subtrees here).
128    universe.systems.process_commands(
129        &mut universe.world,
130        &mut universe.visuals,
131        &mut universe.render_assets,
132        &mut universe.command_queue,
133    );
134
135    engine::Windowing::run_app(universe).expect("Windowing failed");
136}
More examples
Hide additional examples
examples/mesh-factory-example.rs (line 18)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    const LABEL_WRAP_AT: usize = 13;
11
12    let world = engine::ecs::World::default();
13    let mut universe = engine::Universe::new(world);
14
15    // add a clock
16    let clock = universe
17        .world
18        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(60.0));
19    universe.add(clock);
20
21    // Input-driven camera rig.
22    // Topology: I { T { C3D } }
23    let input = universe
24        .world
25        .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
26    let camera3d = universe
27        .world
28        .add_component(engine::ecs::component::Camera3DComponent::new());
29    let rig_transform = universe.world.add_component(
30        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 11.0),
31    );
32    let input_mode = universe.world.add_component(
33        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34    );
35    let _ = universe.attach(input, input_mode);
36    let _ = universe.attach(input, rig_transform);
37    let _ = universe.attach(rig_transform, camera3d);
38
39    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
40    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
41    universe.add(input);
42
43    // Light.
44    let light_transform = universe.world.add_component(
45        engine::ecs::component::TransformComponent::new().with_position(0.0, 2.0, 2.0),
46    );
47    let light = universe.world.add_component(
48        engine::ecs::component::PointLightComponent::new()
49            .with_distance(50.0)
50            .with_color(1.0, 1.0, 1.0),
51    );
52    let _ = universe.attach(light_transform, light);
53    universe.add(light_transform);
54
55    fn spawn_labeled_mesh(
56        universe: &mut engine::Universe,
57        x: f32,
58        y: f32,
59        label: &str,
60        mesh: engine::graphics::primitives::CpuMeshHandle,
61        scale: [f32; 3],
62        color: [f32; 4],
63    ) {
64        use engine::ecs::component::{
65            ActionComponent, AnimationComponent, AnimationState, ColorComponent, EmissiveComponent,
66            KeyframeComponent, RenderableComponent, TextComponent, TransformComponent,
67        };
68        use engine::graphics::primitives::{MaterialHandle, Renderable};
69
70        // Mesh.
71        let root = universe.world.add_component(
72            TransformComponent::new()
73                .with_position(x, y, 0.0)
74                .with_scale(scale[0], scale[1], scale[2]),
75        );
76
77        // Spin each shape around its own +Y axis using AnimationComponent + keyframes.
78        // We fill [0, 2) beats densely so it looks smooth.
79        let anim = universe
80            .world
81            .add_component(AnimationComponent::new().with_state(AnimationState::Looping));
82        let _ = universe.attach(root, anim);
83
84        let steps: usize = 64;
85        for i in 0..steps {
86            let beat = (i as f64) * (2.0 / (steps as f64));
87            let kf = universe.world.add_component(KeyframeComponent::new(beat));
88            let _ = universe.attach(anim, kf);
89
90            // Full turn over 2 beats.
91            let angle = (std::f64::consts::TAU * (beat / 2.0)) as f32;
92            let rotation = utils::math::quat_from_axis_angle([0.0, 1.0, 0.0], angle);
93
94            let action_cid = universe.world.add_component(ActionComponent::new(
95                engine::ecs::IntentValue::UpdateTransform {
96                    component_ids: vec![root],
97                    translation: [x, y, 0.0],
98                    rotation_quat_xyzw: rotation,
99                    scale,
100                },
101            ));
102            let _ = universe.attach(kf, action_cid);
103        }
104
105        let renderable = universe
106            .world
107            .add_component(RenderableComponent::new(Renderable::new(
108                mesh,
109                MaterialHandle::TOON_MESH,
110            )));
111        let color_c = universe
112            .world
113            .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
114        let emissive = universe.world.add_component(EmissiveComponent::on());
115
116        let _ = universe.attach(root, renderable);
117        let _ = universe.attach(renderable, color_c);
118        let _ = universe.attach(renderable, emissive);
119
120        universe.add(root);
121
122        // Label (separate transform so we can scale text independently).
123        let text_root = universe.world.add_component(
124            TransformComponent::new()
125                .with_position(x, y + 0.75, 0.05)
126                .with_scale(0.09, 0.09, 1.0),
127        );
128        let text = universe
129            .world
130            .add_component(TextComponent::with_word_wrap_tokens(
131                label,
132                LABEL_WRAP_AT,
133                ["::", "(", ")", ",", "."],
134            ));
135        let text_color = universe
136            .world
137            .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
138        let text_emissive = universe.world.add_component(EmissiveComponent::on());
139        let _ = universe.attach(text_root, text);
140        let _ = universe.attach(text, text_color);
141        let _ = universe.attach(text, text_emissive);
142        universe.add(text_root);
143    }
144
145    // Built-in meshes (stable ids).
146    let tri = universe
147        .render_assets
148        .get_mesh(engine::graphics::BuiltinMeshType::Triangle2D);
149    let quad = universe
150        .render_assets
151        .get_mesh(engine::graphics::BuiltinMeshType::Quad2D);
152    let cube = universe
153        .render_assets
154        .get_mesh(engine::graphics::BuiltinMeshType::Cube);
155    let tetra = universe
156        .render_assets
157        .get_mesh(engine::graphics::BuiltinMeshType::Tetrahedron);
158    let sphere = universe
159        .render_assets
160        .get_mesh(engine::graphics::BuiltinMeshType::Sphere);
161    let cone = universe
162        .render_assets
163        .get_mesh(engine::graphics::BuiltinMeshType::Cone);
164    let circle = universe
165        .render_assets
166        .get_mesh(engine::graphics::BuiltinMeshType::Circle2D);
167
168    // Layout.
169    let y = 0.0;
170    let dx = 1.8;
171    let x0 = -dx * 3.0;
172
173    spawn_labeled_mesh(
174        &mut universe,
175        x0 + dx * 0.0,
176        y,
177        "Triangle2D\nMeshFactory::triangle_2d()",
178        tri,
179        [1.0, 1.0, 1.0],
180        [1.0, 1.0, 1.0, 1.0],
181    );
182    spawn_labeled_mesh(
183        &mut universe,
184        x0 + dx * 1.0,
185        y,
186        "Quad2D\nMeshFactory::quad_2d()",
187        quad,
188        [1.0, 1.0, 1.0],
189        [1.0, 1.0, 1.0, 1.0],
190    );
191    spawn_labeled_mesh(
192        &mut universe,
193        x0 + dx * 2.0,
194        y,
195        "Cube\nMeshFactory::cube()",
196        cube,
197        [0.9, 0.9, 0.9],
198        [1.0, 1.0, 1.0, 1.0],
199    );
200    spawn_labeled_mesh(
201        &mut universe,
202        x0 + dx * 3.0,
203        y,
204        "Tetrahedron\nMeshFactory::tetrahedron()",
205        tetra,
206        [1.0, 1.0, 1.0],
207        [1.0, 1.0, 1.0, 1.0],
208    );
209    spawn_labeled_mesh(
210        &mut universe,
211        x0 + dx * 4.0,
212        y,
213        "Sphere\nMeshFactory::sphere()",
214        sphere,
215        [1.0, 1.0, 1.0],
216        [1.0, 1.0, 1.0, 1.0],
217    );
218    spawn_labeled_mesh(
219        &mut universe,
220        x0 + dx * 5.0,
221        y,
222        "Cone\nMeshFactory::cone(32)",
223        cone,
224        [1.0, 1.0, 1.0],
225        [1.0, 1.0, 1.0, 1.0],
226    );
227    spawn_labeled_mesh(
228        &mut universe,
229        x0 + dx * 6.0,
230        y,
231        "Circle2D\nMeshFactory::circle_2d(0.45, 0.5, 64)",
232        circle,
233        [1.0, 1.0, 1.0],
234        [1.0, 1.0, 1.0, 1.0],
235    );
236
237    // Process init-time registrations (Text expands into glyph subtrees here).
238    universe.systems.process_commands(
239        &mut universe.world,
240        &mut universe.visuals,
241        &mut universe.render_assets,
242        &mut universe.command_queue,
243    );
244
245    engine::Windowing::run_app(universe).expect("Windowing failed");
246}
examples/animation-for-topology.rs (line 137)
80fn main() {
81    mittens_engine::example_support::ensure_model_assets();
82    utils::logger::init();
83
84    let world = engine::ecs::World::default();
85    let mut universe = engine::Universe::new(world);
86
87    // Minimal scene with a camera so the window opens.
88    let clear = universe
89        .world
90        .add_component(engine::ecs::component::BackgroundColorComponent::new());
91    let clear_c = universe
92        .world
93        .add_component(engine::ecs::component::ColorComponent::rgba(
94            0.06, 0.06, 0.07, 1.0,
95        ));
96    let _ = universe.world.add_child(clear, clear_c);
97    universe.add(clear);
98
99    // Input-driven camera rig.
100    let input = universe
101        .world
102        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
103    let rig_transform = universe.world.add_component(
104        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 9.0),
105    );
106    let input_mode = universe.world.add_component(
107        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
108    );
109    let camera3d = universe.world.add_component(
110        engine::ecs::component::Camera3DComponent::new()
111            .with_far(250.0)
112            .with_fov(70.0),
113    );
114    let _ = universe.attach(input, input_mode);
115    let _ = universe.attach(input, rig_transform);
116    let _ = universe.attach(rig_transform, camera3d);
117
118    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
119    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
120    universe.add(input);
121
122    // Light so we can see non-emissive materials.
123    let light_tx = universe.world.add_component(
124        engine::ecs::component::TransformComponent::new().with_position(2.0, 3.5, 6.0),
125    );
126    let light = universe.world.add_component(
127        engine::ecs::component::PointLightComponent::new()
128            .with_distance(30.0)
129            .with_color(1.0, 1.0, 1.0),
130    );
131    let _ = universe.attach(light_tx, light);
132    universe.add(light_tx);
133
134    // ClockComponent drives the animation timeline in beats.
135    let clock = universe
136        .world
137        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
138    universe.add(clock);
139
140    // Root for all visualization objects.
141    let viz_root = universe.world.add_component(
142        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
143    );
144    universe.add(viz_root);
145
146    let anchor_count = 16usize;
147    let cube_pool_size = 8usize;
148
149    // Three grids side-by-side.
150    let layout_a = GridLayout {
151        origin: (-6.0, 0.0, 0.0),
152        spacing: 1.1,
153    };
154    let layout_b = GridLayout {
155        origin: (0.0, 0.0, 0.0),
156        spacing: 1.1,
157    };
158    let layout_c = GridLayout {
159        origin: (6.0, 0.0, 0.0),
160        spacing: 1.1,
161    };
162
163    // --- Grid A: detach + re-attach (reparent) ---
164    let grid_a_root = universe
165        .world
166        .add_component(engine::ecs::component::TransformComponent::new());
167    let _ = universe.attach(viz_root, grid_a_root);
168
169    let mut anchors_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
170    for i in 0..anchor_count {
171        let (x, y, z) = grid_anchor_local(layout_a, i);
172        let anchor = universe.world.add_component(
173            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
174        );
175        let _ = universe.attach(grid_a_root, anchor);
176        anchors_a.push(anchor);
177        spawn_emissive_marker_cube(
178            &mut universe,
179            anchor,
180            (0.0, -0.35, 0.0),
181            0.06,
182            [0.18, 0.18, 0.22, 1.0],
183        );
184    }
185
186    let mut cubes_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
187    for i in 0..cube_pool_size {
188        let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
189        let rgba = [0.10, 0.40 + 0.50 * t, 0.90 - 0.70 * t, 1.0];
190        cubes_a.push(spawn_detached_cube_prefab(&mut universe, 0.22, rgba));
191    }
192
193    let anim_a = universe
194        .world
195        .add_component(engine::ecs::component::AnimationComponent::new());
196    for i in 0..anchor_count {
197        let cube = cubes_a[i % cube_pool_size];
198        let parent = anchors_a[i];
199
200        // Explicit detach+attach to test topology changes via animations.
201
202        // Put them on separate keyframes so ordering is time-deterministic.
203        let beat_detach = i as f64;
204        let beat_attach = i as f64 + 0.05;
205
206        let kf_detach = universe
207            .world
208            .add_component(engine::ecs::component::KeyframeComponent::new(beat_detach));
209        let _ = universe.attach(anim_a, kf_detach);
210        let detach_action =
211            universe
212                .world
213                .add_component(engine::ecs::component::ActionComponent::new(
214                    engine::ecs::IntentValue::Detach {
215                        component_ids: vec![cube],
216                    },
217                ));
218        let _ = universe.attach(kf_detach, detach_action);
219
220        let kf_attach = universe
221            .world
222            .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
223        let _ = universe.attach(anim_a, kf_attach);
224        let attach_action =
225            universe
226                .world
227                .add_component(engine::ecs::component::ActionComponent::new(
228                    engine::ecs::IntentValue::Attach {
229                        parents: vec![parent],
230                        child: cube,
231                    },
232                ));
233        let _ = universe.attach(kf_attach, attach_action);
234    }
235    universe.add(anim_a);
236
237    // --- Grid B: continuously spawn (attach_clone) + delete-behind (remove_child) ---
238    //
239    // Uses the new actions:
240    // - `Action::attach_clone(parent, prefab_root)`
241    // - `Action::remove_child(parent, index)`
242    //
243    // This avoids needing a pre-built pool of cube ComponentIds.
244    let grid_b_root = universe
245        .world
246        .add_component(engine::ecs::component::TransformComponent::new());
247    let _ = universe.attach(viz_root, grid_b_root);
248
249    let mut anchors_b: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
250    for i in 0..anchor_count {
251        let (x, y, z) = grid_anchor_local(layout_b, i);
252        let anchor = universe.world.add_component(
253            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
254        );
255        let _ = universe.attach(grid_b_root, anchor);
256        anchors_b.push(anchor);
257
258        // Marker is attached under the grid root (not under the anchor), so anchor child index 0
259        // remains reserved for the dynamic cube subtree.
260        spawn_emissive_marker_cube(
261            &mut universe,
262            grid_b_root,
263            (x, y - 0.35, z),
264            0.06,
265            [0.22, 0.18, 0.18, 1.0],
266        );
267    }
268
269    // Detached prefab that will be cloned on demand (reddish cube).
270    let prefab_b = spawn_detached_cube_prefab(&mut universe, 0.20, [0.90, 0.25, 0.15, 1.0]);
271
272    let steps_b = 32usize;
273    let window = 8usize;
274
275    let anim_b = universe
276        .world
277        .add_component(engine::ecs::component::AnimationComponent::new());
278
279    // Phase 1: each beat, spawn a cube under an anchor; delete-behind by removing child(0).
280    for i in 0..steps_b {
281        let parent = anchors_b[i % anchor_count];
282
283        let beat_attach = i as f64;
284        let beat_remove = i as f64 + 0.05;
285
286        let kf_attach = universe
287            .world
288            .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
289        let _ = universe.attach(anim_b, kf_attach);
290
291        let attach_action =
292            universe
293                .world
294                .add_component(engine::ecs::component::ActionComponent::new(
295                    engine::ecs::IntentValue::AttachClone {
296                        parents: vec![parent],
297                        prefab_root: prefab_b,
298                    },
299                ));
300        let _ = universe.attach(kf_attach, attach_action);
301
302        if i >= window {
303            let remove_parent = anchors_b[(i - window) % anchor_count];
304            let kf_remove = universe
305                .world
306                .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
307            let _ = universe.attach(anim_b, kf_remove);
308
309            let remove_action =
310                universe
311                    .world
312                    .add_component(engine::ecs::component::ActionComponent::new(
313                        engine::ecs::IntentValue::RemoveChild {
314                            parents: vec![remove_parent],
315                            index: 0,
316                        },
317                    ));
318            let _ = universe.attach(kf_remove, remove_action);
319        }
320    }
321
322    // Phase 2: cleanup tail so the loop doesn't accumulate cubes.
323    for j in 0..window {
324        let remove_parent = anchors_b[(steps_b - window + j) % anchor_count];
325        let beat_remove = steps_b as f64 + j as f64 + 0.05;
326
327        let kf_remove = universe
328            .world
329            .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
330        let _ = universe.attach(anim_b, kf_remove);
331
332        let remove_action =
333            universe
334                .world
335                .add_component(engine::ecs::component::ActionComponent::new(
336                    engine::ecs::IntentValue::RemoveChild {
337                        parents: vec![remove_parent],
338                        index: 0,
339                    },
340                ));
341        let _ = universe.attach(kf_remove, remove_action);
342    }
343
344    universe.add(anim_b);
345
346    // --- Grid C: move cubes via set_position (no topology changes) ---
347    let grid_c_root = universe
348        .world
349        .add_component(engine::ecs::component::TransformComponent::new());
350    let _ = universe.attach(viz_root, grid_c_root);
351
352    let mut anchors_c: Vec<(f32, f32, f32)> = Vec::with_capacity(anchor_count);
353    for i in 0..anchor_count {
354        let (x, y, z) = grid_anchor_local(layout_c, i);
355        anchors_c.push((x, y, z));
356
357        let anchor = universe.world.add_component(
358            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
359        );
360        let _ = universe.attach(grid_c_root, anchor);
361        spawn_emissive_marker_cube(
362            &mut universe,
363            anchor,
364            (0.0, -0.35, 0.0),
365            0.06,
366            [0.18, 0.22, 0.18, 1.0],
367        );
368    }
369
370    let mut cubes_c: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
371    for i in 0..cube_pool_size {
372        let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
373        let rgba = [0.25, 0.85 - 0.55 * t, 0.25 + 0.55 * t, 1.0];
374        let cube_root = spawn_detached_cube_prefab(&mut universe, 0.22, rgba);
375        let _ = universe.attach(grid_c_root, cube_root);
376        cubes_c.push(cube_root);
377    }
378
379    let anim_c = universe
380        .world
381        .add_component(engine::ecs::component::AnimationComponent::new());
382    for i in 0..anchor_count {
383        let kf = universe
384            .world
385            .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
386        let _ = universe.attach(anim_c, kf);
387
388        let cube = cubes_c[i % cube_pool_size];
389        let (x, y, z) = anchors_c[i];
390        let setpos_action =
391            universe
392                .world
393                .add_component(engine::ecs::component::ActionComponent::new(
394                    engine::ecs::IntentValue::SetPosition {
395                        component_ids: vec![cube],
396                        position: [x, y, z],
397                    },
398                ));
399        let _ = universe.attach(kf, setpos_action);
400    }
401    universe.add(anim_c);
402
403    universe.systems.process_commands(
404        &mut universe.world,
405        &mut universe.visuals,
406        &mut universe.render_assets,
407        &mut universe.command_queue,
408    );
409
410    engine::Windowing::run_app(universe).expect("Windowing failed");
411}
examples/vtuber-joints-example.rs (line 24)
14fn main() {
15    mittens_engine::example_support::ensure_model_assets();
16    utils::logger::init();
17
18    let world = engine::ecs::World::default();
19    let mut universe = engine::Universe::new(world);
20
21    // Slow the global beat clock so beat-based animations run half as fast.
22    let clock = universe
23        .world
24        .add_component(ClockComponent::new().with_bpm(60.0));
25    universe.add(clock);
26
27    // Light pink background.
28    let background = universe
29        .world
30        .add_component(BackgroundColorComponent::new());
31    let background_c = universe
32        .world
33        .add_component(ColorComponent::rgba(1.0, 0.82, 0.90, 1.0));
34    let _ = universe.world.add_child(background, background_c);
35    universe.add(background);
36
37    // Small ambient so shadowed areas aren't pure black.
38    let ambient = universe
39        .world
40        .add_component(AmbientLightComponent::rgb(0.10, 0.10, 0.12));
41    universe.add(ambient);
42
43    // --- Camera rig (WASD + mouse) ---
44    let input = universe
45        .world
46        .add_component(InputComponent::new().with_speed(1.5));
47    let input_mode = universe.world.add_component(
48        InputTransformModeComponent::forward_z()
49            .with_fps_rotation()
50            .with_roll_axis_y(),
51    );
52    let _ = universe.attach(input, input_mode);
53
54    // Start slightly pulled back looking towards the origin.
55    let rig_transform = universe
56        .world
57        .add_component(TransformComponent::new().with_position(0.0, 0.0, 6.0));
58    let _ = universe.attach(input, rig_transform);
59
60    let camera3d = universe.world.add_component(Camera3DComponent::new());
61    let _ = universe.attach(rig_transform, camera3d);
62
63    // Pointer so gizmos can be interacted with.
64    let pointer = universe.world.add_component(PointerComponent::new());
65    let _ = universe.attach(camera3d, pointer);
66
67    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
68    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
69
70    universe.add(input);
71
72    // --- lighting ---
73    let sun = universe.world.add_component(
74        DirectionalLightComponent::new()
75            .with_intensity(1.2)
76            .with_color(1.0, 0.98, 0.94),
77    );
78    let sun_dir = universe
79        .world
80        .add_component(TransformComponent::new().with_position(0.0, -0.35, 1.0));
81    let _ = universe.attach(sun_dir, sun);
82    universe.add(sun_dir);
83
84    let light_transform = universe.world.add_component(
85        TransformComponent::new()
86            .with_position(1.0, 6.0, 3.0)
87            .with_scale(0.1, 0.1, 0.1),
88    );
89    let light = universe.world.add_component(
90        engine::ecs::component::PointLightComponent::new()
91            .with_distance(120.0)
92            .with_color(1.0, 1.0, 1.0),
93    );
94    let _ = universe.attach(light_transform, light);
95    universe.add(light_transform);
96
97    // --- VTuber model ---
98    let model_uri = "assets/models/pc-rei.hoodie.glb";
99
100    // Wrap the model subtree in an editor root so transform-only glTF nodes can be visualized
101    // (and thus raycasted/selected) without affecting non-editor scenes.
102    let editor_root = universe.world.add_component(EditorComponent::new());
103
104    let model_root = universe.world.add_component(TransformComponent::new());
105    let model = universe.world.add_component(GLTFComponent::new(model_uri));
106
107    // emissive for pc-rei
108    let emissive = universe.world.add_component(EmissiveComponent::on());
109    let _ = universe.attach(model, emissive);
110
111    let xr_input = universe.world.add_component(InputXRComponent::on());
112    let xr_gamepad = universe
113        .world
114        .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
115    let xr_head = universe.world.add_component(TransformComponent::new());
116    let xr_camera = universe.world.add_component(CameraXRComponent::on());
117    let _ = universe.attach(xr_input, xr_head);
118    let _ = universe.attach(xr_input, xr_gamepad);
119    let _ = universe.attach(xr_head, xr_camera);
120    let xr_pointer = universe.world.add_component(PointerComponent::new());
121    let _ = universe.attach(xr_camera, xr_pointer);
122    let _ = universe.attach(xr_head, editor_root);
123
124    let _ = universe.attach(model_root, model);
125
126    let _ = universe.attach(editor_root, model_root);
127
128    // Initialize the editor root so GLTFComponent gets registered.
129    universe.add(xr_input);
130    universe.add(editor_root);
131
132    // --- Background clouds (occluded + lit) ---
133    let bg_root = universe.world.add_component(
134        engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
135    );
136    universe.add(bg_root);
137    let mut cloud_params = example_util::CloudRingParams::default();
138    cloud_params.cloud_count = 8; // +3 clusters
139    cloud_params.angle_jitter = 0.35;
140    cloud_params.high_y_probability = 0.5;
141    cloud_params.high_y_multiplier = 1.5;
142    cloud_params.seed = 0x57_55_B0_01u32;
143    example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
144
145    // --- Simple environment ---
146    let spawn_cube = |universe: &mut engine::Universe,
147                      position: (f32, f32, f32),
148                      scale: (f32, f32, f32),
149                      color: (f32, f32, f32, f32)| {
150        let transform = universe.world.add_component(
151            TransformComponent::new()
152                .with_position(position.0, position.1, position.2)
153                .with_scale(scale.0, scale.1, scale.2),
154        );
155        let renderable = universe.world.add_component(RenderableComponent::cube());
156        let color = universe
157            .world
158            .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
159
160        let _ = universe.attach(transform, renderable);
161        let _ = universe.attach(renderable, color);
162
163        universe.add(transform);
164    };
165
166    // floor
167    spawn_cube(
168        &mut universe,
169        (0.0, -0.05, 0.0),
170        (10.0, 0.1, 10.0),
171        (0.92, 0.92, 0.92, 1.0),
172    );
173
174    // back wall
175    spawn_cube(
176        &mut universe,
177        (-3.0, 1.5, -5.0),
178        (3.0, 3.0, 1.0),
179        (0.95, 0.94, 0.96, 1.0),
180    );
181
182    // desk
183    spawn_cube(
184        &mut universe,
185        (0.0, 0.35, 1.0),
186        (1.0, 0.75, 0.5),
187        (0.75, 0.70, 0.65, 1.0),
188    );
189
190    // --- Editor-side stacked cubes (inside the editor subtree for picking/gizmos) ---
191    {
192        let spawn_editor_cube = |universe: &mut engine::Universe,
193                                 editor_root: engine::ecs::ComponentId,
194                                 name: &str,
195                                 position: (f32, f32, f32),
196                                 scale: (f32, f32, f32),
197                                 color: (f32, f32, f32, f32)| {
198            let transform = universe.world.add_component_boxed_named(
199                format!("{name}_t"),
200                Box::new(
201                    TransformComponent::new()
202                        .with_position(position.0, position.1, position.2)
203                        .with_scale(scale.0, scale.1, scale.2),
204                ),
205            );
206            let renderable = universe.world.add_component_boxed_named(
207                format!("{name}_r"),
208                Box::new(RenderableComponent::cube()),
209            );
210            let color_comp = universe.world.add_component_boxed_named(
211                format!("{name}_color"),
212                Box::new(ColorComponent::rgba(color.0, color.1, color.2, color.3)),
213            );
214            let raycastable = universe.world.add_component_boxed_named(
215                format!("{name}_raycastable"),
216                Box::new(RaycastableComponent::enabled()),
217            );
218
219            let _ = universe.world.add_child(transform, renderable);
220            let _ = universe.world.add_child(renderable, color_comp);
221            let _ = universe.world.add_child(renderable, raycastable);
222
223            // One attach into the initialized editor subtree triggers init for the new subtree.
224            let _ = universe.attach(editor_root, transform);
225        };
226
227        // Place the stack beside the desk (a bit to the right).
228        let stack_x = 1.35;
229        let stack_z = 1.0;
230        let s = 0.25;
231        let half = 0.5 * s;
232        let light_brown = (0.80, 0.72, 0.55, 1.0);
233        let cyan = (0.20, 1.00, 1.00, 1.0);
234
235        spawn_editor_cube(
236            &mut universe,
237            editor_root,
238            "editor_stack_0",
239            (stack_x, half, stack_z),
240            (s, s, s),
241            light_brown,
242        );
243        spawn_editor_cube(
244            &mut universe,
245            editor_root,
246            "editor_stack_1",
247            (stack_x, half + 1.0 * s, stack_z),
248            (s, s, s),
249            light_brown,
250        );
251        spawn_editor_cube(
252            &mut universe,
253            editor_root,
254            "editor_stack_2",
255            (stack_x, half + 2.0 * s, stack_z),
256            (s, s, s),
257            light_brown,
258        );
259        spawn_editor_cube(
260            &mut universe,
261            editor_root,
262            "editor_stack_top",
263            (stack_x, half + 3.0 * s, stack_z),
264            (s, s, s),
265            cyan,
266        );
267    }
268
269    let xr_root = universe
270        .world
271        .add_component(engine::ecs::component::XrComponent::on());
272    universe.add(xr_root);
273
274    universe.systems.process_commands(
275        &mut universe.world,
276        &mut universe.visuals,
277        &mut universe.render_assets,
278        &mut universe.command_queue,
279    );
280
281    // Spawn the glTF subtree once up-front so joint ComponentIds exist.
282    {
283        let systems = &mut universe.systems;
284        systems.gltf.tick_with_queue(
285            &mut universe.world,
286            &mut universe.visuals,
287            &mut systems.skinned_mesh,
288            &mut universe.command_queue,
289            0.0,
290        );
291    }
292    universe.systems.process_commands(
293        &mut universe.world,
294        &mut universe.visuals,
295        &mut universe.render_assets,
296        &mut universe.command_queue,
297    );
298
299    // Register imported meshes into RenderAssets early so we can inspect skin weights
300    // (textures will still be uploaded later during normal rendering).
301    universe
302        .systems
303        .gltf
304        .flush_mesh_imports_only(&mut universe.render_assets);
305
306    // --- Joint printout + animation binding (in the example) ---
307    let all_joints = collect_joint_transforms(
308        &universe.world,
309        &universe.systems.skinned_mesh,
310        &universe.visuals,
311        model,
312        model_root,
313    );
314    println!("[vtuber-joints-example] joints found: {}", all_joints.len());
315    for (i, (node_index, joint_tx)) in all_joints.iter().enumerate() {
316        println!("  joint[{i:03}] node_index={node_index} transform={joint_tx:?}");
317    }
318
319    let node_index_to_transform: HashMap<usize, engine::ecs::ComponentId> =
320        all_joints.iter().copied().collect();
321
322    // Example settings are hardcoded to keep this example simple.
323    let joint_offset: usize = 0;
324    let wiggle_count: usize = 16;
325
326    let target_mesh_key = "pc-rei.hoodie:Body_(merged).baked:prim0".to_string();
327    let target_joint_names: Vec<String> = vec![
328        "J_Bip_L_UpperArm".to_string(),
329        "J_Bip_R_UpperArm".to_string(),
330    ];
331
332    let print_transform_updates: bool = false;
333
334    let selected_joint_transforms: Vec<(usize, engine::ecs::ComponentId)> =
335        select_named_joints(&universe.world, &all_joints, &target_joint_names)
336            .or_else(|| {
337                select_body_prim0_influencers(
338                    &universe,
339                    model_root,
340                    &target_mesh_key,
341                    wiggle_count,
342                    &node_index_to_transform,
343                )
344            })
345            .unwrap_or_else(|| select_joint_range(&all_joints, joint_offset, wiggle_count));
346    println!(
347        "[vtuber-joints-example] wiggle selection: target_mesh_key='{}' offset={} count={} selected={}",
348        target_mesh_key,
349        joint_offset,
350        wiggle_count,
351        selected_joint_transforms.len()
352    );
353
354    debug_print_selected_joint_influence(
355        &universe,
356        &target_mesh_key,
357        model_root,
358        &selected_joint_transforms,
359    );
360
361    println!("[vtuber-joints-example] selected joints:");
362    for (i, (node_index, joint_tx)) in selected_joint_transforms.iter().enumerate() {
363        let name = universe
364            .world
365            .get_component_record(*joint_tx)
366            .map(|n| n.name.as_str())
367            .unwrap_or("<unknown>");
368        println!("  sel[{i:02}] node_index={node_index} name={name} transform={joint_tx:?}");
369    }
370
371    if print_transform_updates {
372        println!("[vtuber-joints-example] note: joint animation disabled");
373    }
374
375    universe.systems.process_commands(
376        &mut universe.world,
377        &mut universe.visuals,
378        &mut universe.render_assets,
379        &mut universe.command_queue,
380    );
381
382    universe.enable_repl();
383    engine::Windowing::run_app(universe).expect("Windowing failed");
384}
examples/animation-example.rs (line 63)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    let world = engine::ecs::World::default();
11    let mut universe = engine::Universe::new(world);
12
13    // Minimal scene with a camera so the window opens.
14    let clear = universe
15        .world
16        .add_component(engine::ecs::component::BackgroundColorComponent::new());
17    let clear_c = universe
18        .world
19        .add_component(engine::ecs::component::ColorComponent::rgba(
20            0.08, 0.08, 0.08, 1.0,
21        ));
22    let _ = universe.world.add_child(clear, clear_c);
23    universe.add(clear);
24
25    // Input-driven camera rig.
26    let input = universe
27        .world
28        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
29    let rig_transform = universe.world.add_component(
30        engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
31    );
32    let input_mode = universe.world.add_component(
33        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34    );
35    let camera3d = universe.world.add_component(
36        engine::ecs::component::Camera3DComponent::new()
37            .with_far(250.0)
38            .with_fov(70.0),
39    );
40    let _ = universe.attach(input, input_mode);
41    let _ = universe.attach(input, rig_transform);
42    let _ = universe.attach(rig_transform, camera3d);
43
44    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
45    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
46    universe.add(input);
47
48    // Light so we can see non-emissive materials (even though our cubes are emissive).
49    let light_tx = universe.world.add_component(
50        engine::ecs::component::TransformComponent::new().with_position(0.0, 2.5, 4.0),
51    );
52    let light = universe.world.add_component(
53        engine::ecs::component::PointLightComponent::new()
54            .with_distance(25.0)
55            .with_color(1.0, 1.0, 1.0),
56    );
57    let _ = universe.attach(light_tx, light);
58    universe.add(light_tx);
59
60    // ClockComponent sets global tempo.
61    let clock = universe
62        .world
63        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
64    universe.add(clock);
65
66    // Audio output + oscillators driven by scheduled actions.
67    let audio_out = universe
68        .world
69        .add_component(engine::ecs::component::AudioOutputComponent::new());
70    universe.add(audio_out);
71
72    // Noise oscillator: short white-noise hits.
73    let osc_noise = engine::ecs::component::AudioOscillator::noise()
74        .with_frequency(0.0)
75        .with_amplitude(0.06)
76        .with_enabled(false);
77    let osc_noise_comp =
78        universe
79            .world
80            .add_component(engine::ecs::component::AudioOscillatorComponent::single(
81                osc_noise,
82            ));
83    let _ = universe.attach(audio_out, osc_noise_comp);
84
85    // Drum oscillator: retriggers (phase + sweep) every enable.
86    let osc_drum = engine::ecs::component::AudioOscillator::drum()
87        // A low-ish pitch scale; actual pitch is set by scheduled notes.
88        .with_frequency(32.0)
89        .with_amplitude(0.40)
90        .with_enabled(false);
91    let osc_drum_comp =
92        universe
93            .world
94            .add_component(engine::ecs::component::AudioOscillatorComponent::single(
95                osc_drum,
96            ));
97
98    let _ = universe.attach(audio_out, osc_drum_comp);
99
100    // --- Visual layout helpers ---
101    fn spawn_text(universe: &mut engine::Universe, pos: (f32, f32, f32), scale: f32, text: &str) {
102        let tx = universe.world.add_component(
103            engine::ecs::component::TransformComponent::new()
104                .with_position(pos.0, pos.1, pos.2)
105                .with_scale(scale, scale, 1.0),
106        );
107        let t =
108            universe
109                .world
110                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
111                    text, 38,
112                ));
113        let _ = universe.attach(tx, t);
114        universe.add(tx);
115    }
116
117    fn spawn_emissive_cube(
118        universe: &mut engine::Universe,
119        parent: engine::ecs::ComponentId,
120        pos: (f32, f32, f32),
121        scale: f32,
122        rgba: [f32; 4],
123    ) {
124        let tx = universe.world.add_component(
125            engine::ecs::component::TransformComponent::new()
126                .with_position(pos.0, pos.1, pos.2)
127                .with_scale(scale, scale, scale),
128        );
129        let r = universe
130            .world
131            .add_component(engine::ecs::component::RenderableComponent::cube());
132        let c = universe
133            .world
134            .add_component(engine::ecs::component::ColorComponent::rgba(
135                rgba[0], rgba[1], rgba[2], rgba[3],
136            ));
137        let e = universe
138            .world
139            .add_component(engine::ecs::component::EmissiveComponent::on());
140        let _ = universe.attach(parent, tx);
141        let _ = universe.attach(tx, r);
142        let _ = universe.attach(r, c);
143        let _ = universe.attach(r, e);
144    }
145
146    fn spawn_op_cube(
147        universe: &mut engine::Universe,
148        parent: engine::ecs::ComponentId,
149        pos: (f32, f32, f32),
150        scale: f32,
151        base_rgba: [f32; 4],
152    ) -> engine::ecs::ComponentId {
153        let tx = universe.world.add_component(
154            engine::ecs::component::TransformComponent::new()
155                .with_position(pos.0, pos.1, pos.2)
156                .with_scale(scale, scale, scale),
157        );
158        let r = universe
159            .world
160            .add_component(engine::ecs::component::RenderableComponent::cube());
161        let c = universe
162            .world
163            .add_component(engine::ecs::component::ColorComponent::rgba(
164                base_rgba[0],
165                base_rgba[1],
166                base_rgba[2],
167                base_rgba[3],
168            ));
169        let e = universe
170            .world
171            .add_component(engine::ecs::component::EmissiveComponent::on());
172
173        let _ = universe.attach(parent, tx);
174        let _ = universe.attach(tx, r);
175        let _ = universe.attach(r, c);
176        let _ = universe.attach(r, e);
177
178        tx
179    }
180
181    // --- HUD / lanes ---
182    let lane_x = -2.6_f32;
183    let lane_title_z = -0.4_f32;
184    let lane_cfg_z = -0.4_f32;
185    let _lane_cube_z = -0.8_f32;
186    let drum_y = 0.9_f32;
187    let noise_y = -0.4_f32;
188
189    spawn_text(
190        &mut universe,
191        (lane_x, drum_y + 0.55, lane_title_z),
192        0.09,
193        "Audio Source A: Drum (kick)",
194    );
195    spawn_text(
196        &mut universe,
197        (lane_x, drum_y + 0.25, lane_cfg_z),
198        0.07,
199        "type=Drum\namp=0.40\nbase_freq=32Hz\nkick: C0 dur=0.12 vel=0.90\nlookahead=0.10s",
200    );
201
202    spawn_text(
203        &mut universe,
204        (lane_x, noise_y + 0.55, lane_title_z),
205        0.09,
206        "Audio Source B: Noise",
207    );
208    spawn_text(
209        &mut universe,
210        (lane_x, noise_y + 0.25, lane_cfg_z),
211        0.07,
212        "type=Noise\namp=0.06\nnoise: C9 dur=0.06 vel=0.25\noffset=+0.5 beats\nlookahead=0.10s",
213    );
214
215    // Representative cubes for the two audio sources.
216    let viz_root = universe.world.add_component(
217        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
218    );
219    universe.add(viz_root);
220    spawn_emissive_cube(
221        &mut universe,
222        viz_root,
223        (lane_x - 0.28, drum_y + 0.55, lane_title_z),
224        0.16,
225        [1.00, 0.90, 0.10, 1.0],
226    );
227    spawn_emissive_cube(
228        &mut universe,
229        viz_root,
230        (lane_x - 0.28, noise_y + 0.55, lane_title_z),
231        0.16,
232        [1.00, 1.00, 1.00, 1.0],
233    );
234
235    // Timeline roots so we can "reset all" via a single set_color action.
236    let kick_timeline_root = universe.world.add_component(
237        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
238    );
239    let noise_timeline_root = universe.world.add_component(
240        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
241    );
242    let _ = universe.attach(viz_root, kick_timeline_root);
243    let _ = universe.attach(viz_root, noise_timeline_root);
244
245    // Animation with 16 keyframes: each keyframe schedules a kick at offset 0.0,
246    // and a noise hit at offset 0.5.
247    let anim = universe
248        .world
249        .add_component(engine::ecs::component::AnimationComponent::new());
250
251    let kick_dur = 0.12_f32;
252    let noise_dur = 0.06_f32;
253
254    // Requested palette:
255    // - kick: dark yellow -> bright yellow
256    // - noise: grey -> white
257    let kick_dark = [0.35, 0.28, 0.02, 1.0];
258    let kick_bright = [1.00, 0.90, 0.10, 1.0];
259    let noise_dark = [0.35, 0.35, 0.35, 1.0];
260    let noise_bright = [1.00, 1.00, 1.00, 1.0];
261
262    let beat_spacing = 0.38_f32;
263    for i in 0..16 {
264        let kf_beat = i as f64;
265        let kf = universe
266            .world
267            .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
268
269        let kick = universe
270            .world
271            .add_component(engine::ecs::component::ActionComponent::new(
272                engine::ecs::IntentValue::AudioSchedulePlay {
273                    component_ids: vec![osc_drum_comp],
274                    beat_offset: 0.0,
275                    beat_context: None,
276                    note: Some(
277                        engine::ecs::component::MusicNote::c(0, kick_dur).with_velocity(0.9),
278                    ),
279                    gain: None,
280                    rate: None,
281                    duration: None,
282                },
283            ));
284
285        let noise = universe
286            .world
287            .add_component(engine::ecs::component::ActionComponent::new(
288                engine::ecs::IntentValue::AudioSchedulePlay {
289                    component_ids: vec![osc_noise_comp],
290                    beat_offset: 0.5,
291                    beat_context: None,
292                    note: Some(
293                        engine::ecs::component::MusicNote::c(9, noise_dur).with_velocity(0.25),
294                    ),
295                    gain: None,
296                    rate: None,
297                    duration: None,
298                },
299            ));
300
301        let _ = universe.attach(anim, kf);
302        let _ = universe.attach(kf, kick);
303        let _ = universe.attach(kf, noise);
304
305        // Each keyframe drives visualization purely via normal actions:
306        // 1) reset all timeline cubes to their base colors
307        // 2) brighten the current beat's cubes
308        let reset_kick_lane =
309            universe
310                .world
311                .add_component(engine::ecs::component::ActionComponent::new(
312                    engine::ecs::IntentValue::SetColor {
313                        component_ids: vec![kick_timeline_root],
314                        rgba: kick_dark,
315                    },
316                ));
317        let reset_noise_lane =
318            universe
319                .world
320                .add_component(engine::ecs::component::ActionComponent::new(
321                    engine::ecs::IntentValue::SetColor {
322                        component_ids: vec![noise_timeline_root],
323                        rgba: noise_dark,
324                    },
325                ));
326        let _ = universe.attach(kf, reset_kick_lane);
327        let _ = universe.attach(kf, reset_noise_lane);
328
329        // Timeline cubes: one cube per scheduled audio operation.
330        // Kick (offset 0.0) on the drum lane.
331        let kick_x = (kf_beat as f32) * beat_spacing;
332        let kick_cube = spawn_op_cube(
333            &mut universe,
334            kick_timeline_root,
335            (kick_x, drum_y, -1.3),
336            0.10,
337            kick_dark,
338        );
339
340        // Noise (offset 0.5) on the noise lane.
341        let noise_x = ((kf_beat + 0.5) as f32) * beat_spacing;
342        let noise_cube = spawn_op_cube(
343            &mut universe,
344            noise_timeline_root,
345            (noise_x, noise_y, -1.3),
346            0.10,
347            noise_dark,
348        );
349
350        let brighten_kick =
351            universe
352                .world
353                .add_component(engine::ecs::component::ActionComponent::new(
354                    engine::ecs::IntentValue::SetColor {
355                        component_ids: vec![kick_cube],
356                        rgba: kick_bright,
357                    },
358                ));
359        let brighten_noise =
360            universe
361                .world
362                .add_component(engine::ecs::component::ActionComponent::new(
363                    engine::ecs::IntentValue::SetColor {
364                        component_ids: vec![noise_cube],
365                        rgba: noise_bright,
366                    },
367                ));
368        let _ = universe.attach(kf, brighten_kick);
369        let _ = universe.attach(kf, brighten_noise);
370    }
371
372    universe.add(anim);
373
374    universe.systems.process_commands(
375        &mut universe.world,
376        &mut universe.visuals,
377        &mut universe.render_assets,
378        &mut universe.command_queue,
379    );
380
381    engine::Windowing::run_app(universe).expect("Windowing failed");
382}
examples/audio-graph-example.rs (line 114)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    // Debug toggle: allow isolating stack overflows to audio vs. non-audio init.
11    // Set `CAT_AUDIO_EXAMPLE_DISABLE_AUDIO=1` to skip creating audio components and
12    // skip scheduling audio notes. The UI/text/cube visualization will still spawn.
13    let audio_enabled = std::env::var("CAT_AUDIO_EXAMPLE_DISABLE_AUDIO")
14        .ok()
15        .as_deref()
16        != Some("1");
17
18    // If set, we still build audio graph components but we don't start the CPAL output stream.
19    // This helps distinguish "audio graph / scheduling" issues from "CPAL backend" issues.
20    let audio_output_enabled = std::env::var("CAT_AUDIO_EXAMPLE_AUDIO_OUTPUT_OFF")
21        .ok()
22        .as_deref()
23        != Some("1");
24
25    println!("[audio-graph-example] start");
26
27    let world = engine::ecs::World::default();
28    let mut universe = engine::Universe::new(world);
29
30    println!("[audio-graph-example] universe created");
31
32    // Minimal scene with a camera so the window opens (copied from animation-example).
33    let clear = universe
34        .world
35        .add_component(engine::ecs::component::BackgroundColorComponent::new());
36    let clear_c = universe
37        .world
38        .add_component(engine::ecs::component::ColorComponent::rgba(
39            0.07, 0.07, 0.07, 1.0,
40        ));
41    let _ = universe.world.add_child(clear, clear_c);
42    universe.add(clear);
43
44    // Ambient light so unlit areas aren't pitch black.
45    // Keep it dark to match the background clear color.
46    // (User request) 2.5x brighter.
47    let ambient = universe
48        .world
49        .add_component(engine::ecs::component::AmbientLightComponent::rgb(
50            0.075, 0.075, 0.075,
51        ));
52    universe.add(ambient);
53
54    // Input-driven camera rig.
55    let input = universe
56        .world
57        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
58    let rig_transform = universe.world.add_component(
59        engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
60    );
61    let input_mode = universe.world.add_component(
62        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
63    );
64    let camera3d = universe.world.add_component(
65        engine::ecs::component::Camera3DComponent::new()
66            .with_far(250.0)
67            .with_fov(70.0),
68    );
69    let _ = universe.attach(input, input_mode);
70    let _ = universe.attach(input, rig_transform);
71    let _ = universe.attach(rig_transform, camera3d);
72
73    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
74    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
75    universe.add(input);
76
77    // Directional light (sun-ish). Note: the renderer interprets the node's world position
78    // as a direction vector (see DirectionalLightComponent docs).
79    let light_tx = universe.world.add_component(
80        engine::ecs::component::TransformComponent::new().with_position(0.2, 0.7, 1.0),
81    );
82    let light = universe.world.add_component(
83        engine::ecs::component::DirectionalLightComponent::new()
84            .with_color(1.0, 1.0, 1.0)
85            .with_intensity(0.35),
86    );
87    let _ = universe.attach(light_tx, light);
88    universe.add(light_tx);
89
90    // --- Background clouds (occluded + lit) ---
91    // Mirrors the vtuber example: use a BackgroundComponent stage so the cloud volume
92    // self-occludes and is lit, but renders as background.
93    let bg_root = universe.world.add_component(
94        engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
95    );
96    universe.add(bg_root);
97    let mut cloud_params = example_util::CloudRingParams::default();
98    cloud_params.cloud_count = 7;
99    cloud_params.radius = 22.0;
100    // Move the ring up by ~one cloud height. The cloud generator uses a ~4.0 unit
101    // vertical spread for puff offsets, so +4.0 is a good "one height" bump.
102    cloud_params.center_y = 6.0;
103    cloud_params.puffs_per_cloud = 26;
104    cloud_params.angle_jitter = 0.0;
105    cloud_params.high_y_probability = 0.0;
106    cloud_params.high_y_multiplier = 1.0;
107    cloud_params.seed = 0xA0_D1_0C_01u32;
108    example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
109
110    // ClockComponent sets global tempo.
111    if audio_enabled {
112        let clock = universe
113            .world
114            .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
115        universe.add(clock);
116    }
117
118    if audio_enabled {
119        println!("[audio-graph-example] clock added");
120    } else {
121        println!("[audio-graph-example] audio disabled; skipping clock");
122    }
123
124    // Audio output + 2 oscillator sources.
125    let audio_out = if audio_enabled {
126        let audio_out_comp = if audio_output_enabled {
127            engine::ecs::component::AudioOutputComponent::new()
128        } else {
129            engine::ecs::component::AudioOutputComponent::off()
130        };
131        let audio_out = universe.world.add_component(audio_out_comp);
132        universe.add(audio_out);
133        Some(audio_out)
134    } else {
135        None
136    };
137
138    match (audio_enabled, audio_output_enabled) {
139        (false, _) => println!("[audio-graph-example] audio disabled; skipping audio output"),
140        (true, true) => println!("[audio-graph-example] audio output added (CPAL on)"),
141        (true, false) => println!("[audio-graph-example] audio output added (CPAL off)"),
142    }
143
144    // Track A: square lead.
145    let osc_a_comp =
146        if audio_enabled {
147            let osc_a = engine::ecs::component::AudioOscillator::square()
148                .with_frequency(110.0)
149                .with_amplitude(0.12)
150                .with_enabled(false);
151            let osc_a_comp = universe.world.add_component(
152                engine::ecs::component::AudioOscillatorComponent::single(osc_a),
153            );
154            if let Some(audio_out) = audio_out {
155                let _ = universe.attach(audio_out, osc_a_comp);
156            }
157            Some(osc_a_comp)
158        } else {
159            None
160        };
161
162    if audio_enabled {
163        println!("[audio-graph-example] track A created");
164    } else {
165        println!("[audio-graph-example] audio disabled; skipping track A");
166    }
167
168    // Effect tree A (single chain):
169    // osc_a
170    //   Gain
171    //     BandPass
172    //       Limiter
173    let mut bp_a_comp: Option<engine::ecs::ComponentId> = None;
174    if let Some(osc_a_comp) = osc_a_comp {
175        let gain_a = universe
176            .world
177            .add_component(engine::ecs::component::AudioGainComponent::new(3.2));
178        let bp_a = universe.world.add_component(
179            engine::ecs::component::AudioBandPassFilterComponent::new(120.0, 3.0, 0.40),
180        );
181        bp_a_comp = Some(bp_a);
182        let lim_a =
183            universe
184                .world
185                .add_component(engine::ecs::component::AudioLimiterComponent::new(
186                    4.0, 80.0, 0.90,
187                ));
188
189        let _ = universe.attach(osc_a_comp, gain_a);
190        let _ = universe.attach(gain_a, bp_a);
191        let _ = universe.attach(bp_a, lim_a);
192    }
193
194    if audio_enabled {
195        println!("[audio-graph-example] track A effects attached");
196    }
197
198    // --- Visual layout helpers (copied/adapted from animation-example) ---
199    fn spawn_text(
200        universe: &mut engine::Universe,
201        pos: (f32, f32, f32),
202        scale: f32,
203        wrap_cols: usize,
204        text: &str,
205    ) {
206        let tx = universe.world.add_component(
207            engine::ecs::component::TransformComponent::new()
208                .with_position(pos.0, pos.1, pos.2)
209                .with_scale(scale, scale, 1.0),
210        );
211        let t =
212            universe
213                .world
214                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
215                    text, wrap_cols,
216                ));
217        let _ = universe.attach(tx, t);
218
219        // TextSystem looks for an immediate TextureFilteringComponent child.
220        let filtering = universe
221            .world
222            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
223        let _ = universe.attach(t, filtering);
224
225        // TextSystem also supports styling from immediate Emissive children.
226        let emissive = universe
227            .world
228            .add_component(engine::ecs::component::EmissiveComponent::on());
229        let _ = universe.attach(t, emissive);
230
231        universe.add(tx);
232    }
233
234    fn spawn_emissive_cube(
235        universe: &mut engine::Universe,
236        parent: engine::ecs::ComponentId,
237        pos: (f32, f32, f32),
238        scale: f32,
239        rgba: [f32; 4],
240    ) {
241        let tx = universe.world.add_component(
242            engine::ecs::component::TransformComponent::new()
243                .with_position(pos.0, pos.1, pos.2)
244                .with_scale(scale, scale, scale),
245        );
246        let r = universe
247            .world
248            .add_component(engine::ecs::component::RenderableComponent::cube());
249        let c = universe
250            .world
251            .add_component(engine::ecs::component::ColorComponent::rgba(
252                rgba[0], rgba[1], rgba[2], rgba[3],
253            ));
254        let e = universe
255            .world
256            .add_component(engine::ecs::component::EmissiveComponent::on());
257        let _ = universe.attach(parent, tx);
258        let _ = universe.attach(tx, r);
259        let _ = universe.attach(r, c);
260        let _ = universe.attach(r, e);
261    }
262
263    fn spawn_op_cube(
264        universe: &mut engine::Universe,
265        parent: engine::ecs::ComponentId,
266        pos: (f32, f32, f32),
267        scale: f32,
268        base_rgba: [f32; 4],
269    ) -> engine::ecs::ComponentId {
270        let tx = universe.world.add_component(
271            engine::ecs::component::TransformComponent::new()
272                .with_position(pos.0, pos.1, pos.2)
273                .with_scale(scale, scale, scale),
274        );
275        let r = universe
276            .world
277            .add_component(engine::ecs::component::RenderableComponent::cube());
278        let c = universe
279            .world
280            .add_component(engine::ecs::component::ColorComponent::rgba(
281                base_rgba[0],
282                base_rgba[1],
283                base_rgba[2],
284                base_rgba[3],
285            ));
286        let e = universe
287            .world
288            .add_component(engine::ecs::component::EmissiveComponent::on());
289
290        let _ = universe.attach(parent, tx);
291        let _ = universe.attach(tx, r);
292        let _ = universe.attach(r, c);
293        let _ = universe.attach(r, e);
294
295        tx
296    }
297
298    // --- HUD / lane (single track) ---
299    let lane_x = -2.6_f32;
300    let lane_title_z = -0.4_f32;
301    let lane_cfg_z = -0.4_f32;
302    let lane_pat_z = -1.3_f32;
303    let lane_graph_z = -1.15_f32;
304
305    // Content for pattern/chain/graph starts at x=0; keep section labels aligned there.
306    // This also keeps them from overlapping the config block (which is anchored at lane_x).
307    let lane_labels_x = lane_x + 1.6_f32;
308
309    let track_a_y = 0.9_f32;
310
311    spawn_text(
312        &mut universe,
313        (lane_x, track_a_y + 0.55, lane_title_z),
314        0.09,
315        42,
316        "Track A: AudioOscillator::square()",
317    );
318    spawn_text(
319        &mut universe,
320        (lane_x, track_a_y + 0.25, lane_cfg_z),
321        0.07,
322        48,
323        "oscillators=1\nfrequency_hz=110.0\namplitude=0.12\nenabled=false\nlookahead=0.10s",
324    );
325
326    let viz_root = universe.world.add_component(
327        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
328    );
329    universe.add(viz_root);
330
331    println!("[audio-graph-example] viz root added");
332
333    // Small identifier cubes next to titles.
334    spawn_emissive_cube(
335        &mut universe,
336        viz_root,
337        (lane_x - 0.28, track_a_y + 0.55, lane_title_z),
338        0.16,
339        [0.85, 0.40, 1.00, 1.0],
340    );
341
342    // Pattern roots so we can reset via one SetColor action.
343    let track_a_pattern_root = universe.world.add_component(
344        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
345    );
346    let _ = universe.attach(viz_root, track_a_pattern_root);
347
348    // Labels for pattern/chain/graph sections.
349    spawn_text(
350        &mut universe,
351        (lane_labels_x, track_a_y, lane_title_z),
352        0.06,
353        42,
354        "pattern",
355    );
356    spawn_text(
357        &mut universe,
358        (lane_labels_x, track_a_y - 0.40, lane_title_z),
359        0.06,
360        42,
361        "graph",
362    );
363
364    // --- Processing chain + graph visualization (compiled graph → cubes + labels) ---
365    use engine::ecs::system::audio_graph_compiler::{
366        AudioGraphCompiler, AudioGraphNode, AudioGraphNodeKind,
367    };
368
369    fn effect_grey_for_depth(depth: usize) -> f32 {
370        // depth=1 => medium grey; deeper => lighter, capped.
371        let base = 0.55;
372        let step = 0.13;
373        let d = depth.saturating_sub(1) as f32;
374        (base + step * d).min(0.92)
375    }
376
377    fn node_rgba(node: &AudioGraphNode, depth: usize) -> [f32; 4] {
378        match node.kind {
379            AudioGraphNodeKind::OscillatorSource { .. } => [1.0, 0.78, 0.22, 1.0],
380            _ => {
381                let g = effect_grey_for_depth(depth);
382                [g, g, g, 1.0]
383            }
384        }
385    }
386
387    fn node_label(node: &AudioGraphNode) -> String {
388        match &node.kind {
389            AudioGraphNodeKind::OscillatorSource { voices } => {
390                format!("OscillatorSource voices={voices}")
391            }
392            AudioGraphNodeKind::Gain { gain } => {
393                format!("Gain gain={gain:.3}")
394            }
395            AudioGraphNodeKind::LowPass {
396                cutoff_hz,
397                resonance,
398            } => {
399                format!("LowPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
400            }
401            AudioGraphNodeKind::BandPass {
402                center_hz,
403                bandwidth_octaves,
404                resonance,
405            } => {
406                format!(
407                    "BandPass center={center_hz:.1}Hz bw={bandwidth_octaves:.3}oct res={resonance:.3}"
408                )
409            }
410            AudioGraphNodeKind::HighPass {
411                cutoff_hz,
412                resonance,
413            } => {
414                format!("HighPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
415            }
416            AudioGraphNodeKind::Limiter {
417                attack_ms,
418                release_ms,
419                threshold,
420            } => {
421                format!("Limiter atk={attack_ms:.1}ms rel={release_ms:.1}ms thr={threshold:.3}")
422            }
423            AudioGraphNodeKind::ClipSource => "ClipSource".to_string(),
424        }
425    }
426
427    fn compute_layout(
428        node: &AudioGraphNode,
429        depth: usize,
430        x_cursor: &mut i32,
431        out: &mut std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
432    ) -> i32 {
433        // Depth-only layout:
434        // - keep children directly below their parent on X
435        // - use Z sibling offsets (in spawn_graph_tree) to show branching
436        let _ = x_cursor;
437        for ch in node.children.iter() {
438            let _ = compute_layout(ch, depth + 1, x_cursor, out);
439        }
440
441        let my_x = 0;
442        out.insert(node as *const AudioGraphNode, (my_x, depth));
443        my_x
444    }
445
446    fn spawn_graph_tree(
447        universe: &mut engine::Universe,
448        parent: engine::ecs::ComponentId,
449        node: &AudioGraphNode,
450        bp_component: Option<engine::ecs::ComponentId>,
451        bp_label_out: &mut Option<engine::ecs::ComponentId>,
452        origin: (f32, f32, f32),
453        layout: &std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
454        dx: f32,
455        dy: f32,
456        cube_scale: f32,
457        sibling_index: usize,
458        sibling_count: usize,
459    ) {
460        let Some((x_unit, depth)) = layout.get(&(node as *const AudioGraphNode)).copied() else {
461            return;
462        };
463
464        let x = origin.0 + (x_unit as f32) * dx;
465        let y = origin.1 - (depth as f32) * dy;
466
467        // Push siblings “behind” each other along Z to reduce overlap between
468        // a sibling's cube and another node's label.
469        let dz_sibling = 0.18;
470        let z = origin.2 - (sibling_index as f32) * dz_sibling;
471
472        // Keep text slightly in front of its cube.
473        let z_text = z + 0.03;
474
475        let rgba = node_rgba(node, depth);
476        let cube_tx = spawn_op_cube(universe, parent, (x, y, z), cube_scale, rgba);
477        let _ = cube_tx;
478
479        // Label next to the cube.
480        let label = node_label(node);
481        let tx = universe.world.add_component(
482            engine::ecs::component::TransformComponent::new()
483                .with_position(x + 0.14, y + 0.02, z_text)
484                .with_scale(0.06, 0.06, 1.0),
485        );
486        let t =
487            universe
488                .world
489                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
490                    &label, 25,
491                ));
492        let _ = universe.attach(parent, tx);
493        let _ = universe.attach(tx, t);
494
495        if bp_component == Some(node.component) {
496            *bp_label_out = Some(t);
497        }
498
499        let filtering = universe
500            .world
501            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
502        let _ = universe.attach(t, filtering);
503
504        let emissive = universe
505            .world
506            .add_component(engine::ecs::component::EmissiveComponent::on());
507        let _ = universe.attach(t, emissive);
508
509        universe.add(tx);
510
511        // Mix/branch label if branching.
512        if node.children.len() > 1 {
513            let mut weights: Vec<f32> = Vec::with_capacity(node.children.len());
514            for i in 0..node.children.len() {
515                let w = node
516                    .mix
517                    .as_ref()
518                    .map(|m| m.weights.get(i).copied().unwrap_or(1.0))
519                    .unwrap_or(1.0);
520                weights.push(w);
521            }
522            let mix_label = if node.mix.is_some() {
523                format!("Mix weights={weights:?}")
524            } else {
525                format!("Mix <implicit> w={weights:?}")
526            };
527            let mix_tx = universe.world.add_component(
528                engine::ecs::component::TransformComponent::new()
529                    .with_position(x + 0.14, y - 0.11, z_text)
530                    .with_scale(0.05, 0.05, 1.0),
531            );
532            let mix_t = universe.world.add_component(
533                engine::ecs::component::TextComponent::with_word_wrap(&mix_label, 25),
534            );
535            let _ = universe.attach(parent, mix_tx);
536            let _ = universe.attach(mix_tx, mix_t);
537
538            let filtering = universe
539                .world
540                .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
541            let _ = universe.attach(mix_t, filtering);
542
543            let emissive = universe
544                .world
545                .add_component(engine::ecs::component::EmissiveComponent::on());
546            let _ = universe.attach(mix_t, emissive);
547
548            universe.add(mix_tx);
549        }
550
551        let child_count = node.children.len().max(1);
552        for (i, ch) in node.children.iter().enumerate() {
553            // Each node's children form a sibling group; offset them in Z.
554            // For the root node, sibling_index is 0.
555            let _ = sibling_count;
556            spawn_graph_tree(
557                universe,
558                parent,
559                ch,
560                bp_component,
561                bp_label_out,
562                origin,
563                layout,
564                dx,
565                dy,
566                cube_scale,
567                i,
568                child_count,
569            );
570        }
571    }
572
573    // Compile + display chain and graph for the track.
574    let compiled_a = if audio_enabled {
575        println!("[audio-graph-example] compiling graph...");
576        let Some(osc_a_comp) = osc_a_comp else {
577            panic!("audio_enabled but track A was not created");
578        };
579        let compiled_a =
580            AudioGraphCompiler::compile(&universe.world, osc_a_comp).expect("compile A");
581        println!("[audio-graph-example] graph compiled");
582        Some(compiled_a)
583    } else {
584        println!("[audio-graph-example] audio disabled; skipping graph compilation");
585        None
586    };
587
588    // Full compiled graph visualization (tree layout + mix labels).
589    let mut bp_graph_label_text: Option<engine::ecs::ComponentId> = None;
590    if let Some(compiled_a) = &compiled_a {
591        let mut x_cursor = 0;
592        let mut layout: std::collections::HashMap<*const AudioGraphNode, (i32, usize)> =
593            std::collections::HashMap::new();
594        let _root_x = compute_layout(&compiled_a.root, 0, &mut x_cursor, &mut layout);
595
596        // With depth-only X layout, we keep the tree centered at x=0.
597        // Pull the graph up now that we don't show the separate chain view.
598        let origin = (0.0, track_a_y - 0.40, lane_graph_z);
599
600        spawn_graph_tree(
601            &mut universe,
602            viz_root,
603            &compiled_a.root,
604            bp_a_comp,
605            &mut bp_graph_label_text,
606            origin,
607            &layout,
608            0.45,
609            0.28,
610            0.08,
611            0,
612            1,
613        );
614    } else {
615        spawn_text(
616            &mut universe,
617            (0.0, track_a_y - 0.40, lane_graph_z),
618            0.06,
619            60,
620            "(audio disabled)",
621        );
622    }
623
624    // Animation with 16 keyframes drives scheduled notes + pattern highlights.
625    let anim = universe
626        .world
627        .add_component(engine::ecs::component::AnimationComponent::new());
628
629    let dur_a = 0.85_f32;
630    let beat_spacing = 0.38_f32;
631
632    let a_dark = [0.18, 0.08, 0.22, 1.0];
633    let a_bright = [0.85, 0.40, 1.00, 1.0];
634
635    for i in 0..16 {
636        let kf_beat = i as f64;
637        let kf = universe
638            .world
639            .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
640
641        let _ = universe.attach(anim, kf);
642
643        // Scheduled audio note (sample-accurate via lookahead).
644        // Pulse every beat.
645        if audio_enabled {
646            let Some(osc_a_comp) = osc_a_comp else {
647                panic!("audio_enabled but track A was not created");
648            };
649
650            let note_a = engine::ecs::component::MusicNote::c(1, dur_a).with_velocity(0.80);
651
652            let act_a = universe
653                .world
654                .add_component(engine::ecs::component::ActionComponent::new(
655                    engine::ecs::IntentValue::AudioSchedulePlay {
656                        component_ids: vec![osc_a_comp],
657                        beat_offset: 0.0,
658                        beat_context: None,
659                        note: Some(note_a),
660                        gain: None,
661                        rate: None,
662                        duration: None,
663                    },
664                ));
665
666            let _ = universe.attach(kf, act_a);
667        }
668
669        // Keyframed band-pass center.
670        // This is an immediate parameter update applied RT-side (no graph rebuild).
671        if audio_enabled {
672            if let Some(bp_a_comp) = bp_a_comp {
673                let t = (i as f32) / 15.0;
674                let center_hz = 10.0 + t * (1000.0 - 10.0);
675
676                let bp_center =
677                    universe
678                        .world
679                        .add_component(engine::ecs::component::ActionComponent::new(
680                            engine::ecs::IntentValue::AudioBandPassSetCenterHz {
681                                component_ids: vec![bp_a_comp],
682                                center_hz,
683                            },
684                        ));
685                let _ = universe.attach(kf, bp_center);
686
687                // Update the BandPass node label in the graph visualization.
688                if let Some(text_id) = bp_graph_label_text {
689                    let label =
690                        universe
691                            .world
692                            .add_component(engine::ecs::component::ActionComponent::new(
693                                engine::ecs::IntentValue::SetText {
694                                    component_ids: vec![text_id],
695                                    text: format!("BandPass center={center_hz:.1}Hz"),
696                                },
697                            ));
698                    let _ = universe.attach(kf, label);
699                }
700            }
701        }
702
703        // Visualization reset per keyframe.
704        let reset_a = universe
705            .world
706            .add_component(engine::ecs::component::ActionComponent::new(
707                engine::ecs::IntentValue::SetColor {
708                    component_ids: vec![track_a_pattern_root],
709                    rgba: a_dark,
710                },
711            ));
712        let _ = universe.attach(kf, reset_a);
713
714        // Pattern cube per step for each track.
715        let x = (kf_beat as f32) * beat_spacing;
716        let cube_a = spawn_op_cube(
717            &mut universe,
718            track_a_pattern_root,
719            (x, track_a_y, lane_pat_z),
720            0.10,
721            a_dark,
722        );
723
724        let bright_a = universe
725            .world
726            .add_component(engine::ecs::component::ActionComponent::new(
727                engine::ecs::IntentValue::SetColor {
728                    component_ids: vec![cube_a],
729                    rgba: a_bright,
730                },
731            ));
732        let _ = universe.attach(kf, bright_a);
733    }
734    universe.add(anim);
735
736    println!("[audio-graph-example] animation created");
737
738    // Keep window open.
739    println!("[audio-graph-example] processing commands...");
740    universe.systems.process_commands(
741        &mut universe.world,
742        &mut universe.visuals,
743        &mut universe.render_assets,
744        &mut universe.command_queue,
745    );
746
747    println!("[audio-graph-example] commands processed; launching window");
748
749    engine::Windowing::run_app(universe).expect("Windowing failed");
750}
Source

pub fn with_bpm(self, bpm: f64) -> Self

Examples found in repository?
examples/text-animation.rs (line 97)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    let world = engine::ecs::World::default();
11    let mut universe = engine::Universe::new(world);
12
13    // Input-driven camera rig.
14    // Topology: I { T { C3D } }
15    let input = universe
16        .world
17        .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
18    let camera3d = universe
19        .world
20        .add_component(engine::ecs::component::Camera3DComponent::new());
21    let rig_transform = universe.world.add_component(
22        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.5),
23    );
24    let input_mode = universe.world.add_component(
25        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
26    );
27    let _ = universe.attach(input, input_mode);
28    let _ = universe.attach(input, rig_transform);
29    let _ = universe.attach(rig_transform, camera3d);
30
31    // Small on-screen help.
32    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
33    universe.add(input);
34
35    // Light so we can see non-emissive materials.
36    let light_transform = universe.world.add_component(
37        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 2.0),
38    );
39    let light = universe.world.add_component(
40        engine::ecs::component::PointLightComponent::new()
41            .with_distance(25.0)
42            .with_color(1.0, 1.0, 1.0),
43    );
44    let _ = universe.attach(light_transform, light);
45    universe.add(light_transform);
46
47    use engine::ecs::component::{
48        ColorComponent, TextComponent, TextShadowComponent, TextureComponent,
49        TextureFilteringComponent, TransformComponent, TransparentCutoutComponent,
50    };
51
52    // Styled text root.
53    let text_root = universe.world.add_component(
54        TransformComponent::new()
55            .with_position(0.0, 0.0, 0.0)
56            .with_scale(0.25, 0.25, 1.0),
57    );
58
59    // Color must be an ancestor of the glyph renderables.
60    let color_id = universe
61        .world
62        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
63    let _ = universe.attach(text_root, color_id);
64
65    // This is the component we animate via SetText.
66    let text_id = universe.world.add_component(TextComponent::new(">w<"));
67    let _ = universe.attach(color_id, text_id);
68
69    // Route into cutout pass for cleaner edges.
70    let cutout = universe
71        .world
72        .add_component(TransparentCutoutComponent::new());
73    let _ = universe.attach(text_id, cutout);
74
75    // Font atlas.
76    let tex = universe.world.add_component(TextureComponent::with_uri(
77        "assets/textures/font_system.dds",
78    ));
79    let _ = universe.attach(text_id, tex);
80
81    let shadow = universe.world.add_component(
82        TextShadowComponent::new()
83            .with_scale(1.35)
84            .with_offset([0.06, -0.06, 0.0015]),
85    );
86    let filtering = universe
87        .world
88        .add_component(TextureFilteringComponent::nearest_magnification());
89
90    let _ = universe.attach(text_id, shadow);
91    let _ = universe.attach(text_id, filtering);
92
93    universe.add(text_root);
94
95    let clock_component = universe
96        .world
97        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
98    let _ = universe.add(clock_component);
99
100    // Looping animation: 4 beats long, 4 keyframes.
101    let anim = universe
102        .world
103        .add_component(engine::ecs::component::AnimationComponent::new());
104
105    let faces = [">w<", "^w^", "-_-", "o_o"];
106    for (i, &face) in faces.iter().enumerate() {
107        let kf = universe
108            .world
109            .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
110
111        // Each keyframe emits a SetText intent.
112        let action = universe
113            .world
114            .add_component(engine::ecs::component::ActionComponent::new(
115                engine::ecs::IntentValue::SetText {
116                    component_ids: vec![text_id],
117                    text: face.to_string(),
118                },
119            ));
120
121        let _ = universe.attach(anim, kf);
122        let _ = universe.attach(kf, action);
123    }
124
125    universe.add(anim);
126
127    // Process init-time registrations (Text expands into glyph subtrees here).
128    universe.systems.process_commands(
129        &mut universe.world,
130        &mut universe.visuals,
131        &mut universe.render_assets,
132        &mut universe.command_queue,
133    );
134
135    engine::Windowing::run_app(universe).expect("Windowing failed");
136}
More examples
Hide additional examples
examples/mesh-factory-example.rs (line 18)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    const LABEL_WRAP_AT: usize = 13;
11
12    let world = engine::ecs::World::default();
13    let mut universe = engine::Universe::new(world);
14
15    // add a clock
16    let clock = universe
17        .world
18        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(60.0));
19    universe.add(clock);
20
21    // Input-driven camera rig.
22    // Topology: I { T { C3D } }
23    let input = universe
24        .world
25        .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
26    let camera3d = universe
27        .world
28        .add_component(engine::ecs::component::Camera3DComponent::new());
29    let rig_transform = universe.world.add_component(
30        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 11.0),
31    );
32    let input_mode = universe.world.add_component(
33        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34    );
35    let _ = universe.attach(input, input_mode);
36    let _ = universe.attach(input, rig_transform);
37    let _ = universe.attach(rig_transform, camera3d);
38
39    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
40    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
41    universe.add(input);
42
43    // Light.
44    let light_transform = universe.world.add_component(
45        engine::ecs::component::TransformComponent::new().with_position(0.0, 2.0, 2.0),
46    );
47    let light = universe.world.add_component(
48        engine::ecs::component::PointLightComponent::new()
49            .with_distance(50.0)
50            .with_color(1.0, 1.0, 1.0),
51    );
52    let _ = universe.attach(light_transform, light);
53    universe.add(light_transform);
54
55    fn spawn_labeled_mesh(
56        universe: &mut engine::Universe,
57        x: f32,
58        y: f32,
59        label: &str,
60        mesh: engine::graphics::primitives::CpuMeshHandle,
61        scale: [f32; 3],
62        color: [f32; 4],
63    ) {
64        use engine::ecs::component::{
65            ActionComponent, AnimationComponent, AnimationState, ColorComponent, EmissiveComponent,
66            KeyframeComponent, RenderableComponent, TextComponent, TransformComponent,
67        };
68        use engine::graphics::primitives::{MaterialHandle, Renderable};
69
70        // Mesh.
71        let root = universe.world.add_component(
72            TransformComponent::new()
73                .with_position(x, y, 0.0)
74                .with_scale(scale[0], scale[1], scale[2]),
75        );
76
77        // Spin each shape around its own +Y axis using AnimationComponent + keyframes.
78        // We fill [0, 2) beats densely so it looks smooth.
79        let anim = universe
80            .world
81            .add_component(AnimationComponent::new().with_state(AnimationState::Looping));
82        let _ = universe.attach(root, anim);
83
84        let steps: usize = 64;
85        for i in 0..steps {
86            let beat = (i as f64) * (2.0 / (steps as f64));
87            let kf = universe.world.add_component(KeyframeComponent::new(beat));
88            let _ = universe.attach(anim, kf);
89
90            // Full turn over 2 beats.
91            let angle = (std::f64::consts::TAU * (beat / 2.0)) as f32;
92            let rotation = utils::math::quat_from_axis_angle([0.0, 1.0, 0.0], angle);
93
94            let action_cid = universe.world.add_component(ActionComponent::new(
95                engine::ecs::IntentValue::UpdateTransform {
96                    component_ids: vec![root],
97                    translation: [x, y, 0.0],
98                    rotation_quat_xyzw: rotation,
99                    scale,
100                },
101            ));
102            let _ = universe.attach(kf, action_cid);
103        }
104
105        let renderable = universe
106            .world
107            .add_component(RenderableComponent::new(Renderable::new(
108                mesh,
109                MaterialHandle::TOON_MESH,
110            )));
111        let color_c = universe
112            .world
113            .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
114        let emissive = universe.world.add_component(EmissiveComponent::on());
115
116        let _ = universe.attach(root, renderable);
117        let _ = universe.attach(renderable, color_c);
118        let _ = universe.attach(renderable, emissive);
119
120        universe.add(root);
121
122        // Label (separate transform so we can scale text independently).
123        let text_root = universe.world.add_component(
124            TransformComponent::new()
125                .with_position(x, y + 0.75, 0.05)
126                .with_scale(0.09, 0.09, 1.0),
127        );
128        let text = universe
129            .world
130            .add_component(TextComponent::with_word_wrap_tokens(
131                label,
132                LABEL_WRAP_AT,
133                ["::", "(", ")", ",", "."],
134            ));
135        let text_color = universe
136            .world
137            .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
138        let text_emissive = universe.world.add_component(EmissiveComponent::on());
139        let _ = universe.attach(text_root, text);
140        let _ = universe.attach(text, text_color);
141        let _ = universe.attach(text, text_emissive);
142        universe.add(text_root);
143    }
144
145    // Built-in meshes (stable ids).
146    let tri = universe
147        .render_assets
148        .get_mesh(engine::graphics::BuiltinMeshType::Triangle2D);
149    let quad = universe
150        .render_assets
151        .get_mesh(engine::graphics::BuiltinMeshType::Quad2D);
152    let cube = universe
153        .render_assets
154        .get_mesh(engine::graphics::BuiltinMeshType::Cube);
155    let tetra = universe
156        .render_assets
157        .get_mesh(engine::graphics::BuiltinMeshType::Tetrahedron);
158    let sphere = universe
159        .render_assets
160        .get_mesh(engine::graphics::BuiltinMeshType::Sphere);
161    let cone = universe
162        .render_assets
163        .get_mesh(engine::graphics::BuiltinMeshType::Cone);
164    let circle = universe
165        .render_assets
166        .get_mesh(engine::graphics::BuiltinMeshType::Circle2D);
167
168    // Layout.
169    let y = 0.0;
170    let dx = 1.8;
171    let x0 = -dx * 3.0;
172
173    spawn_labeled_mesh(
174        &mut universe,
175        x0 + dx * 0.0,
176        y,
177        "Triangle2D\nMeshFactory::triangle_2d()",
178        tri,
179        [1.0, 1.0, 1.0],
180        [1.0, 1.0, 1.0, 1.0],
181    );
182    spawn_labeled_mesh(
183        &mut universe,
184        x0 + dx * 1.0,
185        y,
186        "Quad2D\nMeshFactory::quad_2d()",
187        quad,
188        [1.0, 1.0, 1.0],
189        [1.0, 1.0, 1.0, 1.0],
190    );
191    spawn_labeled_mesh(
192        &mut universe,
193        x0 + dx * 2.0,
194        y,
195        "Cube\nMeshFactory::cube()",
196        cube,
197        [0.9, 0.9, 0.9],
198        [1.0, 1.0, 1.0, 1.0],
199    );
200    spawn_labeled_mesh(
201        &mut universe,
202        x0 + dx * 3.0,
203        y,
204        "Tetrahedron\nMeshFactory::tetrahedron()",
205        tetra,
206        [1.0, 1.0, 1.0],
207        [1.0, 1.0, 1.0, 1.0],
208    );
209    spawn_labeled_mesh(
210        &mut universe,
211        x0 + dx * 4.0,
212        y,
213        "Sphere\nMeshFactory::sphere()",
214        sphere,
215        [1.0, 1.0, 1.0],
216        [1.0, 1.0, 1.0, 1.0],
217    );
218    spawn_labeled_mesh(
219        &mut universe,
220        x0 + dx * 5.0,
221        y,
222        "Cone\nMeshFactory::cone(32)",
223        cone,
224        [1.0, 1.0, 1.0],
225        [1.0, 1.0, 1.0, 1.0],
226    );
227    spawn_labeled_mesh(
228        &mut universe,
229        x0 + dx * 6.0,
230        y,
231        "Circle2D\nMeshFactory::circle_2d(0.45, 0.5, 64)",
232        circle,
233        [1.0, 1.0, 1.0],
234        [1.0, 1.0, 1.0, 1.0],
235    );
236
237    // Process init-time registrations (Text expands into glyph subtrees here).
238    universe.systems.process_commands(
239        &mut universe.world,
240        &mut universe.visuals,
241        &mut universe.render_assets,
242        &mut universe.command_queue,
243    );
244
245    engine::Windowing::run_app(universe).expect("Windowing failed");
246}
examples/animation-for-topology.rs (line 137)
80fn main() {
81    mittens_engine::example_support::ensure_model_assets();
82    utils::logger::init();
83
84    let world = engine::ecs::World::default();
85    let mut universe = engine::Universe::new(world);
86
87    // Minimal scene with a camera so the window opens.
88    let clear = universe
89        .world
90        .add_component(engine::ecs::component::BackgroundColorComponent::new());
91    let clear_c = universe
92        .world
93        .add_component(engine::ecs::component::ColorComponent::rgba(
94            0.06, 0.06, 0.07, 1.0,
95        ));
96    let _ = universe.world.add_child(clear, clear_c);
97    universe.add(clear);
98
99    // Input-driven camera rig.
100    let input = universe
101        .world
102        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
103    let rig_transform = universe.world.add_component(
104        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 9.0),
105    );
106    let input_mode = universe.world.add_component(
107        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
108    );
109    let camera3d = universe.world.add_component(
110        engine::ecs::component::Camera3DComponent::new()
111            .with_far(250.0)
112            .with_fov(70.0),
113    );
114    let _ = universe.attach(input, input_mode);
115    let _ = universe.attach(input, rig_transform);
116    let _ = universe.attach(rig_transform, camera3d);
117
118    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
119    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
120    universe.add(input);
121
122    // Light so we can see non-emissive materials.
123    let light_tx = universe.world.add_component(
124        engine::ecs::component::TransformComponent::new().with_position(2.0, 3.5, 6.0),
125    );
126    let light = universe.world.add_component(
127        engine::ecs::component::PointLightComponent::new()
128            .with_distance(30.0)
129            .with_color(1.0, 1.0, 1.0),
130    );
131    let _ = universe.attach(light_tx, light);
132    universe.add(light_tx);
133
134    // ClockComponent drives the animation timeline in beats.
135    let clock = universe
136        .world
137        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(140.0));
138    universe.add(clock);
139
140    // Root for all visualization objects.
141    let viz_root = universe.world.add_component(
142        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
143    );
144    universe.add(viz_root);
145
146    let anchor_count = 16usize;
147    let cube_pool_size = 8usize;
148
149    // Three grids side-by-side.
150    let layout_a = GridLayout {
151        origin: (-6.0, 0.0, 0.0),
152        spacing: 1.1,
153    };
154    let layout_b = GridLayout {
155        origin: (0.0, 0.0, 0.0),
156        spacing: 1.1,
157    };
158    let layout_c = GridLayout {
159        origin: (6.0, 0.0, 0.0),
160        spacing: 1.1,
161    };
162
163    // --- Grid A: detach + re-attach (reparent) ---
164    let grid_a_root = universe
165        .world
166        .add_component(engine::ecs::component::TransformComponent::new());
167    let _ = universe.attach(viz_root, grid_a_root);
168
169    let mut anchors_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
170    for i in 0..anchor_count {
171        let (x, y, z) = grid_anchor_local(layout_a, i);
172        let anchor = universe.world.add_component(
173            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
174        );
175        let _ = universe.attach(grid_a_root, anchor);
176        anchors_a.push(anchor);
177        spawn_emissive_marker_cube(
178            &mut universe,
179            anchor,
180            (0.0, -0.35, 0.0),
181            0.06,
182            [0.18, 0.18, 0.22, 1.0],
183        );
184    }
185
186    let mut cubes_a: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
187    for i in 0..cube_pool_size {
188        let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
189        let rgba = [0.10, 0.40 + 0.50 * t, 0.90 - 0.70 * t, 1.0];
190        cubes_a.push(spawn_detached_cube_prefab(&mut universe, 0.22, rgba));
191    }
192
193    let anim_a = universe
194        .world
195        .add_component(engine::ecs::component::AnimationComponent::new());
196    for i in 0..anchor_count {
197        let cube = cubes_a[i % cube_pool_size];
198        let parent = anchors_a[i];
199
200        // Explicit detach+attach to test topology changes via animations.
201
202        // Put them on separate keyframes so ordering is time-deterministic.
203        let beat_detach = i as f64;
204        let beat_attach = i as f64 + 0.05;
205
206        let kf_detach = universe
207            .world
208            .add_component(engine::ecs::component::KeyframeComponent::new(beat_detach));
209        let _ = universe.attach(anim_a, kf_detach);
210        let detach_action =
211            universe
212                .world
213                .add_component(engine::ecs::component::ActionComponent::new(
214                    engine::ecs::IntentValue::Detach {
215                        component_ids: vec![cube],
216                    },
217                ));
218        let _ = universe.attach(kf_detach, detach_action);
219
220        let kf_attach = universe
221            .world
222            .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
223        let _ = universe.attach(anim_a, kf_attach);
224        let attach_action =
225            universe
226                .world
227                .add_component(engine::ecs::component::ActionComponent::new(
228                    engine::ecs::IntentValue::Attach {
229                        parents: vec![parent],
230                        child: cube,
231                    },
232                ));
233        let _ = universe.attach(kf_attach, attach_action);
234    }
235    universe.add(anim_a);
236
237    // --- Grid B: continuously spawn (attach_clone) + delete-behind (remove_child) ---
238    //
239    // Uses the new actions:
240    // - `Action::attach_clone(parent, prefab_root)`
241    // - `Action::remove_child(parent, index)`
242    //
243    // This avoids needing a pre-built pool of cube ComponentIds.
244    let grid_b_root = universe
245        .world
246        .add_component(engine::ecs::component::TransformComponent::new());
247    let _ = universe.attach(viz_root, grid_b_root);
248
249    let mut anchors_b: Vec<engine::ecs::ComponentId> = Vec::with_capacity(anchor_count);
250    for i in 0..anchor_count {
251        let (x, y, z) = grid_anchor_local(layout_b, i);
252        let anchor = universe.world.add_component(
253            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
254        );
255        let _ = universe.attach(grid_b_root, anchor);
256        anchors_b.push(anchor);
257
258        // Marker is attached under the grid root (not under the anchor), so anchor child index 0
259        // remains reserved for the dynamic cube subtree.
260        spawn_emissive_marker_cube(
261            &mut universe,
262            grid_b_root,
263            (x, y - 0.35, z),
264            0.06,
265            [0.22, 0.18, 0.18, 1.0],
266        );
267    }
268
269    // Detached prefab that will be cloned on demand (reddish cube).
270    let prefab_b = spawn_detached_cube_prefab(&mut universe, 0.20, [0.90, 0.25, 0.15, 1.0]);
271
272    let steps_b = 32usize;
273    let window = 8usize;
274
275    let anim_b = universe
276        .world
277        .add_component(engine::ecs::component::AnimationComponent::new());
278
279    // Phase 1: each beat, spawn a cube under an anchor; delete-behind by removing child(0).
280    for i in 0..steps_b {
281        let parent = anchors_b[i % anchor_count];
282
283        let beat_attach = i as f64;
284        let beat_remove = i as f64 + 0.05;
285
286        let kf_attach = universe
287            .world
288            .add_component(engine::ecs::component::KeyframeComponent::new(beat_attach));
289        let _ = universe.attach(anim_b, kf_attach);
290
291        let attach_action =
292            universe
293                .world
294                .add_component(engine::ecs::component::ActionComponent::new(
295                    engine::ecs::IntentValue::AttachClone {
296                        parents: vec![parent],
297                        prefab_root: prefab_b,
298                    },
299                ));
300        let _ = universe.attach(kf_attach, attach_action);
301
302        if i >= window {
303            let remove_parent = anchors_b[(i - window) % anchor_count];
304            let kf_remove = universe
305                .world
306                .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
307            let _ = universe.attach(anim_b, kf_remove);
308
309            let remove_action =
310                universe
311                    .world
312                    .add_component(engine::ecs::component::ActionComponent::new(
313                        engine::ecs::IntentValue::RemoveChild {
314                            parents: vec![remove_parent],
315                            index: 0,
316                        },
317                    ));
318            let _ = universe.attach(kf_remove, remove_action);
319        }
320    }
321
322    // Phase 2: cleanup tail so the loop doesn't accumulate cubes.
323    for j in 0..window {
324        let remove_parent = anchors_b[(steps_b - window + j) % anchor_count];
325        let beat_remove = steps_b as f64 + j as f64 + 0.05;
326
327        let kf_remove = universe
328            .world
329            .add_component(engine::ecs::component::KeyframeComponent::new(beat_remove));
330        let _ = universe.attach(anim_b, kf_remove);
331
332        let remove_action =
333            universe
334                .world
335                .add_component(engine::ecs::component::ActionComponent::new(
336                    engine::ecs::IntentValue::RemoveChild {
337                        parents: vec![remove_parent],
338                        index: 0,
339                    },
340                ));
341        let _ = universe.attach(kf_remove, remove_action);
342    }
343
344    universe.add(anim_b);
345
346    // --- Grid C: move cubes via set_position (no topology changes) ---
347    let grid_c_root = universe
348        .world
349        .add_component(engine::ecs::component::TransformComponent::new());
350    let _ = universe.attach(viz_root, grid_c_root);
351
352    let mut anchors_c: Vec<(f32, f32, f32)> = Vec::with_capacity(anchor_count);
353    for i in 0..anchor_count {
354        let (x, y, z) = grid_anchor_local(layout_c, i);
355        anchors_c.push((x, y, z));
356
357        let anchor = universe.world.add_component(
358            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
359        );
360        let _ = universe.attach(grid_c_root, anchor);
361        spawn_emissive_marker_cube(
362            &mut universe,
363            anchor,
364            (0.0, -0.35, 0.0),
365            0.06,
366            [0.18, 0.22, 0.18, 1.0],
367        );
368    }
369
370    let mut cubes_c: Vec<engine::ecs::ComponentId> = Vec::with_capacity(cube_pool_size);
371    for i in 0..cube_pool_size {
372        let t = (i as f32) / ((cube_pool_size - 1) as f32).max(1.0);
373        let rgba = [0.25, 0.85 - 0.55 * t, 0.25 + 0.55 * t, 1.0];
374        let cube_root = spawn_detached_cube_prefab(&mut universe, 0.22, rgba);
375        let _ = universe.attach(grid_c_root, cube_root);
376        cubes_c.push(cube_root);
377    }
378
379    let anim_c = universe
380        .world
381        .add_component(engine::ecs::component::AnimationComponent::new());
382    for i in 0..anchor_count {
383        let kf = universe
384            .world
385            .add_component(engine::ecs::component::KeyframeComponent::new(i as f64));
386        let _ = universe.attach(anim_c, kf);
387
388        let cube = cubes_c[i % cube_pool_size];
389        let (x, y, z) = anchors_c[i];
390        let setpos_action =
391            universe
392                .world
393                .add_component(engine::ecs::component::ActionComponent::new(
394                    engine::ecs::IntentValue::SetPosition {
395                        component_ids: vec![cube],
396                        position: [x, y, z],
397                    },
398                ));
399        let _ = universe.attach(kf, setpos_action);
400    }
401    universe.add(anim_c);
402
403    universe.systems.process_commands(
404        &mut universe.world,
405        &mut universe.visuals,
406        &mut universe.render_assets,
407        &mut universe.command_queue,
408    );
409
410    engine::Windowing::run_app(universe).expect("Windowing failed");
411}
examples/vtuber-joints-example.rs (line 24)
14fn main() {
15    mittens_engine::example_support::ensure_model_assets();
16    utils::logger::init();
17
18    let world = engine::ecs::World::default();
19    let mut universe = engine::Universe::new(world);
20
21    // Slow the global beat clock so beat-based animations run half as fast.
22    let clock = universe
23        .world
24        .add_component(ClockComponent::new().with_bpm(60.0));
25    universe.add(clock);
26
27    // Light pink background.
28    let background = universe
29        .world
30        .add_component(BackgroundColorComponent::new());
31    let background_c = universe
32        .world
33        .add_component(ColorComponent::rgba(1.0, 0.82, 0.90, 1.0));
34    let _ = universe.world.add_child(background, background_c);
35    universe.add(background);
36
37    // Small ambient so shadowed areas aren't pure black.
38    let ambient = universe
39        .world
40        .add_component(AmbientLightComponent::rgb(0.10, 0.10, 0.12));
41    universe.add(ambient);
42
43    // --- Camera rig (WASD + mouse) ---
44    let input = universe
45        .world
46        .add_component(InputComponent::new().with_speed(1.5));
47    let input_mode = universe.world.add_component(
48        InputTransformModeComponent::forward_z()
49            .with_fps_rotation()
50            .with_roll_axis_y(),
51    );
52    let _ = universe.attach(input, input_mode);
53
54    // Start slightly pulled back looking towards the origin.
55    let rig_transform = universe
56        .world
57        .add_component(TransformComponent::new().with_position(0.0, 0.0, 6.0));
58    let _ = universe.attach(input, rig_transform);
59
60    let camera3d = universe.world.add_component(Camera3DComponent::new());
61    let _ = universe.attach(rig_transform, camera3d);
62
63    // Pointer so gizmos can be interacted with.
64    let pointer = universe.world.add_component(PointerComponent::new());
65    let _ = universe.attach(camera3d, pointer);
66
67    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
68    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
69
70    universe.add(input);
71
72    // --- lighting ---
73    let sun = universe.world.add_component(
74        DirectionalLightComponent::new()
75            .with_intensity(1.2)
76            .with_color(1.0, 0.98, 0.94),
77    );
78    let sun_dir = universe
79        .world
80        .add_component(TransformComponent::new().with_position(0.0, -0.35, 1.0));
81    let _ = universe.attach(sun_dir, sun);
82    universe.add(sun_dir);
83
84    let light_transform = universe.world.add_component(
85        TransformComponent::new()
86            .with_position(1.0, 6.0, 3.0)
87            .with_scale(0.1, 0.1, 0.1),
88    );
89    let light = universe.world.add_component(
90        engine::ecs::component::PointLightComponent::new()
91            .with_distance(120.0)
92            .with_color(1.0, 1.0, 1.0),
93    );
94    let _ = universe.attach(light_transform, light);
95    universe.add(light_transform);
96
97    // --- VTuber model ---
98    let model_uri = "assets/models/pc-rei.hoodie.glb";
99
100    // Wrap the model subtree in an editor root so transform-only glTF nodes can be visualized
101    // (and thus raycasted/selected) without affecting non-editor scenes.
102    let editor_root = universe.world.add_component(EditorComponent::new());
103
104    let model_root = universe.world.add_component(TransformComponent::new());
105    let model = universe.world.add_component(GLTFComponent::new(model_uri));
106
107    // emissive for pc-rei
108    let emissive = universe.world.add_component(EmissiveComponent::on());
109    let _ = universe.attach(model, emissive);
110
111    let xr_input = universe.world.add_component(InputXRComponent::on());
112    let xr_gamepad = universe
113        .world
114        .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
115    let xr_head = universe.world.add_component(TransformComponent::new());
116    let xr_camera = universe.world.add_component(CameraXRComponent::on());
117    let _ = universe.attach(xr_input, xr_head);
118    let _ = universe.attach(xr_input, xr_gamepad);
119    let _ = universe.attach(xr_head, xr_camera);
120    let xr_pointer = universe.world.add_component(PointerComponent::new());
121    let _ = universe.attach(xr_camera, xr_pointer);
122    let _ = universe.attach(xr_head, editor_root);
123
124    let _ = universe.attach(model_root, model);
125
126    let _ = universe.attach(editor_root, model_root);
127
128    // Initialize the editor root so GLTFComponent gets registered.
129    universe.add(xr_input);
130    universe.add(editor_root);
131
132    // --- Background clouds (occluded + lit) ---
133    let bg_root = universe.world.add_component(
134        engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
135    );
136    universe.add(bg_root);
137    let mut cloud_params = example_util::CloudRingParams::default();
138    cloud_params.cloud_count = 8; // +3 clusters
139    cloud_params.angle_jitter = 0.35;
140    cloud_params.high_y_probability = 0.5;
141    cloud_params.high_y_multiplier = 1.5;
142    cloud_params.seed = 0x57_55_B0_01u32;
143    example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
144
145    // --- Simple environment ---
146    let spawn_cube = |universe: &mut engine::Universe,
147                      position: (f32, f32, f32),
148                      scale: (f32, f32, f32),
149                      color: (f32, f32, f32, f32)| {
150        let transform = universe.world.add_component(
151            TransformComponent::new()
152                .with_position(position.0, position.1, position.2)
153                .with_scale(scale.0, scale.1, scale.2),
154        );
155        let renderable = universe.world.add_component(RenderableComponent::cube());
156        let color = universe
157            .world
158            .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
159
160        let _ = universe.attach(transform, renderable);
161        let _ = universe.attach(renderable, color);
162
163        universe.add(transform);
164    };
165
166    // floor
167    spawn_cube(
168        &mut universe,
169        (0.0, -0.05, 0.0),
170        (10.0, 0.1, 10.0),
171        (0.92, 0.92, 0.92, 1.0),
172    );
173
174    // back wall
175    spawn_cube(
176        &mut universe,
177        (-3.0, 1.5, -5.0),
178        (3.0, 3.0, 1.0),
179        (0.95, 0.94, 0.96, 1.0),
180    );
181
182    // desk
183    spawn_cube(
184        &mut universe,
185        (0.0, 0.35, 1.0),
186        (1.0, 0.75, 0.5),
187        (0.75, 0.70, 0.65, 1.0),
188    );
189
190    // --- Editor-side stacked cubes (inside the editor subtree for picking/gizmos) ---
191    {
192        let spawn_editor_cube = |universe: &mut engine::Universe,
193                                 editor_root: engine::ecs::ComponentId,
194                                 name: &str,
195                                 position: (f32, f32, f32),
196                                 scale: (f32, f32, f32),
197                                 color: (f32, f32, f32, f32)| {
198            let transform = universe.world.add_component_boxed_named(
199                format!("{name}_t"),
200                Box::new(
201                    TransformComponent::new()
202                        .with_position(position.0, position.1, position.2)
203                        .with_scale(scale.0, scale.1, scale.2),
204                ),
205            );
206            let renderable = universe.world.add_component_boxed_named(
207                format!("{name}_r"),
208                Box::new(RenderableComponent::cube()),
209            );
210            let color_comp = universe.world.add_component_boxed_named(
211                format!("{name}_color"),
212                Box::new(ColorComponent::rgba(color.0, color.1, color.2, color.3)),
213            );
214            let raycastable = universe.world.add_component_boxed_named(
215                format!("{name}_raycastable"),
216                Box::new(RaycastableComponent::enabled()),
217            );
218
219            let _ = universe.world.add_child(transform, renderable);
220            let _ = universe.world.add_child(renderable, color_comp);
221            let _ = universe.world.add_child(renderable, raycastable);
222
223            // One attach into the initialized editor subtree triggers init for the new subtree.
224            let _ = universe.attach(editor_root, transform);
225        };
226
227        // Place the stack beside the desk (a bit to the right).
228        let stack_x = 1.35;
229        let stack_z = 1.0;
230        let s = 0.25;
231        let half = 0.5 * s;
232        let light_brown = (0.80, 0.72, 0.55, 1.0);
233        let cyan = (0.20, 1.00, 1.00, 1.0);
234
235        spawn_editor_cube(
236            &mut universe,
237            editor_root,
238            "editor_stack_0",
239            (stack_x, half, stack_z),
240            (s, s, s),
241            light_brown,
242        );
243        spawn_editor_cube(
244            &mut universe,
245            editor_root,
246            "editor_stack_1",
247            (stack_x, half + 1.0 * s, stack_z),
248            (s, s, s),
249            light_brown,
250        );
251        spawn_editor_cube(
252            &mut universe,
253            editor_root,
254            "editor_stack_2",
255            (stack_x, half + 2.0 * s, stack_z),
256            (s, s, s),
257            light_brown,
258        );
259        spawn_editor_cube(
260            &mut universe,
261            editor_root,
262            "editor_stack_top",
263            (stack_x, half + 3.0 * s, stack_z),
264            (s, s, s),
265            cyan,
266        );
267    }
268
269    let xr_root = universe
270        .world
271        .add_component(engine::ecs::component::XrComponent::on());
272    universe.add(xr_root);
273
274    universe.systems.process_commands(
275        &mut universe.world,
276        &mut universe.visuals,
277        &mut universe.render_assets,
278        &mut universe.command_queue,
279    );
280
281    // Spawn the glTF subtree once up-front so joint ComponentIds exist.
282    {
283        let systems = &mut universe.systems;
284        systems.gltf.tick_with_queue(
285            &mut universe.world,
286            &mut universe.visuals,
287            &mut systems.skinned_mesh,
288            &mut universe.command_queue,
289            0.0,
290        );
291    }
292    universe.systems.process_commands(
293        &mut universe.world,
294        &mut universe.visuals,
295        &mut universe.render_assets,
296        &mut universe.command_queue,
297    );
298
299    // Register imported meshes into RenderAssets early so we can inspect skin weights
300    // (textures will still be uploaded later during normal rendering).
301    universe
302        .systems
303        .gltf
304        .flush_mesh_imports_only(&mut universe.render_assets);
305
306    // --- Joint printout + animation binding (in the example) ---
307    let all_joints = collect_joint_transforms(
308        &universe.world,
309        &universe.systems.skinned_mesh,
310        &universe.visuals,
311        model,
312        model_root,
313    );
314    println!("[vtuber-joints-example] joints found: {}", all_joints.len());
315    for (i, (node_index, joint_tx)) in all_joints.iter().enumerate() {
316        println!("  joint[{i:03}] node_index={node_index} transform={joint_tx:?}");
317    }
318
319    let node_index_to_transform: HashMap<usize, engine::ecs::ComponentId> =
320        all_joints.iter().copied().collect();
321
322    // Example settings are hardcoded to keep this example simple.
323    let joint_offset: usize = 0;
324    let wiggle_count: usize = 16;
325
326    let target_mesh_key = "pc-rei.hoodie:Body_(merged).baked:prim0".to_string();
327    let target_joint_names: Vec<String> = vec![
328        "J_Bip_L_UpperArm".to_string(),
329        "J_Bip_R_UpperArm".to_string(),
330    ];
331
332    let print_transform_updates: bool = false;
333
334    let selected_joint_transforms: Vec<(usize, engine::ecs::ComponentId)> =
335        select_named_joints(&universe.world, &all_joints, &target_joint_names)
336            .or_else(|| {
337                select_body_prim0_influencers(
338                    &universe,
339                    model_root,
340                    &target_mesh_key,
341                    wiggle_count,
342                    &node_index_to_transform,
343                )
344            })
345            .unwrap_or_else(|| select_joint_range(&all_joints, joint_offset, wiggle_count));
346    println!(
347        "[vtuber-joints-example] wiggle selection: target_mesh_key='{}' offset={} count={} selected={}",
348        target_mesh_key,
349        joint_offset,
350        wiggle_count,
351        selected_joint_transforms.len()
352    );
353
354    debug_print_selected_joint_influence(
355        &universe,
356        &target_mesh_key,
357        model_root,
358        &selected_joint_transforms,
359    );
360
361    println!("[vtuber-joints-example] selected joints:");
362    for (i, (node_index, joint_tx)) in selected_joint_transforms.iter().enumerate() {
363        let name = universe
364            .world
365            .get_component_record(*joint_tx)
366            .map(|n| n.name.as_str())
367            .unwrap_or("<unknown>");
368        println!("  sel[{i:02}] node_index={node_index} name={name} transform={joint_tx:?}");
369    }
370
371    if print_transform_updates {
372        println!("[vtuber-joints-example] note: joint animation disabled");
373    }
374
375    universe.systems.process_commands(
376        &mut universe.world,
377        &mut universe.visuals,
378        &mut universe.render_assets,
379        &mut universe.command_queue,
380    );
381
382    universe.enable_repl();
383    engine::Windowing::run_app(universe).expect("Windowing failed");
384}
examples/animation-example.rs (line 63)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    let world = engine::ecs::World::default();
11    let mut universe = engine::Universe::new(world);
12
13    // Minimal scene with a camera so the window opens.
14    let clear = universe
15        .world
16        .add_component(engine::ecs::component::BackgroundColorComponent::new());
17    let clear_c = universe
18        .world
19        .add_component(engine::ecs::component::ColorComponent::rgba(
20            0.08, 0.08, 0.08, 1.0,
21        ));
22    let _ = universe.world.add_child(clear, clear_c);
23    universe.add(clear);
24
25    // Input-driven camera rig.
26    let input = universe
27        .world
28        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
29    let rig_transform = universe.world.add_component(
30        engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
31    );
32    let input_mode = universe.world.add_component(
33        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34    );
35    let camera3d = universe.world.add_component(
36        engine::ecs::component::Camera3DComponent::new()
37            .with_far(250.0)
38            .with_fov(70.0),
39    );
40    let _ = universe.attach(input, input_mode);
41    let _ = universe.attach(input, rig_transform);
42    let _ = universe.attach(rig_transform, camera3d);
43
44    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
45    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
46    universe.add(input);
47
48    // Light so we can see non-emissive materials (even though our cubes are emissive).
49    let light_tx = universe.world.add_component(
50        engine::ecs::component::TransformComponent::new().with_position(0.0, 2.5, 4.0),
51    );
52    let light = universe.world.add_component(
53        engine::ecs::component::PointLightComponent::new()
54            .with_distance(25.0)
55            .with_color(1.0, 1.0, 1.0),
56    );
57    let _ = universe.attach(light_tx, light);
58    universe.add(light_tx);
59
60    // ClockComponent sets global tempo.
61    let clock = universe
62        .world
63        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
64    universe.add(clock);
65
66    // Audio output + oscillators driven by scheduled actions.
67    let audio_out = universe
68        .world
69        .add_component(engine::ecs::component::AudioOutputComponent::new());
70    universe.add(audio_out);
71
72    // Noise oscillator: short white-noise hits.
73    let osc_noise = engine::ecs::component::AudioOscillator::noise()
74        .with_frequency(0.0)
75        .with_amplitude(0.06)
76        .with_enabled(false);
77    let osc_noise_comp =
78        universe
79            .world
80            .add_component(engine::ecs::component::AudioOscillatorComponent::single(
81                osc_noise,
82            ));
83    let _ = universe.attach(audio_out, osc_noise_comp);
84
85    // Drum oscillator: retriggers (phase + sweep) every enable.
86    let osc_drum = engine::ecs::component::AudioOscillator::drum()
87        // A low-ish pitch scale; actual pitch is set by scheduled notes.
88        .with_frequency(32.0)
89        .with_amplitude(0.40)
90        .with_enabled(false);
91    let osc_drum_comp =
92        universe
93            .world
94            .add_component(engine::ecs::component::AudioOscillatorComponent::single(
95                osc_drum,
96            ));
97
98    let _ = universe.attach(audio_out, osc_drum_comp);
99
100    // --- Visual layout helpers ---
101    fn spawn_text(universe: &mut engine::Universe, pos: (f32, f32, f32), scale: f32, text: &str) {
102        let tx = universe.world.add_component(
103            engine::ecs::component::TransformComponent::new()
104                .with_position(pos.0, pos.1, pos.2)
105                .with_scale(scale, scale, 1.0),
106        );
107        let t =
108            universe
109                .world
110                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
111                    text, 38,
112                ));
113        let _ = universe.attach(tx, t);
114        universe.add(tx);
115    }
116
117    fn spawn_emissive_cube(
118        universe: &mut engine::Universe,
119        parent: engine::ecs::ComponentId,
120        pos: (f32, f32, f32),
121        scale: f32,
122        rgba: [f32; 4],
123    ) {
124        let tx = universe.world.add_component(
125            engine::ecs::component::TransformComponent::new()
126                .with_position(pos.0, pos.1, pos.2)
127                .with_scale(scale, scale, scale),
128        );
129        let r = universe
130            .world
131            .add_component(engine::ecs::component::RenderableComponent::cube());
132        let c = universe
133            .world
134            .add_component(engine::ecs::component::ColorComponent::rgba(
135                rgba[0], rgba[1], rgba[2], rgba[3],
136            ));
137        let e = universe
138            .world
139            .add_component(engine::ecs::component::EmissiveComponent::on());
140        let _ = universe.attach(parent, tx);
141        let _ = universe.attach(tx, r);
142        let _ = universe.attach(r, c);
143        let _ = universe.attach(r, e);
144    }
145
146    fn spawn_op_cube(
147        universe: &mut engine::Universe,
148        parent: engine::ecs::ComponentId,
149        pos: (f32, f32, f32),
150        scale: f32,
151        base_rgba: [f32; 4],
152    ) -> engine::ecs::ComponentId {
153        let tx = universe.world.add_component(
154            engine::ecs::component::TransformComponent::new()
155                .with_position(pos.0, pos.1, pos.2)
156                .with_scale(scale, scale, scale),
157        );
158        let r = universe
159            .world
160            .add_component(engine::ecs::component::RenderableComponent::cube());
161        let c = universe
162            .world
163            .add_component(engine::ecs::component::ColorComponent::rgba(
164                base_rgba[0],
165                base_rgba[1],
166                base_rgba[2],
167                base_rgba[3],
168            ));
169        let e = universe
170            .world
171            .add_component(engine::ecs::component::EmissiveComponent::on());
172
173        let _ = universe.attach(parent, tx);
174        let _ = universe.attach(tx, r);
175        let _ = universe.attach(r, c);
176        let _ = universe.attach(r, e);
177
178        tx
179    }
180
181    // --- HUD / lanes ---
182    let lane_x = -2.6_f32;
183    let lane_title_z = -0.4_f32;
184    let lane_cfg_z = -0.4_f32;
185    let _lane_cube_z = -0.8_f32;
186    let drum_y = 0.9_f32;
187    let noise_y = -0.4_f32;
188
189    spawn_text(
190        &mut universe,
191        (lane_x, drum_y + 0.55, lane_title_z),
192        0.09,
193        "Audio Source A: Drum (kick)",
194    );
195    spawn_text(
196        &mut universe,
197        (lane_x, drum_y + 0.25, lane_cfg_z),
198        0.07,
199        "type=Drum\namp=0.40\nbase_freq=32Hz\nkick: C0 dur=0.12 vel=0.90\nlookahead=0.10s",
200    );
201
202    spawn_text(
203        &mut universe,
204        (lane_x, noise_y + 0.55, lane_title_z),
205        0.09,
206        "Audio Source B: Noise",
207    );
208    spawn_text(
209        &mut universe,
210        (lane_x, noise_y + 0.25, lane_cfg_z),
211        0.07,
212        "type=Noise\namp=0.06\nnoise: C9 dur=0.06 vel=0.25\noffset=+0.5 beats\nlookahead=0.10s",
213    );
214
215    // Representative cubes for the two audio sources.
216    let viz_root = universe.world.add_component(
217        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
218    );
219    universe.add(viz_root);
220    spawn_emissive_cube(
221        &mut universe,
222        viz_root,
223        (lane_x - 0.28, drum_y + 0.55, lane_title_z),
224        0.16,
225        [1.00, 0.90, 0.10, 1.0],
226    );
227    spawn_emissive_cube(
228        &mut universe,
229        viz_root,
230        (lane_x - 0.28, noise_y + 0.55, lane_title_z),
231        0.16,
232        [1.00, 1.00, 1.00, 1.0],
233    );
234
235    // Timeline roots so we can "reset all" via a single set_color action.
236    let kick_timeline_root = universe.world.add_component(
237        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
238    );
239    let noise_timeline_root = universe.world.add_component(
240        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
241    );
242    let _ = universe.attach(viz_root, kick_timeline_root);
243    let _ = universe.attach(viz_root, noise_timeline_root);
244
245    // Animation with 16 keyframes: each keyframe schedules a kick at offset 0.0,
246    // and a noise hit at offset 0.5.
247    let anim = universe
248        .world
249        .add_component(engine::ecs::component::AnimationComponent::new());
250
251    let kick_dur = 0.12_f32;
252    let noise_dur = 0.06_f32;
253
254    // Requested palette:
255    // - kick: dark yellow -> bright yellow
256    // - noise: grey -> white
257    let kick_dark = [0.35, 0.28, 0.02, 1.0];
258    let kick_bright = [1.00, 0.90, 0.10, 1.0];
259    let noise_dark = [0.35, 0.35, 0.35, 1.0];
260    let noise_bright = [1.00, 1.00, 1.00, 1.0];
261
262    let beat_spacing = 0.38_f32;
263    for i in 0..16 {
264        let kf_beat = i as f64;
265        let kf = universe
266            .world
267            .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
268
269        let kick = universe
270            .world
271            .add_component(engine::ecs::component::ActionComponent::new(
272                engine::ecs::IntentValue::AudioSchedulePlay {
273                    component_ids: vec![osc_drum_comp],
274                    beat_offset: 0.0,
275                    beat_context: None,
276                    note: Some(
277                        engine::ecs::component::MusicNote::c(0, kick_dur).with_velocity(0.9),
278                    ),
279                    gain: None,
280                    rate: None,
281                    duration: None,
282                },
283            ));
284
285        let noise = universe
286            .world
287            .add_component(engine::ecs::component::ActionComponent::new(
288                engine::ecs::IntentValue::AudioSchedulePlay {
289                    component_ids: vec![osc_noise_comp],
290                    beat_offset: 0.5,
291                    beat_context: None,
292                    note: Some(
293                        engine::ecs::component::MusicNote::c(9, noise_dur).with_velocity(0.25),
294                    ),
295                    gain: None,
296                    rate: None,
297                    duration: None,
298                },
299            ));
300
301        let _ = universe.attach(anim, kf);
302        let _ = universe.attach(kf, kick);
303        let _ = universe.attach(kf, noise);
304
305        // Each keyframe drives visualization purely via normal actions:
306        // 1) reset all timeline cubes to their base colors
307        // 2) brighten the current beat's cubes
308        let reset_kick_lane =
309            universe
310                .world
311                .add_component(engine::ecs::component::ActionComponent::new(
312                    engine::ecs::IntentValue::SetColor {
313                        component_ids: vec![kick_timeline_root],
314                        rgba: kick_dark,
315                    },
316                ));
317        let reset_noise_lane =
318            universe
319                .world
320                .add_component(engine::ecs::component::ActionComponent::new(
321                    engine::ecs::IntentValue::SetColor {
322                        component_ids: vec![noise_timeline_root],
323                        rgba: noise_dark,
324                    },
325                ));
326        let _ = universe.attach(kf, reset_kick_lane);
327        let _ = universe.attach(kf, reset_noise_lane);
328
329        // Timeline cubes: one cube per scheduled audio operation.
330        // Kick (offset 0.0) on the drum lane.
331        let kick_x = (kf_beat as f32) * beat_spacing;
332        let kick_cube = spawn_op_cube(
333            &mut universe,
334            kick_timeline_root,
335            (kick_x, drum_y, -1.3),
336            0.10,
337            kick_dark,
338        );
339
340        // Noise (offset 0.5) on the noise lane.
341        let noise_x = ((kf_beat + 0.5) as f32) * beat_spacing;
342        let noise_cube = spawn_op_cube(
343            &mut universe,
344            noise_timeline_root,
345            (noise_x, noise_y, -1.3),
346            0.10,
347            noise_dark,
348        );
349
350        let brighten_kick =
351            universe
352                .world
353                .add_component(engine::ecs::component::ActionComponent::new(
354                    engine::ecs::IntentValue::SetColor {
355                        component_ids: vec![kick_cube],
356                        rgba: kick_bright,
357                    },
358                ));
359        let brighten_noise =
360            universe
361                .world
362                .add_component(engine::ecs::component::ActionComponent::new(
363                    engine::ecs::IntentValue::SetColor {
364                        component_ids: vec![noise_cube],
365                        rgba: noise_bright,
366                    },
367                ));
368        let _ = universe.attach(kf, brighten_kick);
369        let _ = universe.attach(kf, brighten_noise);
370    }
371
372    universe.add(anim);
373
374    universe.systems.process_commands(
375        &mut universe.world,
376        &mut universe.visuals,
377        &mut universe.render_assets,
378        &mut universe.command_queue,
379    );
380
381    engine::Windowing::run_app(universe).expect("Windowing failed");
382}
examples/audio-graph-example.rs (line 114)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    // Debug toggle: allow isolating stack overflows to audio vs. non-audio init.
11    // Set `CAT_AUDIO_EXAMPLE_DISABLE_AUDIO=1` to skip creating audio components and
12    // skip scheduling audio notes. The UI/text/cube visualization will still spawn.
13    let audio_enabled = std::env::var("CAT_AUDIO_EXAMPLE_DISABLE_AUDIO")
14        .ok()
15        .as_deref()
16        != Some("1");
17
18    // If set, we still build audio graph components but we don't start the CPAL output stream.
19    // This helps distinguish "audio graph / scheduling" issues from "CPAL backend" issues.
20    let audio_output_enabled = std::env::var("CAT_AUDIO_EXAMPLE_AUDIO_OUTPUT_OFF")
21        .ok()
22        .as_deref()
23        != Some("1");
24
25    println!("[audio-graph-example] start");
26
27    let world = engine::ecs::World::default();
28    let mut universe = engine::Universe::new(world);
29
30    println!("[audio-graph-example] universe created");
31
32    // Minimal scene with a camera so the window opens (copied from animation-example).
33    let clear = universe
34        .world
35        .add_component(engine::ecs::component::BackgroundColorComponent::new());
36    let clear_c = universe
37        .world
38        .add_component(engine::ecs::component::ColorComponent::rgba(
39            0.07, 0.07, 0.07, 1.0,
40        ));
41    let _ = universe.world.add_child(clear, clear_c);
42    universe.add(clear);
43
44    // Ambient light so unlit areas aren't pitch black.
45    // Keep it dark to match the background clear color.
46    // (User request) 2.5x brighter.
47    let ambient = universe
48        .world
49        .add_component(engine::ecs::component::AmbientLightComponent::rgb(
50            0.075, 0.075, 0.075,
51        ));
52    universe.add(ambient);
53
54    // Input-driven camera rig.
55    let input = universe
56        .world
57        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
58    let rig_transform = universe.world.add_component(
59        engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
60    );
61    let input_mode = universe.world.add_component(
62        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
63    );
64    let camera3d = universe.world.add_component(
65        engine::ecs::component::Camera3DComponent::new()
66            .with_far(250.0)
67            .with_fov(70.0),
68    );
69    let _ = universe.attach(input, input_mode);
70    let _ = universe.attach(input, rig_transform);
71    let _ = universe.attach(rig_transform, camera3d);
72
73    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
74    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
75    universe.add(input);
76
77    // Directional light (sun-ish). Note: the renderer interprets the node's world position
78    // as a direction vector (see DirectionalLightComponent docs).
79    let light_tx = universe.world.add_component(
80        engine::ecs::component::TransformComponent::new().with_position(0.2, 0.7, 1.0),
81    );
82    let light = universe.world.add_component(
83        engine::ecs::component::DirectionalLightComponent::new()
84            .with_color(1.0, 1.0, 1.0)
85            .with_intensity(0.35),
86    );
87    let _ = universe.attach(light_tx, light);
88    universe.add(light_tx);
89
90    // --- Background clouds (occluded + lit) ---
91    // Mirrors the vtuber example: use a BackgroundComponent stage so the cloud volume
92    // self-occludes and is lit, but renders as background.
93    let bg_root = universe.world.add_component(
94        engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
95    );
96    universe.add(bg_root);
97    let mut cloud_params = example_util::CloudRingParams::default();
98    cloud_params.cloud_count = 7;
99    cloud_params.radius = 22.0;
100    // Move the ring up by ~one cloud height. The cloud generator uses a ~4.0 unit
101    // vertical spread for puff offsets, so +4.0 is a good "one height" bump.
102    cloud_params.center_y = 6.0;
103    cloud_params.puffs_per_cloud = 26;
104    cloud_params.angle_jitter = 0.0;
105    cloud_params.high_y_probability = 0.0;
106    cloud_params.high_y_multiplier = 1.0;
107    cloud_params.seed = 0xA0_D1_0C_01u32;
108    example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
109
110    // ClockComponent sets global tempo.
111    if audio_enabled {
112        let clock = universe
113            .world
114            .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
115        universe.add(clock);
116    }
117
118    if audio_enabled {
119        println!("[audio-graph-example] clock added");
120    } else {
121        println!("[audio-graph-example] audio disabled; skipping clock");
122    }
123
124    // Audio output + 2 oscillator sources.
125    let audio_out = if audio_enabled {
126        let audio_out_comp = if audio_output_enabled {
127            engine::ecs::component::AudioOutputComponent::new()
128        } else {
129            engine::ecs::component::AudioOutputComponent::off()
130        };
131        let audio_out = universe.world.add_component(audio_out_comp);
132        universe.add(audio_out);
133        Some(audio_out)
134    } else {
135        None
136    };
137
138    match (audio_enabled, audio_output_enabled) {
139        (false, _) => println!("[audio-graph-example] audio disabled; skipping audio output"),
140        (true, true) => println!("[audio-graph-example] audio output added (CPAL on)"),
141        (true, false) => println!("[audio-graph-example] audio output added (CPAL off)"),
142    }
143
144    // Track A: square lead.
145    let osc_a_comp =
146        if audio_enabled {
147            let osc_a = engine::ecs::component::AudioOscillator::square()
148                .with_frequency(110.0)
149                .with_amplitude(0.12)
150                .with_enabled(false);
151            let osc_a_comp = universe.world.add_component(
152                engine::ecs::component::AudioOscillatorComponent::single(osc_a),
153            );
154            if let Some(audio_out) = audio_out {
155                let _ = universe.attach(audio_out, osc_a_comp);
156            }
157            Some(osc_a_comp)
158        } else {
159            None
160        };
161
162    if audio_enabled {
163        println!("[audio-graph-example] track A created");
164    } else {
165        println!("[audio-graph-example] audio disabled; skipping track A");
166    }
167
168    // Effect tree A (single chain):
169    // osc_a
170    //   Gain
171    //     BandPass
172    //       Limiter
173    let mut bp_a_comp: Option<engine::ecs::ComponentId> = None;
174    if let Some(osc_a_comp) = osc_a_comp {
175        let gain_a = universe
176            .world
177            .add_component(engine::ecs::component::AudioGainComponent::new(3.2));
178        let bp_a = universe.world.add_component(
179            engine::ecs::component::AudioBandPassFilterComponent::new(120.0, 3.0, 0.40),
180        );
181        bp_a_comp = Some(bp_a);
182        let lim_a =
183            universe
184                .world
185                .add_component(engine::ecs::component::AudioLimiterComponent::new(
186                    4.0, 80.0, 0.90,
187                ));
188
189        let _ = universe.attach(osc_a_comp, gain_a);
190        let _ = universe.attach(gain_a, bp_a);
191        let _ = universe.attach(bp_a, lim_a);
192    }
193
194    if audio_enabled {
195        println!("[audio-graph-example] track A effects attached");
196    }
197
198    // --- Visual layout helpers (copied/adapted from animation-example) ---
199    fn spawn_text(
200        universe: &mut engine::Universe,
201        pos: (f32, f32, f32),
202        scale: f32,
203        wrap_cols: usize,
204        text: &str,
205    ) {
206        let tx = universe.world.add_component(
207            engine::ecs::component::TransformComponent::new()
208                .with_position(pos.0, pos.1, pos.2)
209                .with_scale(scale, scale, 1.0),
210        );
211        let t =
212            universe
213                .world
214                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
215                    text, wrap_cols,
216                ));
217        let _ = universe.attach(tx, t);
218
219        // TextSystem looks for an immediate TextureFilteringComponent child.
220        let filtering = universe
221            .world
222            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
223        let _ = universe.attach(t, filtering);
224
225        // TextSystem also supports styling from immediate Emissive children.
226        let emissive = universe
227            .world
228            .add_component(engine::ecs::component::EmissiveComponent::on());
229        let _ = universe.attach(t, emissive);
230
231        universe.add(tx);
232    }
233
234    fn spawn_emissive_cube(
235        universe: &mut engine::Universe,
236        parent: engine::ecs::ComponentId,
237        pos: (f32, f32, f32),
238        scale: f32,
239        rgba: [f32; 4],
240    ) {
241        let tx = universe.world.add_component(
242            engine::ecs::component::TransformComponent::new()
243                .with_position(pos.0, pos.1, pos.2)
244                .with_scale(scale, scale, scale),
245        );
246        let r = universe
247            .world
248            .add_component(engine::ecs::component::RenderableComponent::cube());
249        let c = universe
250            .world
251            .add_component(engine::ecs::component::ColorComponent::rgba(
252                rgba[0], rgba[1], rgba[2], rgba[3],
253            ));
254        let e = universe
255            .world
256            .add_component(engine::ecs::component::EmissiveComponent::on());
257        let _ = universe.attach(parent, tx);
258        let _ = universe.attach(tx, r);
259        let _ = universe.attach(r, c);
260        let _ = universe.attach(r, e);
261    }
262
263    fn spawn_op_cube(
264        universe: &mut engine::Universe,
265        parent: engine::ecs::ComponentId,
266        pos: (f32, f32, f32),
267        scale: f32,
268        base_rgba: [f32; 4],
269    ) -> engine::ecs::ComponentId {
270        let tx = universe.world.add_component(
271            engine::ecs::component::TransformComponent::new()
272                .with_position(pos.0, pos.1, pos.2)
273                .with_scale(scale, scale, scale),
274        );
275        let r = universe
276            .world
277            .add_component(engine::ecs::component::RenderableComponent::cube());
278        let c = universe
279            .world
280            .add_component(engine::ecs::component::ColorComponent::rgba(
281                base_rgba[0],
282                base_rgba[1],
283                base_rgba[2],
284                base_rgba[3],
285            ));
286        let e = universe
287            .world
288            .add_component(engine::ecs::component::EmissiveComponent::on());
289
290        let _ = universe.attach(parent, tx);
291        let _ = universe.attach(tx, r);
292        let _ = universe.attach(r, c);
293        let _ = universe.attach(r, e);
294
295        tx
296    }
297
298    // --- HUD / lane (single track) ---
299    let lane_x = -2.6_f32;
300    let lane_title_z = -0.4_f32;
301    let lane_cfg_z = -0.4_f32;
302    let lane_pat_z = -1.3_f32;
303    let lane_graph_z = -1.15_f32;
304
305    // Content for pattern/chain/graph starts at x=0; keep section labels aligned there.
306    // This also keeps them from overlapping the config block (which is anchored at lane_x).
307    let lane_labels_x = lane_x + 1.6_f32;
308
309    let track_a_y = 0.9_f32;
310
311    spawn_text(
312        &mut universe,
313        (lane_x, track_a_y + 0.55, lane_title_z),
314        0.09,
315        42,
316        "Track A: AudioOscillator::square()",
317    );
318    spawn_text(
319        &mut universe,
320        (lane_x, track_a_y + 0.25, lane_cfg_z),
321        0.07,
322        48,
323        "oscillators=1\nfrequency_hz=110.0\namplitude=0.12\nenabled=false\nlookahead=0.10s",
324    );
325
326    let viz_root = universe.world.add_component(
327        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
328    );
329    universe.add(viz_root);
330
331    println!("[audio-graph-example] viz root added");
332
333    // Small identifier cubes next to titles.
334    spawn_emissive_cube(
335        &mut universe,
336        viz_root,
337        (lane_x - 0.28, track_a_y + 0.55, lane_title_z),
338        0.16,
339        [0.85, 0.40, 1.00, 1.0],
340    );
341
342    // Pattern roots so we can reset via one SetColor action.
343    let track_a_pattern_root = universe.world.add_component(
344        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
345    );
346    let _ = universe.attach(viz_root, track_a_pattern_root);
347
348    // Labels for pattern/chain/graph sections.
349    spawn_text(
350        &mut universe,
351        (lane_labels_x, track_a_y, lane_title_z),
352        0.06,
353        42,
354        "pattern",
355    );
356    spawn_text(
357        &mut universe,
358        (lane_labels_x, track_a_y - 0.40, lane_title_z),
359        0.06,
360        42,
361        "graph",
362    );
363
364    // --- Processing chain + graph visualization (compiled graph → cubes + labels) ---
365    use engine::ecs::system::audio_graph_compiler::{
366        AudioGraphCompiler, AudioGraphNode, AudioGraphNodeKind,
367    };
368
369    fn effect_grey_for_depth(depth: usize) -> f32 {
370        // depth=1 => medium grey; deeper => lighter, capped.
371        let base = 0.55;
372        let step = 0.13;
373        let d = depth.saturating_sub(1) as f32;
374        (base + step * d).min(0.92)
375    }
376
377    fn node_rgba(node: &AudioGraphNode, depth: usize) -> [f32; 4] {
378        match node.kind {
379            AudioGraphNodeKind::OscillatorSource { .. } => [1.0, 0.78, 0.22, 1.0],
380            _ => {
381                let g = effect_grey_for_depth(depth);
382                [g, g, g, 1.0]
383            }
384        }
385    }
386
387    fn node_label(node: &AudioGraphNode) -> String {
388        match &node.kind {
389            AudioGraphNodeKind::OscillatorSource { voices } => {
390                format!("OscillatorSource voices={voices}")
391            }
392            AudioGraphNodeKind::Gain { gain } => {
393                format!("Gain gain={gain:.3}")
394            }
395            AudioGraphNodeKind::LowPass {
396                cutoff_hz,
397                resonance,
398            } => {
399                format!("LowPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
400            }
401            AudioGraphNodeKind::BandPass {
402                center_hz,
403                bandwidth_octaves,
404                resonance,
405            } => {
406                format!(
407                    "BandPass center={center_hz:.1}Hz bw={bandwidth_octaves:.3}oct res={resonance:.3}"
408                )
409            }
410            AudioGraphNodeKind::HighPass {
411                cutoff_hz,
412                resonance,
413            } => {
414                format!("HighPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
415            }
416            AudioGraphNodeKind::Limiter {
417                attack_ms,
418                release_ms,
419                threshold,
420            } => {
421                format!("Limiter atk={attack_ms:.1}ms rel={release_ms:.1}ms thr={threshold:.3}")
422            }
423            AudioGraphNodeKind::ClipSource => "ClipSource".to_string(),
424        }
425    }
426
427    fn compute_layout(
428        node: &AudioGraphNode,
429        depth: usize,
430        x_cursor: &mut i32,
431        out: &mut std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
432    ) -> i32 {
433        // Depth-only layout:
434        // - keep children directly below their parent on X
435        // - use Z sibling offsets (in spawn_graph_tree) to show branching
436        let _ = x_cursor;
437        for ch in node.children.iter() {
438            let _ = compute_layout(ch, depth + 1, x_cursor, out);
439        }
440
441        let my_x = 0;
442        out.insert(node as *const AudioGraphNode, (my_x, depth));
443        my_x
444    }
445
446    fn spawn_graph_tree(
447        universe: &mut engine::Universe,
448        parent: engine::ecs::ComponentId,
449        node: &AudioGraphNode,
450        bp_component: Option<engine::ecs::ComponentId>,
451        bp_label_out: &mut Option<engine::ecs::ComponentId>,
452        origin: (f32, f32, f32),
453        layout: &std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
454        dx: f32,
455        dy: f32,
456        cube_scale: f32,
457        sibling_index: usize,
458        sibling_count: usize,
459    ) {
460        let Some((x_unit, depth)) = layout.get(&(node as *const AudioGraphNode)).copied() else {
461            return;
462        };
463
464        let x = origin.0 + (x_unit as f32) * dx;
465        let y = origin.1 - (depth as f32) * dy;
466
467        // Push siblings “behind” each other along Z to reduce overlap between
468        // a sibling's cube and another node's label.
469        let dz_sibling = 0.18;
470        let z = origin.2 - (sibling_index as f32) * dz_sibling;
471
472        // Keep text slightly in front of its cube.
473        let z_text = z + 0.03;
474
475        let rgba = node_rgba(node, depth);
476        let cube_tx = spawn_op_cube(universe, parent, (x, y, z), cube_scale, rgba);
477        let _ = cube_tx;
478
479        // Label next to the cube.
480        let label = node_label(node);
481        let tx = universe.world.add_component(
482            engine::ecs::component::TransformComponent::new()
483                .with_position(x + 0.14, y + 0.02, z_text)
484                .with_scale(0.06, 0.06, 1.0),
485        );
486        let t =
487            universe
488                .world
489                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
490                    &label, 25,
491                ));
492        let _ = universe.attach(parent, tx);
493        let _ = universe.attach(tx, t);
494
495        if bp_component == Some(node.component) {
496            *bp_label_out = Some(t);
497        }
498
499        let filtering = universe
500            .world
501            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
502        let _ = universe.attach(t, filtering);
503
504        let emissive = universe
505            .world
506            .add_component(engine::ecs::component::EmissiveComponent::on());
507        let _ = universe.attach(t, emissive);
508
509        universe.add(tx);
510
511        // Mix/branch label if branching.
512        if node.children.len() > 1 {
513            let mut weights: Vec<f32> = Vec::with_capacity(node.children.len());
514            for i in 0..node.children.len() {
515                let w = node
516                    .mix
517                    .as_ref()
518                    .map(|m| m.weights.get(i).copied().unwrap_or(1.0))
519                    .unwrap_or(1.0);
520                weights.push(w);
521            }
522            let mix_label = if node.mix.is_some() {
523                format!("Mix weights={weights:?}")
524            } else {
525                format!("Mix <implicit> w={weights:?}")
526            };
527            let mix_tx = universe.world.add_component(
528                engine::ecs::component::TransformComponent::new()
529                    .with_position(x + 0.14, y - 0.11, z_text)
530                    .with_scale(0.05, 0.05, 1.0),
531            );
532            let mix_t = universe.world.add_component(
533                engine::ecs::component::TextComponent::with_word_wrap(&mix_label, 25),
534            );
535            let _ = universe.attach(parent, mix_tx);
536            let _ = universe.attach(mix_tx, mix_t);
537
538            let filtering = universe
539                .world
540                .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
541            let _ = universe.attach(mix_t, filtering);
542
543            let emissive = universe
544                .world
545                .add_component(engine::ecs::component::EmissiveComponent::on());
546            let _ = universe.attach(mix_t, emissive);
547
548            universe.add(mix_tx);
549        }
550
551        let child_count = node.children.len().max(1);
552        for (i, ch) in node.children.iter().enumerate() {
553            // Each node's children form a sibling group; offset them in Z.
554            // For the root node, sibling_index is 0.
555            let _ = sibling_count;
556            spawn_graph_tree(
557                universe,
558                parent,
559                ch,
560                bp_component,
561                bp_label_out,
562                origin,
563                layout,
564                dx,
565                dy,
566                cube_scale,
567                i,
568                child_count,
569            );
570        }
571    }
572
573    // Compile + display chain and graph for the track.
574    let compiled_a = if audio_enabled {
575        println!("[audio-graph-example] compiling graph...");
576        let Some(osc_a_comp) = osc_a_comp else {
577            panic!("audio_enabled but track A was not created");
578        };
579        let compiled_a =
580            AudioGraphCompiler::compile(&universe.world, osc_a_comp).expect("compile A");
581        println!("[audio-graph-example] graph compiled");
582        Some(compiled_a)
583    } else {
584        println!("[audio-graph-example] audio disabled; skipping graph compilation");
585        None
586    };
587
588    // Full compiled graph visualization (tree layout + mix labels).
589    let mut bp_graph_label_text: Option<engine::ecs::ComponentId> = None;
590    if let Some(compiled_a) = &compiled_a {
591        let mut x_cursor = 0;
592        let mut layout: std::collections::HashMap<*const AudioGraphNode, (i32, usize)> =
593            std::collections::HashMap::new();
594        let _root_x = compute_layout(&compiled_a.root, 0, &mut x_cursor, &mut layout);
595
596        // With depth-only X layout, we keep the tree centered at x=0.
597        // Pull the graph up now that we don't show the separate chain view.
598        let origin = (0.0, track_a_y - 0.40, lane_graph_z);
599
600        spawn_graph_tree(
601            &mut universe,
602            viz_root,
603            &compiled_a.root,
604            bp_a_comp,
605            &mut bp_graph_label_text,
606            origin,
607            &layout,
608            0.45,
609            0.28,
610            0.08,
611            0,
612            1,
613        );
614    } else {
615        spawn_text(
616            &mut universe,
617            (0.0, track_a_y - 0.40, lane_graph_z),
618            0.06,
619            60,
620            "(audio disabled)",
621        );
622    }
623
624    // Animation with 16 keyframes drives scheduled notes + pattern highlights.
625    let anim = universe
626        .world
627        .add_component(engine::ecs::component::AnimationComponent::new());
628
629    let dur_a = 0.85_f32;
630    let beat_spacing = 0.38_f32;
631
632    let a_dark = [0.18, 0.08, 0.22, 1.0];
633    let a_bright = [0.85, 0.40, 1.00, 1.0];
634
635    for i in 0..16 {
636        let kf_beat = i as f64;
637        let kf = universe
638            .world
639            .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
640
641        let _ = universe.attach(anim, kf);
642
643        // Scheduled audio note (sample-accurate via lookahead).
644        // Pulse every beat.
645        if audio_enabled {
646            let Some(osc_a_comp) = osc_a_comp else {
647                panic!("audio_enabled but track A was not created");
648            };
649
650            let note_a = engine::ecs::component::MusicNote::c(1, dur_a).with_velocity(0.80);
651
652            let act_a = universe
653                .world
654                .add_component(engine::ecs::component::ActionComponent::new(
655                    engine::ecs::IntentValue::AudioSchedulePlay {
656                        component_ids: vec![osc_a_comp],
657                        beat_offset: 0.0,
658                        beat_context: None,
659                        note: Some(note_a),
660                        gain: None,
661                        rate: None,
662                        duration: None,
663                    },
664                ));
665
666            let _ = universe.attach(kf, act_a);
667        }
668
669        // Keyframed band-pass center.
670        // This is an immediate parameter update applied RT-side (no graph rebuild).
671        if audio_enabled {
672            if let Some(bp_a_comp) = bp_a_comp {
673                let t = (i as f32) / 15.0;
674                let center_hz = 10.0 + t * (1000.0 - 10.0);
675
676                let bp_center =
677                    universe
678                        .world
679                        .add_component(engine::ecs::component::ActionComponent::new(
680                            engine::ecs::IntentValue::AudioBandPassSetCenterHz {
681                                component_ids: vec![bp_a_comp],
682                                center_hz,
683                            },
684                        ));
685                let _ = universe.attach(kf, bp_center);
686
687                // Update the BandPass node label in the graph visualization.
688                if let Some(text_id) = bp_graph_label_text {
689                    let label =
690                        universe
691                            .world
692                            .add_component(engine::ecs::component::ActionComponent::new(
693                                engine::ecs::IntentValue::SetText {
694                                    component_ids: vec![text_id],
695                                    text: format!("BandPass center={center_hz:.1}Hz"),
696                                },
697                            ));
698                    let _ = universe.attach(kf, label);
699                }
700            }
701        }
702
703        // Visualization reset per keyframe.
704        let reset_a = universe
705            .world
706            .add_component(engine::ecs::component::ActionComponent::new(
707                engine::ecs::IntentValue::SetColor {
708                    component_ids: vec![track_a_pattern_root],
709                    rgba: a_dark,
710                },
711            ));
712        let _ = universe.attach(kf, reset_a);
713
714        // Pattern cube per step for each track.
715        let x = (kf_beat as f32) * beat_spacing;
716        let cube_a = spawn_op_cube(
717            &mut universe,
718            track_a_pattern_root,
719            (x, track_a_y, lane_pat_z),
720            0.10,
721            a_dark,
722        );
723
724        let bright_a = universe
725            .world
726            .add_component(engine::ecs::component::ActionComponent::new(
727                engine::ecs::IntentValue::SetColor {
728                    component_ids: vec![cube_a],
729                    rgba: a_bright,
730                },
731            ));
732        let _ = universe.attach(kf, bright_a);
733    }
734    universe.add(anim);
735
736    println!("[audio-graph-example] animation created");
737
738    // Keep window open.
739    println!("[audio-graph-example] processing commands...");
740    universe.systems.process_commands(
741        &mut universe.world,
742        &mut universe.visuals,
743        &mut universe.render_assets,
744        &mut universe.command_queue,
745    );
746
747    println!("[audio-graph-example] commands processed; launching window");
748
749    engine::Windowing::run_app(universe).expect("Windowing failed");
750}
Source

pub fn id(&self) -> Option<ComponentId>

Trait Implementations§

Source§

impl Clone for ClockComponent

Source§

fn clone(&self) -> ClockComponent

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Component for ClockComponent

Source§

fn set_id(&mut self, component: ComponentId)

Source§

fn name(&self) -> &'static str

Short debug/type name for this component kind (e.g. “transform”, “camera”).
Source§

fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)

Called when component is added to the World
Source§

fn as_any(&self) -> &dyn Any

Source§

fn as_any_mut(&mut self) -> &mut dyn Any

Source§

fn to_mms_ast(&self, _world: &World) -> ComponentExpression

Encode this component as an MMS Component Expression AST node. Read more
Source§

fn cleanup(&mut self, _emit: &mut dyn SignalEmitter, _component: ComponentId)

Called when component is removed from the World.
Source§

fn encode(&self) -> HashMap<String, Value>

Encode component data to a HashMap for serialization. Read more
Source§

fn decode(&mut self, _data: &HashMap<String, Value>) -> Result<(), String>

Decode component data from a HashMap after deserialization. Read more
Source§

impl Copy for ClockComponent

Source§

impl Debug for ClockComponent

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for ClockComponent

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<F, T> IntoSample<T> for F
where T: FromSample<F>,

Source§

fn into_sample(self) -> T

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> ToSample<U> for T
where U: FromSample<T>,

Source§

fn to_sample_(self) -> U

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more