Skip to main content

World

Struct World 

Source
pub struct World { /* private fields */ }
Expand description

World: owns all global components.

Implementations§

Source§

impl World

Source

pub fn component_id_by_guid(&self, guid: Uuid) -> Option<ComponentId>

Fast GUID -> ComponentId lookup.

Source

pub fn set_component_guid( &mut self, id: ComponentId, new_guid: Uuid, ) -> Result<(), String>

Replace the GUID of an already-inserted component, keeping its ComponentId stable. Used by spawn paths that restore an authored guid = "..." property after create_component has already minted a fresh GUID.

Returns Err if the target id is missing, or if the new guid is already taken by another component (would silently overwrite the reverse index otherwise).

Source

pub fn add_component<T: Component>(&mut self, c: T) -> ComponentId

Add a new component to the world (no parent) and return its id.

Note: this currently does not call Component::init. That should happen via a higher-level API that has access to SystemWorld and VisualWorld.

Examples found in repository?
examples/transparent-cutout-example.rs (lines 19-23)
12fn spawn_gold_cube(
13    universe: &mut engine::Universe,
14    parent: engine::ecs::ComponentId,
15    position: (f32, f32, f32),
16    scale: f32,
17    color: (f32, f32, f32),
18) {
19    let t = universe.world.add_component(
20        TransformComponent::new()
21            .with_position(position.0, position.1, position.2)
22            .with_scale(scale, scale, scale),
23    );
24    let r = universe.world.add_component(RenderableComponent::cube());
25    let c = universe
26        .world
27        .add_component(ColorComponent::rgba(color.0, color.1, color.2, 1.0));
28
29    let _ = universe.attach(parent, t);
30    let _ = universe.attach(t, r);
31    let _ = universe.attach(r, c);
32}
33
34fn main() {
35    mittens_engine::example_support::ensure_model_assets();
36    utils::logger::init();
37
38    let world = engine::ecs::World::default();
39    let mut universe = engine::Universe::new(world);
40
41    // Orange/yellow-ish clear color so cutout edges read.
42    let clear = universe
43        .world
44        .add_component(BackgroundColorComponent::new());
45    let clear_c = universe
46        .world
47        .add_component(ColorComponent::rgba(0.98, 0.72, 0.22, 1.0));
48    let _ = universe.world.add_child(clear, clear_c);
49    universe.add(clear);
50
51    // Warm-ish ambient so the gold cubes don’t go too dark.
52    let ambient = universe
53        .world
54        .add_component(AmbientLightComponent::rgb(0.22, 0.16, 0.08));
55    universe.add(ambient);
56
57    // --- Camera rig (WASD/QE) ---
58    let input = universe
59        .world
60        .add_component(InputComponent::new().with_speed(2.0));
61    let input_mode = universe
62        .world
63        .add_component(InputTransformModeComponent::forward_z().with_roll_axis_y());
64    let _ = universe.attach(input, input_mode);
65
66    // Start a bit pulled back, looking toward the origin.
67    let rig_transform = universe
68        .world
69        .add_component(TransformComponent::new().with_position(0.0, 0.0, 5.0));
70    let _ = universe.attach(input, rig_transform);
71
72    let camera3d = universe
73        .world
74        .add_component(Camera3DComponent::new().with_far(200.0).with_fov(55.0));
75    let _ = universe.attach(rig_transform, camera3d);
76
77    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
78    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
79    universe.add(input);
80
81    // Key light for toon shading.
82    let light = universe.world.add_component(
83        PointLightComponent::new()
84            .with_distance(50.0)
85            .with_intensity(2.2)
86            .with_color(1.0, 0.98, 0.92),
87    );
88    let light_transform = universe
89        .world
90        .add_component(TransformComponent::new().with_position(2.0, 3.0, 4.0));
91    let _ = universe.attach(light_transform, light);
92    universe.add(light_transform);
93
94    // --- Transparent cutout: 100 cat faces in a 10x10 grid ---
95    // Place them *behind* the starting camera position (camera starts at z=+5.0).
96    // Not attached to the camera rig.
97    let cat_grid_root = universe
98        .world
99        .add_component(TransformComponent::new().with_position(0.0, 0.0, 9.0));
100    universe.add(cat_grid_root);
101
102    let grid_w: i32 = 10;
103    let grid_h: i32 = 10;
104    let spacing: f32 = 0.7;
105    let half_w = (grid_w as f32 - 1.0) * spacing * 0.5;
106    let half_h = (grid_h as f32 - 1.0) * spacing * 0.5;
107
108    for y in 0..grid_h {
109        for x in 0..grid_w {
110            // Topology: cat_grid_root { T_quad { R_quad { Texture + Filtering + Cutout + Color } } }
111            let px = x as f32 * spacing - half_w;
112            let py = y as f32 * spacing - half_h;
113
114            let pz: f32 = (x as f32) % half_w;
115
116            let quad_t = universe.world.add_component(
117                TransformComponent::new()
118                    .with_position(px, py, pz)
119                    .with_scale(0.55, 0.55, 1.0),
120            );
121            let quad_r = universe.world.add_component(RenderableComponent::square());
122
123            let quad_tex = universe.world.add_component(TextureComponent::with_uri(
124                "assets/textures/cat-face-amused.dds",
125            ));
126            let quad_filtering = universe
127                .world
128                .add_component(TextureFilteringComponent::linear());
129            let quad_cutout = universe
130                .world
131                .add_component(TransparentCutoutComponent::new());
132            let quad_color = universe
133                .world
134                .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
135
136            let _ = universe.attach(cat_grid_root, quad_t);
137            let _ = universe.attach(quad_t, quad_r);
138            let _ = universe.attach(quad_r, quad_tex);
139            let _ = universe.attach(quad_r, quad_filtering);
140            let _ = universe.attach(quad_r, quad_cutout);
141            let _ = universe.attach(quad_r, quad_color);
142        }
143    }
144
145    // --- Gold/yellow cubes behind the quad ---
146    // Parent them under a transform so it’s easy to tweak their depth.
147    let cubes_root = universe
148        .world
149        .add_component(TransformComponent::new().with_position(0.0, 0.0, 4.6));
150
151    // point light for cats
152    let cat_light_tx = universe
153        .world
154        .add_component(TransformComponent::new().with_position(0.0, 2.0, 7.0));
155
156    let cat_light = universe.world.add_component(
157        PointLightComponent::new()
158            .with_distance(150.0)
159            .with_intensity(1.5)
160            .with_color(1.0, 0.98, 0.92),
161    );
162    let _ = universe.attach(cat_light_tx, cat_light);
163    let _ = universe.attach(cubes_root, cat_light_tx);
164
165    universe.add(cubes_root);
166
167    let gold_a = (1.0, 0.86, 0.22);
168    let gold_b = (1.0, 0.74, 0.10);
169    let gold_c = (0.95, 0.92, 0.32);
170
171    // A loose cluster that’s visible through the cutout (transparent) area.
172    spawn_gold_cube(&mut universe, cubes_root, (-1.1, -0.3, -0.2), 0.45, gold_a);
173    spawn_gold_cube(&mut universe, cubes_root, (1.0, -0.4, -0.4), 0.40, gold_b);
174    spawn_gold_cube(&mut universe, cubes_root, (-0.2, 0.9, -0.6), 0.38, gold_c);
175    spawn_gold_cube(&mut universe, cubes_root, (0.7, 0.6, -0.9), 0.32, gold_a);
176    spawn_gold_cube(&mut universe, cubes_root, (-0.8, 0.4, -1.1), 0.36, gold_b);
177
178    // Bigger orange/yellow cubes behind the cluster (to make the cutout depth obvious).
179    let orange_gold_a = (1.0, 0.62, 0.10);
180    let orange_gold_b = (1.0, 0.78, 0.18);
181    spawn_gold_cube(
182        &mut universe,
183        cubes_root,
184        (0.0, -0.1, -2.1),
185        1.15,
186        orange_gold_b,
187    );
188    spawn_gold_cube(
189        &mut universe,
190        cubes_root,
191        (-1.8, 0.6, -2.6),
192        0.95,
193        orange_gold_a,
194    );
195    spawn_gold_cube(
196        &mut universe,
197        cubes_root,
198        (1.9, 0.7, -2.9),
199        1.05,
200        orange_gold_b,
201    );
202    spawn_gold_cube(
203        &mut universe,
204        cubes_root,
205        (0.9, 1.8, -3.2),
206        0.90,
207        orange_gold_a,
208    );
209    spawn_gold_cube(
210        &mut universe,
211        cubes_root,
212        (-0.9, 1.7, -3.5),
213        1.10,
214        orange_gold_b,
215    );
216
217    // Process init-time registrations (loads textures, registers renderables, etc.).
218    universe.systems.process_commands(
219        &mut universe.world,
220        &mut universe.visuals,
221        &mut universe.render_assets,
222        &mut universe.command_queue,
223    );
224
225    universe.enable_repl();
226    engine::Windowing::run_app(universe).expect("Windowing failed");
227}
More examples
Hide additional examples
examples/opacity-example.rs (lines 20-24)
12fn spawn_cube(
13    universe: &mut engine::Universe,
14    parent: engine::ecs::ComponentId,
15    position: (f32, f32, f32),
16    scale: (f32, f32, f32),
17    color: Option<(f32, f32, f32, f32)>,
18    opacity: Option<OpacityComponent>,
19) {
20    let t = universe.world.add_component(
21        TransformComponent::new()
22            .with_position(position.0, position.1, position.2)
23            .with_scale(scale.0, scale.1, scale.2),
24    );
25    let r = universe.world.add_component(RenderableComponent::cube());
26
27    let _ = universe.attach(parent, t);
28    let _ = universe.attach(t, r);
29
30    if let Some((cr, cg, cb, ca)) = color {
31        let c = universe
32            .world
33            .add_component(ColorComponent::rgba(cr, cg, cb, ca));
34        let _ = universe.attach(r, c);
35    }
36
37    if let Some(o) = opacity {
38        let o = universe.world.add_component(o);
39        let _ = universe.attach(r, o);
40    }
41}
42
43fn spawn_text_label(universe: &mut engine::Universe, position: (f32, f32, f32), text: &str) {
44    // T_root { T_scale { TXT { filtering } } }
45    let text_root = universe
46        .world
47        .add_component(TransformComponent::new().with_position(position.0, position.1, position.2));
48
49    let text_scale = universe
50        .world
51        .add_component(TransformComponent::new().with_scale(0.18, 0.18, 1.0));
52    let _ = universe.attach(text_root, text_scale);
53
54    let text_c = universe.world.add_component(TextComponent::new(text));
55    let _ = universe.attach(text_scale, text_c);
56
57    // Keep it crisp.
58    let filtering = universe
59        .world
60        .add_component(TextureFilteringComponent::nearest());
61    let _ = universe.attach(text_c, filtering);
62
63    // White label.
64    let color = universe
65        .world
66        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
67    let _ = universe.attach(text_c, color);
68
69    universe.add(text_root);
70}
71
72fn text_block_dimensions(text: &str) -> (usize, usize) {
73    let mut max_cols = 0usize;
74    let mut rows = 1usize;
75    let mut cur = 0usize;
76
77    for ch in text.chars() {
78        if ch == '\n' {
79            max_cols = max_cols.max(cur);
80            cur = 0;
81            rows += 1;
82        } else {
83            cur += 1;
84        }
85    }
86    max_cols = max_cols.max(cur);
87
88    (max_cols.max(1), rows.max(1))
89}
90
91fn spawn_text_label_with_bg(
92    universe: &mut engine::Universe,
93    position: (f32, f32, f32),
94    text: &str,
95    bg_opacity: f32,
96) {
97    // T_root {
98    //   T_bg { R_bg { Color black, Opacity } }
99    //   T_scale { TXT { filtering } }
100    // }
101
102    let text_root = universe
103        .world
104        .add_component(TransformComponent::new().with_position(position.0, position.1, position.2));
105
106    // Background quad (slightly behind the glyph quads).
107    let (cols, rows) = text_block_dimensions(text);
108    let text_scale = 0.18_f32;
109    let pad_x = 0.55_f32;
110    let pad_y = 0.45_f32;
111    let bg_w = cols as f32 * text_scale + pad_x;
112    let bg_h = rows as f32 * text_scale + pad_y;
113
114    let bg_t = universe.world.add_component(
115        TransformComponent::new()
116            .with_position(1.5, 0.0, -0.02)
117            .with_scale(bg_w, bg_h, 1.0),
118    );
119    let bg_r = universe.world.add_component(RenderableComponent::square());
120    let _ = universe.attach(text_root, bg_t);
121    let _ = universe.attach(bg_t, bg_r);
122
123    let bg_c = universe
124        .world
125        .add_component(ColorComponent::rgba(0.0, 0.0, 0.0, 1.0));
126    let _ = universe.attach(bg_r, bg_c);
127
128    let bg_o = universe
129        .world
130        .add_component(OpacityComponent::new().with_opacity(bg_opacity));
131    let _ = universe.attach(bg_r, bg_o);
132
133    let text_scale_t = universe
134        .world
135        .add_component(TransformComponent::new().with_scale(text_scale, text_scale, 1.0));
136    let _ = universe.attach(text_root, text_scale_t);
137
138    let text_c = universe.world.add_component(TextComponent::new(text));
139    let _ = universe.attach(text_scale_t, text_c);
140
141    // Keep it crisp.
142    let filtering = universe
143        .world
144        .add_component(TextureFilteringComponent::nearest());
145    let _ = universe.attach(text_c, filtering);
146
147    // Force the label into the transparent pass so it layers correctly with the background.
148    // (Pass selection currently does not consider texture alpha.)
149    let color = universe
150        .world
151        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 0.998));
152    let _ = universe.attach(text_c, color);
153
154    universe.add(text_root);
155}
156
157fn spawn_demo_spot(
158    universe: &mut engine::Universe,
159    spot_pos: (f32, f32, f32),
160    opacity: f32,
161    multiple_layers: bool,
162    grid_n: i32,
163) {
164    let spot = universe
165        .world
166        .add_component(TransformComponent::new().with_position(spot_pos.0, spot_pos.1, spot_pos.2));
167
168    // All cubes inherit this color.
169    let white = universe
170        .world
171        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
172    let _ = universe.attach(spot, white);
173
174    // All cubes inherit this opacity (no per-cube OpacityComponent needed).
175    let mut oc = OpacityComponent::new().with_opacity(opacity);
176    if multiple_layers {
177        oc = oc.with_multiple_layers();
178    }
179    let oc = universe.world.add_component(oc);
180    let _ = universe.attach(spot, oc);
181
182    universe.add(spot);
183
184    // Cube grid.
185    let n = grid_n.max(1);
186    let spacing = if n <= 2 { 1.0 } else { 0.6 };
187    let cube_scale = if n <= 2 { 0.8 } else { 0.45 };
188    let half = (n as f32 - 1.0) * spacing * 0.5;
189
190    for z in 0..n {
191        for y in 0..n {
192            for x in 0..n {
193                let px = x as f32 * spacing - half;
194                let py = y as f32 * spacing;
195                let pz = z as f32 * spacing - half;
196
197                spawn_cube(
198                    universe,
199                    spot,
200                    (px, py, pz),
201                    (cube_scale, cube_scale, cube_scale),
202                    None,
203                    None,
204                );
205            }
206        }
207    }
208
209    // Label above.
210    let label = format!(
211        "opacity: {:.2}\nmulti-layer: {}",
212        opacity,
213        if multiple_layers { "true" } else { "false" }
214    );
215    // Keep labels away from the cube volume so they don't intersect.
216    let label_y = spot_pos.1 + (n as f32 * spacing) + 1.8;
217    let label_x = spot_pos.0 - (half + 0.6);
218    let label_z = spot_pos.2 - (half + 1.0);
219    spawn_text_label(universe, (label_x, label_y, label_z), &label);
220}
221
222fn spawn_demo_strip_pair(
223    universe: &mut engine::Universe,
224    spot_pos: (f32, f32, f32),
225    transparent_multiple_layers: bool,
226) {
227    let n_z: i32 = 4;
228    let spacing = 1.0;
229    let cube_scale = 0.8;
230    let half_z = (n_z as f32 - 1.0) * spacing * 0.5;
231
232    // Transparent strip (1x4 along Z).
233    let transparent_root = universe
234        .world
235        .add_component(TransformComponent::new().with_position(spot_pos.0, spot_pos.1, spot_pos.2));
236
237    let white = universe
238        .world
239        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
240    let _ = universe.attach(transparent_root, white);
241
242    let mut oc = OpacityComponent::new().with_opacity(0.50);
243    if transparent_multiple_layers {
244        oc = oc.with_multiple_layers();
245    }
246    let oc = universe.world.add_component(oc);
247    let _ = universe.attach(transparent_root, oc);
248
249    universe.add(transparent_root);
250
251    for z in 0..n_z {
252        let pz = z as f32 * spacing - half_z;
253        spawn_cube(
254            universe,
255            transparent_root,
256            (0.0, 0.0, pz),
257            (cube_scale, cube_scale, cube_scale),
258            None,
259            None,
260        );
261    }
262
263    // Opaque strip to the left, same spacing/scale.
264    let opaque_root = universe
265        .world
266        .add_component(TransformComponent::new().with_position(
267            spot_pos.0 - spacing,
268            spot_pos.1,
269            spot_pos.2,
270        ));
271    let white = universe
272        .world
273        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
274    let _ = universe.attach(opaque_root, white);
275    universe.add(opaque_root);
276
277    for z in 0..n_z {
278        let pz = z as f32 * spacing - half_z;
279        spawn_cube(
280            universe,
281            opaque_root,
282            (0.0, 0.0, pz),
283            (cube_scale, cube_scale, cube_scale),
284            None,
285            None,
286        );
287    }
288
289    let label = format!(
290        "mini: opaque + transparent strip\ntransparent opacity: 0.50\nmulti-layer: {}",
291        if transparent_multiple_layers {
292            "true"
293        } else {
294            "false"
295        }
296    );
297    let label_y = spot_pos.1 + spacing + 1.8;
298    let label_x = spot_pos.0 - (spacing * 2.6);
299    let label_z = spot_pos.2 - (half_z + 1.0);
300    spawn_text_label(universe, (label_x, label_y, label_z), &label);
301}
302
303fn spawn_demo_xy_plane(
304    universe: &mut engine::Universe,
305    spot_pos: (f32, f32, f32),
306    opacity: f32,
307    n_x: i32,
308    n_y: i32,
309    z: f32,
310) {
311    let spot = universe
312        .world
313        .add_component(TransformComponent::new().with_position(spot_pos.0, spot_pos.1, spot_pos.2));
314
315    let white = universe
316        .world
317        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
318    let _ = universe.attach(spot, white);
319
320    let oc = universe
321        .world
322        .add_component(OpacityComponent::new().with_opacity(opacity));
323    let _ = universe.attach(spot, oc);
324
325    universe.add(spot);
326
327    let nx = n_x.max(1);
328    let ny = n_y.max(1);
329    let spacing = 0.45;
330    let cube_scale = 0.35;
331    let half_x = (nx as f32 - 1.0) * spacing * 0.5;
332
333    for y in 0..ny {
334        for x in 0..nx {
335            let px = x as f32 * spacing - half_x;
336            let py = y as f32 * spacing;
337            spawn_cube(
338                universe,
339                spot,
340                (px, py, z),
341                (cube_scale, cube_scale, cube_scale),
342                None,
343                None,
344            );
345        }
346    }
347
348    // Label in front of the plane.
349    let label = format!(
350        "XY plane: {}x{}\nopacity: {:.2}\nmulti-layer: false",
351        nx, ny, opacity
352    );
353    let label_x = spot_pos.0 - (half_x + 0.6);
354    let label_y = spot_pos.1 + (ny as f32 * spacing) + 1.2;
355    let label_z = spot_pos.2 - 2.5;
356    spawn_text_label_with_bg(universe, (label_x, label_y, label_z), &label, 0.50);
357}
358
359fn main() {
360    mittens_engine::example_support::ensure_model_assets();
361    utils::logger::init();
362
363    let world = engine::ecs::World::default();
364    let mut universe = engine::Universe::new(world);
365
366    // Dark brown / pink background.
367    let bg = universe
368        .world
369        .add_component(BackgroundColorComponent::new());
370    let bg_c = universe
371        .world
372        .add_component(ColorComponent::rgba(0.22, 0.08, 0.10, 1.0));
373    let _ = universe.world.add_child(bg, bg_c);
374    universe.add(bg);
375
376    // Ambient so unlit areas aren't black.
377    let ambient = universe
378        .world
379        .add_component(AmbientLightComponent::rgb(0.35, 0.35, 0.40));
380    universe.add(ambient);
381
382    // A bright overhead light.
383    let light_t = universe.world.add_component(
384        TransformComponent::new()
385            .with_position(0.0, 18.0, 10.0)
386            .with_scale(0.2, 0.2, 0.2),
387    );
388    let light = universe.world.add_component(
389        PointLightComponent::new()
390            .with_color(1.0, 1.0, 1.0)
391            .with_intensity(1.6)
392            .with_distance(80.0),
393    );
394    let _ = universe.attach(light_t, light);
395    universe.add(light_t);
396
397    // --- Camera rig ---
398    // I { T { C3D { with_fps_rotation with_roll_axis_y } } }
399    let input = universe
400        .world
401        .add_component(InputComponent::new().with_speed(3.0));
402    let input_mode = universe.world.add_component(
403        InputTransformModeComponent::forward_z()
404            .with_roll_axis_y()
405            .with_fps_rotation(),
406    );
407    let _ = universe.attach(input, input_mode);
408
409    let rig_transform = universe
410        .world
411        .add_component(TransformComponent::new().with_position(0.0, 6.0, 20.0));
412    let _ = universe.attach(input, rig_transform);
413
414    let camera = universe.world.add_component(Camera3DComponent::new());
415    let _ = universe.attach(rig_transform, camera);
416
417    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
418    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
419
420    universe.add(input);
421
422    // --- Ground ---
423    let ground = universe.world.add_component(
424        TransformComponent::new()
425            .with_position(0.0, -0.6, 0.0)
426            .with_scale(60.0, 1.0, 60.0),
427    );
428    let ground_r = universe.world.add_component(RenderableComponent::cube());
429    let ground_c = universe
430        .world
431        .add_component(ColorComponent::rgba(0.95, 0.95, 0.95, 1.0));
432    let _ = universe.attach(ground, ground_r);
433    let _ = universe.attach(ground_r, ground_c);
434    universe.add(ground);
435
436    // --- Opaque yellow cubes behind (contrast reference) ---
437    for i in 0..6 {
438        let x = -10.0 + i as f32 * 4.0;
439        spawn_cube(
440            &mut universe,
441            ground,
442            (x, 1.2, -18.0),
443            (2.0, 2.0, 2.0),
444            Some((1.0, 0.92, 0.15, 1.0)),
445            None,
446        );
447    }
448
449    // --- Demo spots ---
450    // Left of the left big spot: a single-layer transparent XY plane (16x16).
451    spawn_demo_xy_plane(&mut universe, (-14.0, 0.0, 0.0), 0.50, 16, 16, 0.0);
452
453    // Big spots: 8x8x8
454    // Left: single-layer transparent (instanced)
455    spawn_demo_spot(&mut universe, (-4.0, 0.0, 0.0), 0.50, false, 8);
456
457    // Right: multi-layer transparent (sorted)
458    spawn_demo_spot(&mut universe, (4.0, 0.0, 0.0), 0.50, true, 8);
459
460    // Small spots: opaque 1x4 strip + transparent 1x4 strip (along Z).
461    spawn_demo_strip_pair(&mut universe, (-4.0, 0.0, 10.0), false);
462    spawn_demo_strip_pair(&mut universe, (4.0, 0.0, 10.0), true);
463
464    // Process init-time registrations.
465    universe.systems.process_commands(
466        &mut universe.world,
467        &mut universe.visuals,
468        &mut universe.render_assets,
469        &mut universe.command_queue,
470    );
471
472    universe.enable_repl();
473
474    engine::Windowing::run_app(universe).expect("Windowing failed");
475}
examples/animation-for-topology.rs (lines 27-31)
20fn spawn_emissive_marker_cube(
21    universe: &mut engine::Universe,
22    parent: engine::ecs::ComponentId,
23    local_pos: (f32, f32, f32),
24    scale: f32,
25    rgba: [f32; 4],
26) {
27    let tx = universe.world.add_component(
28        engine::ecs::component::TransformComponent::new()
29            .with_position(local_pos.0, local_pos.1, local_pos.2)
30            .with_scale(scale, scale, scale),
31    );
32    let r = universe
33        .world
34        .add_component(engine::ecs::component::RenderableComponent::cube());
35    let c = universe
36        .world
37        .add_component(engine::ecs::component::ColorComponent::rgba(
38            rgba[0], rgba[1], rgba[2], rgba[3],
39        ));
40    let e = universe
41        .world
42        .add_component(engine::ecs::component::EmissiveComponent::on());
43
44    let _ = universe.attach(parent, tx);
45    let _ = universe.attach(tx, r);
46    let _ = universe.attach(r, c);
47    let _ = universe.attach(r, e);
48}
49
50/// Create a cube subtree ahead-of-time (not attached to the scene).
51///
52/// Returns the root `TransformComponent` id.
53fn spawn_detached_cube_prefab(
54    universe: &mut engine::Universe,
55    scale: f32,
56    rgba: [f32; 4],
57) -> engine::ecs::ComponentId {
58    let tx = universe.world.add_component(
59        engine::ecs::component::TransformComponent::new().with_scale(scale, scale, scale),
60    );
61    let r = universe
62        .world
63        .add_component(engine::ecs::component::RenderableComponent::cube());
64    let c = universe
65        .world
66        .add_component(engine::ecs::component::ColorComponent::rgba(
67            rgba[0], rgba[1], rgba[2], rgba[3],
68        ));
69    let e = universe
70        .world
71        .add_component(engine::ecs::component::EmissiveComponent::on());
72
73    let _ = universe.attach(tx, r);
74    let _ = universe.attach(r, c);
75    let _ = universe.attach(r, e);
76
77    tx
78}
79
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/router.rs (line 32)
3fn spawn_runtime_text(
4    universe: &mut engine::Universe,
5    owner: engine::ecs::ComponentId,
6    label: &str,
7) {
8    use engine::ecs::component::{
9        ColorComponent, EdgeInsets, SizeDimension, StyleComponent, TextComponent,
10        TransformComponent,
11    };
12
13    let root = universe
14        .world
15        .add_component_boxed_named(label, Box::new(TransformComponent::new()));
16    let style = universe.world.add_component_boxed_named(
17        format!("{label}_style"),
18        Box::new({
19            let mut style = StyleComponent::new();
20            style.margin = EdgeInsets::all(0.5);
21            style.padding = EdgeInsets::axes(0.75, 0.5);
22            style.width = SizeDimension::Auto;
23            style.background_color = Some([0.93, 0.88, 0.98, 1.0]);
24            style
25        }),
26    );
27    let text = universe
28        .world
29        .add_component_boxed_named(format!("{label}_text"), Box::new(TextComponent::new(label)));
30    let color = universe
31        .world
32        .add_component(ColorComponent::rgba(0.38, 0.14, 0.62, 1.0));
33
34    let _ = universe.world.add_child(root, style);
35    let _ = universe.world.add_child(root, text);
36    let _ = universe.world.add_child(text, color);
37
38    universe.attach(owner, root).expect("attach routed child");
39}
examples/example_util/mod.rs (line 27)
15pub fn spawn_mms_demo_rig(
16    universe: &mut engine::Universe,
17    cam_pos: [f32; 3],
18) -> engine::ecs::ComponentId {
19    use engine::ecs::component::{
20        BackgroundColorComponent, Camera3DComponent, InputComponent, InputTransformModeComponent,
21        TransformComponent,
22    };
23
24    // Dark blue clear colour.
25    let bg_color = universe
26        .world
27        .add_component(BackgroundColorComponent::new());
28    let bg_color_c = universe
29        .world
30        .add_component(ColorComponent::rgba(0.02, 0.03, 0.10, 1.0));
31    let _ = universe.world.add_child(bg_color, bg_color_c);
32    universe.add(bg_color);
33
34    // Camera rig: Input → Transform → Camera3D.
35    let input = universe
36        .world
37        .add_component(InputComponent::new().with_speed(3.0));
38    let input_mode = universe
39        .world
40        .add_component(InputTransformModeComponent::forward_z().with_roll_axis_y());
41    let _ = universe.attach(input, input_mode);
42
43    let cam_transform = universe
44        .world
45        .add_component(TransformComponent::new().with_position(cam_pos[0], cam_pos[1], cam_pos[2]));
46    let _ = universe.attach(input, cam_transform);
47
48    let camera = universe.world.add_component(Camera3DComponent::new());
49    let _ = universe.attach(cam_transform, camera);
50
51    universe.add(input);
52
53    spawn_desktop_camera_controls_hint(universe, cam_transform);
54
55    cam_transform
56}
57
58fn hash_u32(mut x: u32) -> u32 {
59    x ^= x >> 16;
60    x = x.wrapping_mul(0x7feb_352d);
61    x ^= x >> 15;
62    x = x.wrapping_mul(0x846c_a68b);
63    x ^= x >> 16;
64    x
65}
66
67fn rand01(seed: u32) -> f32 {
68    (hash_u32(seed) as f32) / (u32::MAX as f32)
69}
70
71#[derive(Debug, Clone, Copy)]
72pub struct CloudRingParams {
73    pub cloud_count: u32,
74    pub radius: f32,
75    pub center_y: f32,
76    pub puffs_per_cloud: u32,
77    /// 0.0 = perfectly evenly spaced, 1.0 = up to one full step of jitter.
78    pub angle_jitter: f32,
79    /// Probability a given cluster is placed higher than `center_y`.
80    pub high_y_probability: f32,
81    /// Multiplier applied to `center_y` when a cluster is chosen to be high.
82    pub high_y_multiplier: f32,
83    /// Seed used for deterministic layout variation.
84    pub seed: u32,
85}
86
87impl Default for CloudRingParams {
88    fn default() -> Self {
89        Self {
90            cloud_count: 5,
91            radius: 26.0,
92            center_y: 2.0,
93            puffs_per_cloud: 28,
94            angle_jitter: 0.0,
95            high_y_probability: 0.0,
96            high_y_multiplier: 1.0,
97            seed: 0xC10u32,
98        }
99    }
100}
101
102/// Spawns a ring of "cloud" puff clusters under an existing background root.
103///
104/// Assumes `bg_root` is a `BackgroundComponent` (usually `with_occlusion_and_lighting()`).
105pub fn spawn_cloud_ring(
106    universe: &mut engine::Universe,
107    bg_root: engine::ecs::ComponentId,
108    p: CloudRingParams,
109) {
110    if p.cloud_count == 0 || p.radius == 0.0 {
111        return;
112    }
113
114    let cube_mesh = universe
115        .render_assets
116        .get_mesh(engine::graphics::BuiltinMeshType::Cube);
117
118    let step = std::f32::consts::TAU / (p.cloud_count as f32);
119    let angle_jitter = p.angle_jitter.clamp(0.0, 1.0);
120    let high_y_probability = p.high_y_probability.clamp(0.0, 1.0);
121
122    for i in 0..p.cloud_count {
123        let seed_i = p.seed ^ i.wrapping_mul(0x9E37_79B9);
124        let jitter = (rand01(seed_i ^ 0xa53a_9d2d) - 0.5) * step * angle_jitter;
125        let a = (i as f32) * step + jitter;
126        let cx = p.radius * a.cos();
127        let cz = p.radius * a.sin();
128
129        let cy = if rand01(seed_i ^ 0x7f4a_7c15) < high_y_probability {
130            p.center_y * p.high_y_multiplier
131        } else {
132            p.center_y
133        };
134
135        let center_tx = universe.world.add_component(
136            engine::ecs::component::TransformComponent::new().with_position(cx, cy, cz),
137        );
138        let _ = universe.attach(bg_root, center_tx);
139
140        let base_seed = seed_i ^ 0x243f_6a88;
141        for puff_i in 0..p.puffs_per_cloud {
142            let seed = base_seed ^ puff_i.wrapping_mul(1_103_515_245);
143
144            let ox = (rand01(seed ^ 0x68bc_21eb) - 0.5) * 9.0;
145            let oy = (rand01(seed ^ 0x02e5_be93) - 0.5) * 4.0;
146            let oz = (rand01(seed ^ 0xa1d3_4f2b) - 0.5) * 9.0;
147
148            let base = 0.7 + rand01(seed ^ 0x9e37_79b9) * 2.8;
149            let sx = base * (0.7 + rand01(seed ^ 0x243f_6a88) * 0.9);
150            let sy = base * (0.6 + rand01(seed ^ 0x85a3_08d3) * 1.0);
151            let sz = base * (0.7 + rand01(seed ^ 0x1319_8a2e) * 0.9);
152
153            let tx = universe.world.add_component(
154                engine::ecs::component::TransformComponent::new()
155                    .with_position(ox, oy, oz)
156                    .with_scale(sx, sy, sz),
157            );
158            let renderable =
159                universe
160                    .world
161                    .add_component(engine::ecs::component::RenderableComponent::new(
162                        engine::graphics::primitives::Renderable::new(
163                            cube_mesh,
164                            engine::graphics::primitives::MaterialHandle::TOON_MESH,
165                        ),
166                    ));
167
168            let t = rand01(seed ^ 0x7f4a_7c15);
169            let r = 0.70 + 0.10 * t;
170            let g = 0.72 + 0.10 * t;
171            let b = 0.80 + 0.12 * t;
172            let color = universe
173                .world
174                .add_component(engine::ecs::component::ColorComponent::rgba(r, g, b, 1.0));
175
176            let _ = universe.attach(center_tx, tx);
177            let _ = universe.attach(tx, renderable);
178            let _ = universe.attach(renderable, color);
179        }
180    }
181}
182
183/// Spawn a small camera-attached help text for desktop controls.
184///
185/// The returned component is the hint root `TransformComponent`.
186///
187/// This is intended for examples that use the common topology:
188///
189/// `I { T { C3D } }` (InputComponent → TransformComponent → Camera3DComponent)
190pub fn spawn_desktop_camera_controls_hint(
191    universe: &mut engine::Universe,
192    camera_transform: engine::ecs::ComponentId,
193) -> engine::ecs::ComponentId {
194    use engine::ecs::component::{
195        ColorComponent, EditorComponent, EmissiveComponent, RaycastableComponent, TextComponent,
196        TextureFilteringComponent, TransformComponent,
197    };
198
199    // We intentionally do NOT parent the hint under the camera rig.
200    // This keeps it out of the camera subtree (and avoids inheriting camera motion/rotation).
201    //
202    // Instead, we place it in world space based on the camera rig's current pose.
203    // That makes it start out “in front-right of the camera” without being attached.
204    let (cam_pos, cam_rot) = universe
205        .world
206        .get_component_by_id_as::<TransformComponent>(camera_transform)
207        .map(|t| (t.transform.translation, t.transform.rotation))
208        .unwrap_or(([0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]));
209
210    // Camera convention in examples: forward is -Z.
211    // Local-space offset from the camera rig at spawn time.
212    let local_offset = [0.65, 0.25, -1.7];
213
214    // Rotate the offset by the camera rig's rotation so it appears in front-right of the view.
215    let world_offset = mittens_engine::utils::math::quat_rotate_vec3(cam_rot, local_offset);
216    let world_pos = [
217        cam_pos[0] + world_offset[0],
218        cam_pos[1] + world_offset[1],
219        cam_pos[2] + world_offset[2],
220    ];
221
222    let hint_root = universe.world.add_component(
223        TransformComponent::new()
224            .with_position(world_pos[0], world_pos[1], world_pos[2])
225            .with_scale(0.055, 0.055, 1.0),
226    );
227
228    // Put the hint into an editor subtree so clicking it attaches gizmos.
229    let editor_root = universe.world.add_component(EditorComponent::new());
230    let _ = universe.attach(hint_root, editor_root);
231
232    let text = universe.world.add_component(TextComponent::new(
233        "use wasd/rf/qe\nand right-mouse\nclick and drag\nto move/look",
234    ));
235
236    // Make glyph renderables raycastable so the hint can be clicked without adding a
237    // large invisible pick plane (clicking is per-glyph / per-text-quad).
238    let raycastable = universe
239        .world
240        .add_component(RaycastableComponent::enabled());
241    let _ = universe.attach(text, raycastable);
242
243    // Text color should be an immediate child of the TextComponent root.
244    let color = universe
245        .world
246        .add_component(ColorComponent::rgba(0.0, 0.0, 0.0, 1.0));
247    let _ = universe.attach(text, color);
248
249    // TextSystem looks for these as immediate children of the TextComponent root.
250    let emissive = universe.world.add_component(EmissiveComponent::on());
251    let filtering = universe
252        .world
253        .add_component(TextureFilteringComponent::nearest_magnification());
254
255    let cutout = universe
256        .world
257        .add_component(TransparentCutoutComponent::new());
258    let _ = universe.attach(text, cutout);
259
260    let _ = universe.attach(editor_root, text);
261    let _ = universe.attach(text, emissive);
262    let _ = universe.attach(text, filtering);
263
264    // Ensure the text is initialized even if the caller only adds the camera rig.
265    universe.add(hint_root);
266
267    hint_root
268}
examples/simple-demo.rs (lines 33-38)
6fn build_demo_scene_7_shapes(universe: &mut engine::Universe) {
7    use engine::ecs::component::{
8        Camera3DComponent, ColorComponent, EmissiveComponent, GLTFComponent, InputComponent,
9        InputTransformModeComponent, PointLightComponent, RenderableComponent, TextureComponent,
10        TransformComponent,
11    };
12    use engine::graphics::BuiltinMeshType;
13    use engine::graphics::primitives::MaterialHandle;
14
15    // Built-in CPU meshes are pre-registered; just fetch stable handles.
16    let tri_mesh = universe.render_assets.get_mesh(BuiltinMeshType::Triangle2D);
17    let square_mesh = universe.render_assets.get_mesh(BuiltinMeshType::Quad2D);
18    let tetra_mesh = universe
19        .render_assets
20        .get_mesh(BuiltinMeshType::Tetrahedron);
21
22    fn spawn(
23        universe: &mut engine::Universe,
24        mesh: engine::graphics::primitives::CpuMeshHandle,
25        x: f32,
26        y: f32,
27        s: f32,
28        r: f32,
29        color: [f32; 4],
30        input_driven: bool,
31        emissive: bool,
32    ) -> engine::ecs::ComponentId {
33        let transform = universe.world.add_component(
34            TransformComponent::new()
35                .with_position(x, y, 0.0)
36                .with_scale(s, s, 1.0)
37                .with_rotation_euler(0.0, 0.0, r),
38        );
39        let renderable = universe.world.add_component(RenderableComponent::new(
40            engine::graphics::primitives::Renderable::new(mesh, MaterialHandle::TOON_MESH),
41        ));
42        let color_c = universe.world.add_component(ColorComponent { rgba: color });
43
44        if emissive {
45            let emissive_c = universe.world.add_component(EmissiveComponent::on());
46            let _ = universe.attach(renderable, emissive_c);
47        }
48
49        // Topology: (optional Input) -> Transform -> Renderable
50        let _ = universe.attach(transform, renderable);
51        let _ = universe.attach(renderable, color_c);
52
53        if input_driven {
54            let input = universe
55                .world
56                .add_component(InputComponent::new().with_speed(0.5));
57            let _ = universe.attach(input, transform);
58            universe.add(input);
59        } else {
60            universe.add(transform);
61        }
62
63        transform
64    }
65
66    fn spawn_3d(
67        universe: &mut engine::Universe,
68        mesh: engine::graphics::primitives::CpuMeshHandle,
69        x: f32,
70        y: f32,
71        z: f32,
72        s: f32,
73        rx: f32,
74        ry: f32,
75        rz: f32,
76        color: [f32; 4],
77    ) -> engine::ecs::ComponentId {
78        let transform = universe.world.add_component(
79            TransformComponent::new()
80                .with_position(x, y, z)
81                .with_scale(s, s, s)
82                .with_rotation_euler(rx, ry, rz),
83        );
84        let renderable = universe.world.add_component(RenderableComponent::new(
85            engine::graphics::primitives::Renderable::new(mesh, MaterialHandle::TOON_MESH),
86        ));
87        let color_c = universe.world.add_component(ColorComponent { rgba: color });
88
89        let _ = universe.attach(transform, renderable);
90        let _ = universe.attach(renderable, color_c);
91        universe.add(transform);
92
93        transform
94    }
95
96    // Spawn shapes.
97    // One triangle is input-driven (WASD/QE). Build a small "rig" so both the triangle
98    // and the camera can be driven by the same InputComponent.
99
100    // Topology: Input -> (InputTransformMode) -> RigTransform -> (CameraTransform -> Camera3D), (TriRootTransform -> ...)
101    let tri_input = universe
102        .world
103        .add_component(InputComponent::new().with_speed(0.5));
104    let input_mode = universe
105        .world
106        .add_component(InputTransformModeComponent::forward_z().with_roll_axis_y());
107    let _ = universe.attach(tri_input, input_mode);
108
109    // Start pulled back so the demo meshes at z=0 are in view.
110    // The camera will be attached directly under this transform, so there is no local
111    // camera offset that would cause orbiting when yawing.
112    let rig_transform = universe
113        .world
114        .add_component(TransformComponent::new().with_position(0.0, 0.0, 2.5));
115    let _ = universe.attach(tri_input, rig_transform);
116
117    // Camera: attached directly to the rig transform.
118    let camera3d = universe.world.add_component(Camera3DComponent::new());
119    let _ = universe.attach(rig_transform, camera3d);
120
121    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
122    example_util::spawn_desktop_camera_controls_hint(universe, rig_transform);
123
124    let tri_root_transform = universe
125        .world
126        .add_component(TransformComponent::new().with_position(0.5, 0.50, 0.0));
127
128    // Visual transform under the root; this is where we apply rotation/scale.
129    let tri_visual_transform = universe.world.add_component(
130        TransformComponent::new()
131            .with_scale(0.30, 0.30, 1.0)
132            .with_rotation_euler(0.0, 0.0, (2.0 * 3.14159 / 3.0) + 3.14159),
133    );
134    let tri_renderable = universe.world.add_component(RenderableComponent::new(
135        engine::graphics::primitives::Renderable::new(tri_mesh, MaterialHandle::TOON_MESH),
136    ));
137    let tri_color = universe
138        .world
139        .add_component(ColorComponent::rgba(0.2, 1.0, 0.2, 1.0));
140
141    let _ = universe.attach(rig_transform, tri_root_transform);
142    let _ = universe.attach(tri_root_transform, tri_visual_transform);
143    let _ = universe.attach(tri_visual_transform, tri_renderable);
144    let _ = universe.attach(tri_renderable, tri_color);
145
146    let tri_light = universe.world.add_component(
147        PointLightComponent::new()
148            .with_distance(10.0)
149            .with_color(1.0, 1.0, 1.0),
150    );
151
152    let light_transform = universe.world.add_component(
153        TransformComponent::new()
154            .with_position(0.5, 0.50, 1.0)
155            .with_scale(0.1, 0.1, 0.1),
156    );
157
158    let _ = universe.attach(light_transform, tri_light);
159
160    universe.add(tri_input);
161    universe.add(light_transform);
162
163    spawn(
164        universe,
165        square_mesh,
166        -0.80,
167        -0.30,
168        0.25,
169        0.0,
170        [1.0, 0.2, 0.2, 1.0],
171        false,
172        true,
173    );
174    spawn(
175        universe,
176        square_mesh,
177        -0.40,
178        -0.30,
179        0.25,
180        0.0,
181        [1.0, 0.6, 0.2, 1.0],
182        false,
183        true,
184    );
185
186    // 3D primitive: tetrahedron.
187    spawn_3d(
188        universe,
189        tetra_mesh,
190        0.55,
191        -0.15,
192        0.0,
193        0.35,
194        0.75,
195        0.55,
196        0.0,
197        [0.2, 0.7, 1.0, 1.0],
198    );
199    spawn(
200        universe,
201        square_mesh,
202        0.00,
203        -0.30,
204        0.25,
205        0.0,
206        [1.0, 1.0, 0.2, 1.0],
207        false,
208        true,
209    );
210    spawn(
211        universe,
212        square_mesh,
213        0.40,
214        -0.30,
215        0.25,
216        0.0,
217        [0.2, 0.6, 1.0, 1.0],
218        false,
219        true,
220    );
221    spawn(
222        universe,
223        square_mesh,
224        0.80,
225        -0.30,
226        0.25,
227        0.0,
228        [0.8, 0.2, 1.0, 1.0],
229        false,
230        true,
231    );
232    spawn(
233        universe,
234        tri_mesh,
235        0.30,
236        0.35,
237        0.30,
238        -3.14159,
239        [1.0, 1.0, 1.0, 1.0],
240        false,
241        false,
242    );
243
244    // Textured square.
245    let tex_transform = universe.world.add_component(
246        TransformComponent::new()
247            .with_position(0.0, 0.1, 0.0)
248            .with_scale(0.45, 0.45, 1.0),
249    );
250    let tex_renderable = universe.world.add_component(RenderableComponent::new(
251        engine::graphics::primitives::Renderable::new(square_mesh, MaterialHandle::TOON_MESH),
252    ));
253    let tex_color = universe
254        .world
255        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
256    let tex = universe.world.add_component(TextureComponent::from_dds(
257        "assets/textures/cat-face-amused.dds",
258    ));
259
260    let _ = universe.attach(tex_transform, tex_renderable);
261    let _ = universe.attach(tex_renderable, tex_color);
262    let _ = universe.attach(tex_renderable, tex);
263    universe.add(tex_transform);
264
265    // glTF: color-cat
266    // Attach GLTFComponent under a Transform so GLTFSystem can use it as an anchor.
267    let cat_anchor = universe.world.add_component(
268        TransformComponent::new()
269            .with_position(0.0, -0.10, -4.0)
270            .with_scale(0.50, 0.50, 0.50)
271            .with_rotation_euler(0.0, 0.0, 0.0),
272    );
273    let cat_gltf = universe
274        .world
275        .add_component(GLTFComponent::new("assets/models/color-cat.2.glb"));
276    let _ = universe.attach(cat_anchor, cat_gltf);
277    universe.add(cat_anchor);
278}
Source

pub fn register<T: Component>(&mut self, c: T) -> ComponentId

Register a new component in the world and return its id.

This is intentionally a storage/identity operation only: it does not call Component::init.

Examples found in repository?
examples/raycast-topology-animation.rs (line 49)
30fn ensure_emissive_on(
31    world: &mut engine::ecs::World,
32    emit: &mut dyn engine::ecs::SignalEmitter,
33    renderable_cid: engine::ecs::ComponentId,
34) {
35    let existing = world
36        .children_of(renderable_cid)
37        .iter()
38        .copied()
39        .find(|&ch| {
40            world
41                .get_component_by_id_as::<engine::ecs::component::EmissiveComponent>(ch)
42                .is_some()
43        });
44
45    if existing.is_some() {
46        return;
47    }
48
49    let emissive_cid = world.register(engine::ecs::component::EmissiveComponent::on());
50    let _ = world.add_child(renderable_cid, emissive_cid);
51    emit.push_intent_now(
52        emissive_cid,
53        engine::ecs::IntentValue::RegisterEmissive {
54            component_ids: vec![emissive_cid],
55        },
56    );
57}
More examples
Hide additional examples
examples/gravity-fields.rs (lines 60-62)
27fn set_renderable_color_rgba(
28    world: &mut engine::ecs::World,
29    emit: &mut dyn engine::ecs::SignalEmitter,
30    renderable_cid: engine::ecs::ComponentId,
31    rgba: [f32; 4],
32) {
33    // Prefer existing ColorComponent.
34    let existing = world
35        .children_of(renderable_cid)
36        .iter()
37        .copied()
38        .find(|&ch| {
39            world
40                .get_component_by_id_as::<engine::ecs::component::ColorComponent>(ch)
41                .is_some()
42        });
43
44    if let Some(color_cid) = existing {
45        if let Some(c) =
46            world.get_component_by_id_as_mut::<engine::ecs::component::ColorComponent>(color_cid)
47        {
48            c.rgba = rgba;
49            emit.push_intent_now(
50                color_cid,
51                engine::ecs::IntentValue::RegisterColor {
52                    component_ids: vec![color_cid],
53                },
54            );
55        }
56        return;
57    }
58
59    // Fallback: add a ColorComponent child if missing.
60    let color_cid = world.register(engine::ecs::component::ColorComponent::rgba(
61        rgba[0], rgba[1], rgba[2], rgba[3],
62    ));
63    let _ = world.add_child(renderable_cid, color_cid);
64    emit.push_intent_now(
65        color_cid,
66        engine::ecs::IntentValue::RegisterColor {
67            component_ids: vec![color_cid],
68        },
69    );
70}
Source

pub fn is_initialized(&self, c: ComponentId) -> bool

Whether this component has had Component::init invoked.

Source

pub fn add_component_boxed(&mut self, c: Box<dyn Component>) -> ComponentId

Add a new component to the world (no parent). Returns its global id.

Source

pub fn add_component_boxed_named( &mut self, name: impl Into<String>, c: Box<dyn Component>, ) -> ComponentId

Add a new boxed component with an explicit stored name.

Examples found in repository?
examples/router.rs (line 15)
3fn spawn_runtime_text(
4    universe: &mut engine::Universe,
5    owner: engine::ecs::ComponentId,
6    label: &str,
7) {
8    use engine::ecs::component::{
9        ColorComponent, EdgeInsets, SizeDimension, StyleComponent, TextComponent,
10        TransformComponent,
11    };
12
13    let root = universe
14        .world
15        .add_component_boxed_named(label, Box::new(TransformComponent::new()));
16    let style = universe.world.add_component_boxed_named(
17        format!("{label}_style"),
18        Box::new({
19            let mut style = StyleComponent::new();
20            style.margin = EdgeInsets::all(0.5);
21            style.padding = EdgeInsets::axes(0.75, 0.5);
22            style.width = SizeDimension::Auto;
23            style.background_color = Some([0.93, 0.88, 0.98, 1.0]);
24            style
25        }),
26    );
27    let text = universe
28        .world
29        .add_component_boxed_named(format!("{label}_text"), Box::new(TextComponent::new(label)));
30    let color = universe
31        .world
32        .add_component(ColorComponent::rgba(0.38, 0.14, 0.62, 1.0));
33
34    let _ = universe.world.add_child(root, style);
35    let _ = universe.world.add_child(root, text);
36    let _ = universe.world.add_child(text, color);
37
38    universe.attach(owner, root).expect("attach routed child");
39}
More examples
Hide additional examples
examples/diy-panel.rs (lines 15-18)
5fn spawn_runtime_text(
6    universe: &mut engine::Universe,
7    owner: engine::ecs::ComponentId,
8    label: &str,
9) {
10    use engine::ecs::component::{
11        ColorComponent, EdgeInsets, SizeDimension, StyleComponent, TextComponent,
12        TransformComponent,
13    };
14
15    let root = universe.world.add_component_boxed_named(
16        label,
17        Box::new(TransformComponent::new().with_position(0.0, 0.0, 0.2)),
18    );
19    let style = universe.world.add_component_boxed_named(
20        format!("{label}_style"),
21        Box::new({
22            let mut style = StyleComponent::new();
23            style.margin = EdgeInsets::axes(0.25, 0.25);
24            style.padding = EdgeInsets::axes(0.5, 0.5);
25            style.height = SizeDimension::GlyphUnits(2.5);
26            style.width = SizeDimension::Auto;
27            style.background_color = Some([1.0, 0.80, 0.80, 1.0]);
28            style
29        }),
30    );
31    let text_root = universe.world.add_component_boxed_named(
32        format!("{label}_text_root"),
33        Box::new(TransformComponent::new().with_position(0.0, 0.0, 0.2)),
34    );
35    let text = universe
36        .world
37        .add_component_boxed_named(format!("{label}_text"), Box::new(TextComponent::new(label)));
38    let color = universe
39        .world
40        .add_component(ColorComponent::rgba(0.40, 0.05, 0.05, 1.0));
41
42    let _ = universe.world.add_child(root, style);
43    let _ = universe.world.add_child(root, text_root);
44    let _ = universe.world.add_child(text_root, text);
45    let _ = universe.world.add_child(text, color);
46
47    universe.attach(owner, root).expect("attach routed child");
48}
examples/triage/panel-pierce.rs (lines 26-29)
16fn spawn_row(
17    universe: &mut engine::Universe,
18    scrolling: engine::ecs::ComponentId,
19    index: usize,
20) -> engine::ecs::ComponentId {
21    use engine::ecs::component::{
22        ColorComponent, RenderableComponent, TextComponent, TransformComponent,
23    };
24
25    let label = format!("row_{index:02}");
26    let row = universe.world.add_component_boxed_named(
27        label.clone(),
28        Box::new(TransformComponent::new().with_position(0.0, -(index as f32) * 1.15, 0.05)),
29    );
30    let panel = universe.world.add_component_boxed_named(
31        format!("{label}_panel"),
32        Box::new(
33            TransformComponent::new()
34                .with_position(2.3, -0.45, 0.0)
35                .with_scale(4.6, 0.9, 1.0),
36        ),
37    );
38    let panel_renderable = universe.world.add_component_boxed_named(
39        format!("{label}_rend"),
40        Box::new(RenderableComponent::square()),
41    );
42    let panel_color = universe
43        .world
44        .add_component(ColorComponent::rgba(0.98, 0.94, 0.78, 1.0));
45
46    let text_anchor = universe.world.add_component_boxed_named(
47        format!("{label}_text_anchor"),
48        Box::new(
49            TransformComponent::new()
50                .with_position(0.35, -0.38, 0.02)
51                .with_scale(0.11, 0.11, 0.11),
52        ),
53    );
54    let text = universe.world.add_component_boxed_named(
55        format!("{label}_text"),
56        Box::new(TextComponent::new(label.clone())),
57    );
58    let text_color = universe
59        .world
60        .add_component(ColorComponent::rgba(0.20, 0.16, 0.08, 1.0));
61
62    let _ = universe.world.add_child(row, panel);
63    let _ = universe.world.add_child(panel, panel_renderable);
64    let _ = universe.world.add_child(panel_renderable, panel_color);
65    let _ = universe.world.add_child(row, text_anchor);
66    let _ = universe.world.add_child(text_anchor, text);
67    let _ = universe.world.add_child(text, text_color);
68
69    universe.attach(scrolling, row).expect("attach scroll row");
70    row
71}
examples/scrolling.rs (lines 43-46)
30fn spawn_scroll_item(
31    universe: &mut engine::Universe,
32    scrolling: engine::ecs::ComponentId,
33    prefix: &str,
34    index: usize,
35    panel_rgba: [f32; 4],
36    text_rgba: [f32; 4],
37) -> engine::ecs::ComponentId {
38    use engine::ecs::component::{
39        ColorComponent, RenderableComponent, TextComponent, TransformComponent,
40    };
41
42    let label = format!("{prefix}_item_{index:02}");
43    let root = universe.world.add_component_boxed_named(
44        label.clone(),
45        Box::new(TransformComponent::new().with_position(0.0, -(index as f32) * 1.15, 0.05)),
46    );
47    let panel = universe.world.add_component_boxed_named(
48        format!("{label}_panel"),
49        Box::new(
50            TransformComponent::new()
51                .with_position(2.3, -0.45, 0.0)
52                .with_scale(4.6, 0.9, 1.0),
53        ),
54    );
55    let panel_renderable = universe.world.add_component_boxed_named(
56        format!("{label}_renderable"),
57        Box::new(RenderableComponent::square()),
58    );
59    let panel_color = universe.world.add_component(ColorComponent::rgba(
60        panel_rgba[0],
61        panel_rgba[1],
62        panel_rgba[2],
63        panel_rgba[3],
64    ));
65
66    let text_anchor = universe.world.add_component_boxed_named(
67        format!("{label}_text_anchor"),
68        Box::new(
69            TransformComponent::new()
70                .with_position(0.35, -0.38, 0.02)
71                .with_scale(0.11, 0.11, 0.11),
72        ),
73    );
74    let text = universe.world.add_component_boxed_named(
75        format!("{label}_text"),
76        Box::new(TextComponent::new(label.clone())),
77    );
78    let text_color = universe.world.add_component(ColorComponent::rgba(
79        text_rgba[0],
80        text_rgba[1],
81        text_rgba[2],
82        text_rgba[3],
83    ));
84
85    let _ = universe.world.add_child(root, panel);
86    let _ = universe.world.add_child(panel, panel_renderable);
87    let _ = universe.world.add_child(panel_renderable, panel_color);
88    let _ = universe.world.add_child(root, text_anchor);
89    let _ = universe.world.add_child(text_anchor, text);
90    let _ = universe.world.add_child(text, text_color);
91
92    universe
93        .attach(scrolling, root)
94        .expect("attach scroll item");
95    root
96}
examples/vtuber-joints-example.rs (lines 198-205)
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}
Source

pub fn add_component_boxed_with_guid_named( &mut self, guid: Uuid, name: impl Into<String>, c: Box<dyn Component>, ) -> ComponentId

Add a new boxed component with a restored GUID and explicit stored name.

This is intended for deserialization.

Source

pub fn spawn_component_boxed(&mut self, c: Box<dyn Component>) -> ComponentId

Temporary alias during migration.

Source

pub fn get_component_record(&self, id: ComponentId) -> Option<&ComponentNode>

Examples found in repository?
examples/vtuber-joints-example.rs (line 365)
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}
385
386fn select_named_joints(
387    world: &engine::ecs::World,
388    all_joints: &[(usize, engine::ecs::ComponentId)],
389    names: &[String],
390) -> Option<Vec<(usize, engine::ecs::ComponentId)>> {
391    if names.is_empty() {
392        return None;
393    }
394
395    let mut out: Vec<(usize, engine::ecs::ComponentId)> = Vec::new();
396    for wanted in names {
397        let mut found: Option<(usize, engine::ecs::ComponentId)> = None;
398        for &(node_index, joint_tx) in all_joints.iter() {
399            let name = world
400                .get_component_record(joint_tx)
401                .map(|r| r.name.as_str())
402                .unwrap_or("");
403            if name == wanted {
404                found = Some((node_index, joint_tx));
405                break;
406            }
407        }
408
409        if let Some(v) = found {
410            out.push(v);
411        } else {
412            println!(
413                "[vtuber-joints-example] warning: target joint '{}' not found (check names via REPL tree)",
414                wanted
415            );
416        }
417    }
418
419    if out.is_empty() { None } else { Some(out) }
420}
421
422fn debug_print_selected_joint_influence(
423    universe: &engine::Universe,
424    mesh_key: &str,
425    model_root: engine::ecs::ComponentId,
426    selected: &[(usize, engine::ecs::ComponentId)],
427) {
428    let Some(renderable) = find_renderable_by_mesh_key(&universe.world, model_root, mesh_key)
429    else {
430        println!(
431            "[vtuber-joints-example] influence: mesh_key='{}' renderable not found",
432            mesh_key
433        );
434        return;
435    };
436
437    let renderable_handle = universe
438        .world
439        .get_component_by_id_as::<RenderableComponent>(renderable)
440        .and_then(|r| r.get_handle());
441    println!(
442        "[vtuber-joints-example] influence: mesh_key='{}' renderable={:?} instance_handle={:?}",
443        mesh_key, renderable, renderable_handle
444    );
445    let Some(skin_id) = find_skin_id_for_renderable(&universe.world, renderable) else {
446        println!(
447            "[vtuber-joints-example] influence: mesh_key='{}' has no skin_id",
448            mesh_key
449        );
450        return;
451    };
452    let Some(skin) = universe.visuals.skin(skin_id) else {
453        println!(
454            "[vtuber-joints-example] influence: skin_id={:?} missing in visuals",
455            skin_id
456        );
457        return;
458    };
459    let Some(cpu_h) = universe.render_assets.imported_mesh(mesh_key) else {
460        println!(
461            "[vtuber-joints-example] influence: mesh_key='{}' missing in RenderAssets (did meshes register?)",
462            mesh_key
463        );
464        return;
465    };
466    let Some(cpu) = universe.render_assets.cpu_mesh(cpu_h) else {
467        println!(
468            "[vtuber-joints-example] influence: mesh_key='{}' cpu mesh handle invalid",
469            mesh_key
470        );
471        return;
472    };
473    let (Some(joints0), Some(weights0)) = (cpu.joints0.as_ref(), cpu.weights0.as_ref()) else {
474        println!(
475            "[vtuber-joints-example] influence: mesh_key='{}' has no skin attributes",
476            mesh_key
477        );
478        return;
479    };
480
481    let joint_count = skin.joint_count();
482    let totals = compute_total_weight_per_joint(joints0, weights0, joint_count);
483
484    println!(
485        "[vtuber-joints-example] influence: mesh_key='{}' verts={} joints={}",
486        mesh_key,
487        cpu.vertices.len(),
488        joint_count
489    );
490
491    for &(node_index, joint_tx) in selected.iter() {
492        // Find the skin joint index that maps to this node.
493        let skin_joint_index = skin
494            .joint_node_indices
495            .iter()
496            .position(|&ni| ni == node_index);
497
498        let name = universe
499            .world
500            .get_component_record(joint_tx)
501            .map(|r| r.name.as_str())
502            .unwrap_or("<unknown>");
503
504        match skin_joint_index {
505            Some(j) => {
506                let total = totals.get(j).copied().unwrap_or(0.0);
507                println!(
508                    "  influence: name='{}' node_index={} skin_joint_index={} total_weight={:.3}",
509                    name, node_index, j, total
510                );
511            }
512            None => {
513                println!(
514                    "  influence: name='{}' node_index={} skin_joint_index=<none>",
515                    name, node_index
516                );
517            }
518        }
519    }
520}
521
522fn compute_total_weight_per_joint(
523    joints0: &[[u16; 4]],
524    weights0: &[[f32; 4]],
525    joint_count: usize,
526) -> Vec<f32> {
527    let mut totals = vec![0.0f32; joint_count];
528    if joint_count == 0 {
529        return totals;
530    }
531    for (jv, wv) in joints0.iter().zip(weights0.iter()) {
532        for lane in 0..4 {
533            let j = jv[lane] as usize;
534            if j >= joint_count {
535                continue;
536            }
537            let w = wv[lane];
538            if w > 0.0 {
539                totals[j] += w;
540            }
541        }
542    }
543    totals
544}
545
546fn select_body_prim0_influencers(
547    universe: &engine::Universe,
548    model_root: engine::ecs::ComponentId,
549    mesh_key: &str,
550    count: usize,
551    node_index_to_transform: &HashMap<usize, engine::ecs::ComponentId>,
552) -> Option<Vec<(usize, engine::ecs::ComponentId)>> {
553    if count == 0 {
554        return Some(Vec::new());
555    }
556
557    let renderable = find_renderable_by_mesh_key(&universe.world, model_root, mesh_key)?;
558    let skin_id = find_skin_id_for_renderable(&universe.world, renderable)?;
559    let skin = universe.visuals.skin(skin_id)?;
560
561    let cpu_h = universe.render_assets.imported_mesh(mesh_key)?;
562    let cpu = universe.render_assets.cpu_mesh(cpu_h)?;
563    let joints0 = cpu.joints0.as_ref()?;
564    let weights0 = cpu.weights0.as_ref()?;
565
566    let joint_count = skin.joint_count();
567    if joint_count == 0 {
568        return None;
569    }
570
571    let mut total_weight_per_joint: Vec<f32> = vec![0.0; joint_count];
572    for (jv, wv) in joints0.iter().zip(weights0.iter()) {
573        for lane in 0..4 {
574            let j = jv[lane] as usize;
575            if j >= joint_count {
576                continue;
577            }
578            let w = wv[lane];
579            if w > 0.0 {
580                total_weight_per_joint[j] += w;
581            }
582        }
583    }
584
585    let mut ranked: Vec<(usize, f32)> =
586        total_weight_per_joint.iter().copied().enumerate().collect();
587    ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
588
589    let min_total: f32 = 10.0;
590
591    println!(
592        "[vtuber-joints-example] auto joints for mesh_key='{}': verts={} joint_count={} min_total={}",
593        mesh_key,
594        cpu.vertices.len(),
595        joint_count,
596        min_total
597    );
598
599    let mut out: Vec<(usize, engine::ecs::ComponentId)> = Vec::with_capacity(count);
600    let mut seen: HashSet<engine::ecs::ComponentId> = HashSet::new();
601
602    for (joint_index, total) in ranked.into_iter() {
603        if out.len() >= count {
604            break;
605        }
606        if total < min_total {
607            continue;
608        }
609
610        let node_index = *skin.joint_node_indices.get(joint_index)?;
611        let Some(&joint_tx) = node_index_to_transform.get(&node_index) else {
612            continue;
613        };
614        if !seen.insert(joint_tx) {
615            continue;
616        }
617
618        let name = universe
619            .world
620            .get_component_record(joint_tx)
621            .map(|n| n.name.clone())
622            .unwrap_or_else(|| "<unknown>".to_string());
623
624        println!(
625            "  auto[{:<2}] joint_index={:<3} total_weight={:<10.3} node_index={:<3} name={} tx={:?}",
626            out.len(),
627            joint_index,
628            total,
629            node_index,
630            name,
631            joint_tx
632        );
633
634        out.push((node_index, joint_tx));
635    }
636
637    if out.is_empty() { None } else { Some(out) }
638}
Source

pub fn get_component_node(&self, id: ComponentId) -> Option<&ComponentNode>

Alias for get_component_record.

Source

pub fn get_component_record_mut( &mut self, id: ComponentId, ) -> Option<&mut ComponentNode>

Source

pub fn component_name(&self, id: ComponentId) -> Option<&str>

Returns the engine type identifier for this component (e.g. "transform").

Examples found in repository?
examples/bisket-vr-demo.rs (line 77)
76fn component_label(world: &World, id: ComponentId) -> String {
77    world.component_name(id).unwrap_or("?").to_string()
78}
More examples
Hide additional examples
examples/vtuber-editor-example.rs (line 11)
10fn component_label(world: &World, id: ComponentId) -> String {
11    world.component_name(id).unwrap_or("?").to_string()
12}
examples/vtuber-mirror-example.rs (line 11)
10fn component_label(world: &World, id: ComponentId) -> String {
11    world.component_name(id).unwrap_or("?").to_string()
12}
examples/bisket-vr-debug.rs (line 1013)
1010fn describe_component(world: &World, id: ComponentId) -> String {
1011    let label = world
1012        .component_label(id)
1013        .or_else(|| world.component_name(id));
1014    format!("{}({id:?})", label.unwrap_or("?"))
1015}
examples/qdebug.rs (line 24)
2fn main() {
3    mittens_engine::example_support::ensure_model_assets();
4    utils::logger::init();
5    let world = engine::ecs::World::default();
6    let mut universe = engine::Universe::new(world);
7    let src = include_str!("qdebug.mms");
8    let out = scripting::MeowMeowRunner::eval_with_world(
9        src,
10        &mut universe.world,
11        &mut universe.systems.rx,
12        &mut universe.command_queue,
13    );
14    for e in &out.errors {
15        eprintln!("[mms-err] {e}");
16    }
17    println!("intents: {}", out.intents.len());
18    for c in universe.world.all_components() {
19        if let Some(label) = universe.world.component_label(c) {
20            {
21                println!(
22                    "  comp {:?} type={:?} name={:?}",
23                    c,
24                    universe.world.component_name(c),
25                    label
26                );
27            }
28        }
29    }
30}
Source

pub fn component_label(&self, id: ComponentId) -> Option<&str>

Returns the user-assigned label for this component (empty string if unset).

Examples found in repository?
examples/bisket-vr-debug.rs (line 1012)
1010fn describe_component(world: &World, id: ComponentId) -> String {
1011    let label = world
1012        .component_label(id)
1013        .or_else(|| world.component_name(id));
1014    format!("{}({id:?})", label.unwrap_or("?"))
1015}
1016
1017fn set_local_transform(
1018    emit: &mut dyn SignalEmitter,
1019    id: ComponentId,
1020    translation: [f32; 3],
1021    rotation_quat_xyzw: [f32; 4],
1022    scale: [f32; 3],
1023) {
1024    emit.push_intent_now(
1025        id,
1026        IntentValue::UpdateTransform {
1027            component_ids: vec![id],
1028            translation,
1029            rotation_quat_xyzw,
1030            scale,
1031        },
1032    );
1033}
1034
1035fn world_pose_to_local(
1036    world: &World,
1037    id: ComponentId,
1038    desired_world_pos: [f32; 3],
1039    desired_world_rot: [f32; 4],
1040) -> ([f32; 3], [f32; 4], [f32; 3]) {
1041    let scale = world
1042        .get_component_by_id_as::<TransformComponent>(id)
1043        .map(|t| t.transform.scale)
1044        .unwrap_or([1.0, 1.0, 1.0]);
1045
1046    let parent_world_mat = parent_world_matrix(world, id).unwrap_or(mat4_identity());
1047    let parent_world_rot = parent_world_rotation(world, id).unwrap_or(ident_quat());
1048    let inv_parent = mat4_inverse(parent_world_mat).unwrap_or(mat4_identity());
1049    let local_pos4 = mat4_mul_vec4(
1050        inv_parent,
1051        [
1052            desired_world_pos[0],
1053            desired_world_pos[1],
1054            desired_world_pos[2],
1055            1.0,
1056        ],
1057    );
1058    let local_rot = quat_mul(quat_conjugate(parent_world_rot), desired_world_rot);
1059    (
1060        [local_pos4[0], local_pos4[1], local_pos4[2]],
1061        local_rot,
1062        scale,
1063    )
1064}
1065
1066fn parent_world_matrix(world: &World, id: ComponentId) -> Option<[[f32; 4]; 4]> {
1067    let parent = world.parent_of(id)?;
1068    world
1069        .get_component_by_id_as::<TransformComponent>(parent)
1070        .map(|t| t.transform.matrix_world)
1071}
1072
1073fn parent_world_rotation(world: &World, id: ComponentId) -> Option<[f32; 4]> {
1074    let parent = world.parent_of(id)?;
1075    world
1076        .get_component_by_id_as::<TransformComponent>(parent)
1077        .map(|t| mat_to_quat(t.transform.matrix_world))
1078}
1079
1080fn forearm_axis_world(world: &World, lower_arm: ComponentId, hand: ComponentId) -> [f32; 3] {
1081    let axis = vec3_sub(world_pos(world, hand), world_pos(world, lower_arm));
1082    if vec3_len(axis) > 1e-6 {
1083        vec3_normalize(axis)
1084    } else {
1085        [0.0, 0.0, 1.0]
1086    }
1087}
1088
1089fn quat_delta_deg(from: [f32; 4], to: [f32; 4]) -> f32 {
1090    let (_, angle) = quat_to_axis_angle(quat_mul(quat_conjugate(from), to));
1091    angle.abs().to_degrees()
1092}
1093
1094fn fmt_quat(q: [f32; 4]) -> String {
1095    format!("[{:+.3},{:+.3},{:+.3},{:+.3}]", q[0], q[1], q[2], q[3])
1096}
1097
1098fn ident_quat() -> [f32; 4] {
1099    [0.0, 0.0, 0.0, 1.0]
1100}
1101
1102fn drain(universe: &mut engine::Universe) {
1103    universe.systems.process_commands(
1104        &mut universe.world,
1105        &mut universe.visuals,
1106        &mut universe.render_assets,
1107        &mut universe.command_queue,
1108    );
1109}
1110
1111fn tick_gltf(universe: &mut engine::Universe) {
1112    let systems = &mut universe.systems;
1113    systems.gltf.tick_with_queue(
1114        &mut universe.world,
1115        &mut universe.visuals,
1116        &mut systems.skinned_mesh,
1117        &mut universe.command_queue,
1118        0.0,
1119    );
1120}
1121
1122fn settle_world(universe: &mut engine::Universe, ticks: usize) {
1123    let input = InputState::default();
1124    for _ in 0..ticks {
1125        universe.systems.tick(
1126            &mut universe.world,
1127            &mut universe.visuals,
1128            &mut universe.render_assets,
1129            &input,
1130            &mut universe.command_queue,
1131            1.0 / 60.0,
1132        );
1133        drain(universe);
1134    }
1135}
1136
1137fn find_components_by_name(world: &World, name: &str) -> Vec<ComponentId> {
1138    world
1139        .all_components()
1140        .filter(|&id| world.component_label(id).map_or(false, |n| n == name))
1141        .collect()
1142}
More examples
Hide additional examples
examples/scrolling.rs (line 6)
3fn find_named(world: &engine::ecs::World, label: &str) -> engine::ecs::ComponentId {
4    world
5        .all_components()
6        .find(|&cid| world.component_label(cid) == Some(label))
7        .unwrap_or_else(|| panic!("missing component {label:?}"))
8}
examples/triage/panel-pierce.rs (line 12)
9fn find_named(world: &engine::ecs::World, label: &str) -> engine::ecs::ComponentId {
10    world
11        .all_components()
12        .find(|&cid| world.component_label(cid) == Some(label))
13        .unwrap_or_else(|| panic!("missing component {label:?}"))
14}
examples/diy-panel.rs (line 65)
50fn collect_container_items(
51    world: &engine::ecs::World,
52    container: engine::ecs::ComponentId,
53) -> Vec<engine::ecs::ComponentId> {
54    use engine::ecs::component::TransformComponent;
55
56    world
57        .children_of(container)
58        .iter()
59        .copied()
60        .filter(|&child| {
61            world
62                .get_component_by_id_as::<TransformComponent>(child)
63                .is_some()
64                && !world
65                    .component_label(child)
66                    .map(|label| label.starts_with("__"))
67                    .unwrap_or(false)
68        })
69        .collect()
70}
71
72fn decompose_world_trs(matrix_world: [[f32; 4]; 4]) -> ([f32; 3], [f32; 4], [f32; 3]) {
73    let translation = [matrix_world[3][0], matrix_world[3][1], matrix_world[3][2]];
74    let scale = [
75        (matrix_world[0][0] * matrix_world[0][0]
76            + matrix_world[0][1] * matrix_world[0][1]
77            + matrix_world[0][2] * matrix_world[0][2])
78            .sqrt(),
79        (matrix_world[1][0] * matrix_world[1][0]
80            + matrix_world[1][1] * matrix_world[1][1]
81            + matrix_world[1][2] * matrix_world[1][2])
82            .sqrt(),
83        (matrix_world[2][0] * matrix_world[2][0]
84            + matrix_world[2][1] * matrix_world[2][1]
85            + matrix_world[2][2] * matrix_world[2][2])
86            .sqrt(),
87    ];
88    let rotation = mittens_engine::utils::math::mat_to_quat(matrix_world);
89    (translation, rotation, scale)
90}
91
92fn spawn_container_item_debug_clones(
93    universe: &mut engine::Universe,
94    container: engine::ecs::ComponentId,
95) {
96    use engine::ecs::IntentValue;
97    use engine::ecs::system::TransformSystem;
98    use mittens_engine::scripting::component_registry::{
99        ce_ast_to_materialized, spawn_tree, subtree_to_ce_ast,
100    };
101
102    let items = collect_container_items(&universe.world, container);
103    for (index, item) in items.into_iter().enumerate() {
104        let Some(matrix_world) = TransformSystem::world_model(&universe.world, item) else {
105            println!("[diy-panel-debug] skip {:?}: no world matrix", item);
106            continue;
107        };
108
109        let item_label = universe
110            .world
111            .component_label(item)
112            .unwrap_or("")
113            .to_string();
114        println!(
115            "[diy-panel-debug] item={} label={:?} matrix_world={:?}",
116            index, item_label, matrix_world,
117        );
118
119        // Subtree → MMS AST → MaterializedCE → fresh spawn.
120        // Same path attach_clone uses; gives the clone fresh GUIDs and
121        // routes everything through the standard component registry.
122        let Ok(ce_ast) = subtree_to_ce_ast(&universe.world, item) else {
123            println!(
124                "[diy-panel-debug] skip {:?}: failed to encode subtree",
125                item
126            );
127            continue;
128        };
129        let Ok(materialized) = ce_ast_to_materialized(&ce_ast) else {
130            println!(
131                "[diy-panel-debug] skip {:?}: failed to materialize CE",
132                item
133            );
134            continue;
135        };
136        let Ok(clone_root) = spawn_tree(
137            &materialized,
138            None,
139            &mut universe.world,
140            &mut universe.command_queue,
141        ) else {
142            println!("[diy-panel-debug] skip {:?}: failed to spawn clone", item);
143            continue;
144        };
145
146        universe.add(clone_root);
147
148        let (mut clone_translation, clone_rotation, clone_scale) =
149            decompose_world_trs(matrix_world);
150        clone_translation[0] += DEBUG_CLONE_WORLD_OFFSET[0];
151        clone_translation[1] += DEBUG_CLONE_WORLD_OFFSET[1];
152        clone_translation[2] += DEBUG_CLONE_WORLD_OFFSET[2];
153
154        universe.command_queue.push_intent_now(
155            clone_root,
156            IntentValue::UpdateTransform {
157                component_ids: vec![clone_root],
158                translation: clone_translation,
159                rotation_quat_xyzw: clone_rotation,
160                scale: clone_scale,
161            },
162        );
163
164        let label = if item_label.is_empty() {
165            format!("item_{index}")
166        } else {
167            format!("{}_{}", item_label, index)
168        };
169        println!(
170            "[diy-panel-debug] cloned item={} label={:?} clone_root={:?} offset_translation={:?}",
171            index, label, clone_root, clone_translation,
172        );
173    }
174}
175
176fn main() {
177    mittens_engine::example_support::ensure_model_assets();
178    utils::logger::init();
179
180    let output = scripting::MeowMeowRunner::eval(include_str!("diy-panel.mms"));
181
182    for error in &output.errors {
183        eprintln!("[mms] {error}");
184    }
185    println!(
186        "[mms] {} intent(s) from diy-panel.mms",
187        output.intents.len()
188    );
189
190    if !output.errors.is_empty() {
191        std::process::exit(1);
192    }
193
194    let world = engine::ecs::World::default();
195    let mut universe = engine::Universe::new(world);
196
197    let scope = engine::ecs::ComponentId::default();
198    for intent in output.intents {
199        universe.command_queue.push_intent_now(scope, intent);
200    }
201
202    universe.systems.process_commands(
203        &mut universe.world,
204        &mut universe.visuals,
205        &mut universe.render_assets,
206        &mut universe.command_queue,
207    );
208
209    universe.enable_repl();
210
211    let demo_root = universe
212        .world
213        .all_components()
214        .find(|&cid| universe.world.component_label(cid) == Some("diy_panel_demo"))
215        .expect("diy panel demo root");
216    let container = universe
217        .find_component(demo_root, "[name='container']")
218        .expect("container target");
219    let authored_child = universe
220        .find_component(demo_root, "[name='authored_child']")
221        .expect("authored child");
222
223    assert_eq!(universe.parent_of(authored_child), Some(container));
224
225    spawn_runtime_text(&mut universe, demo_root, "late child routed on attach");
226
227    let late_child = universe
228        .find_component(demo_root, "[name='late child routed on attach']")
229        .expect("late child");
230    assert_eq!(universe.parent_of(late_child), Some(container));
231
232    let input = engine::user_input::InputState::default();
233    universe.systems.tick(
234        &mut universe.world,
235        &mut universe.visuals,
236        &mut universe.render_assets,
237        &input,
238        &mut universe.command_queue,
239        0.0,
240    );
241    universe.systems.process_commands(
242        &mut universe.world,
243        &mut universe.visuals,
244        &mut universe.render_assets,
245        &mut universe.command_queue,
246    );
247
248    //spawn_container_item_debug_clones(&mut universe, container);
249    universe.systems.process_commands(
250        &mut universe.world,
251        &mut universe.visuals,
252        &mut universe.render_assets,
253        &mut universe.command_queue,
254    );
255
256    let scroll = universe
257        .world
258        .children_of(container)
259        .iter()
260        .copied()
261        .find(|&child| {
262            universe.world.component_label(child) == Some("__scroll")
263                && universe
264                    .world
265                    .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(child)
266                    .is_some()
267        })
268        .expect("layout-owned scroll wrapper");
269    let track = universe
270        .world
271        .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(scroll)
272        .and_then(|sc| sc.track)
273        .expect("layout-owned scroll track");
274
275    assert_eq!(universe.parent_of(authored_child), Some(track));
276    assert_eq!(universe.parent_of(late_child), Some(track));
277
278    println!(
279        "[diy-panel] init-time routing verified, then layout-owned scrolling moved content into __scroll_track"
280    );
281
282    engine::Windowing::run_app(universe).expect("Windowing failed");
283}
examples/qdebug.rs (line 19)
2fn main() {
3    mittens_engine::example_support::ensure_model_assets();
4    utils::logger::init();
5    let world = engine::ecs::World::default();
6    let mut universe = engine::Universe::new(world);
7    let src = include_str!("qdebug.mms");
8    let out = scripting::MeowMeowRunner::eval_with_world(
9        src,
10        &mut universe.world,
11        &mut universe.systems.rx,
12        &mut universe.command_queue,
13    );
14    for e in &out.errors {
15        eprintln!("[mms-err] {e}");
16    }
17    println!("intents: {}", out.intents.len());
18    for c in universe.world.all_components() {
19        if let Some(label) = universe.world.component_label(c) {
20            {
21                println!(
22                    "  comp {:?} type={:?} name={:?}",
23                    c,
24                    universe.world.component_name(c),
25                    label
26                );
27            }
28        }
29    }
30}
examples/router.rs (line 82)
41fn main() {
42    mittens_engine::example_support::ensure_model_assets();
43    utils::logger::init();
44
45    let world = engine::ecs::World::default();
46    let mut universe = engine::Universe::new(world);
47
48    let output = scripting::MeowMeowRunner::eval_with_world_at_path(
49        include_str!("router.mms"),
50        Some("examples/router.mms"),
51        &mut universe.world,
52        &mut universe.systems.rx,
53        &mut universe.command_queue,
54    );
55
56    for error in &output.errors {
57        eprintln!("[mms] {error}");
58    }
59    println!("[mms] {} intent(s) from router.mms", output.intents.len());
60
61    if !output.errors.is_empty() {
62        std::process::exit(1);
63    }
64
65    let scope = engine::ecs::ComponentId::default();
66    for intent in output.intents {
67        universe.command_queue.push_intent_now(scope, intent);
68    }
69
70    universe.systems.process_commands(
71        &mut universe.world,
72        &mut universe.visuals,
73        &mut universe.render_assets,
74        &mut universe.command_queue,
75    );
76
77    universe.enable_repl();
78
79    let demo_root = universe
80        .world
81        .all_components()
82        .find(|&cid| universe.world.component_label(cid) == Some("router_demo"))
83        .expect("router demo root");
84    let container = universe
85        .find_component(demo_root, "[name='container']")
86        .expect("container target");
87    let authored_child = universe
88        .find_component(demo_root, "[name='authored_child']")
89        .expect("authored child");
90
91    assert_eq!(universe.parent_of(authored_child), Some(container));
92
93    spawn_runtime_text(&mut universe, demo_root, "late child routed on attach");
94
95    let late_child = universe
96        .find_component(demo_root, "[name='late child routed on attach']")
97        .expect("late child");
98    assert_eq!(universe.parent_of(late_child), Some(container));
99
100    println!("[router-demo] init-time and attach-time routing verified");
101
102    engine::Windowing::run_app(universe).expect("Windowing failed");
103}
Source

pub fn parent_of(&self, c: ComponentId) -> Option<ComponentId>

Examples found in repository?
examples/bisket-vr-demo.rs (line 86)
80fn controller_hand_for_component(world: &World, start: ComponentId) -> Option<ControllerHand> {
81    let mut cur = start;
82    loop {
83        if let Some(ctrl) = world.get_component_by_id_as::<ControllerXRComponent>(cur) {
84            return Some(ctrl.hand);
85        }
86        cur = world.parent_of(cur)?;
87    }
88}
89
90fn on_xr_pointer_event(world: &mut World, _emit: &mut dyn SignalEmitter, env: &Signal) {
91    let Some(event) = env.event.as_ref() else {
92        return;
93    };
94    let (kind, raycaster, renderable, hit_point) = match event {
95        EventSignal::DragStart {
96            raycaster,
97            renderable,
98            hit_point,
99            ..
100        } => ("DragStart", *raycaster, *renderable, Some(*hit_point)),
101        EventSignal::DragMove {
102            raycaster,
103            renderable,
104            hit_point,
105            ..
106        } => ("DragMove", *raycaster, *renderable, Some(*hit_point)),
107        EventSignal::DragEnd {
108            raycaster,
109            renderable,
110            hit_point,
111        } => ("DragEnd", *raycaster, *renderable, *hit_point),
112        EventSignal::Click {
113            raycaster,
114            renderable,
115            hit_point,
116            ..
117        } => ("Click", *raycaster, *renderable, Some(*hit_point)),
118        _ => return,
119    };
120
121    let Some(hand) = controller_hand_for_component(world, raycaster) else {
122        return;
123    };
124    let hand_label = match hand {
125        ControllerHand::Left => "Left",
126        ControllerHand::Right => "Right",
127    };
128    let renderable_name = component_label(world, renderable);
129    let ray_name = component_label(world, raycaster);
130    if let Some(p) = hit_point {
131        println!(
132            "[xr-pointer] hand={} kind={} raycaster={} renderable={} hit=[{:+.3},{:+.3},{:+.3}]",
133            hand_label, kind, ray_name, renderable_name, p[0], p[1], p[2]
134        );
135    } else {
136        println!(
137            "[xr-pointer] hand={} kind={} raycaster={} renderable={}",
138            hand_label, kind, ray_name, renderable_name
139        );
140    }
141}
142
143fn on_dump_click(world: &mut World, _emit: &mut dyn SignalEmitter, env: &Signal) {
144    if !matches!(env.event.as_ref(), Some(EventSignal::Click { .. })) {
145        return;
146    }
147    let Some(targets) = DUMP_TARGETS.get() else {
148        return;
149    };
150
151    let driven_p = world_pos(world, targets.driven_t);
152    let driven_q = world_rot_quat(world, targets.driven_t);
153    let splice_p = world_pos(world, targets.splice_head);
154    let head_p = world_pos(world, targets.head_bone);
155    let head_q = world_rot_quat(world, targets.head_bone);
156
157    // Expected head pivot from HMD pose + authored eye offset convention.
158    let neg_eye = [
159        -targets.authored_eye_offset_head_local[0],
160        -targets.authored_eye_offset_head_local[1],
161        -targets.authored_eye_offset_head_local[2],
162    ];
163    let head_target_offset_local =
164        quat_rotate_vec3(quat_rotation_y(targets.head_ik_offset_yaw), neg_eye);
165    let head_target_offset_world = quat_rotate_vec3(driven_q, head_target_offset_local);
166    let expected_splice = [
167        driven_p[0] + head_target_offset_world[0],
168        driven_p[1] + head_target_offset_world[1],
169        driven_p[2] + head_target_offset_world[2],
170    ];
171    let splice_err = [
172        splice_p[0] - expected_splice[0],
173        splice_p[1] - expected_splice[1],
174        splice_p[2] - expected_splice[2],
175    ];
176
177    // Camera-wrapper vs head local-offset consistency check.
178    let mut cam_rel_report = String::from("camera_wrapper: <none>");
179    if let Some(cam_t) = targets.camera_wrapper_t {
180        let cam_p = world_pos(world, cam_t);
181        let expected_cam_world_off =
182            quat_rotate_vec3(head_q, targets.authored_eye_offset_head_local);
183        let expected_cam = [
184            head_p[0] + expected_cam_world_off[0],
185            head_p[1] + expected_cam_world_off[1],
186            head_p[2] + expected_cam_world_off[2],
187        ];
188        let cam_err = [
189            cam_p[0] - expected_cam[0],
190            cam_p[1] - expected_cam[1],
191            cam_p[2] - expected_cam[2],
192        ];
193        cam_rel_report = format!(
194            "camera_wrapper  pos=[{:+.3},{:+.3},{:+.3}]  expected_from_head=[{:+.3},{:+.3},{:+.3}]  err=[{:+.3},{:+.3},{:+.3}] |err|={:.4}m",
195            cam_p[0],
196            cam_p[1],
197            cam_p[2],
198            expected_cam[0],
199            expected_cam[1],
200            expected_cam[2],
201            cam_err[0],
202            cam_err[1],
203            cam_err[2],
204            v3_len(cam_err)
205        );
206    }
207
208    // Torso tilt diagnostic (forward pitch of upper chest).
209    let mut torso_report = String::from("upper_chest_tilt: <not found>");
210    if let Some(upper_chest) = targets.upper_chest {
211        let chest_q = world_rot_quat(world, upper_chest);
212        let fwd = quat_rotate_vec3(chest_q, [0.0, 0.0, 1.0]);
213        let pitch_deg = deg(fwd[1].atan2((fwd[0] * fwd[0] + fwd[2] * fwd[2]).sqrt()));
214        torso_report = format!(
215            "upper_chest_fwd=[{:+.3},{:+.3},{:+.3}] pitch={:+.2}deg",
216            fwd[0], fwd[1], fwd[2], pitch_deg
217        );
218    }
219
220    println!("\n================ BONE DUMP (click) ================");
221    println!(
222        "driven_t  pos=[{:+.3},{:+.3},{:+.3}]  rot_xyzw=[{:+.3},{:+.3},{:+.3},{:+.3}]",
223        driven_p[0], driven_p[1], driven_p[2], driven_q[0], driven_q[1], driven_q[2], driven_q[3],
224    );
225    println!(
226        "splice_head  pos=[{:+.3},{:+.3},{:+.3}]  expected=[{:+.3},{:+.3},{:+.3}]  err=[{:+.3},{:+.3},{:+.3}] |err|={:.4}m",
227        splice_p[0],
228        splice_p[1],
229        splice_p[2],
230        expected_splice[0],
231        expected_splice[1],
232        expected_splice[2],
233        splice_err[0],
234        splice_err[1],
235        splice_err[2],
236        v3_len(splice_err),
237    );
238    println!("{}", cam_rel_report);
239    println!("{}", torso_report);
240    println!(
241        "\n  {:<22}  {:>7}  {:>7}  {:>7}     {:>+7}  {:>+7}  {:>+7}",
242        "bone", "x", "y", "z", "Δx_to_drv", "Δy_to_drv", "Δz_to_drv"
243    );
244    let mut prev_pos: Option<[f32; 3]> = None;
245    for (i, (label, bone_id)) in targets.bones.iter().enumerate() {
246        let p = world_pos(world, *bone_id);
247        println!(
248            "  {:<22}  {:>+7.3}  {:>+7.3}  {:>+7.3}     {:>+7.3}  {:>+7.3}  {:>+7.3}",
249            label,
250            p[0],
251            p[1],
252            p[2],
253            p[0] - driven_p[0],
254            p[1] - driven_p[1],
255            p[2] - driven_p[2],
256        );
257        // Segment length to previous bone (only meaningful within a chain —
258        // spine bones are contiguous; arm bones are split per side, so we
259        // insert a blank gap when the chain breaks).
260        if let Some(prev) = prev_pos {
261            let is_spine = label.starts_with("J_Bip_C_");
262            let prev_was_spine = targets.bones[i - 1].0.starts_with("J_Bip_C_");
263            let is_left_arm_step =
264                label.starts_with("J_Bip_L_") && targets.bones[i - 1].0.starts_with("J_Bip_L_");
265            let is_right_arm_step =
266                label.starts_with("J_Bip_R_") && targets.bones[i - 1].0.starts_with("J_Bip_R_");
267            if (is_spine && prev_was_spine) || is_left_arm_step || is_right_arm_step {
268                let d = [(p[0] - prev[0]), (p[1] - prev[1]), (p[2] - prev[2])];
269                let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
270                println!("    └─ segment from prev: len={:.4}m", len);
271            }
272        }
273        prev_pos = Some(p);
274    }
275    println!("====================================================\n");
276}
277
278fn main() {
279    mittens_engine::example_support::ensure_model_assets();
280    utils::logger::init();
281
282    let world = engine::ecs::World::default();
283    let mut universe = engine::Universe::new(world);
284
285    let output = scripting::MeowMeowRunner::eval_with_world_and_assets_at_path(
286        include_str!("bisket-vr-demo.mms"),
287        Some("examples/bisket-vr-demo.mms"),
288        &mut universe.world,
289        &mut universe.systems.rx,
290        Some(&mut universe.render_assets),
291        &mut universe.command_queue,
292    );
293
294    for error in &output.errors {
295        eprintln!("[mms] {error}");
296    }
297    println!(
298        "[mms] {} intent(s) from bisket-vr-demo.mms",
299        output.intents.len()
300    );
301
302    let scope = engine::ecs::ComponentId::default();
303    for intent in output.intents {
304        universe.command_queue.push_intent_now(scope, intent);
305    }
306
307    universe.systems.process_commands(
308        &mut universe.world,
309        &mut universe.visuals,
310        &mut universe.render_assets,
311        &mut universe.command_queue,
312    );
313
314    // Force the glTF subtree to spawn so the armature exists before we query for
315    // bone markers (otherwise the bone components aren't in the world yet).
316    {
317        let systems = &mut universe.systems;
318        systems.gltf.tick_with_queue(
319            &mut universe.world,
320            &mut universe.visuals,
321            &mut systems.skinned_mesh,
322            &mut universe.command_queue,
323            0.0,
324        );
325    }
326    universe.systems.process_commands(
327        &mut universe.world,
328        &mut universe.visuals,
329        &mut universe.render_assets,
330        &mut universe.command_queue,
331    );
332
333    // Find any global root to start the descendant search from. Scanning all
334    // components is fine here — this only runs once at startup.
335    let roots: Vec<engine::ecs::ComponentId> = universe
336        .world
337        .all_components()
338        .filter(|&id| universe.world.parent_of(id).is_none())
339        .collect();
340
341    // ----------------------------------------------------------------------
342    // Dump button — click to print a bone snapshot for offline analysis.
343    //
344    // Topology: scene_root → OV → button_t → button_r (+ C, EM, Raycastable).
345    // OV phase so the cube is visible through the avatar body in first-person VR.
346    // Click handler walks the cached bone list, prints world pos + segment
347    // lengths to stdout.  Compare with bisket-vr-debug harness output.
348    // ----------------------------------------------------------------------
349    {
350        // Resolve AVC + driven_t for the dump.
351        let avc_id_opt = universe.world.all_components().find(|&id| {
352            universe
353                .world
354                .get_component_by_id_as::<AvatarControlComponent>(id)
355                .is_some()
356        });
357        if let Some(avc_id) = avc_id_opt {
358            let driven_t = universe.world.parent_of(avc_id).unwrap_or(avc_id);
359            let head_ik_offset_yaw = universe
360                .world
361                .get_component_by_id_as::<AvatarControlComponent>(avc_id)
362                .map(|c| {
363                    if c.forward_plus_z {
364                        0.0
365                    } else {
366                        std::f32::consts::PI
367                    }
368                })
369                .unwrap_or(std::f32::consts::PI);
370
371            // Find each bone under any root (model_root is unique per avatar here).
372            let mut bones: Vec<(String, ComponentId)> = Vec::new();
373            for name in DUMP_BONES {
374                let sel = format!("[name='{}']", name);
375                if let Some(bone) = roots.iter().find_map(|&r| universe.find_component(r, &sel)) {
376                    bones.push((name.to_string(), bone));
377                } else {
378                    eprintln!("[dump] bone not found: {}", name);
379                }
380            }
381            // splice_head = parent of head bone (runtime-inserted by AVC).
382            let splice_head = bones
383                .iter()
384                .find(|(n, _)| n == "J_Bip_C_Head")
385                .and_then(|(_, head)| universe.world.parent_of(*head))
386                .unwrap_or(avc_id);
387
388            let head_bone = bones
389                .iter()
390                .find(|(n, _)| n == "J_Bip_C_Head")
391                .map(|(_, id)| *id)
392                .unwrap_or(splice_head);
393
394            let upper_chest = bones
395                .iter()
396                .find(|(n, _)| n == "J_Bip_C_UpperChest")
397                .map(|(_, id)| *id);
398
399            // Find CameraXR + its transform wrapper to recover authored eye offset.
400            let cxr_id = universe.world.all_components().find(|&cid| {
401                universe
402                    .world
403                    .get_component_by_id_as::<mittens_engine::engine::ecs::component::CameraXRComponent>(cid)
404                    .is_some()
405            });
406            let camera_wrapper_t = cxr_id.and_then(|cid| {
407                universe.world.parent_of(cid).filter(|&p| {
408                    universe
409                        .world
410                        .get_component_by_id_as::<TransformComponent>(p)
411                        .is_some()
412                })
413            });
414            let authored_eye_offset_head_local = camera_wrapper_t
415                .and_then(|t| {
416                    universe
417                        .world
418                        .get_component_by_id_as::<TransformComponent>(t)
419                })
420                .map(|t| t.transform.translation)
421                .unwrap_or([0.0, 0.0, 0.0]);
422
423            let _ = DUMP_TARGETS.set(DumpTargets {
424                bones,
425                driven_t,
426                splice_head,
427                head_bone,
428                upper_chest,
429                camera_wrapper_t,
430                authored_eye_offset_head_local,
431                head_ik_offset_yaw,
432            });
433
434            // Spawn the button.  Position chosen so it's reachable in-VR from
435            // a standing pose: 0.35 m to the right, hip-height, slightly out.
436            let btn_t = universe.world.add_component(
437                TransformComponent::new()
438                    .with_position(0.35, 1.05, -0.35)
439                    .with_scale(0.06, 0.06, 0.06),
440            );
441            let btn_r = universe.world.add_component(RenderableComponent::cube());
442            let btn_c = universe
443                .world
444                .add_component(ColorComponent::rgba(0.95, 0.20, 0.40, 1.0));
445            let btn_em = universe.world.add_component(EmissiveComponent::on());
446            let btn_rcast = universe
447                .world
448                .add_component(RaycastableComponent::enabled());
449            let btn_ov = universe.world.add_component(OverlayComponent::new());
450            let _ = universe.world.add_child(btn_r, btn_c);
451            let _ = universe.world.add_child(btn_r, btn_em);
452            let _ = universe.world.add_child(btn_r, btn_rcast);
453            let _ = universe.world.add_child(btn_t, btn_r);
454            let _ = universe.world.add_child(btn_ov, btn_t);
455
456            // Attach to a stable scene root (the first global root we found).
457            let scene_root = roots
458                .iter()
459                .copied()
460                .find(|&r| {
461                    universe
462                        .world
463                        .get_component_by_id_as::<TransformComponent>(r)
464                        .is_some()
465                        || universe.world.children_of(r).len() > 0
466                })
467                .unwrap_or_else(|| roots[0]);
468            let _ = universe.attach(scene_root, btn_ov);
469
470            // Wire Click handler.  Scope = the renderable (where the raycast hits).
471            universe.add_signal_handler(SignalKind::Click, btn_r, on_dump_click);
472
473            println!(
474                "[dump] button spawned at [0.35, 1.05, -0.35] — click in VR or with desktop pointer to dump bones"
475            );
476        } else {
477            eprintln!("[dump] AvatarControlComponent not found — skipping dump button");
478        }
479    }
480
481    for kind in [
482        SignalKind::DragStart,
483        SignalKind::DragMove,
484        SignalKind::DragEnd,
485        SignalKind::Click,
486    ] {
487        universe
488            .systems
489            .rx
490            .add_global_handler(kind, on_xr_pointer_event);
491    }
492
493    universe.enable_meow_meow_repl();
494    engine::Windowing::run_app(universe).expect("Windowing failed");
495}
More examples
Hide additional examples
examples/vtuber-editor-example.rs (line 20)
14fn controller_hand_for_component(world: &World, start: ComponentId) -> Option<ControllerHand> {
15    let mut cur = start;
16    loop {
17        if let Some(ctrl) = world.get_component_by_id_as::<ControllerXRComponent>(cur) {
18            return Some(ctrl.hand);
19        }
20        cur = world.parent_of(cur)?;
21    }
22}
examples/vtuber-mirror-example.rs (line 20)
14fn controller_hand_for_component(world: &World, start: ComponentId) -> Option<ControllerHand> {
15    let mut cur = start;
16    loop {
17        if let Some(ctrl) = world.get_component_by_id_as::<ControllerXRComponent>(cur) {
18            return Some(ctrl.hand);
19        }
20        cur = world.parent_of(cur)?;
21    }
22}
examples/vtuber-joints-example.rs (line 663)
659fn find_parent_renderable(
660    world: &engine::ecs::World,
661    mut cid: engine::ecs::ComponentId,
662) -> Option<engine::ecs::ComponentId> {
663    while let Some(parent) = world.parent_of(cid) {
664        if world
665            .get_component_by_id_as::<RenderableComponent>(parent)
666            .is_some()
667        {
668            return Some(parent);
669        }
670        cid = parent;
671    }
672    None
673}
examples/gravity-fields.rs (line 10)
6fn cube_renderable_sibling_of_collider(
7    world: &engine::ecs::World,
8    collider_cid: engine::ecs::ComponentId,
9) -> Option<engine::ecs::ComponentId> {
10    let t = world.parent_of(collider_cid)?;
11    for &sib in world.children_of(t).iter() {
12        if sib == collider_cid {
13            continue;
14        }
15        let Some(r) =
16            world.get_component_by_id_as::<engine::ecs::component::RenderableComponent>(sib)
17        else {
18            continue;
19        };
20        if r.renderable.base_mesh == engine::graphics::primitives::CpuMeshHandle::CUBE {
21            return Some(sib);
22        }
23    }
24    None
25}
examples/pointer-events.rs (line 64)
42fn on_click_cube(
43    world: &mut engine::ecs::World,
44    emit: &mut dyn engine::ecs::SignalEmitter,
45    env: &engine::ecs::Signal,
46) {
47    if !matches!(
48        env.event.as_ref(),
49        Some(engine::ecs::EventSignal::Click { .. })
50    ) {
51        return;
52    }
53
54    // env.scope is the hit renderable; walk up to find the cube root in the map.
55    let map = CLICK_CUBES.get_or_init(|| Mutex::new(Default::default()));
56    let mut guard = map.lock().unwrap();
57    let mut cur = Some(env.scope);
58    let mut found_root = None;
59    while let Some(id) = cur {
60        if guard.contains_key(&id) {
61            found_root = Some(id);
62            break;
63        }
64        cur = world.parent_of(id);
65    }
66    let Some(root) = found_root else { return };
67    let Some(state) = guard.get_mut(&root) else {
68        return;
69    };
70
71    state.click_count += 1;
72    let colors = click_cube_colors();
73    let rgba = colors[state.click_count as usize % colors.len()];
74
75    emit.push_intent_now(
76        root,
77        engine::ecs::IntentValue::SetColor {
78            component_ids: vec![state.color_node],
79            rgba,
80        },
81    );
82}
83
84fn spawn_click_cube(
85    universe: &mut engine::Universe,
86    parent: engine::ecs::ComponentId,
87    pos: [f32; 3],
88) -> engine::ecs::ComponentId {
89    use engine::ecs::component::{
90        ColorComponent, RaycastableComponent, RenderableComponent, TransformComponent,
91    };
92
93    let root = universe.world.add_component(
94        TransformComponent::new()
95            .with_position(pos[0], pos[1], pos[2])
96            .with_scale(0.40, 0.40, 0.40),
97    );
98    let color_node = universe
99        .world
100        .add_component(ColorComponent::rgba(0.25, 0.55, 1.0, 1.0));
101    let r = universe.world.add_component(RenderableComponent::cube());
102    let rc = universe
103        .world
104        .add_component(RaycastableComponent::enabled());
105
106    // Build the subtree before connecting to the live parent so that
107    // RaycastableComponent is already a child of RenderableComponent when
108    // RegisterRenderable is processed (renderable_is_raycastable checks children).
109    let _ = universe.attach(root, color_node);
110    let _ = universe.attach(color_node, r);
111    let _ = universe.attach(r, rc);
112    let _ = universe.attach(parent, root);
113
114    CLICK_CUBES
115        .get_or_init(|| Mutex::new(Default::default()))
116        .lock()
117        .unwrap()
118        .insert(
119            root,
120            ClickCubeState {
121                color_node,
122                click_count: 0,
123            },
124        );
125
126    universe.add_signal_handler(engine::ecs::SignalKind::Click, root, on_click_cube);
127
128    root
129}
130
131// ---------------------------------------------------------------------------
132// Drag cube — follows pointer while held
133// ---------------------------------------------------------------------------
134
135fn on_drag_move(
136    world: &mut engine::ecs::World,
137    emit: &mut dyn engine::ecs::SignalEmitter,
138    env: &engine::ecs::Signal,
139) {
140    let Some(engine::ecs::EventSignal::DragMove { delta_world, .. }) = env.event.as_ref() else {
141        return;
142    };
143    let delta = *delta_world;
144
145    // Walk up from the hit renderable to find the TransformComponent to move.
146    let mut cur = Some(env.scope);
147    while let Some(id) = cur {
148        if world
149            .get_component_by_id_as::<engine::ecs::component::TransformComponent>(id)
150            .is_some()
151        {
152            let [x, y, z] = world
153                .get_component_by_id_as::<engine::ecs::component::TransformComponent>(id)
154                .map(|t| t.transform.translation)
155                .unwrap_or([0.0; 3]);
156
157            emit.push_intent_now(
158                id,
159                engine::ecs::IntentValue::SetPosition {
160                    component_ids: vec![id],
161                    position: [x + delta[0], y + delta[1], z + delta[2]],
162                },
163            );
164            return;
165        }
166        cur = world.parent_of(id);
167    }
168}
169
170fn spawn_drag_cube(
171    universe: &mut engine::Universe,
172    parent: engine::ecs::ComponentId,
173    pos: [f32; 3],
174    color: [f32; 4],
175) -> engine::ecs::ComponentId {
176    use engine::ecs::component::{
177        ColorComponent, RaycastableComponent, RenderableComponent, TransformComponent,
178    };
179
180    let root = universe.world.add_component(
181        TransformComponent::new()
182            .with_position(pos[0], pos[1], pos[2])
183            .with_scale(0.45, 0.45, 0.45),
184    );
185    let color_node = universe
186        .world
187        .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
188    let r = universe.world.add_component(RenderableComponent::cube());
189    let rc = universe
190        .world
191        .add_component(RaycastableComponent::enabled());
192
193    let _ = universe.attach(root, color_node);
194    let _ = universe.attach(color_node, r);
195    let _ = universe.attach(r, rc);
196    let _ = universe.attach(parent, root);
197
198    universe.add_signal_handler(engine::ecs::SignalKind::DragMove, r, on_drag_move);
199
200    root
201}
202
203// ---------------------------------------------------------------------------
204// "Both" cube — drag to move, click to cycle color
205// ---------------------------------------------------------------------------
206
207#[derive(Debug)]
208struct BothCubeState {
209    color_node: engine::ecs::ComponentId,
210    click_count: u32,
211}
212
213static BOTH_CUBES: OnceLock<
214    Mutex<std::collections::HashMap<engine::ecs::ComponentId, BothCubeState>>,
215> = OnceLock::new();
216
217fn on_both_cube(
218    world: &mut engine::ecs::World,
219    emit: &mut dyn engine::ecs::SignalEmitter,
220    env: &engine::ecs::Signal,
221) {
222    match env.event.as_ref() {
223        Some(engine::ecs::EventSignal::DragMove { delta_world, .. }) => {
224            on_drag_move(world, emit, env);
225            let _ = delta_world;
226        }
227        Some(engine::ecs::EventSignal::Click { .. }) => {
228            let map = BOTH_CUBES.get_or_init(|| Mutex::new(Default::default()));
229            let mut guard = map.lock().unwrap();
230            // scope is the renderable; walk up to find the cube root registered in the map
231            let mut cur = Some(env.scope);
232            let mut found_root = None;
233            while let Some(id) = cur {
234                if guard.contains_key(&id) {
235                    found_root = Some(id);
236                    break;
237                }
238                cur = world.parent_of(id);
239            }
240            let Some(root) = found_root else { return };
241            let Some(state) = guard.get_mut(&root) else {
242                return;
243            };
244
245            state.click_count += 1;
246            let colors = click_cube_colors();
247            let rgba = colors[state.click_count as usize % colors.len()];
248            emit.push_intent_now(
249                root,
250                engine::ecs::IntentValue::SetColor {
251                    component_ids: vec![state.color_node],
252                    rgba,
253                },
254            );
255        }
256        _ => {}
257    }
258}
Source

pub fn all_components(&self) -> impl Iterator<Item = ComponentId> + '_

Iterator over all component IDs in the world.

Examples found in repository?
examples/scrolling.rs (line 5)
3fn find_named(world: &engine::ecs::World, label: &str) -> engine::ecs::ComponentId {
4    world
5        .all_components()
6        .find(|&cid| world.component_label(cid) == Some(label))
7        .unwrap_or_else(|| panic!("missing component {label:?}"))
8}
More examples
Hide additional examples
examples/triage/panel-pierce.rs (line 11)
9fn find_named(world: &engine::ecs::World, label: &str) -> engine::ecs::ComponentId {
10    world
11        .all_components()
12        .find(|&cid| world.component_label(cid) == Some(label))
13        .unwrap_or_else(|| panic!("missing component {label:?}"))
14}
examples/qdebug.rs (line 18)
2fn main() {
3    mittens_engine::example_support::ensure_model_assets();
4    utils::logger::init();
5    let world = engine::ecs::World::default();
6    let mut universe = engine::Universe::new(world);
7    let src = include_str!("qdebug.mms");
8    let out = scripting::MeowMeowRunner::eval_with_world(
9        src,
10        &mut universe.world,
11        &mut universe.systems.rx,
12        &mut universe.command_queue,
13    );
14    for e in &out.errors {
15        eprintln!("[mms-err] {e}");
16    }
17    println!("intents: {}", out.intents.len());
18    for c in universe.world.all_components() {
19        if let Some(label) = universe.world.component_label(c) {
20            {
21                println!(
22                    "  comp {:?} type={:?} name={:?}",
23                    c,
24                    universe.world.component_name(c),
25                    label
26                );
27            }
28        }
29    }
30}
examples/router.rs (line 81)
41fn main() {
42    mittens_engine::example_support::ensure_model_assets();
43    utils::logger::init();
44
45    let world = engine::ecs::World::default();
46    let mut universe = engine::Universe::new(world);
47
48    let output = scripting::MeowMeowRunner::eval_with_world_at_path(
49        include_str!("router.mms"),
50        Some("examples/router.mms"),
51        &mut universe.world,
52        &mut universe.systems.rx,
53        &mut universe.command_queue,
54    );
55
56    for error in &output.errors {
57        eprintln!("[mms] {error}");
58    }
59    println!("[mms] {} intent(s) from router.mms", output.intents.len());
60
61    if !output.errors.is_empty() {
62        std::process::exit(1);
63    }
64
65    let scope = engine::ecs::ComponentId::default();
66    for intent in output.intents {
67        universe.command_queue.push_intent_now(scope, intent);
68    }
69
70    universe.systems.process_commands(
71        &mut universe.world,
72        &mut universe.visuals,
73        &mut universe.render_assets,
74        &mut universe.command_queue,
75    );
76
77    universe.enable_repl();
78
79    let demo_root = universe
80        .world
81        .all_components()
82        .find(|&cid| universe.world.component_label(cid) == Some("router_demo"))
83        .expect("router demo root");
84    let container = universe
85        .find_component(demo_root, "[name='container']")
86        .expect("container target");
87    let authored_child = universe
88        .find_component(demo_root, "[name='authored_child']")
89        .expect("authored child");
90
91    assert_eq!(universe.parent_of(authored_child), Some(container));
92
93    spawn_runtime_text(&mut universe, demo_root, "late child routed on attach");
94
95    let late_child = universe
96        .find_component(demo_root, "[name='late child routed on attach']")
97        .expect("late child");
98    assert_eq!(universe.parent_of(late_child), Some(container));
99
100    println!("[router-demo] init-time and attach-time routing verified");
101
102    engine::Windowing::run_app(universe).expect("Windowing failed");
103}
examples/bisket-vr-debug.rs (line 195)
164fn main() {
165    mittens_engine::example_support::ensure_model_assets();
166    utils::logger::init();
167    let opts = parse_args();
168
169    let world = engine::ecs::World::default();
170    let mut universe = engine::Universe::new(world);
171
172    let source = include_str!("bisket-vr-debug.mms");
173    let output = scripting::MeowMeowRunner::eval_with_world_at_path(
174        source,
175        Some("examples/bisket-vr-debug.mms"),
176        &mut universe.world,
177        &mut universe.systems.rx,
178        &mut universe.command_queue,
179    );
180    for e in &output.errors {
181        eprintln!("[mms] {e}");
182    }
183    println!("[mms] {} intent(s)", output.intents.len());
184    for intent in output.intents {
185        universe
186            .command_queue
187            .push_intent_now(ComponentId::default(), intent);
188    }
189    drain(&mut universe);
190    tick_gltf(&mut universe);
191    drain(&mut universe);
192
193    let avc_id = universe
194        .world
195        .all_components()
196        .find(|&id| {
197            universe
198                .world
199                .get_component_by_id_as::<AvatarControlComponent>(id)
200                .is_some()
201        })
202        .expect("AvatarControlComponent not found — scene didn't load?");
203    let driven_t = universe
204        .world
205        .parent_of(avc_id)
206        .expect("AVC has no parent (driven_t)");
207
208    println!("[debug] avc_id={:?} driven_t={:?}", avc_id, driven_t);
209    println!(
210        "[debug] options: compare_copy_end_rotation={} compare_hand_offsets={} neutralize_hand_offsets={}",
211        opts.compare_copy_end_rotation, opts.compare_hand_offsets, opts.neutralize_hand_offsets
212    );
213
214    let head_target_offset_world = head_target_offset_in_target_local(&universe.world, avc_id);
215    println!(
216        "[debug] head_target_offset (target-local) = {:?}",
217        head_target_offset_world
218    );
219
220    settle_world(&mut universe, 10);
221
222    let (ref_bones, avc_bones) = resolve_spine_bones(&universe.world, avc_id);
223    println!(
224        "[debug] resolved {} spine bones in both subtrees",
225        SPINE_BONES.len()
226    );
227
228    let avc_head_bone = avc_bones[SPINE_BONES
229        .iter()
230        .position(|n| *n == "J_Bip_C_Head")
231        .unwrap()];
232    let splice_head = universe
233        .world
234        .parent_of(avc_head_bone)
235        .expect("head_bone has no parent — splice didn't run?");
236
237    run_spine_probes(
238        &mut universe,
239        driven_t,
240        splice_head,
241        &ref_bones,
242        &avc_bones,
243        head_target_offset_world,
244    );
245
246    let arms = resolve_arm_chains(&universe.world, avc_id);
247    print_arm_startup_report(&universe.world, &arms);
248    run_arm_probes(&mut universe, driven_t, &arms, opts);
249
250    println!("\n[debug] done. Exiting (no window loop).");
251}
252
253fn parse_args() -> HarnessOptions {
254    let mut opts = HarnessOptions {
255        compare_copy_end_rotation: false,
256        compare_hand_offsets: false,
257        neutralize_hand_offsets: false,
258    };
259    for arg in env::args().skip(1) {
260        match arg.as_str() {
261            "--compare-copy-end-rotation" => opts.compare_copy_end_rotation = true,
262            "--compare-hand-offsets" => opts.compare_hand_offsets = true,
263            "--neutralize-hand-offsets" => opts.neutralize_hand_offsets = true,
264            other => {
265                eprintln!("[debug] ignoring unknown arg: {other}");
266            }
267        }
268    }
269    opts
270}
271
272fn run_spine_probes(
273    universe: &mut engine::Universe,
274    driven_t: ComponentId,
275    splice_head: ComponentId,
276    ref_bones: &[ComponentId],
277    avc_bones: &[ComponentId],
278    head_target_offset_world: [f32; 3],
279) {
280    let poses = [
281        Pose {
282            name: "rest",
283            description: "driven_t at HMD height (1.55m), identity rotation",
284            t: [0.0, 1.55, 0.0],
285            rot: ident_quat(),
286        },
287        Pose {
288            name: "pitch_up_30",
289            description: "head pitched up 30° around X",
290            t: [0.0, 1.55, 0.0],
291            rot: quat_from_axis_angle([1.0, 0.0, 0.0], 0.5236),
292        },
293        Pose {
294            name: "pitch_dn_30",
295            description: "head pitched down 30° around X",
296            t: [0.0, 1.55, 0.0],
297            rot: quat_from_axis_angle([1.0, 0.0, 0.0], -0.5236),
298        },
299        Pose {
300            name: "yaw_right_45",
301            description: "head yawed right 45° around Y",
302            t: [0.0, 1.55, 0.0],
303            rot: quat_from_axis_angle([0.0, 1.0, 0.0], 0.7854),
304        },
305        Pose {
306            name: "lean_forward",
307            description: "driven_t translated +0.2m on Z (head leans forward over toes)",
308            t: [0.0, 1.45, 0.20],
309            rot: ident_quat(),
310        },
311        Pose {
312            name: "crouch",
313            description: "driven_t lowered to 1.10m (player crouches)",
314            t: [0.0, 1.10, 0.0],
315            rot: ident_quat(),
316        },
317        Pose {
318            name: "walk_forward_0.5m",
319            description: "driven_t walks +0.5m on -Z (OpenXR forward = -Z)",
320            t: [0.0, 1.55, -0.5],
321            rot: ident_quat(),
322        },
323        Pose {
324            name: "walk_strafe_right_0.3m",
325            description: "driven_t side-step +0.3m on X",
326            t: [0.3, 1.55, 0.0],
327            rot: ident_quat(),
328        },
329    ];
330
331    let ref_y_z: Vec<[f32; 2]> = ref_bones
332        .iter()
333        .map(|&id| world_yz(&universe.world, id))
334        .collect();
335
336    for pose in &poses {
337        set_local_transform(
338            &mut universe.command_queue,
339            driven_t,
340            pose.t,
341            pose.rot,
342            [1.0, 1.0, 1.0],
343        );
344        settle_world(universe, SETTLE_TICKS_PER_POSE);
345
346        let avc_y_z: Vec<[f32; 2]> = avc_bones
347            .iter()
348            .map(|&id| world_yz(&universe.world, id))
349            .collect();
350        let avc_pos: Vec<[f32; 3]> = avc_bones
351            .iter()
352            .map(|&id| world_pos(&universe.world, id))
353            .collect();
354        let splice_pos = world_pos(&universe.world, splice_head);
355        let driven_pos = world_pos(&universe.world, driven_t);
356        let driven_rot = world_rot(&universe.world, driven_t);
357
358        println!("\n================================================================");
359        println!("spine pose: {}", pose.name);
360        println!("  {}", pose.description);
361        println!(
362            "  driven_t world pos: [{:+.3}, {:+.3}, {:+.3}] (model offset +1.2 on X)",
363            driven_pos[0], driven_pos[1], driven_pos[2]
364        );
365
366        println!("\nspine bones — model-local Y/Z (REF vs AVC)");
367        println!(
368            "  {:<22}  {:>7}  {:>7}    {:>7}  {:>7}     {:>+7}  {:>+7}",
369            "bone", "ref_y", "ref_z", "avc_y", "avc_z", "Δy", "Δz",
370        );
371        for (i, name) in SPINE_BONES.iter().enumerate() {
372            let r = ref_y_z[i];
373            let a = avc_y_z[i];
374            println!(
375                "  {:<22}  {:>+7.3}  {:>+7.3}    {:>+7.3}  {:>+7.3}     {:>+7.3}  {:>+7.3}",
376                name,
377                r[0],
378                r[1],
379                a[0],
380                a[1],
381                a[0] - r[0],
382                a[1] - r[1],
383            );
384        }
385
386        println!("\ninvariants");
387
388        let ref_seg_lens = segment_lengths(ref_bones, &universe.world);
389        let avc_seg_lens = segment_lengths(avc_bones, &universe.world);
390        let mut bone_length_ok = true;
391        for i in 0..ref_seg_lens.len() {
392            let drift = (avc_seg_lens[i] - ref_seg_lens[i]).abs();
393            let tag = if drift <= BONE_LENGTH_TOL {
394                "ok"
395            } else {
396                "FAIL"
397            };
398            if drift > BONE_LENGTH_TOL {
399                bone_length_ok = false;
400            }
401            println!(
402                "  bone_length  {:<14}→{:<14}  ref={:.4}  avc={:.4}  drift={:+.4}  {}",
403                SPINE_BONES[i],
404                SPINE_BONES[i + 1],
405                ref_seg_lens[i],
406                avc_seg_lens[i],
407                avc_seg_lens[i] - ref_seg_lens[i],
408                tag,
409            );
410        }
411        if bone_length_ok {
412            println!(
413                "  → all spine bone-lengths preserved within {:.0}mm",
414                BONE_LENGTH_TOL * 1000.0
415            );
416        }
417
418        let mut mono_ok = true;
419        let mut prev_y = f32::NEG_INFINITY;
420        for (i, name) in SPINE_BONES.iter().enumerate() {
421            if *name == "J_Bip_C_Head" {
422                break;
423            }
424            let y = avc_pos[i][1];
425            if y + MONOTONIC_Y_TOL < prev_y {
426                println!(
427                    "  monotonic_y  FAIL at {}: y={:+.3} drops below prev {:+.3}",
428                    name, y, prev_y
429                );
430                mono_ok = false;
431            }
432            prev_y = y;
433        }
434        if mono_ok {
435            println!(
436                "  monotonic_y  ok (hips → neck all non-decreasing within {:.0}mm)",
437                MONOTONIC_Y_TOL * 1000.0
438            );
439        }
440
441        let hips_idx = SPINE_BONES
442            .iter()
443            .position(|n| *n == "J_Bip_C_Hips")
444            .unwrap();
445        let hips_xz = [avc_pos[hips_idx][0], avc_pos[hips_idx][2]];
446        let dx = hips_xz[0] - splice_pos[0];
447        let dz = hips_xz[1] - splice_pos[2];
448        let xz_drift = (dx * dx + dz * dz).sqrt();
449        const HIPS_UNDER_HEAD_TOL: f32 = 0.050;
450        println!(
451            "  hips_under_head  splice_xz=[{:+.3},{:+.3}]  hips_xz=[{:+.3},{:+.3}]  drift={:.4}m  {}",
452            splice_pos[0],
453            splice_pos[2],
454            hips_xz[0],
455            hips_xz[1],
456            xz_drift,
457            if xz_drift <= HIPS_UNDER_HEAD_TOL {
458                "ok"
459            } else {
460                "FAIL"
461            },
462        );
463
464        let predicted_splice_world = {
465            let off_world = quat_rotate_vec3(driven_rot, head_target_offset_world);
466            [
467                driven_pos[0] + off_world[0],
468                driven_pos[1] + off_world[1],
469                driven_pos[2] + off_world[2],
470            ]
471        };
472        let splice_drift = vec3_dist(splice_pos, predicted_splice_world);
473        println!(
474            "  splice_head  expected=[{:+.3},{:+.3},{:+.3}]  actual=[{:+.3},{:+.3},{:+.3}]  drift={:.4}m  {}",
475            predicted_splice_world[0],
476            predicted_splice_world[1],
477            predicted_splice_world[2],
478            splice_pos[0],
479            splice_pos[1],
480            splice_pos[2],
481            splice_drift,
482            if splice_drift <= SPLICE_TARGET_TOL {
483                "ok"
484            } else {
485                "FAIL"
486            },
487        );
488    }
489}
490
491fn resolve_spine_bones(
492    world: &World,
493    _avc_id: ComponentId,
494) -> (Vec<ComponentId>, Vec<ComponentId>) {
495    let mut ref_bones = Vec::with_capacity(SPINE_BONES.len());
496    let mut avc_bones = Vec::with_capacity(SPINE_BONES.len());
497    for name in SPINE_BONES {
498        let all = find_components_by_name(world, name);
499        let (avc_side, ref_side): (Vec<_>, Vec<_>) = all
500            .into_iter()
501            .partition(|&id| world_pos(world, id)[0] > 0.0);
502        let avc = avc_side
503            .first()
504            .copied()
505            .unwrap_or_else(|| panic!("bone {} not found on +X AVC side", name));
506        let reference = ref_side
507            .first()
508            .copied()
509            .unwrap_or_else(|| panic!("bone {} not found on -X reference side", name));
510        ref_bones.push(reference);
511        avc_bones.push(avc);
512    }
513    (ref_bones, avc_bones)
514}
515
516fn resolve_arm_chains(world: &World, avc_id: ComponentId) -> Vec<ArmChainRuntime> {
517    let avc = world
518        .get_component_by_id_as::<AvatarControlComponent>(avc_id)
519        .expect("avc missing");
520    let hand_map = [
521        (
522            ArmSide::Left,
523            avc.left_hand_bone
524                .as_deref()
525                .expect("left_hand_bone config missing"),
526        ),
527        (
528            ArmSide::Right,
529            avc.right_hand_bone
530                .as_deref()
531                .expect("right_hand_bone config missing"),
532        ),
533    ];
534
535    hand_map
536        .into_iter()
537        .map(|(side, hand_name)| {
538            let hand_id = find_components_by_name(world, hand_name)
539                .into_iter()
540                .find(|&id| is_descendant_of(world, id, avc_id))
541                .unwrap_or_else(|| panic!("{} bone not found under AVC subtree", hand_name));
542            let (ik_chain_id, solver, target_id) = world
543                .all_components()
544                .find_map(|id| {
545                    let ik = world.get_component_by_id_as::<IKChainComponent>(id)?;
546                    if !is_descendant_of(world, id, avc_id) {
547                        return None;
548                    }
549                    if ik.end_effector_id != hand_id {
550                        return None;
551                    }
552                    Some((id, ik.solver, ik.target_id))
553                })
554                .unwrap_or_else(|| panic!("{} arm IK chain not found", side.as_str()));
555            let (upper_arm, lower_arm, copy_end_rotation) = match solver {
556                IKSolver::TwoBoneIK {
557                    root_joint_id,
558                    mid_joint_id,
559                    copy_end_rotation,
560                    ..
561                } => (root_joint_id, mid_joint_id, copy_end_rotation),
562                _ => panic!("{} arm chain resolved to non-TwoBoneIK", side.as_str()),
563            };
564            let authored_target_local_rotation = local_rot(world, target_id);
565            let rest_hand_world_pos = world_pos(world, hand_id);
566
567            ArmChainRuntime {
568                side,
569                ik_chain_id,
570                upper_arm,
571                lower_arm,
572                hand: hand_id,
573                target: target_id,
574                authored_target_local_rotation,
575                rest_hand_world_pos,
576                original_copy_end_rotation: copy_end_rotation,
577            }
578        })
579        .collect()
580}
581
582fn print_arm_startup_report(world: &World, arms: &[ArmChainRuntime]) {
583    println!("\n================================================================");
584    println!("arm chain startup");
585    for arm in arms {
586        let upward = upward_chain_labels(world, arm.hand, Some(arm.upper_arm), 8);
587        let skipped = skipped_chain_labels(world, arm.hand, arm.lower_arm, arm.upper_arm);
588        println!(
589            "  [{}] ik_chain={} target={} upper={} lower={} hand={}",
590            arm.side.as_str(),
591            describe_component(world, arm.ik_chain_id),
592            describe_component(world, arm.target),
593            describe_component(world, arm.upper_arm),
594            describe_component(world, arm.lower_arm),
595            describe_component(world, arm.hand),
596        );
597        println!("    hand→upper chain: {}", upward.join(" <- "));
598        if skipped.is_empty() {
599            println!("    skipped between hand and upper: none (direct upper->lower->hand)");
600        } else {
601            println!(
602                "    skipped between hand and upper: {}",
603                skipped.join(" <- ")
604            );
605        }
606        println!(
607            "    target parent chain: {}",
608            upward_chain_labels(world, arm.target, None, 4).join(" <- ")
609        );
610    }
611}
612
613fn run_arm_probes(
614    universe: &mut engine::Universe,
615    driven_t: ComponentId,
616    arms: &[ArmChainRuntime],
617    opts: HarnessOptions,
618) {
619    let hand_offset_modes: Vec<HandOffsetMode> = if opts.compare_hand_offsets {
620        vec![HandOffsetMode::Authored, HandOffsetMode::Neutralized]
621    } else if opts.neutralize_hand_offsets {
622        vec![HandOffsetMode::Neutralized]
623    } else {
624        vec![HandOffsetMode::Authored]
625    };
626    let copy_modes: Vec<CopyEndRotationMode> = if opts.compare_copy_end_rotation {
627        vec![
628            CopyEndRotationMode::SolverDefault,
629            CopyEndRotationMode::ForcedOff,
630        ]
631    } else {
632        vec![CopyEndRotationMode::SolverDefault]
633    };
634
635    let poses = [
636        ArmProbePose {
637            name: "neutral_rest",
638            description: "both hand targets at rest hand position and pass baseline rotation",
639            left_twist_rad: 0.0,
640            right_twist_rad: 0.0,
641        },
642        ArmProbePose {
643            name: "left_palm_down",
644            description: "left hand target pronated around the current forearm axis",
645            left_twist_rad: -WRIST_TWIST_RAD,
646            right_twist_rad: 0.0,
647        },
648        ArmProbePose {
649            name: "left_palm_up",
650            description: "left hand target supinated around the current forearm axis",
651            left_twist_rad: WRIST_TWIST_RAD,
652            right_twist_rad: 0.0,
653        },
654        ArmProbePose {
655            name: "right_palm_down",
656            description: "right hand target pronated around the current forearm axis",
657            left_twist_rad: 0.0,
658            right_twist_rad: WRIST_TWIST_RAD,
659        },
660        ArmProbePose {
661            name: "right_palm_up",
662            description: "right hand target supinated around the current forearm axis",
663            left_twist_rad: 0.0,
664            right_twist_rad: -WRIST_TWIST_RAD,
665        },
666    ];
667
668    set_local_transform(
669        &mut universe.command_queue,
670        driven_t,
671        [0.0, 1.55, 0.0],
672        ident_quat(),
673        [1.0, 1.0, 1.0],
674    );
675    settle_world(universe, SETTLE_TICKS_PER_POSE);
676
677    for hand_mode in hand_offset_modes {
678        for copy_mode in &copy_modes {
679            println!("\n================================================================");
680            println!("arm pass: {} | {}", hand_mode.as_str(), copy_mode.as_str());
681
682            apply_pass_settings(&mut universe.world, arms, hand_mode, *copy_mode);
683            for arm in arms {
684                reset_arm_target_to_pass_baseline(universe, *arm, hand_mode);
685            }
686            settle_world(universe, SETTLE_TICKS_PER_POSE);
687
688            let baselines: Vec<(ArmSide, ArmPassBaseline)> = arms
689                .iter()
690                .map(|arm| {
691                    (
692                        arm.side,
693                        ArmPassBaseline {
694                            hand_world_pos: arm.rest_hand_world_pos,
695                            target_world_rot: world_rot(&universe.world, arm.target),
696                            forearm_axis_world: forearm_axis_world(
697                                &universe.world,
698                                arm.lower_arm,
699                                arm.hand,
700                            ),
701                        },
702                    )
703                })
704                .collect();
705
706            for pose in &poses {
707                for arm in arms {
708                    let baseline = baselines
709                        .iter()
710                        .find(|(side, _)| *side == arm.side)
711                        .map(|(_, b)| *b)
712                        .unwrap();
713                    let twist_rad = match arm.side {
714                        ArmSide::Left => pose.left_twist_rad,
715                        ArmSide::Right => pose.right_twist_rad,
716                    };
717                    set_arm_target_world_pose(
718                        universe,
719                        *arm,
720                        baseline.hand_world_pos,
721                        quat_mul(
722                            quat_from_axis_angle(baseline.forearm_axis_world, twist_rad),
723                            baseline.target_world_rot,
724                        ),
725                    );
726                }
727                settle_world(universe, SETTLE_TICKS_PER_POSE);
728
729                let left = arms
730                    .iter()
731                    .find(|arm| matches!(arm.side, ArmSide::Left))
732                    .copied()
733                    .map(|arm| sample_arm_pose(&universe.world, arm))
734                    .expect("left arm sample missing");
735                let right = arms
736                    .iter()
737                    .find(|arm| matches!(arm.side, ArmSide::Right))
738                    .copied()
739                    .map(|arm| sample_arm_pose(&universe.world, arm))
740                    .expect("right arm sample missing");
741
742                println!("\npose: {}", pose.name);
743                println!("  {}", pose.description);
744                print_arm_pose_sample(&universe.world, ArmSide::Left, &left);
745                print_arm_pose_sample(&universe.world, ArmSide::Right, &right);
746
747                if pose.name == "left_palm_down" || pose.name == "right_palm_down" {
748                    continue;
749                }
750            }
751
752            let left_down = sample_signature_for_pose(
753                universe,
754                arms,
755                &baselines,
756                "left_palm_down",
757                -WRIST_TWIST_RAD,
758                0.0,
759            );
760            let right_down = sample_signature_for_pose(
761                universe,
762                arms,
763                &baselines,
764                "right_palm_down",
765                0.0,
766                WRIST_TWIST_RAD,
767            );
768            let left_up = sample_signature_for_pose(
769                universe,
770                arms,
771                &baselines,
772                "left_palm_up",
773                WRIST_TWIST_RAD,
774                0.0,
775            );
776            let right_up = sample_signature_for_pose(
777                universe,
778                arms,
779                &baselines,
780                "right_palm_up",
781                0.0,
782                -WRIST_TWIST_RAD,
783            );
784
785            println!("\nmirror summary");
786            print_mirror_compare("palm_down", left_down, right_down);
787            print_mirror_compare("palm_up", left_up, right_up);
788        }
789    }
790}
791
792fn sample_signature_for_pose(
793    universe: &mut engine::Universe,
794    arms: &[ArmChainRuntime],
795    baselines: &[(ArmSide, ArmPassBaseline)],
796    _pose_name: &str,
797    left_twist_rad: f32,
798    right_twist_rad: f32,
799) -> MirrorSignature {
800    for arm in arms {
801        let baseline = baselines
802            .iter()
803            .find(|(side, _)| *side == arm.side)
804            .map(|(_, b)| *b)
805            .unwrap();
806        let twist = match arm.side {
807            ArmSide::Left => left_twist_rad,
808            ArmSide::Right => right_twist_rad,
809        };
810        set_arm_target_world_pose(
811            universe,
812            *arm,
813            baseline.hand_world_pos,
814            quat_mul(
815                quat_from_axis_angle(baseline.forearm_axis_world, twist),
816                baseline.target_world_rot,
817            ),
818        );
819    }
820    settle_world(universe, SETTLE_TICKS_PER_POSE);
821
822    let active_arm = if left_twist_rad.abs() > right_twist_rad.abs() {
823        arms.iter()
824            .find(|arm| matches!(arm.side, ArmSide::Left))
825            .copied()
826            .unwrap()
827    } else {
828        arms.iter()
829            .find(|arm| matches!(arm.side, ArmSide::Right))
830            .copied()
831            .unwrap()
832    };
833    let sample = sample_arm_pose(&universe.world, active_arm);
834    MirrorSignature {
835        lower_to_hand_deg: sample.lower_to_hand_deg,
836        lower_to_target_deg: sample.lower_to_target_deg,
837        hand_to_target_deg: sample.hand_to_target_deg,
838    }
839}
840
841fn apply_pass_settings(
842    world: &mut World,
843    arms: &[ArmChainRuntime],
844    hand_mode: HandOffsetMode,
845    copy_mode: CopyEndRotationMode,
846) {
847    for arm in arms {
848        if let Some(tc) = world.get_component_by_id_as_mut::<TransformComponent>(arm.target) {
849            tc.transform.rotation = match hand_mode {
850                HandOffsetMode::Authored => arm.authored_target_local_rotation,
851                HandOffsetMode::Neutralized => ident_quat(),
852            };
853            tc.transform.recompute_model();
854        }
855        if let Some(ik) = world.get_component_by_id_as_mut::<IKChainComponent>(arm.ik_chain_id) {
856            if let IKSolver::TwoBoneIK {
857                root_joint_id,
858                mid_joint_id,
859                pole_direction,
860                copy_end_rotation: enabled,
861            } = &mut ik.solver
862            {
863                let _ = (root_joint_id, mid_joint_id, pole_direction);
864                *enabled = match copy_mode {
865                    CopyEndRotationMode::SolverDefault => arm.original_copy_end_rotation,
866                    CopyEndRotationMode::ForcedOff => false,
867                };
868            }
869        }
870    }
871}
872
873fn reset_arm_target_to_pass_baseline(
874    universe: &mut engine::Universe,
875    arm: ArmChainRuntime,
876    hand_mode: HandOffsetMode,
877) {
878    let world_rot = match hand_mode {
879        HandOffsetMode::Authored => world_rot(&universe.world, arm.target),
880        HandOffsetMode::Neutralized => world_rot(&universe.world, arm.target),
881    };
882    set_arm_target_world_pose(universe, arm, arm.rest_hand_world_pos, world_rot);
883}
884
885fn set_arm_target_world_pose(
886    universe: &mut engine::Universe,
887    arm: ArmChainRuntime,
888    desired_world_pos: [f32; 3],
889    desired_world_rot: [f32; 4],
890) {
891    let (local_translation, local_rotation, local_scale) = world_pose_to_local(
892        &universe.world,
893        arm.target,
894        desired_world_pos,
895        desired_world_rot,
896    );
897    set_local_transform(
898        &mut universe.command_queue,
899        arm.target,
900        local_translation,
901        local_rotation,
902        local_scale,
903    );
904}
905
906fn sample_arm_pose(world: &World, arm: ArmChainRuntime) -> ArmPoseSample {
907    let upper_arm = sample_node_rotation(world, arm.upper_arm);
908    let lower_arm = sample_node_rotation(world, arm.lower_arm);
909    let hand = sample_node_rotation(world, arm.hand);
910    let target = sample_node_rotation(world, arm.target);
911    ArmPoseSample {
912        upper_arm,
913        lower_arm,
914        hand,
915        target,
916        lower_to_hand_deg: quat_delta_deg(lower_arm.world, hand.world),
917        lower_to_target_deg: quat_delta_deg(lower_arm.world, target.world),
918        hand_to_target_deg: quat_delta_deg(hand.world, target.world),
919    }
920}
921
922fn sample_node_rotation(world: &World, id: ComponentId) -> NodeRotationSample {
923    NodeRotationSample {
924        world: world_rot(world, id),
925        local: local_rot(world, id),
926    }
927}
928
929fn print_arm_pose_sample(world: &World, side: ArmSide, sample: &ArmPoseSample) {
930    println!("  [{}]", side.as_str());
931    print_node_rot("upper_arm", sample.upper_arm);
932    print_node_rot("lower_arm", sample.lower_arm);
933    print_node_rot("hand", sample.hand);
934    print_node_rot("target", sample.target);
935    println!(
936        "    deltas: lower→hand={:>6.2}°  lower→target={:>6.2}°  hand→target={:>6.2}°",
937        sample.lower_to_hand_deg, sample.lower_to_target_deg, sample.hand_to_target_deg
938    );
939    let _ = world;
940}
941
942fn print_node_rot(label: &str, sample: NodeRotationSample) {
943    println!(
944        "    {:<9} world={}  local={}",
945        label,
946        fmt_quat(sample.world),
947        fmt_quat(sample.local)
948    );
949}
950
951fn print_mirror_compare(name: &str, left: MirrorSignature, right: MirrorSignature) {
952    println!(
953        "  {:<10} lower→hand L/R={:>6.2}°/{:>6.2}°  lower→target L/R={:>6.2}°/{:>6.2}°  hand→target L/R={:>6.2}°/{:>6.2}°",
954        name,
955        left.lower_to_hand_deg,
956        right.lower_to_hand_deg,
957        left.lower_to_target_deg,
958        right.lower_to_target_deg,
959        left.hand_to_target_deg,
960        right.hand_to_target_deg,
961    );
962}
963
964fn upward_chain_labels(
965    world: &World,
966    start: ComponentId,
967    stop_at: Option<ComponentId>,
968    max_hops: usize,
969) -> Vec<String> {
970    let mut out = vec![describe_component(world, start)];
971    let mut cur = start;
972    for _ in 0..max_hops {
973        let Some(parent) = world.parent_of(cur) else {
974            break;
975        };
976        out.push(describe_component(world, parent));
977        if Some(parent) == stop_at {
978            break;
979        }
980        cur = parent;
981    }
982    out
983}
984
985fn skipped_chain_labels(
986    world: &World,
987    hand: ComponentId,
988    lower: ComponentId,
989    upper: ComponentId,
990) -> Vec<String> {
991    let mut skipped = Vec::new();
992    let mut cur = hand;
993    let mut seen_lower = false;
994    for _ in 0..8 {
995        let Some(parent) = world.parent_of(cur) else {
996            break;
997        };
998        if parent == lower {
999            seen_lower = true;
1000        } else if parent == upper {
1001            break;
1002        } else if seen_lower {
1003            skipped.push(describe_component(world, parent));
1004        }
1005        cur = parent;
1006    }
1007    skipped
1008}
1009
1010fn describe_component(world: &World, id: ComponentId) -> String {
1011    let label = world
1012        .component_label(id)
1013        .or_else(|| world.component_name(id));
1014    format!("{}({id:?})", label.unwrap_or("?"))
1015}
1016
1017fn set_local_transform(
1018    emit: &mut dyn SignalEmitter,
1019    id: ComponentId,
1020    translation: [f32; 3],
1021    rotation_quat_xyzw: [f32; 4],
1022    scale: [f32; 3],
1023) {
1024    emit.push_intent_now(
1025        id,
1026        IntentValue::UpdateTransform {
1027            component_ids: vec![id],
1028            translation,
1029            rotation_quat_xyzw,
1030            scale,
1031        },
1032    );
1033}
1034
1035fn world_pose_to_local(
1036    world: &World,
1037    id: ComponentId,
1038    desired_world_pos: [f32; 3],
1039    desired_world_rot: [f32; 4],
1040) -> ([f32; 3], [f32; 4], [f32; 3]) {
1041    let scale = world
1042        .get_component_by_id_as::<TransformComponent>(id)
1043        .map(|t| t.transform.scale)
1044        .unwrap_or([1.0, 1.0, 1.0]);
1045
1046    let parent_world_mat = parent_world_matrix(world, id).unwrap_or(mat4_identity());
1047    let parent_world_rot = parent_world_rotation(world, id).unwrap_or(ident_quat());
1048    let inv_parent = mat4_inverse(parent_world_mat).unwrap_or(mat4_identity());
1049    let local_pos4 = mat4_mul_vec4(
1050        inv_parent,
1051        [
1052            desired_world_pos[0],
1053            desired_world_pos[1],
1054            desired_world_pos[2],
1055            1.0,
1056        ],
1057    );
1058    let local_rot = quat_mul(quat_conjugate(parent_world_rot), desired_world_rot);
1059    (
1060        [local_pos4[0], local_pos4[1], local_pos4[2]],
1061        local_rot,
1062        scale,
1063    )
1064}
1065
1066fn parent_world_matrix(world: &World, id: ComponentId) -> Option<[[f32; 4]; 4]> {
1067    let parent = world.parent_of(id)?;
1068    world
1069        .get_component_by_id_as::<TransformComponent>(parent)
1070        .map(|t| t.transform.matrix_world)
1071}
1072
1073fn parent_world_rotation(world: &World, id: ComponentId) -> Option<[f32; 4]> {
1074    let parent = world.parent_of(id)?;
1075    world
1076        .get_component_by_id_as::<TransformComponent>(parent)
1077        .map(|t| mat_to_quat(t.transform.matrix_world))
1078}
1079
1080fn forearm_axis_world(world: &World, lower_arm: ComponentId, hand: ComponentId) -> [f32; 3] {
1081    let axis = vec3_sub(world_pos(world, hand), world_pos(world, lower_arm));
1082    if vec3_len(axis) > 1e-6 {
1083        vec3_normalize(axis)
1084    } else {
1085        [0.0, 0.0, 1.0]
1086    }
1087}
1088
1089fn quat_delta_deg(from: [f32; 4], to: [f32; 4]) -> f32 {
1090    let (_, angle) = quat_to_axis_angle(quat_mul(quat_conjugate(from), to));
1091    angle.abs().to_degrees()
1092}
1093
1094fn fmt_quat(q: [f32; 4]) -> String {
1095    format!("[{:+.3},{:+.3},{:+.3},{:+.3}]", q[0], q[1], q[2], q[3])
1096}
1097
1098fn ident_quat() -> [f32; 4] {
1099    [0.0, 0.0, 0.0, 1.0]
1100}
1101
1102fn drain(universe: &mut engine::Universe) {
1103    universe.systems.process_commands(
1104        &mut universe.world,
1105        &mut universe.visuals,
1106        &mut universe.render_assets,
1107        &mut universe.command_queue,
1108    );
1109}
1110
1111fn tick_gltf(universe: &mut engine::Universe) {
1112    let systems = &mut universe.systems;
1113    systems.gltf.tick_with_queue(
1114        &mut universe.world,
1115        &mut universe.visuals,
1116        &mut systems.skinned_mesh,
1117        &mut universe.command_queue,
1118        0.0,
1119    );
1120}
1121
1122fn settle_world(universe: &mut engine::Universe, ticks: usize) {
1123    let input = InputState::default();
1124    for _ in 0..ticks {
1125        universe.systems.tick(
1126            &mut universe.world,
1127            &mut universe.visuals,
1128            &mut universe.render_assets,
1129            &input,
1130            &mut universe.command_queue,
1131            1.0 / 60.0,
1132        );
1133        drain(universe);
1134    }
1135}
1136
1137fn find_components_by_name(world: &World, name: &str) -> Vec<ComponentId> {
1138    world
1139        .all_components()
1140        .filter(|&id| world.component_label(id).map_or(false, |n| n == name))
1141        .collect()
1142}
examples/diy-panel.rs (line 213)
176fn main() {
177    mittens_engine::example_support::ensure_model_assets();
178    utils::logger::init();
179
180    let output = scripting::MeowMeowRunner::eval(include_str!("diy-panel.mms"));
181
182    for error in &output.errors {
183        eprintln!("[mms] {error}");
184    }
185    println!(
186        "[mms] {} intent(s) from diy-panel.mms",
187        output.intents.len()
188    );
189
190    if !output.errors.is_empty() {
191        std::process::exit(1);
192    }
193
194    let world = engine::ecs::World::default();
195    let mut universe = engine::Universe::new(world);
196
197    let scope = engine::ecs::ComponentId::default();
198    for intent in output.intents {
199        universe.command_queue.push_intent_now(scope, intent);
200    }
201
202    universe.systems.process_commands(
203        &mut universe.world,
204        &mut universe.visuals,
205        &mut universe.render_assets,
206        &mut universe.command_queue,
207    );
208
209    universe.enable_repl();
210
211    let demo_root = universe
212        .world
213        .all_components()
214        .find(|&cid| universe.world.component_label(cid) == Some("diy_panel_demo"))
215        .expect("diy panel demo root");
216    let container = universe
217        .find_component(demo_root, "[name='container']")
218        .expect("container target");
219    let authored_child = universe
220        .find_component(demo_root, "[name='authored_child']")
221        .expect("authored child");
222
223    assert_eq!(universe.parent_of(authored_child), Some(container));
224
225    spawn_runtime_text(&mut universe, demo_root, "late child routed on attach");
226
227    let late_child = universe
228        .find_component(demo_root, "[name='late child routed on attach']")
229        .expect("late child");
230    assert_eq!(universe.parent_of(late_child), Some(container));
231
232    let input = engine::user_input::InputState::default();
233    universe.systems.tick(
234        &mut universe.world,
235        &mut universe.visuals,
236        &mut universe.render_assets,
237        &input,
238        &mut universe.command_queue,
239        0.0,
240    );
241    universe.systems.process_commands(
242        &mut universe.world,
243        &mut universe.visuals,
244        &mut universe.render_assets,
245        &mut universe.command_queue,
246    );
247
248    //spawn_container_item_debug_clones(&mut universe, container);
249    universe.systems.process_commands(
250        &mut universe.world,
251        &mut universe.visuals,
252        &mut universe.render_assets,
253        &mut universe.command_queue,
254    );
255
256    let scroll = universe
257        .world
258        .children_of(container)
259        .iter()
260        .copied()
261        .find(|&child| {
262            universe.world.component_label(child) == Some("__scroll")
263                && universe
264                    .world
265                    .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(child)
266                    .is_some()
267        })
268        .expect("layout-owned scroll wrapper");
269    let track = universe
270        .world
271        .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(scroll)
272        .and_then(|sc| sc.track)
273        .expect("layout-owned scroll track");
274
275    assert_eq!(universe.parent_of(authored_child), Some(track));
276    assert_eq!(universe.parent_of(late_child), Some(track));
277
278    println!(
279        "[diy-panel] init-time routing verified, then layout-owned scrolling moved content into __scroll_track"
280    );
281
282    engine::Windowing::run_app(universe).expect("Windowing failed");
283}
Source

pub fn component_count(&self) -> usize

Source

pub fn children_of(&self, c: ComponentId) -> &[ComponentId]

Examples found in repository?
examples/vtuber-joints-example.rs (line 652)
640fn find_renderable_by_mesh_key(
641    world: &engine::ecs::World,
642    root: engine::ecs::ComponentId,
643    mesh_key: &str,
644) -> Option<engine::ecs::ComponentId> {
645    let mut stack = vec![root];
646    while let Some(node) = stack.pop() {
647        if let Some(m) = world.get_component_by_id_as::<MeshComponent>(node) {
648            if m.key == mesh_key {
649                return find_parent_renderable(world, node);
650            }
651        }
652        for &ch in world.children_of(node) {
653            stack.push(ch);
654        }
655    }
656    None
657}
658
659fn find_parent_renderable(
660    world: &engine::ecs::World,
661    mut cid: engine::ecs::ComponentId,
662) -> Option<engine::ecs::ComponentId> {
663    while let Some(parent) = world.parent_of(cid) {
664        if world
665            .get_component_by_id_as::<RenderableComponent>(parent)
666            .is_some()
667        {
668            return Some(parent);
669        }
670        cid = parent;
671    }
672    None
673}
674
675fn find_skin_id_for_renderable(
676    world: &engine::ecs::World,
677    renderable: engine::ecs::ComponentId,
678) -> Option<engine::graphics::SkinId> {
679    let mut stack = vec![renderable];
680    while let Some(node) = stack.pop() {
681        if let Some(sm) = world.get_component_by_id_as::<SkinnedMeshComponent>(node) {
682            if let Some(id) = sm.skin_id {
683                return Some(id);
684            }
685        }
686        for &ch in world.children_of(node) {
687            stack.push(ch);
688        }
689    }
690    None
691}
692
693fn collect_joint_transforms(
694    world: &engine::ecs::World,
695    skinned_mesh: &engine::ecs::system::SkinnedMeshSystem,
696    visuals: &engine::graphics::VisualWorld,
697    gltf_component: engine::ecs::ComponentId,
698    root: engine::ecs::ComponentId,
699) -> Vec<(usize, engine::ecs::ComponentId)> {
700    fn find_first_skin_id_in_subtree(
701        world: &engine::ecs::World,
702        root: engine::ecs::ComponentId,
703    ) -> Option<engine::graphics::SkinId> {
704        let mut stack = vec![root];
705        while let Some(node) = stack.pop() {
706            if let Some(sm) = world.get_component_by_id_as::<SkinnedMeshComponent>(node) {
707                if let Some(id) = sm.skin_id {
708                    return Some(id);
709                }
710            }
711            for &ch in world.children_of(node) {
712                stack.push(ch);
713            }
714        }
715        None
716    }
More examples
Hide additional examples
examples/scrolling.rs (line 22)
10fn first_renderable_in_subtree(
11    world: &engine::ecs::World,
12    root: engine::ecs::ComponentId,
13) -> engine::ecs::ComponentId {
14    let mut stack = vec![root];
15    while let Some(node) = stack.pop() {
16        if world
17            .get_component_by_id_as::<engine::ecs::component::RenderableComponent>(node)
18            .is_some()
19        {
20            return node;
21        }
22        for &child in world.children_of(node).iter().rev() {
23            stack.push(child);
24        }
25    }
26
27    panic!("missing renderable under subtree {:?}", root);
28}
examples/diy-panel.rs (line 57)
50fn collect_container_items(
51    world: &engine::ecs::World,
52    container: engine::ecs::ComponentId,
53) -> Vec<engine::ecs::ComponentId> {
54    use engine::ecs::component::TransformComponent;
55
56    world
57        .children_of(container)
58        .iter()
59        .copied()
60        .filter(|&child| {
61            world
62                .get_component_by_id_as::<TransformComponent>(child)
63                .is_some()
64                && !world
65                    .component_label(child)
66                    .map(|label| label.starts_with("__"))
67                    .unwrap_or(false)
68        })
69        .collect()
70}
71
72fn decompose_world_trs(matrix_world: [[f32; 4]; 4]) -> ([f32; 3], [f32; 4], [f32; 3]) {
73    let translation = [matrix_world[3][0], matrix_world[3][1], matrix_world[3][2]];
74    let scale = [
75        (matrix_world[0][0] * matrix_world[0][0]
76            + matrix_world[0][1] * matrix_world[0][1]
77            + matrix_world[0][2] * matrix_world[0][2])
78            .sqrt(),
79        (matrix_world[1][0] * matrix_world[1][0]
80            + matrix_world[1][1] * matrix_world[1][1]
81            + matrix_world[1][2] * matrix_world[1][2])
82            .sqrt(),
83        (matrix_world[2][0] * matrix_world[2][0]
84            + matrix_world[2][1] * matrix_world[2][1]
85            + matrix_world[2][2] * matrix_world[2][2])
86            .sqrt(),
87    ];
88    let rotation = mittens_engine::utils::math::mat_to_quat(matrix_world);
89    (translation, rotation, scale)
90}
91
92fn spawn_container_item_debug_clones(
93    universe: &mut engine::Universe,
94    container: engine::ecs::ComponentId,
95) {
96    use engine::ecs::IntentValue;
97    use engine::ecs::system::TransformSystem;
98    use mittens_engine::scripting::component_registry::{
99        ce_ast_to_materialized, spawn_tree, subtree_to_ce_ast,
100    };
101
102    let items = collect_container_items(&universe.world, container);
103    for (index, item) in items.into_iter().enumerate() {
104        let Some(matrix_world) = TransformSystem::world_model(&universe.world, item) else {
105            println!("[diy-panel-debug] skip {:?}: no world matrix", item);
106            continue;
107        };
108
109        let item_label = universe
110            .world
111            .component_label(item)
112            .unwrap_or("")
113            .to_string();
114        println!(
115            "[diy-panel-debug] item={} label={:?} matrix_world={:?}",
116            index, item_label, matrix_world,
117        );
118
119        // Subtree → MMS AST → MaterializedCE → fresh spawn.
120        // Same path attach_clone uses; gives the clone fresh GUIDs and
121        // routes everything through the standard component registry.
122        let Ok(ce_ast) = subtree_to_ce_ast(&universe.world, item) else {
123            println!(
124                "[diy-panel-debug] skip {:?}: failed to encode subtree",
125                item
126            );
127            continue;
128        };
129        let Ok(materialized) = ce_ast_to_materialized(&ce_ast) else {
130            println!(
131                "[diy-panel-debug] skip {:?}: failed to materialize CE",
132                item
133            );
134            continue;
135        };
136        let Ok(clone_root) = spawn_tree(
137            &materialized,
138            None,
139            &mut universe.world,
140            &mut universe.command_queue,
141        ) else {
142            println!("[diy-panel-debug] skip {:?}: failed to spawn clone", item);
143            continue;
144        };
145
146        universe.add(clone_root);
147
148        let (mut clone_translation, clone_rotation, clone_scale) =
149            decompose_world_trs(matrix_world);
150        clone_translation[0] += DEBUG_CLONE_WORLD_OFFSET[0];
151        clone_translation[1] += DEBUG_CLONE_WORLD_OFFSET[1];
152        clone_translation[2] += DEBUG_CLONE_WORLD_OFFSET[2];
153
154        universe.command_queue.push_intent_now(
155            clone_root,
156            IntentValue::UpdateTransform {
157                component_ids: vec![clone_root],
158                translation: clone_translation,
159                rotation_quat_xyzw: clone_rotation,
160                scale: clone_scale,
161            },
162        );
163
164        let label = if item_label.is_empty() {
165            format!("item_{index}")
166        } else {
167            format!("{}_{}", item_label, index)
168        };
169        println!(
170            "[diy-panel-debug] cloned item={} label={:?} clone_root={:?} offset_translation={:?}",
171            index, label, clone_root, clone_translation,
172        );
173    }
174}
175
176fn main() {
177    mittens_engine::example_support::ensure_model_assets();
178    utils::logger::init();
179
180    let output = scripting::MeowMeowRunner::eval(include_str!("diy-panel.mms"));
181
182    for error in &output.errors {
183        eprintln!("[mms] {error}");
184    }
185    println!(
186        "[mms] {} intent(s) from diy-panel.mms",
187        output.intents.len()
188    );
189
190    if !output.errors.is_empty() {
191        std::process::exit(1);
192    }
193
194    let world = engine::ecs::World::default();
195    let mut universe = engine::Universe::new(world);
196
197    let scope = engine::ecs::ComponentId::default();
198    for intent in output.intents {
199        universe.command_queue.push_intent_now(scope, intent);
200    }
201
202    universe.systems.process_commands(
203        &mut universe.world,
204        &mut universe.visuals,
205        &mut universe.render_assets,
206        &mut universe.command_queue,
207    );
208
209    universe.enable_repl();
210
211    let demo_root = universe
212        .world
213        .all_components()
214        .find(|&cid| universe.world.component_label(cid) == Some("diy_panel_demo"))
215        .expect("diy panel demo root");
216    let container = universe
217        .find_component(demo_root, "[name='container']")
218        .expect("container target");
219    let authored_child = universe
220        .find_component(demo_root, "[name='authored_child']")
221        .expect("authored child");
222
223    assert_eq!(universe.parent_of(authored_child), Some(container));
224
225    spawn_runtime_text(&mut universe, demo_root, "late child routed on attach");
226
227    let late_child = universe
228        .find_component(demo_root, "[name='late child routed on attach']")
229        .expect("late child");
230    assert_eq!(universe.parent_of(late_child), Some(container));
231
232    let input = engine::user_input::InputState::default();
233    universe.systems.tick(
234        &mut universe.world,
235        &mut universe.visuals,
236        &mut universe.render_assets,
237        &input,
238        &mut universe.command_queue,
239        0.0,
240    );
241    universe.systems.process_commands(
242        &mut universe.world,
243        &mut universe.visuals,
244        &mut universe.render_assets,
245        &mut universe.command_queue,
246    );
247
248    //spawn_container_item_debug_clones(&mut universe, container);
249    universe.systems.process_commands(
250        &mut universe.world,
251        &mut universe.visuals,
252        &mut universe.render_assets,
253        &mut universe.command_queue,
254    );
255
256    let scroll = universe
257        .world
258        .children_of(container)
259        .iter()
260        .copied()
261        .find(|&child| {
262            universe.world.component_label(child) == Some("__scroll")
263                && universe
264                    .world
265                    .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(child)
266                    .is_some()
267        })
268        .expect("layout-owned scroll wrapper");
269    let track = universe
270        .world
271        .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(scroll)
272        .and_then(|sc| sc.track)
273        .expect("layout-owned scroll track");
274
275    assert_eq!(universe.parent_of(authored_child), Some(track));
276    assert_eq!(universe.parent_of(late_child), Some(track));
277
278    println!(
279        "[diy-panel] init-time routing verified, then layout-owned scrolling moved content into __scroll_track"
280    );
281
282    engine::Windowing::run_app(universe).expect("Windowing failed");
283}
examples/gravity-fields.rs (line 11)
6fn cube_renderable_sibling_of_collider(
7    world: &engine::ecs::World,
8    collider_cid: engine::ecs::ComponentId,
9) -> Option<engine::ecs::ComponentId> {
10    let t = world.parent_of(collider_cid)?;
11    for &sib in world.children_of(t).iter() {
12        if sib == collider_cid {
13            continue;
14        }
15        let Some(r) =
16            world.get_component_by_id_as::<engine::ecs::component::RenderableComponent>(sib)
17        else {
18            continue;
19        };
20        if r.renderable.base_mesh == engine::graphics::primitives::CpuMeshHandle::CUBE {
21            return Some(sib);
22        }
23    }
24    None
25}
26
27fn set_renderable_color_rgba(
28    world: &mut engine::ecs::World,
29    emit: &mut dyn engine::ecs::SignalEmitter,
30    renderable_cid: engine::ecs::ComponentId,
31    rgba: [f32; 4],
32) {
33    // Prefer existing ColorComponent.
34    let existing = world
35        .children_of(renderable_cid)
36        .iter()
37        .copied()
38        .find(|&ch| {
39            world
40                .get_component_by_id_as::<engine::ecs::component::ColorComponent>(ch)
41                .is_some()
42        });
43
44    if let Some(color_cid) = existing {
45        if let Some(c) =
46            world.get_component_by_id_as_mut::<engine::ecs::component::ColorComponent>(color_cid)
47        {
48            c.rgba = rgba;
49            emit.push_intent_now(
50                color_cid,
51                engine::ecs::IntentValue::RegisterColor {
52                    component_ids: vec![color_cid],
53                },
54            );
55        }
56        return;
57    }
58
59    // Fallback: add a ColorComponent child if missing.
60    let color_cid = world.register(engine::ecs::component::ColorComponent::rgba(
61        rgba[0], rgba[1], rgba[2], rgba[3],
62    ));
63    let _ = world.add_child(renderable_cid, color_cid);
64    emit.push_intent_now(
65        color_cid,
66        engine::ecs::IntentValue::RegisterColor {
67            component_ids: vec![color_cid],
68        },
69    );
70}
71
72fn kinetic_response_child_of_collider(
73    world: &engine::ecs::World,
74    collider_cid: engine::ecs::ComponentId,
75) -> Option<engine::ecs::ComponentId> {
76    for &ch in world.children_of(collider_cid).iter() {
77        if world
78            .get_component_by_id_as::<engine::ecs::component::KineticResponseComponent>(ch)
79            .is_some()
80        {
81            return Some(ch);
82        }
83    }
84    None
85}
examples/raycast-topology-animation.rs (line 36)
30fn ensure_emissive_on(
31    world: &mut engine::ecs::World,
32    emit: &mut dyn engine::ecs::SignalEmitter,
33    renderable_cid: engine::ecs::ComponentId,
34) {
35    let existing = world
36        .children_of(renderable_cid)
37        .iter()
38        .copied()
39        .find(|&ch| {
40            world
41                .get_component_by_id_as::<engine::ecs::component::EmissiveComponent>(ch)
42                .is_some()
43        });
44
45    if existing.is_some() {
46        return;
47    }
48
49    let emissive_cid = world.register(engine::ecs::component::EmissiveComponent::on());
50    let _ = world.add_child(renderable_cid, emissive_cid);
51    emit.push_intent_now(
52        emissive_cid,
53        engine::ecs::IntentValue::RegisterEmissive {
54            component_ids: vec![emissive_cid],
55        },
56    );
57}
examples/bisket-vr-debug.rs (line 1204)
1199fn head_target_offset_in_target_local(world: &World, avc_id: ComponentId) -> [f32; 3] {
1200    let avc = world
1201        .get_component_by_id_as::<AvatarControlComponent>(avc_id)
1202        .expect("avc missing");
1203    let mut eye_offset = [0.0, 0.0, 0.0];
1204    for &ch in world.children_of(avc_id) {
1205        let is_t = world
1206            .get_component_by_id_as::<TransformComponent>(ch)
1207            .is_some();
1208        if !is_t {
1209            continue;
1210        }
1211        let wraps_cam = world.children_of(ch).iter().any(|&gc| {
1212            world
1213                .get_component_by_id_as::<mittens_engine::engine::ecs::component::Camera3DComponent>(gc)
1214                .is_some()
1215                || world
1216                    .get_component_by_id_as::<mittens_engine::engine::ecs::component::CameraXRComponent>(gc)
1217                    .is_some()
1218        });
1219        if wraps_cam {
1220            eye_offset = world
1221                .get_component_by_id_as::<TransformComponent>(ch)
1222                .unwrap()
1223                .transform
1224                .translation;
1225            break;
1226        }
1227    }
1228    let offset_yaw = if avc.forward_plus_z {
1229        0.0
1230    } else {
1231        std::f32::consts::PI
1232    };
1233    let neg_eye = [-eye_offset[0], -eye_offset[1], -eye_offset[2]];
1234    quat_rotate_vec3(quat_rotation_y(offset_yaw), neg_eye)
1235}
Source

pub fn get_component_by_id_as<T: 'static>(&self, c: ComponentId) -> Option<&T>

Examples found in repository?
examples/bisket-vr-demo.rs (line 53)
51fn world_pos(world: &World, id: ComponentId) -> [f32; 3] {
52    world
53        .get_component_by_id_as::<TransformComponent>(id)
54        .map(|t| {
55            let m = t.transform.matrix_world;
56            [m[3][0], m[3][1], m[3][2]]
57        })
58        .unwrap_or([0.0; 3])
59}
60
61fn world_rot_quat(world: &World, id: ComponentId) -> [f32; 4] {
62    world
63        .get_component_by_id_as::<TransformComponent>(id)
64        .map(|t| mat_to_quat(t.transform.matrix_world))
65        .unwrap_or([0.0, 0.0, 0.0, 1.0])
66}
67
68fn v3_len(v: [f32; 3]) -> f32 {
69    (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
70}
71
72fn deg(rad: f32) -> f32 {
73    rad * (180.0 / std::f32::consts::PI)
74}
75
76fn component_label(world: &World, id: ComponentId) -> String {
77    world.component_name(id).unwrap_or("?").to_string()
78}
79
80fn controller_hand_for_component(world: &World, start: ComponentId) -> Option<ControllerHand> {
81    let mut cur = start;
82    loop {
83        if let Some(ctrl) = world.get_component_by_id_as::<ControllerXRComponent>(cur) {
84            return Some(ctrl.hand);
85        }
86        cur = world.parent_of(cur)?;
87    }
88}
89
90fn on_xr_pointer_event(world: &mut World, _emit: &mut dyn SignalEmitter, env: &Signal) {
91    let Some(event) = env.event.as_ref() else {
92        return;
93    };
94    let (kind, raycaster, renderable, hit_point) = match event {
95        EventSignal::DragStart {
96            raycaster,
97            renderable,
98            hit_point,
99            ..
100        } => ("DragStart", *raycaster, *renderable, Some(*hit_point)),
101        EventSignal::DragMove {
102            raycaster,
103            renderable,
104            hit_point,
105            ..
106        } => ("DragMove", *raycaster, *renderable, Some(*hit_point)),
107        EventSignal::DragEnd {
108            raycaster,
109            renderable,
110            hit_point,
111        } => ("DragEnd", *raycaster, *renderable, *hit_point),
112        EventSignal::Click {
113            raycaster,
114            renderable,
115            hit_point,
116            ..
117        } => ("Click", *raycaster, *renderable, Some(*hit_point)),
118        _ => return,
119    };
120
121    let Some(hand) = controller_hand_for_component(world, raycaster) else {
122        return;
123    };
124    let hand_label = match hand {
125        ControllerHand::Left => "Left",
126        ControllerHand::Right => "Right",
127    };
128    let renderable_name = component_label(world, renderable);
129    let ray_name = component_label(world, raycaster);
130    if let Some(p) = hit_point {
131        println!(
132            "[xr-pointer] hand={} kind={} raycaster={} renderable={} hit=[{:+.3},{:+.3},{:+.3}]",
133            hand_label, kind, ray_name, renderable_name, p[0], p[1], p[2]
134        );
135    } else {
136        println!(
137            "[xr-pointer] hand={} kind={} raycaster={} renderable={}",
138            hand_label, kind, ray_name, renderable_name
139        );
140    }
141}
142
143fn on_dump_click(world: &mut World, _emit: &mut dyn SignalEmitter, env: &Signal) {
144    if !matches!(env.event.as_ref(), Some(EventSignal::Click { .. })) {
145        return;
146    }
147    let Some(targets) = DUMP_TARGETS.get() else {
148        return;
149    };
150
151    let driven_p = world_pos(world, targets.driven_t);
152    let driven_q = world_rot_quat(world, targets.driven_t);
153    let splice_p = world_pos(world, targets.splice_head);
154    let head_p = world_pos(world, targets.head_bone);
155    let head_q = world_rot_quat(world, targets.head_bone);
156
157    // Expected head pivot from HMD pose + authored eye offset convention.
158    let neg_eye = [
159        -targets.authored_eye_offset_head_local[0],
160        -targets.authored_eye_offset_head_local[1],
161        -targets.authored_eye_offset_head_local[2],
162    ];
163    let head_target_offset_local =
164        quat_rotate_vec3(quat_rotation_y(targets.head_ik_offset_yaw), neg_eye);
165    let head_target_offset_world = quat_rotate_vec3(driven_q, head_target_offset_local);
166    let expected_splice = [
167        driven_p[0] + head_target_offset_world[0],
168        driven_p[1] + head_target_offset_world[1],
169        driven_p[2] + head_target_offset_world[2],
170    ];
171    let splice_err = [
172        splice_p[0] - expected_splice[0],
173        splice_p[1] - expected_splice[1],
174        splice_p[2] - expected_splice[2],
175    ];
176
177    // Camera-wrapper vs head local-offset consistency check.
178    let mut cam_rel_report = String::from("camera_wrapper: <none>");
179    if let Some(cam_t) = targets.camera_wrapper_t {
180        let cam_p = world_pos(world, cam_t);
181        let expected_cam_world_off =
182            quat_rotate_vec3(head_q, targets.authored_eye_offset_head_local);
183        let expected_cam = [
184            head_p[0] + expected_cam_world_off[0],
185            head_p[1] + expected_cam_world_off[1],
186            head_p[2] + expected_cam_world_off[2],
187        ];
188        let cam_err = [
189            cam_p[0] - expected_cam[0],
190            cam_p[1] - expected_cam[1],
191            cam_p[2] - expected_cam[2],
192        ];
193        cam_rel_report = format!(
194            "camera_wrapper  pos=[{:+.3},{:+.3},{:+.3}]  expected_from_head=[{:+.3},{:+.3},{:+.3}]  err=[{:+.3},{:+.3},{:+.3}] |err|={:.4}m",
195            cam_p[0],
196            cam_p[1],
197            cam_p[2],
198            expected_cam[0],
199            expected_cam[1],
200            expected_cam[2],
201            cam_err[0],
202            cam_err[1],
203            cam_err[2],
204            v3_len(cam_err)
205        );
206    }
207
208    // Torso tilt diagnostic (forward pitch of upper chest).
209    let mut torso_report = String::from("upper_chest_tilt: <not found>");
210    if let Some(upper_chest) = targets.upper_chest {
211        let chest_q = world_rot_quat(world, upper_chest);
212        let fwd = quat_rotate_vec3(chest_q, [0.0, 0.0, 1.0]);
213        let pitch_deg = deg(fwd[1].atan2((fwd[0] * fwd[0] + fwd[2] * fwd[2]).sqrt()));
214        torso_report = format!(
215            "upper_chest_fwd=[{:+.3},{:+.3},{:+.3}] pitch={:+.2}deg",
216            fwd[0], fwd[1], fwd[2], pitch_deg
217        );
218    }
219
220    println!("\n================ BONE DUMP (click) ================");
221    println!(
222        "driven_t  pos=[{:+.3},{:+.3},{:+.3}]  rot_xyzw=[{:+.3},{:+.3},{:+.3},{:+.3}]",
223        driven_p[0], driven_p[1], driven_p[2], driven_q[0], driven_q[1], driven_q[2], driven_q[3],
224    );
225    println!(
226        "splice_head  pos=[{:+.3},{:+.3},{:+.3}]  expected=[{:+.3},{:+.3},{:+.3}]  err=[{:+.3},{:+.3},{:+.3}] |err|={:.4}m",
227        splice_p[0],
228        splice_p[1],
229        splice_p[2],
230        expected_splice[0],
231        expected_splice[1],
232        expected_splice[2],
233        splice_err[0],
234        splice_err[1],
235        splice_err[2],
236        v3_len(splice_err),
237    );
238    println!("{}", cam_rel_report);
239    println!("{}", torso_report);
240    println!(
241        "\n  {:<22}  {:>7}  {:>7}  {:>7}     {:>+7}  {:>+7}  {:>+7}",
242        "bone", "x", "y", "z", "Δx_to_drv", "Δy_to_drv", "Δz_to_drv"
243    );
244    let mut prev_pos: Option<[f32; 3]> = None;
245    for (i, (label, bone_id)) in targets.bones.iter().enumerate() {
246        let p = world_pos(world, *bone_id);
247        println!(
248            "  {:<22}  {:>+7.3}  {:>+7.3}  {:>+7.3}     {:>+7.3}  {:>+7.3}  {:>+7.3}",
249            label,
250            p[0],
251            p[1],
252            p[2],
253            p[0] - driven_p[0],
254            p[1] - driven_p[1],
255            p[2] - driven_p[2],
256        );
257        // Segment length to previous bone (only meaningful within a chain —
258        // spine bones are contiguous; arm bones are split per side, so we
259        // insert a blank gap when the chain breaks).
260        if let Some(prev) = prev_pos {
261            let is_spine = label.starts_with("J_Bip_C_");
262            let prev_was_spine = targets.bones[i - 1].0.starts_with("J_Bip_C_");
263            let is_left_arm_step =
264                label.starts_with("J_Bip_L_") && targets.bones[i - 1].0.starts_with("J_Bip_L_");
265            let is_right_arm_step =
266                label.starts_with("J_Bip_R_") && targets.bones[i - 1].0.starts_with("J_Bip_R_");
267            if (is_spine && prev_was_spine) || is_left_arm_step || is_right_arm_step {
268                let d = [(p[0] - prev[0]), (p[1] - prev[1]), (p[2] - prev[2])];
269                let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
270                println!("    └─ segment from prev: len={:.4}m", len);
271            }
272        }
273        prev_pos = Some(p);
274    }
275    println!("====================================================\n");
276}
277
278fn main() {
279    mittens_engine::example_support::ensure_model_assets();
280    utils::logger::init();
281
282    let world = engine::ecs::World::default();
283    let mut universe = engine::Universe::new(world);
284
285    let output = scripting::MeowMeowRunner::eval_with_world_and_assets_at_path(
286        include_str!("bisket-vr-demo.mms"),
287        Some("examples/bisket-vr-demo.mms"),
288        &mut universe.world,
289        &mut universe.systems.rx,
290        Some(&mut universe.render_assets),
291        &mut universe.command_queue,
292    );
293
294    for error in &output.errors {
295        eprintln!("[mms] {error}");
296    }
297    println!(
298        "[mms] {} intent(s) from bisket-vr-demo.mms",
299        output.intents.len()
300    );
301
302    let scope = engine::ecs::ComponentId::default();
303    for intent in output.intents {
304        universe.command_queue.push_intent_now(scope, intent);
305    }
306
307    universe.systems.process_commands(
308        &mut universe.world,
309        &mut universe.visuals,
310        &mut universe.render_assets,
311        &mut universe.command_queue,
312    );
313
314    // Force the glTF subtree to spawn so the armature exists before we query for
315    // bone markers (otherwise the bone components aren't in the world yet).
316    {
317        let systems = &mut universe.systems;
318        systems.gltf.tick_with_queue(
319            &mut universe.world,
320            &mut universe.visuals,
321            &mut systems.skinned_mesh,
322            &mut universe.command_queue,
323            0.0,
324        );
325    }
326    universe.systems.process_commands(
327        &mut universe.world,
328        &mut universe.visuals,
329        &mut universe.render_assets,
330        &mut universe.command_queue,
331    );
332
333    // Find any global root to start the descendant search from. Scanning all
334    // components is fine here — this only runs once at startup.
335    let roots: Vec<engine::ecs::ComponentId> = universe
336        .world
337        .all_components()
338        .filter(|&id| universe.world.parent_of(id).is_none())
339        .collect();
340
341    // ----------------------------------------------------------------------
342    // Dump button — click to print a bone snapshot for offline analysis.
343    //
344    // Topology: scene_root → OV → button_t → button_r (+ C, EM, Raycastable).
345    // OV phase so the cube is visible through the avatar body in first-person VR.
346    // Click handler walks the cached bone list, prints world pos + segment
347    // lengths to stdout.  Compare with bisket-vr-debug harness output.
348    // ----------------------------------------------------------------------
349    {
350        // Resolve AVC + driven_t for the dump.
351        let avc_id_opt = universe.world.all_components().find(|&id| {
352            universe
353                .world
354                .get_component_by_id_as::<AvatarControlComponent>(id)
355                .is_some()
356        });
357        if let Some(avc_id) = avc_id_opt {
358            let driven_t = universe.world.parent_of(avc_id).unwrap_or(avc_id);
359            let head_ik_offset_yaw = universe
360                .world
361                .get_component_by_id_as::<AvatarControlComponent>(avc_id)
362                .map(|c| {
363                    if c.forward_plus_z {
364                        0.0
365                    } else {
366                        std::f32::consts::PI
367                    }
368                })
369                .unwrap_or(std::f32::consts::PI);
370
371            // Find each bone under any root (model_root is unique per avatar here).
372            let mut bones: Vec<(String, ComponentId)> = Vec::new();
373            for name in DUMP_BONES {
374                let sel = format!("[name='{}']", name);
375                if let Some(bone) = roots.iter().find_map(|&r| universe.find_component(r, &sel)) {
376                    bones.push((name.to_string(), bone));
377                } else {
378                    eprintln!("[dump] bone not found: {}", name);
379                }
380            }
381            // splice_head = parent of head bone (runtime-inserted by AVC).
382            let splice_head = bones
383                .iter()
384                .find(|(n, _)| n == "J_Bip_C_Head")
385                .and_then(|(_, head)| universe.world.parent_of(*head))
386                .unwrap_or(avc_id);
387
388            let head_bone = bones
389                .iter()
390                .find(|(n, _)| n == "J_Bip_C_Head")
391                .map(|(_, id)| *id)
392                .unwrap_or(splice_head);
393
394            let upper_chest = bones
395                .iter()
396                .find(|(n, _)| n == "J_Bip_C_UpperChest")
397                .map(|(_, id)| *id);
398
399            // Find CameraXR + its transform wrapper to recover authored eye offset.
400            let cxr_id = universe.world.all_components().find(|&cid| {
401                universe
402                    .world
403                    .get_component_by_id_as::<mittens_engine::engine::ecs::component::CameraXRComponent>(cid)
404                    .is_some()
405            });
406            let camera_wrapper_t = cxr_id.and_then(|cid| {
407                universe.world.parent_of(cid).filter(|&p| {
408                    universe
409                        .world
410                        .get_component_by_id_as::<TransformComponent>(p)
411                        .is_some()
412                })
413            });
414            let authored_eye_offset_head_local = camera_wrapper_t
415                .and_then(|t| {
416                    universe
417                        .world
418                        .get_component_by_id_as::<TransformComponent>(t)
419                })
420                .map(|t| t.transform.translation)
421                .unwrap_or([0.0, 0.0, 0.0]);
422
423            let _ = DUMP_TARGETS.set(DumpTargets {
424                bones,
425                driven_t,
426                splice_head,
427                head_bone,
428                upper_chest,
429                camera_wrapper_t,
430                authored_eye_offset_head_local,
431                head_ik_offset_yaw,
432            });
433
434            // Spawn the button.  Position chosen so it's reachable in-VR from
435            // a standing pose: 0.35 m to the right, hip-height, slightly out.
436            let btn_t = universe.world.add_component(
437                TransformComponent::new()
438                    .with_position(0.35, 1.05, -0.35)
439                    .with_scale(0.06, 0.06, 0.06),
440            );
441            let btn_r = universe.world.add_component(RenderableComponent::cube());
442            let btn_c = universe
443                .world
444                .add_component(ColorComponent::rgba(0.95, 0.20, 0.40, 1.0));
445            let btn_em = universe.world.add_component(EmissiveComponent::on());
446            let btn_rcast = universe
447                .world
448                .add_component(RaycastableComponent::enabled());
449            let btn_ov = universe.world.add_component(OverlayComponent::new());
450            let _ = universe.world.add_child(btn_r, btn_c);
451            let _ = universe.world.add_child(btn_r, btn_em);
452            let _ = universe.world.add_child(btn_r, btn_rcast);
453            let _ = universe.world.add_child(btn_t, btn_r);
454            let _ = universe.world.add_child(btn_ov, btn_t);
455
456            // Attach to a stable scene root (the first global root we found).
457            let scene_root = roots
458                .iter()
459                .copied()
460                .find(|&r| {
461                    universe
462                        .world
463                        .get_component_by_id_as::<TransformComponent>(r)
464                        .is_some()
465                        || universe.world.children_of(r).len() > 0
466                })
467                .unwrap_or_else(|| roots[0]);
468            let _ = universe.attach(scene_root, btn_ov);
469
470            // Wire Click handler.  Scope = the renderable (where the raycast hits).
471            universe.add_signal_handler(SignalKind::Click, btn_r, on_dump_click);
472
473            println!(
474                "[dump] button spawned at [0.35, 1.05, -0.35] — click in VR or with desktop pointer to dump bones"
475            );
476        } else {
477            eprintln!("[dump] AvatarControlComponent not found — skipping dump button");
478        }
479    }
480
481    for kind in [
482        SignalKind::DragStart,
483        SignalKind::DragMove,
484        SignalKind::DragEnd,
485        SignalKind::Click,
486    ] {
487        universe
488            .systems
489            .rx
490            .add_global_handler(kind, on_xr_pointer_event);
491    }
492
493    universe.enable_meow_meow_repl();
494    engine::Windowing::run_app(universe).expect("Windowing failed");
495}
More examples
Hide additional examples
examples/vtuber-editor-example.rs (line 17)
14fn controller_hand_for_component(world: &World, start: ComponentId) -> Option<ControllerHand> {
15    let mut cur = start;
16    loop {
17        if let Some(ctrl) = world.get_component_by_id_as::<ControllerXRComponent>(cur) {
18            return Some(ctrl.hand);
19        }
20        cur = world.parent_of(cur)?;
21    }
22}
examples/vtuber-mirror-example.rs (line 17)
14fn controller_hand_for_component(world: &World, start: ComponentId) -> Option<ControllerHand> {
15    let mut cur = start;
16    loop {
17        if let Some(ctrl) = world.get_component_by_id_as::<ControllerXRComponent>(cur) {
18            return Some(ctrl.hand);
19        }
20        cur = world.parent_of(cur)?;
21    }
22}
examples/scrolling.rs (line 17)
10fn first_renderable_in_subtree(
11    world: &engine::ecs::World,
12    root: engine::ecs::ComponentId,
13) -> engine::ecs::ComponentId {
14    let mut stack = vec![root];
15    while let Some(node) = stack.pop() {
16        if world
17            .get_component_by_id_as::<engine::ecs::component::RenderableComponent>(node)
18            .is_some()
19        {
20            return node;
21        }
22        for &child in world.children_of(node).iter().rev() {
23            stack.push(child);
24        }
25    }
26
27    panic!("missing renderable under subtree {:?}", root);
28}
29
30fn spawn_scroll_item(
31    universe: &mut engine::Universe,
32    scrolling: engine::ecs::ComponentId,
33    prefix: &str,
34    index: usize,
35    panel_rgba: [f32; 4],
36    text_rgba: [f32; 4],
37) -> engine::ecs::ComponentId {
38    use engine::ecs::component::{
39        ColorComponent, RenderableComponent, TextComponent, TransformComponent,
40    };
41
42    let label = format!("{prefix}_item_{index:02}");
43    let root = universe.world.add_component_boxed_named(
44        label.clone(),
45        Box::new(TransformComponent::new().with_position(0.0, -(index as f32) * 1.15, 0.05)),
46    );
47    let panel = universe.world.add_component_boxed_named(
48        format!("{label}_panel"),
49        Box::new(
50            TransformComponent::new()
51                .with_position(2.3, -0.45, 0.0)
52                .with_scale(4.6, 0.9, 1.0),
53        ),
54    );
55    let panel_renderable = universe.world.add_component_boxed_named(
56        format!("{label}_renderable"),
57        Box::new(RenderableComponent::square()),
58    );
59    let panel_color = universe.world.add_component(ColorComponent::rgba(
60        panel_rgba[0],
61        panel_rgba[1],
62        panel_rgba[2],
63        panel_rgba[3],
64    ));
65
66    let text_anchor = universe.world.add_component_boxed_named(
67        format!("{label}_text_anchor"),
68        Box::new(
69            TransformComponent::new()
70                .with_position(0.35, -0.38, 0.02)
71                .with_scale(0.11, 0.11, 0.11),
72        ),
73    );
74    let text = universe.world.add_component_boxed_named(
75        format!("{label}_text"),
76        Box::new(TextComponent::new(label.clone())),
77    );
78    let text_color = universe.world.add_component(ColorComponent::rgba(
79        text_rgba[0],
80        text_rgba[1],
81        text_rgba[2],
82        text_rgba[3],
83    ));
84
85    let _ = universe.world.add_child(root, panel);
86    let _ = universe.world.add_child(panel, panel_renderable);
87    let _ = universe.world.add_child(panel_renderable, panel_color);
88    let _ = universe.world.add_child(root, text_anchor);
89    let _ = universe.world.add_child(text_anchor, text);
90    let _ = universe.world.add_child(text, text_color);
91
92    universe
93        .attach(scrolling, root)
94        .expect("attach scroll item");
95    root
96}
97
98fn populate_scrolling(
99    universe: &mut engine::Universe,
100    scrolling: engine::ecs::ComponentId,
101    prefix: &str,
102    panel_rgba: [f32; 4],
103    text_rgba: [f32; 4],
104    count: usize,
105) {
106    for index in 0..count {
107        let root = spawn_scroll_item(universe, scrolling, prefix, index, panel_rgba, text_rgba);
108        let track = universe
109            .world
110            .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(scrolling)
111            .and_then(|sc| sc.track)
112            .expect("owned scroll track");
113        assert_eq!(universe.parent_of(root), Some(track));
114    }
115}
116
117fn main() {
118    mittens_engine::example_support::ensure_model_assets();
119    utils::logger::init();
120
121    let output = scripting::MeowMeowRunner::eval(include_str!("scrolling.mms"));
122
123    for error in &output.errors {
124        eprintln!("[mms] {error}");
125    }
126    println!(
127        "[mms] {} intent(s) from scrolling.mms",
128        output.intents.len()
129    );
130
131    if !output.errors.is_empty() {
132        std::process::exit(1);
133    }
134
135    let world = engine::ecs::World::default();
136    let mut universe = engine::Universe::new(world);
137
138    let scope = engine::ecs::ComponentId::default();
139    for intent in output.intents {
140        universe.command_queue.push_intent_now(scope, intent);
141    }
142
143    universe.systems.process_commands(
144        &mut universe.world,
145        &mut universe.visuals,
146        &mut universe.render_assets,
147        &mut universe.command_queue,
148    );
149
150    let manual_scroll = find_named(&universe.world, "manual_scroll");
151    let layout_scroll = find_named(&universe.world, "layout_scroll");
152    let layout_bg = find_named(&universe.world, "__bg");
153
154    let layout_bg_renderable = first_renderable_in_subtree(&universe.world, layout_bg);
155
156    let manual_drag_scope = universe
157        .world
158        .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(manual_scroll)
159        .and_then(|sc| sc.drag_scope)
160        .expect("manual drag scope");
161    let layout_drag_scope = universe
162        .world
163        .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(layout_scroll)
164        .and_then(|sc| sc.drag_scope)
165        .expect("layout drag scope");
166
167    assert_ne!(manual_drag_scope, layout_bg_renderable);
168    assert_eq!(layout_drag_scope, layout_bg_renderable);
169
170    populate_scrolling(
171        &mut universe,
172        manual_scroll,
173        "manual",
174        [0.78, 0.87, 0.98, 1.0],
175        [0.08, 0.16, 0.28, 1.0],
176        18,
177    );
178    populate_scrolling(
179        &mut universe,
180        layout_scroll,
181        "layout",
182        [0.98, 0.87, 0.74, 1.0],
183        [0.30, 0.14, 0.05, 1.0],
184        18,
185    );
186
187    println!(
188        "[scrolling-demo] manual drag_scope={:?} layout drag_scope={:?} late attach routing verified",
189        manual_drag_scope, layout_drag_scope
190    );
191
192    universe.enable_repl();
193    engine::Windowing::run_app(universe).expect("Windowing failed");
194}
examples/diy-panel.rs (line 62)
50fn collect_container_items(
51    world: &engine::ecs::World,
52    container: engine::ecs::ComponentId,
53) -> Vec<engine::ecs::ComponentId> {
54    use engine::ecs::component::TransformComponent;
55
56    world
57        .children_of(container)
58        .iter()
59        .copied()
60        .filter(|&child| {
61            world
62                .get_component_by_id_as::<TransformComponent>(child)
63                .is_some()
64                && !world
65                    .component_label(child)
66                    .map(|label| label.starts_with("__"))
67                    .unwrap_or(false)
68        })
69        .collect()
70}
71
72fn decompose_world_trs(matrix_world: [[f32; 4]; 4]) -> ([f32; 3], [f32; 4], [f32; 3]) {
73    let translation = [matrix_world[3][0], matrix_world[3][1], matrix_world[3][2]];
74    let scale = [
75        (matrix_world[0][0] * matrix_world[0][0]
76            + matrix_world[0][1] * matrix_world[0][1]
77            + matrix_world[0][2] * matrix_world[0][2])
78            .sqrt(),
79        (matrix_world[1][0] * matrix_world[1][0]
80            + matrix_world[1][1] * matrix_world[1][1]
81            + matrix_world[1][2] * matrix_world[1][2])
82            .sqrt(),
83        (matrix_world[2][0] * matrix_world[2][0]
84            + matrix_world[2][1] * matrix_world[2][1]
85            + matrix_world[2][2] * matrix_world[2][2])
86            .sqrt(),
87    ];
88    let rotation = mittens_engine::utils::math::mat_to_quat(matrix_world);
89    (translation, rotation, scale)
90}
91
92fn spawn_container_item_debug_clones(
93    universe: &mut engine::Universe,
94    container: engine::ecs::ComponentId,
95) {
96    use engine::ecs::IntentValue;
97    use engine::ecs::system::TransformSystem;
98    use mittens_engine::scripting::component_registry::{
99        ce_ast_to_materialized, spawn_tree, subtree_to_ce_ast,
100    };
101
102    let items = collect_container_items(&universe.world, container);
103    for (index, item) in items.into_iter().enumerate() {
104        let Some(matrix_world) = TransformSystem::world_model(&universe.world, item) else {
105            println!("[diy-panel-debug] skip {:?}: no world matrix", item);
106            continue;
107        };
108
109        let item_label = universe
110            .world
111            .component_label(item)
112            .unwrap_or("")
113            .to_string();
114        println!(
115            "[diy-panel-debug] item={} label={:?} matrix_world={:?}",
116            index, item_label, matrix_world,
117        );
118
119        // Subtree → MMS AST → MaterializedCE → fresh spawn.
120        // Same path attach_clone uses; gives the clone fresh GUIDs and
121        // routes everything through the standard component registry.
122        let Ok(ce_ast) = subtree_to_ce_ast(&universe.world, item) else {
123            println!(
124                "[diy-panel-debug] skip {:?}: failed to encode subtree",
125                item
126            );
127            continue;
128        };
129        let Ok(materialized) = ce_ast_to_materialized(&ce_ast) else {
130            println!(
131                "[diy-panel-debug] skip {:?}: failed to materialize CE",
132                item
133            );
134            continue;
135        };
136        let Ok(clone_root) = spawn_tree(
137            &materialized,
138            None,
139            &mut universe.world,
140            &mut universe.command_queue,
141        ) else {
142            println!("[diy-panel-debug] skip {:?}: failed to spawn clone", item);
143            continue;
144        };
145
146        universe.add(clone_root);
147
148        let (mut clone_translation, clone_rotation, clone_scale) =
149            decompose_world_trs(matrix_world);
150        clone_translation[0] += DEBUG_CLONE_WORLD_OFFSET[0];
151        clone_translation[1] += DEBUG_CLONE_WORLD_OFFSET[1];
152        clone_translation[2] += DEBUG_CLONE_WORLD_OFFSET[2];
153
154        universe.command_queue.push_intent_now(
155            clone_root,
156            IntentValue::UpdateTransform {
157                component_ids: vec![clone_root],
158                translation: clone_translation,
159                rotation_quat_xyzw: clone_rotation,
160                scale: clone_scale,
161            },
162        );
163
164        let label = if item_label.is_empty() {
165            format!("item_{index}")
166        } else {
167            format!("{}_{}", item_label, index)
168        };
169        println!(
170            "[diy-panel-debug] cloned item={} label={:?} clone_root={:?} offset_translation={:?}",
171            index, label, clone_root, clone_translation,
172        );
173    }
174}
175
176fn main() {
177    mittens_engine::example_support::ensure_model_assets();
178    utils::logger::init();
179
180    let output = scripting::MeowMeowRunner::eval(include_str!("diy-panel.mms"));
181
182    for error in &output.errors {
183        eprintln!("[mms] {error}");
184    }
185    println!(
186        "[mms] {} intent(s) from diy-panel.mms",
187        output.intents.len()
188    );
189
190    if !output.errors.is_empty() {
191        std::process::exit(1);
192    }
193
194    let world = engine::ecs::World::default();
195    let mut universe = engine::Universe::new(world);
196
197    let scope = engine::ecs::ComponentId::default();
198    for intent in output.intents {
199        universe.command_queue.push_intent_now(scope, intent);
200    }
201
202    universe.systems.process_commands(
203        &mut universe.world,
204        &mut universe.visuals,
205        &mut universe.render_assets,
206        &mut universe.command_queue,
207    );
208
209    universe.enable_repl();
210
211    let demo_root = universe
212        .world
213        .all_components()
214        .find(|&cid| universe.world.component_label(cid) == Some("diy_panel_demo"))
215        .expect("diy panel demo root");
216    let container = universe
217        .find_component(demo_root, "[name='container']")
218        .expect("container target");
219    let authored_child = universe
220        .find_component(demo_root, "[name='authored_child']")
221        .expect("authored child");
222
223    assert_eq!(universe.parent_of(authored_child), Some(container));
224
225    spawn_runtime_text(&mut universe, demo_root, "late child routed on attach");
226
227    let late_child = universe
228        .find_component(demo_root, "[name='late child routed on attach']")
229        .expect("late child");
230    assert_eq!(universe.parent_of(late_child), Some(container));
231
232    let input = engine::user_input::InputState::default();
233    universe.systems.tick(
234        &mut universe.world,
235        &mut universe.visuals,
236        &mut universe.render_assets,
237        &input,
238        &mut universe.command_queue,
239        0.0,
240    );
241    universe.systems.process_commands(
242        &mut universe.world,
243        &mut universe.visuals,
244        &mut universe.render_assets,
245        &mut universe.command_queue,
246    );
247
248    //spawn_container_item_debug_clones(&mut universe, container);
249    universe.systems.process_commands(
250        &mut universe.world,
251        &mut universe.visuals,
252        &mut universe.render_assets,
253        &mut universe.command_queue,
254    );
255
256    let scroll = universe
257        .world
258        .children_of(container)
259        .iter()
260        .copied()
261        .find(|&child| {
262            universe.world.component_label(child) == Some("__scroll")
263                && universe
264                    .world
265                    .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(child)
266                    .is_some()
267        })
268        .expect("layout-owned scroll wrapper");
269    let track = universe
270        .world
271        .get_component_by_id_as::<engine::ecs::component::ScrollingComponent>(scroll)
272        .and_then(|sc| sc.track)
273        .expect("layout-owned scroll track");
274
275    assert_eq!(universe.parent_of(authored_child), Some(track));
276    assert_eq!(universe.parent_of(late_child), Some(track));
277
278    println!(
279        "[diy-panel] init-time routing verified, then layout-owned scrolling moved content into __scroll_track"
280    );
281
282    engine::Windowing::run_app(universe).expect("Windowing failed");
283}
examples/gravity-fields.rs (line 16)
6fn cube_renderable_sibling_of_collider(
7    world: &engine::ecs::World,
8    collider_cid: engine::ecs::ComponentId,
9) -> Option<engine::ecs::ComponentId> {
10    let t = world.parent_of(collider_cid)?;
11    for &sib in world.children_of(t).iter() {
12        if sib == collider_cid {
13            continue;
14        }
15        let Some(r) =
16            world.get_component_by_id_as::<engine::ecs::component::RenderableComponent>(sib)
17        else {
18            continue;
19        };
20        if r.renderable.base_mesh == engine::graphics::primitives::CpuMeshHandle::CUBE {
21            return Some(sib);
22        }
23    }
24    None
25}
26
27fn set_renderable_color_rgba(
28    world: &mut engine::ecs::World,
29    emit: &mut dyn engine::ecs::SignalEmitter,
30    renderable_cid: engine::ecs::ComponentId,
31    rgba: [f32; 4],
32) {
33    // Prefer existing ColorComponent.
34    let existing = world
35        .children_of(renderable_cid)
36        .iter()
37        .copied()
38        .find(|&ch| {
39            world
40                .get_component_by_id_as::<engine::ecs::component::ColorComponent>(ch)
41                .is_some()
42        });
43
44    if let Some(color_cid) = existing {
45        if let Some(c) =
46            world.get_component_by_id_as_mut::<engine::ecs::component::ColorComponent>(color_cid)
47        {
48            c.rgba = rgba;
49            emit.push_intent_now(
50                color_cid,
51                engine::ecs::IntentValue::RegisterColor {
52                    component_ids: vec![color_cid],
53                },
54            );
55        }
56        return;
57    }
58
59    // Fallback: add a ColorComponent child if missing.
60    let color_cid = world.register(engine::ecs::component::ColorComponent::rgba(
61        rgba[0], rgba[1], rgba[2], rgba[3],
62    ));
63    let _ = world.add_child(renderable_cid, color_cid);
64    emit.push_intent_now(
65        color_cid,
66        engine::ecs::IntentValue::RegisterColor {
67            component_ids: vec![color_cid],
68        },
69    );
70}
71
72fn kinetic_response_child_of_collider(
73    world: &engine::ecs::World,
74    collider_cid: engine::ecs::ComponentId,
75) -> Option<engine::ecs::ComponentId> {
76    for &ch in world.children_of(collider_cid).iter() {
77        if world
78            .get_component_by_id_as::<engine::ecs::component::KineticResponseComponent>(ch)
79            .is_some()
80        {
81            return Some(ch);
82        }
83    }
84    None
85}
86
87fn on_collision_turn_white(
88    world: &mut engine::ecs::World,
89    emit: &mut dyn engine::ecs::SignalEmitter,
90    signal: &engine::ecs::Signal,
91) {
92    let Some(engine::ecs::EventSignal::CollisionStarted { a, b, .. }) = signal.event.as_ref()
93    else {
94        return;
95    };
96
97    let self_collider = signal.scope;
98    let other = if self_collider == *a {
99        *b
100    } else if self_collider == *b {
101        *a
102    } else {
103        return;
104    };
105
106    // Only react to cube-vs-cube touches (ignore static walls/floor and non-renderable colliders like camera).
107    let Some(other_cn) =
108        world.get_component_by_id_as::<engine::ecs::component::CollisionComponent>(other)
109    else {
110        return;
111    };
112    if other_cn.mode == engine::ecs::component::CollisionMode::Static {
113        return;
114    }
115    if cube_renderable_sibling_of_collider(world, other).is_none() {
116        return;
117    }
118
119    let Some(self_renderable) = cube_renderable_sibling_of_collider(world, self_collider) else {
120        return;
121    };
122    set_renderable_color_rgba(world, emit, self_renderable, [1.0, 1.0, 1.0, 1.0]);
123}
124
125fn on_collision_freeze_gravity(
126    world: &mut engine::ecs::World,
127    _emit: &mut dyn engine::ecs::SignalEmitter,
128    signal: &engine::ecs::Signal,
129) {
130    let Some(engine::ecs::EventSignal::CollisionStarted { a, b, .. }) = signal.event.as_ref()
131    else {
132        return;
133    };
134
135    let self_collider = signal.scope;
136    let other = if self_collider == *a {
137        *b
138    } else if self_collider == *b {
139        *a
140    } else {
141        return;
142    };
143
144    // Only react to cube-vs-cube touches.
145    let Some(other_cn) =
146        world.get_component_by_id_as::<engine::ecs::component::CollisionComponent>(other)
147    else {
148        return;
149    };
150    if other_cn.mode == engine::ecs::component::CollisionMode::Static {
151        return;
152    }
153    if cube_renderable_sibling_of_collider(world, other).is_none() {
154        return;
155    }
156
157    let Some(response_cid) = kinetic_response_child_of_collider(world, self_collider) else {
158        return;
159    };
160    if let Some(r) = world
161        .get_component_by_id_as_mut::<engine::ecs::component::KineticResponseComponent>(
162            response_cid,
163        )
164    {
165        // Gravity is a cached per-responder coefficient; set it to 0 once touched.
166        r.gravity_coefficient = 0.0;
167    }
168}
Source

pub fn get_component_by_id_as_mut<T: 'static>( &mut self, c: ComponentId, ) -> Option<&mut T>

Examples found in repository?
examples/bisket-vr-debug.rs (line 848)
841fn apply_pass_settings(
842    world: &mut World,
843    arms: &[ArmChainRuntime],
844    hand_mode: HandOffsetMode,
845    copy_mode: CopyEndRotationMode,
846) {
847    for arm in arms {
848        if let Some(tc) = world.get_component_by_id_as_mut::<TransformComponent>(arm.target) {
849            tc.transform.rotation = match hand_mode {
850                HandOffsetMode::Authored => arm.authored_target_local_rotation,
851                HandOffsetMode::Neutralized => ident_quat(),
852            };
853            tc.transform.recompute_model();
854        }
855        if let Some(ik) = world.get_component_by_id_as_mut::<IKChainComponent>(arm.ik_chain_id) {
856            if let IKSolver::TwoBoneIK {
857                root_joint_id,
858                mid_joint_id,
859                pole_direction,
860                copy_end_rotation: enabled,
861            } = &mut ik.solver
862            {
863                let _ = (root_joint_id, mid_joint_id, pole_direction);
864                *enabled = match copy_mode {
865                    CopyEndRotationMode::SolverDefault => arm.original_copy_end_rotation,
866                    CopyEndRotationMode::ForcedOff => false,
867                };
868            }
869        }
870    }
871}
More examples
Hide additional examples
examples/gravity-fields.rs (line 46)
27fn set_renderable_color_rgba(
28    world: &mut engine::ecs::World,
29    emit: &mut dyn engine::ecs::SignalEmitter,
30    renderable_cid: engine::ecs::ComponentId,
31    rgba: [f32; 4],
32) {
33    // Prefer existing ColorComponent.
34    let existing = world
35        .children_of(renderable_cid)
36        .iter()
37        .copied()
38        .find(|&ch| {
39            world
40                .get_component_by_id_as::<engine::ecs::component::ColorComponent>(ch)
41                .is_some()
42        });
43
44    if let Some(color_cid) = existing {
45        if let Some(c) =
46            world.get_component_by_id_as_mut::<engine::ecs::component::ColorComponent>(color_cid)
47        {
48            c.rgba = rgba;
49            emit.push_intent_now(
50                color_cid,
51                engine::ecs::IntentValue::RegisterColor {
52                    component_ids: vec![color_cid],
53                },
54            );
55        }
56        return;
57    }
58
59    // Fallback: add a ColorComponent child if missing.
60    let color_cid = world.register(engine::ecs::component::ColorComponent::rgba(
61        rgba[0], rgba[1], rgba[2], rgba[3],
62    ));
63    let _ = world.add_child(renderable_cid, color_cid);
64    emit.push_intent_now(
65        color_cid,
66        engine::ecs::IntentValue::RegisterColor {
67            component_ids: vec![color_cid],
68        },
69    );
70}
71
72fn kinetic_response_child_of_collider(
73    world: &engine::ecs::World,
74    collider_cid: engine::ecs::ComponentId,
75) -> Option<engine::ecs::ComponentId> {
76    for &ch in world.children_of(collider_cid).iter() {
77        if world
78            .get_component_by_id_as::<engine::ecs::component::KineticResponseComponent>(ch)
79            .is_some()
80        {
81            return Some(ch);
82        }
83    }
84    None
85}
86
87fn on_collision_turn_white(
88    world: &mut engine::ecs::World,
89    emit: &mut dyn engine::ecs::SignalEmitter,
90    signal: &engine::ecs::Signal,
91) {
92    let Some(engine::ecs::EventSignal::CollisionStarted { a, b, .. }) = signal.event.as_ref()
93    else {
94        return;
95    };
96
97    let self_collider = signal.scope;
98    let other = if self_collider == *a {
99        *b
100    } else if self_collider == *b {
101        *a
102    } else {
103        return;
104    };
105
106    // Only react to cube-vs-cube touches (ignore static walls/floor and non-renderable colliders like camera).
107    let Some(other_cn) =
108        world.get_component_by_id_as::<engine::ecs::component::CollisionComponent>(other)
109    else {
110        return;
111    };
112    if other_cn.mode == engine::ecs::component::CollisionMode::Static {
113        return;
114    }
115    if cube_renderable_sibling_of_collider(world, other).is_none() {
116        return;
117    }
118
119    let Some(self_renderable) = cube_renderable_sibling_of_collider(world, self_collider) else {
120        return;
121    };
122    set_renderable_color_rgba(world, emit, self_renderable, [1.0, 1.0, 1.0, 1.0]);
123}
124
125fn on_collision_freeze_gravity(
126    world: &mut engine::ecs::World,
127    _emit: &mut dyn engine::ecs::SignalEmitter,
128    signal: &engine::ecs::Signal,
129) {
130    let Some(engine::ecs::EventSignal::CollisionStarted { a, b, .. }) = signal.event.as_ref()
131    else {
132        return;
133    };
134
135    let self_collider = signal.scope;
136    let other = if self_collider == *a {
137        *b
138    } else if self_collider == *b {
139        *a
140    } else {
141        return;
142    };
143
144    // Only react to cube-vs-cube touches.
145    let Some(other_cn) =
146        world.get_component_by_id_as::<engine::ecs::component::CollisionComponent>(other)
147    else {
148        return;
149    };
150    if other_cn.mode == engine::ecs::component::CollisionMode::Static {
151        return;
152    }
153    if cube_renderable_sibling_of_collider(world, other).is_none() {
154        return;
155    }
156
157    let Some(response_cid) = kinetic_response_child_of_collider(world, self_collider) else {
158        return;
159    };
160    if let Some(r) = world
161        .get_component_by_id_as_mut::<engine::ecs::component::KineticResponseComponent>(
162            response_cid,
163        )
164    {
165        // Gravity is a cached per-responder coefficient; set it to 0 once touched.
166        r.gravity_coefficient = 0.0;
167    }
168}
Source

pub fn find_component( &self, root: ComponentId, selector: &str, ) -> Option<ComponentId>

Find the first component under root matching selector.

selector is parsed as MMQ — #name, Type, Type#name, [name='...'], etc. See crates/mittens-query/src/mmq/parser.rs.

Source

pub fn find_all_components( &self, root: ComponentId, selector: &str, ) -> Vec<ComponentId>

Find all components under root matching selector (DFS pre-order).

Source

pub fn component_matches_selector( &self, component: ComponentId, selector: &str, ) -> bool

Source

pub fn world_roots(&self) -> Vec<ComponentId>

Source

pub fn get_parent_as<T: 'static>( &self, c: ComponentId, ) -> Option<(ComponentId, &T)>

Source

pub fn get_parent_as_mut<T: 'static>( &mut self, c: ComponentId, ) -> Option<(ComponentId, &mut T)>

Source

pub fn add_child( &mut self, parent: ComponentId, child: ComponentId, ) -> Result<(), &'static str>

Attach child under parent.

This is a lower-level graph mutation API.

It updates only the parent/child links in the world graph. It does not emit ParentChanged, does not refresh topology-dependent systems, and does not route through runtime helpers like routers/scroll ownership.

Prefer IntentValue::Attach or Universe::attach(...) when attaching a subtree into an already-live parent and you expect normal runtime side effects.

add_child(...) is appropriate for offline subtree assembly before init, tests, and internal structural building where the caller intentionally manages any needed follow-up work.

Safety rules:

  • Both ids must exist.
  • child is detached from its current parent first.
  • Cycles are rejected.
Examples found in repository?
examples/raycast-topology-animation.rs (line 50)
30fn ensure_emissive_on(
31    world: &mut engine::ecs::World,
32    emit: &mut dyn engine::ecs::SignalEmitter,
33    renderable_cid: engine::ecs::ComponentId,
34) {
35    let existing = world
36        .children_of(renderable_cid)
37        .iter()
38        .copied()
39        .find(|&ch| {
40            world
41                .get_component_by_id_as::<engine::ecs::component::EmissiveComponent>(ch)
42                .is_some()
43        });
44
45    if existing.is_some() {
46        return;
47    }
48
49    let emissive_cid = world.register(engine::ecs::component::EmissiveComponent::on());
50    let _ = world.add_child(renderable_cid, emissive_cid);
51    emit.push_intent_now(
52        emissive_cid,
53        engine::ecs::IntentValue::RegisterEmissive {
54            component_ids: vec![emissive_cid],
55        },
56    );
57}
58
59fn ring_a_handler(
60    world: &mut engine::ecs::World,
61    emit: &mut dyn engine::ecs::SignalEmitter,
62    env: &engine::ecs::Signal,
63) {
64    match env.event.as_ref() {
65        Some(engine::ecs::EventSignal::RayIntersected { renderable, .. }) => {
66            println!(
67                "[ring_a_handler] fired scope={:?} renderable={:?}",
68                env.scope, renderable
69            );
70
71            ensure_emissive_on(world, emit, *renderable);
72
73            emit.push_intent_now(
74                env.scope,
75                engine::ecs::IntentValue::SetColor {
76                    component_ids: vec![*renderable],
77                    rgba: [1.0, 1.0, 0.0, 1.0],
78                },
79            );
80        }
81        _ => {}
82    }
83}
84
85fn ring_b_handler(
86    world: &mut engine::ecs::World,
87    emit: &mut dyn engine::ecs::SignalEmitter,
88    env: &engine::ecs::Signal,
89) {
90    match env.event.as_ref() {
91        Some(engine::ecs::EventSignal::RayIntersected { renderable, .. }) => {
92            println!(
93                "[ring_b_handler] fired scope={:?} renderable={:?}",
94                env.scope, renderable
95            );
96
97            ensure_emissive_on(world, emit, *renderable);
98
99            emit.push_intent_now(
100                env.scope,
101                engine::ecs::IntentValue::SetColor {
102                    component_ids: vec![*renderable],
103                    rgba: [0.0, 1.0, 1.0, 1.0],
104                },
105            );
106        }
107        _ => {}
108    }
109}
110
111fn main() {
112    mittens_engine::example_support::ensure_model_assets();
113    utils::logger::init();
114
115    let world = engine::ecs::World::default();
116    let mut universe = engine::Universe::new(world);
117
118    // Background.
119    let bg_color = universe
120        .world
121        .add_component(engine::ecs::component::BackgroundColorComponent::new());
122    let bg_color_c = universe
123        .world
124        .add_component(engine::ecs::component::ColorComponent::rgba(
125            0.1, 0.1, 0.1, 1.0,
126        ));
127    let _ = universe.world.add_child(bg_color, bg_color_c);
128    universe.add(bg_color);
129
130    // Camera rig so we can see the scene.
131    let input = universe
132        .world
133        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
134
135    let rig_transform = universe.world.add_component(
136        engine::ecs::component::TransformComponent::new()
137            .with_position(0.0, 2.0, 8.0)
138            .with_rotation_euler(-0.25, 0.0, 0.0),
139    );
140
141    let camera3d = universe
142        .world
143        .add_component(engine::ecs::component::Camera3DComponent::new());
144
145    let input_mode = universe.world.add_component(
146        engine::ecs::component::InputTransformModeComponent::forward_z()
147            .with_roll_axis_y()
148            .with_fps_rotation(),
149    );
150
151    let _ = universe.attach(input, input_mode);
152    let _ = universe.attach(input, rig_transform);
153    let _ = universe.attach(rig_transform, camera3d);
154
155    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
156    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
157    universe.add(input);
158
159    // Two rotating anchor transforms that the raycaster will be reparented under.
160    // Each ring has its own anchor at a different height.
161    let anchor_a = universe.world.add_component(
162        engine::ecs::component::TransformComponent::new().with_position(0.0, 1.0, 0.0),
163    );
164    let anchor_b = universe.world.add_component(
165        engine::ecs::component::TransformComponent::new().with_position(0.0, 2.2, 0.0),
166    );
167
168    // Visual markers for A/B.
169    fn marker(universe: &mut engine::Universe, parent: engine::ecs::ComponentId, rgba: [f32; 4]) {
170        let r = universe
171            .world
172            .add_component(engine::ecs::component::RenderableComponent::cube());
173        let rc = universe
174            .world
175            .add_component(engine::ecs::component::RaycastableComponent::disabled());
176        let c = universe
177            .world
178            .add_component(engine::ecs::component::ColorComponent::rgba(
179                rgba[0], rgba[1], rgba[2], rgba[3],
180            ));
181        let e = universe
182            .world
183            .add_component(engine::ecs::component::EmissiveComponent::on());
184        let t = universe.world.add_component(
185            engine::ecs::component::TransformComponent::new().with_scale(0.15, 0.15, 0.15),
186        );
187        let _ = universe.attach(parent, t);
188        let _ = universe.attach(t, r);
189        let _ = universe.attach(r, rc);
190        let _ = universe.attach(r, c);
191        let _ = universe.attach(r, e);
192    }
193
194    marker(&mut universe, anchor_a, [0.9, 0.2, 0.2, 1.0]);
195    marker(&mut universe, anchor_b, [0.2, 0.2, 0.9, 1.0]);
196
197    universe.add(anchor_a);
198    universe.add(anchor_b);
199
200    // A ring of cubes around the origin to see which one gets hit.
201    fn ring_cube(
202        universe: &mut engine::Universe,
203        ring_root: engine::ecs::ComponentId,
204        x: f32,
205        y: f32,
206        z: f32,
207        rgba: [f32; 4],
208    ) {
209        let t = universe.world.add_component(
210            engine::ecs::component::TransformComponent::new()
211                .with_position(x, y, z)
212                .with_scale(0.35, 0.35, 0.35),
213        );
214        let r = universe
215            .world
216            .add_component(engine::ecs::component::RenderableComponent::cube());
217        let rc = universe
218            .world
219            .add_component(engine::ecs::component::RaycastableComponent::enabled());
220        let c = universe
221            .world
222            .add_component(engine::ecs::component::ColorComponent::rgba(
223                rgba[0], rgba[1], rgba[2], rgba[3],
224            ));
225        let _ = universe.attach(ring_root, t);
226        let _ = universe.attach(t, r);
227        let _ = universe.attach(r, rc);
228        let _ = universe.attach(r, c);
229    }
230
231    // Two rings: one per anchor height.
232    let n = 28;
233    let (radius_a, y_a) = (4.0, 1.0);
234    let (radius_b, y_b) = (2.6, 2.2);
235
236    let ring_a_root = universe
237        .world
238        .add_component(engine::ecs::component::TransformComponent::new());
239    let ring_b_root = universe
240        .world
241        .add_component(engine::ecs::component::TransformComponent::new());
242
243    for i in 0..n {
244        let a = (i as f32) * (std::f32::consts::TAU / (n as f32));
245        let (x, z) = (radius_a * a.cos(), radius_a * a.sin());
246        let color = if i % 2 == 0 {
247            [0.55, 0.20, 0.20, 1.0]
248        } else {
249            [0.20, 0.55, 0.20, 1.0]
250        };
251        ring_cube(&mut universe, ring_a_root, x, y_a, z, color);
252    }
253
254    for i in 0..n {
255        let a = (i as f32) * (std::f32::consts::TAU / (n as f32));
256        let (x, z) = (radius_b * a.cos(), radius_b * a.sin());
257        let color = if i % 2 == 0 {
258            [0.20, 0.25, 0.60, 1.0]
259        } else {
260            [0.20, 0.55, 0.55, 1.0]
261        };
262        ring_cube(&mut universe, ring_b_root, x, y_b, z, color);
263    }
264
265    // Init rings and attach scoped interaction handlers.
266    universe.add(ring_a_root);
267    universe.add(ring_b_root);
268    universe.add_signal_handler(
269        engine::ecs::SignalKind::RayIntersected,
270        ring_a_root,
271        ring_a_handler,
272    );
273    universe.add_signal_handler(
274        engine::ecs::SignalKind::RayIntersected,
275        ring_b_root,
276        ring_b_handler,
277    );
278
279    // The raycaster component we will move between parents.
280    // Source is inferred from topology:
281    // - Under transforms A/B (no camera child) => parent-forward (-Z)
282    // - Under camera rig transform (has camera child) => cursor-through-camera
283    let raycaster = universe.world.add_component(
284        engine::ecs::component::RayCastComponent::event_driven().with_max_distance(25.0),
285    );
286
287    // Global animation: move the raycaster between anchors.
288    // Loop length is 8 beats (we include a noop keyframe at beat 7.0 to force loop_len=8).
289    let anim_global = universe
290        .world
291        .add_component(engine::ecs::component::AnimationComponent::new());
292    {
293        // beat 0: attach to A.
294        let kf0 = universe
295            .world
296            .add_component(engine::ecs::component::KeyframeComponent::new(0.0));
297        let act0 = universe
298            .world
299            .add_component(engine::ecs::component::ActionComponent::new(
300                engine::ecs::IntentValue::Attach {
301                    parents: vec![anchor_a],
302                    child: raycaster,
303                },
304            ));
305        let _ = universe.attach(kf0, act0);
306        let _ = universe.attach(anim_global, kf0);
307
308        // beat 4: attach to B.
309        let kf4 = universe
310            .world
311            .add_component(engine::ecs::component::KeyframeComponent::new(4.0));
312        let act4 = universe
313            .world
314            .add_component(engine::ecs::component::ActionComponent::new(
315                engine::ecs::IntentValue::Attach {
316                    parents: vec![anchor_b],
317                    child: raycaster,
318                },
319            ));
320        let _ = universe.attach(kf4, act4);
321        let _ = universe.attach(anim_global, kf4);
322
323        // beat 7: noop to make loop_len=8.
324        let kf7 = universe
325            .world
326            .add_component(engine::ecs::component::KeyframeComponent::new(7.0));
327        let noop = universe
328            .world
329            .add_component(engine::ecs::component::ActionComponent::new(
330                engine::ecs::IntentValue::Noop,
331            ));
332        let _ = universe.attach(kf7, noop);
333        let _ = universe.attach(anim_global, kf7);
334    }
335    universe.add(anim_global);
336
337    // Ring A animation: rotate anchor A around Y (1 rev / 8 beats).
338    // Also triggers Action::raycast(raycaster) on downbeats in the first half: beats 0,1,2,3.
339    let anim_a = universe
340        .world
341        .add_component(engine::ecs::component::AnimationComponent::new());
342
343    // Ring B animation: rotate anchor B differently (yaw + pitch).
344    // Also triggers Action::raycast(raycaster) on offbeats in the second half: beats 4.5,5.5,6.5,7.5.
345    let anim_b = universe
346        .world
347        .add_component(engine::ecs::component::AnimationComponent::new());
348
349    let loop_beats = 8.0;
350    let steps = 64;
351
352    for step in 0..=steps {
353        let t = (step as f32) / (steps as f32);
354        let beat = (t as f64) * (loop_beats as f64);
355
356        // Keyframe beats are per-animation local beats.
357        let kf_a = universe
358            .world
359            .add_component(engine::ecs::component::KeyframeComponent::new(beat));
360        let kf_b = universe
361            .world
362            .add_component(engine::ecs::component::KeyframeComponent::new(beat));
363
364        // Anchor A rotation: smooth yaw.
365        let yaw_a = t * std::f32::consts::TAU;
366        let a_set = engine::ecs::component::ActionComponent::new(
367            engine::ecs::IntentValue::UpdateTransform {
368                component_ids: vec![anchor_a],
369                translation: [0.0, 1.0, 0.0],
370                rotation_quat_xyzw: quat_from_yaw(yaw_a),
371                scale: [1.0, 1.0, 1.0],
372            },
373        );
374        let a_set_id = universe.world.add_component(a_set);
375        let _ = universe.attach(kf_a, a_set_id);
376
377        // Anchor B rotation: yaw + pitch (different pattern).
378        let yaw_b = -t * std::f32::consts::TAU * 1.5;
379        let pitch_b = (t * std::f32::consts::TAU).sin() * 0.35;
380        let rot_b = quat_mul(quat_from_yaw(yaw_b), quat_from_pitch(pitch_b));
381        let b_set = engine::ecs::component::ActionComponent::new(
382            engine::ecs::IntentValue::UpdateTransform {
383                component_ids: vec![anchor_b],
384                translation: [0.0, 2.2, 0.0],
385                rotation_quat_xyzw: rot_b,
386                scale: [1.0, 1.0, 1.0],
387            },
388        );
389        let b_set_id = universe.world.add_component(b_set);
390        let _ = universe.attach(kf_b, b_set_id);
391
392        // Per-ring raycast patterns.
393        // A: downbeats in first half (0..4): step % 8 == 0 -> beats 0,1,2,3,4.
394        if beat < 4.0 && step % 8 == 0 {
395            let act = universe
396                .world
397                .add_component(engine::ecs::component::ActionComponent::new(
398                    engine::ecs::IntentValue::RequestRaycast {
399                        component_ids: vec![raycaster],
400                    },
401                ));
402            let _ = universe.attach(kf_a, act);
403        }
404
405        // B: offbeats in second half (4..8): step % 8 == 4 -> beats 0.5,1.5,...,7.5.
406        if beat >= 4.0 && step % 8 == 4 {
407            let act = universe
408                .world
409                .add_component(engine::ecs::component::ActionComponent::new(
410                    engine::ecs::IntentValue::RequestRaycast {
411                        component_ids: vec![raycaster],
412                    },
413                ));
414            let _ = universe.attach(kf_b, act);
415        }
416
417        let _ = universe.attach(anim_a, kf_a);
418        let _ = universe.attach(anim_b, kf_b);
419    }
420
421    universe.add(anim_a);
422    universe.add(anim_b);
423
424    // Process init-time registrations.
425    universe.systems.process_commands(
426        &mut universe.world,
427        &mut universe.visuals,
428        &mut universe.render_assets,
429        &mut universe.command_queue,
430    );
431
432    engine::Windowing::run_app(universe).expect("Windowing failed");
433}
More examples
Hide additional examples
examples/router.rs (line 34)
3fn spawn_runtime_text(
4    universe: &mut engine::Universe,
5    owner: engine::ecs::ComponentId,
6    label: &str,
7) {
8    use engine::ecs::component::{
9        ColorComponent, EdgeInsets, SizeDimension, StyleComponent, TextComponent,
10        TransformComponent,
11    };
12
13    let root = universe
14        .world
15        .add_component_boxed_named(label, Box::new(TransformComponent::new()));
16    let style = universe.world.add_component_boxed_named(
17        format!("{label}_style"),
18        Box::new({
19            let mut style = StyleComponent::new();
20            style.margin = EdgeInsets::all(0.5);
21            style.padding = EdgeInsets::axes(0.75, 0.5);
22            style.width = SizeDimension::Auto;
23            style.background_color = Some([0.93, 0.88, 0.98, 1.0]);
24            style
25        }),
26    );
27    let text = universe
28        .world
29        .add_component_boxed_named(format!("{label}_text"), Box::new(TextComponent::new(label)));
30    let color = universe
31        .world
32        .add_component(ColorComponent::rgba(0.38, 0.14, 0.62, 1.0));
33
34    let _ = universe.world.add_child(root, style);
35    let _ = universe.world.add_child(root, text);
36    let _ = universe.world.add_child(text, color);
37
38    universe.attach(owner, root).expect("attach routed child");
39}
examples/gravity-fields.rs (line 63)
27fn set_renderable_color_rgba(
28    world: &mut engine::ecs::World,
29    emit: &mut dyn engine::ecs::SignalEmitter,
30    renderable_cid: engine::ecs::ComponentId,
31    rgba: [f32; 4],
32) {
33    // Prefer existing ColorComponent.
34    let existing = world
35        .children_of(renderable_cid)
36        .iter()
37        .copied()
38        .find(|&ch| {
39            world
40                .get_component_by_id_as::<engine::ecs::component::ColorComponent>(ch)
41                .is_some()
42        });
43
44    if let Some(color_cid) = existing {
45        if let Some(c) =
46            world.get_component_by_id_as_mut::<engine::ecs::component::ColorComponent>(color_cid)
47        {
48            c.rgba = rgba;
49            emit.push_intent_now(
50                color_cid,
51                engine::ecs::IntentValue::RegisterColor {
52                    component_ids: vec![color_cid],
53                },
54            );
55        }
56        return;
57    }
58
59    // Fallback: add a ColorComponent child if missing.
60    let color_cid = world.register(engine::ecs::component::ColorComponent::rgba(
61        rgba[0], rgba[1], rgba[2], rgba[3],
62    ));
63    let _ = world.add_child(renderable_cid, color_cid);
64    emit.push_intent_now(
65        color_cid,
66        engine::ecs::IntentValue::RegisterColor {
67            component_ids: vec![color_cid],
68        },
69    );
70}
71
72fn kinetic_response_child_of_collider(
73    world: &engine::ecs::World,
74    collider_cid: engine::ecs::ComponentId,
75) -> Option<engine::ecs::ComponentId> {
76    for &ch in world.children_of(collider_cid).iter() {
77        if world
78            .get_component_by_id_as::<engine::ecs::component::KineticResponseComponent>(ch)
79            .is_some()
80        {
81            return Some(ch);
82        }
83    }
84    None
85}
86
87fn on_collision_turn_white(
88    world: &mut engine::ecs::World,
89    emit: &mut dyn engine::ecs::SignalEmitter,
90    signal: &engine::ecs::Signal,
91) {
92    let Some(engine::ecs::EventSignal::CollisionStarted { a, b, .. }) = signal.event.as_ref()
93    else {
94        return;
95    };
96
97    let self_collider = signal.scope;
98    let other = if self_collider == *a {
99        *b
100    } else if self_collider == *b {
101        *a
102    } else {
103        return;
104    };
105
106    // Only react to cube-vs-cube touches (ignore static walls/floor and non-renderable colliders like camera).
107    let Some(other_cn) =
108        world.get_component_by_id_as::<engine::ecs::component::CollisionComponent>(other)
109    else {
110        return;
111    };
112    if other_cn.mode == engine::ecs::component::CollisionMode::Static {
113        return;
114    }
115    if cube_renderable_sibling_of_collider(world, other).is_none() {
116        return;
117    }
118
119    let Some(self_renderable) = cube_renderable_sibling_of_collider(world, self_collider) else {
120        return;
121    };
122    set_renderable_color_rgba(world, emit, self_renderable, [1.0, 1.0, 1.0, 1.0]);
123}
124
125fn on_collision_freeze_gravity(
126    world: &mut engine::ecs::World,
127    _emit: &mut dyn engine::ecs::SignalEmitter,
128    signal: &engine::ecs::Signal,
129) {
130    let Some(engine::ecs::EventSignal::CollisionStarted { a, b, .. }) = signal.event.as_ref()
131    else {
132        return;
133    };
134
135    let self_collider = signal.scope;
136    let other = if self_collider == *a {
137        *b
138    } else if self_collider == *b {
139        *a
140    } else {
141        return;
142    };
143
144    // Only react to cube-vs-cube touches.
145    let Some(other_cn) =
146        world.get_component_by_id_as::<engine::ecs::component::CollisionComponent>(other)
147    else {
148        return;
149    };
150    if other_cn.mode == engine::ecs::component::CollisionMode::Static {
151        return;
152    }
153    if cube_renderable_sibling_of_collider(world, other).is_none() {
154        return;
155    }
156
157    let Some(response_cid) = kinetic_response_child_of_collider(world, self_collider) else {
158        return;
159    };
160    if let Some(r) = world
161        .get_component_by_id_as_mut::<engine::ecs::component::KineticResponseComponent>(
162            response_cid,
163        )
164    {
165        // Gravity is a cached per-responder coefficient; set it to 0 once touched.
166        r.gravity_coefficient = 0.0;
167    }
168}
169
170fn main() {
171    mittens_engine::example_support::ensure_model_assets();
172    utils::logger::init();
173
174    let world = engine::ecs::World::default();
175    let mut universe = engine::Universe::new(world);
176
177    let bg_color = universe
178        .world
179        .add_component(engine::ecs::component::BackgroundColorComponent::new());
180    let bg_color_c = universe
181        .world
182        .add_component(engine::ecs::component::ColorComponent::rgba(
183            0.06, 0.04, 0.07, 1.0,
184        ));
185    let _ = universe.world.add_child(bg_color, bg_color_c);
186    universe.add(bg_color);
187
188    // overhead directional light
189    let directional_light = universe.world.add_component(
190        engine::ecs::component::DirectionalLightComponent::new()
191            .with_color(0.16, 0.14, 0.12)
192            .with_intensity(0.7),
193    );
194    let directional_light_t = universe.world.add_component(
195        engine::ecs::component::TransformComponent::new().with_position(0.0, 1.0, 0.0),
196    );
197    let _ = universe.attach(directional_light_t, directional_light);
198    universe.add(directional_light_t);
199
200    // Simple input-driven camera.
201    let input = universe
202        .world
203        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
204
205    let cam_t = universe.world.add_component(
206        engine::ecs::component::TransformComponent::new().with_position(0.0, 4.0, 10.0),
207    );
208    let cam = universe.world.add_component(
209        engine::ecs::component::Camera3DComponent::new()
210            // The default (150) clips background dressing (city + clouds).
211            .with_far(600.0)
212            .with_fov(70.0),
213    );
214
215    // Make the camera affect the collision system (same pattern as collision-perimeter).
216    let cam_collision = universe
217        .world
218        .add_component(engine::ecs::component::CollisionComponent::RIGGED());
219    let cam_response = universe
220        .world
221        .add_component(engine::ecs::component::KineticResponseComponent::slide());
222    let cam_shape =
223        universe
224            .world
225            .add_component(engine::ecs::component::CollisionShapeComponent::new(
226                engine::ecs::component::CollisionShape::sphere_radius(0.25),
227            ));
228
229    let input_mode = universe.world.add_component(
230        engine::ecs::component::InputTransformModeComponent::forward_z()
231            .with_roll_axis_y()
232            .with_fps_rotation(),
233    );
234
235    let _ = universe.attach(input, input_mode);
236    let _ = universe.attach(input, cam_t);
237    let _ = universe.attach(cam_t, cam);
238    let _ = universe.attach(cam_t, cam_collision);
239    let _ = universe.attach(cam_collision, cam_response);
240    let _ = universe.attach(cam_collision, cam_shape);
241
242    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
243    example_util::spawn_desktop_camera_controls_hint(&mut universe, cam_t);
244    universe.add(input);
245
246    let arena_half = 30.0;
247
248    // --- Background world (occluded + lit) ---
249    // Buildings are scene dressing and should not occlude foreground cubes.
250    let bg_root = universe.world.add_component(
251        engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
252    );
253    universe.add(bg_root);
254
255    // Background ground plane (rendered in background pass) with top surface at y=0.
256    // This should not occlude foreground due to depth clear between passes.
257    let bg_ground_thickness = 0.20;
258    let bg_ground_root_t = universe
259        .world
260        .add_component(engine::ecs::component::TransformComponent::new());
261    let bg_ground_geom_t = universe.world.add_component(
262        engine::ecs::component::TransformComponent::new()
263            .with_position(0.0, -bg_ground_thickness, 0.0)
264            .with_scale(
265                arena_half * 10.0,
266                bg_ground_thickness * 2.0,
267                arena_half * 10.0,
268            ),
269    );
270    let bg_ground_r = universe
271        .world
272        .add_component(engine::ecs::component::RenderableComponent::cube());
273    let bg_ground_c = universe
274        .world
275        .add_component(engine::ecs::component::ColorComponent::rgba(
276            0.03, 0.03, 0.035, 1.0,
277        ));
278    let _ = universe.attach(bg_root, bg_ground_root_t);
279    let _ = universe.attach(bg_ground_root_t, bg_ground_geom_t);
280    let _ = universe.attach(bg_ground_geom_t, bg_ground_r);
281    let _ = universe.attach(bg_ground_r, bg_ground_c);
282
283    // Background clouds (occluded + lit) using the shared example helper.
284    // NOTE: `spawn_cloud_ring` places a ring centered at the parent transform's origin.
285    // If we attach directly to `bg_root`, the ring can end up mostly out of view.
286    // So we offset a cloud root forward along -Z and keep the radius within the camera far clip.
287    let bg_cloud_root = universe.world.add_component(
288        engine::ecs::component::TransformComponent::new().with_position(
289            0.0,
290            0.0,
291            -arena_half * 2.8,
292        ),
293    );
294    let _ = universe.attach(bg_root, bg_cloud_root);
295
296    let mut cloud_params = example_util::CloudRingParams::default();
297    cloud_params.cloud_count = 9;
298    cloud_params.radius = arena_half * 1.9;
299    cloud_params.center_y = 18.0;
300    cloud_params.puffs_per_cloud = 26;
301    cloud_params.angle_jitter = 0.35;
302    cloud_params.high_y_probability = 0.35;
303    cloud_params.high_y_multiplier = 1.35;
304    cloud_params.seed = 0xC10_71A5u32;
305    example_util::spawn_cloud_ring(&mut universe, bg_cloud_root, cloud_params);
306
307    // Foreground city: NOT background-layer.
308    let fg_city_root = universe
309        .world
310        .add_component(engine::ecs::component::TransformComponent::new());
311    universe.add(fg_city_root);
312
313    // Background city: nested under the BackgroundComponent.
314    let bg_city_root = universe
315        .world
316        .add_component(engine::ecs::component::TransformComponent::new());
317    let _ = universe.attach(bg_root, bg_city_root);
318
319    fn hash_u32(mut x: u32) -> u32 {
320        // Small, deterministic integer hash (no external RNG dependency).
321        x ^= x >> 16;
322        x = x.wrapping_mul(0x7FEB_352D);
323        x ^= x >> 15;
324        x = x.wrapping_mul(0x846C_A68B);
325        x ^= x >> 16;
326        x
327    }
328
329    fn rand01(seed: u32) -> f32 {
330        let x = hash_u32(seed);
331        (x as f32) / (u32::MAX as f32)
332    }
333
334    fn spawn_floor(
335        universe: &mut engine::Universe,
336        building_root: engine::ecs::ComponentId,
337        floor_center_y: f32,
338        footprint_x: f32,
339        footprint_z: f32,
340        floor_h: f32,
341        floor_rgb: [f32; 4],
342        building_scale: f32,
343        window_mask: &[bool],
344        window_height_coeff: f32,
345        seed: u32,
346    ) {
347        // Floor geometry.
348        // IMPORTANT: don't put non-uniform scale on the node that windows attach to,
349        // otherwise child window meshes inherit the scale and look stretched.
350        let floor_root_t = universe.world.add_component(
351            engine::ecs::component::TransformComponent::new().with_position(
352                0.0,
353                floor_center_y,
354                0.0,
355            ),
356        );
357        let floor_geom_t = universe.world.add_component(
358            engine::ecs::component::TransformComponent::new().with_scale(
359                footprint_x,
360                floor_h,
361                footprint_z,
362            ),
363        );
364        let floor_r = universe
365            .world
366            .add_component(engine::ecs::component::RenderableComponent::cube());
367        let floor_c = universe
368            .world
369            .add_component(engine::ecs::component::ColorComponent::rgba(
370                floor_rgb[0],
371                floor_rgb[1],
372                floor_rgb[2],
373                floor_rgb[3],
374            ));
375
376        let _ = universe.attach(building_root, floor_root_t);
377        let _ = universe.attach(floor_root_t, floor_geom_t);
378        let _ = universe.attach(floor_geom_t, floor_r);
379        let _ = universe.attach(floor_r, floor_c);
380
381        // Windows: a single clean row per floor.
382        // Each floor receives a Vec<bool> (window_mask) that indicates which window slots are present.
383        let slots = window_mask.len().max(1);
384        let building_scale = building_scale.max(0.01);
385        let window_height_coeff = window_height_coeff.clamp(0.05, 2.0);
386
387        // Keep window size independent from floor height; floor height is already controlled by height_scale.
388        let nominal_window = 0.32 * building_scale;
389        let window_d = 0.06 * building_scale;
390
391        // Compute per-side spacing and clamp window size to fit.
392        let margin_x = (0.65 * nominal_window).min(0.18 * footprint_x);
393        let margin_z = (0.65 * nominal_window).min(0.18 * footprint_z);
394        let available_x = (footprint_x - 2.0 * margin_x).max(nominal_window);
395        let available_z = (footprint_z - 2.0 * margin_z).max(nominal_window);
396        let step_x = available_x / (slots as f32);
397        let step_z = available_z / (slots as f32);
398
399        let window_wx = nominal_window.min(step_x * 0.85);
400        let window_wz = nominal_window.min(step_z * 0.85);
401        let window_h = (nominal_window * window_height_coeff).min(floor_h * 0.80);
402
403        let warm = 0.70 + 0.25 * rand01(seed ^ 0x1F12_6A77);
404        let window_rgb = [1.0, 0.78 * warm, 0.48 * warm, 1.0];
405
406        let mut spawn_window = |local_pos: (f32, f32, f32), local_scale: (f32, f32, f32)| {
407            let window_t = universe.world.add_component(
408                engine::ecs::component::TransformComponent::new()
409                    .with_position(local_pos.0, local_pos.1, local_pos.2)
410                    .with_scale(local_scale.0, local_scale.1, local_scale.2),
411            );
412            let window_r = universe
413                .world
414                .add_component(engine::ecs::component::RenderableComponent::cube());
415            let window_c =
416                universe
417                    .world
418                    .add_component(engine::ecs::component::ColorComponent::rgba(
419                        window_rgb[0],
420                        window_rgb[1],
421                        window_rgb[2],
422                        window_rgb[3],
423                    ));
424            let window_e = universe
425                .world
426                .add_component(engine::ecs::component::EmissiveComponent::on());
427
428            let _ = universe.attach(floor_root_t, window_t);
429            let _ = universe.attach(window_t, window_r);
430            let _ = universe.attach(window_r, window_c);
431            let _ = universe.attach(window_r, window_e);
432        };
433
434        // Place the row at the center of the floor segment.
435        let wy = 0.0;
436
437        for i in 0..slots {
438            if !window_mask[i] {
439                continue;
440            }
441            let tx = -0.5 * footprint_x + margin_x + (i as f32 + 0.5) * step_x;
442            let tz = -0.5 * footprint_z + margin_z + (i as f32 + 0.5) * step_z;
443
444            // +Z/-Z faces: windows laid out along X.
445            for z_sign in [1.0_f32, -1.0_f32] {
446                spawn_window(
447                    (tx, wy, z_sign * (footprint_z * 0.5 + window_d * 0.6)),
448                    (window_wx, window_h, window_d),
449                );
450            }
451
452            // +X/-X faces: windows laid out along Z.
453            for x_sign in [1.0_f32, -1.0_f32] {
454                spawn_window(
455                    (x_sign * (footprint_x * 0.5 + window_d * 0.6), wy, tz),
456                    (window_d, window_h, window_wz),
457                );
458            }
459        }
460    }
461
462    fn spawn_building(
463        universe: &mut engine::Universe,
464        parent: engine::ecs::ComponentId,
465        x: f32,
466        y_offset: f32,
467        z: f32,
468        floors: u32,
469        seed: u32,
470        building_scale: f32,
471        height_scale: f32,
472        total_windows_per_floor_per_building_side: u32,
473        window_height_coeff: f32,
474    ) {
475        let building_scale = building_scale.max(0.01);
476        let height_scale = height_scale.max(0.01);
477
478        let floors = floors.max(1);
479        let windows_per_side = total_windows_per_floor_per_building_side.max(1) as usize;
480
481        let footprint_x = 3.2 * building_scale;
482        let footprint_z = 3.2 * building_scale;
483        let floor_h = 1.15 * building_scale * height_scale;
484
485        let base_t = universe.world.add_component(
486            engine::ecs::component::TransformComponent::new().with_position(x, y_offset, z),
487        );
488        let _ = universe.attach(parent, base_t);
489
490        // Slight per-building tint.
491        let tint = 0.03 * (rand01(seed ^ 0xA511_E9B3) - 0.5);
492        let base_rgb = [0.08 + tint, 0.085 + tint, 0.095 + tint, 1.0];
493
494        // Probability that a given window slot exists.
495        // (False means no window cube at that slot.)
496        let window_fill_probability = 0.80_f32;
497
498        for floor_i in 0..floors {
499            let floor_seed = seed ^ floor_i.wrapping_mul(0x9E37_79B9) ^ 0xB17D_1E55;
500
501            let mut window_mask = Vec::with_capacity(windows_per_side);
502            for i in 0..windows_per_side {
503                let s = floor_seed ^ (i as u32).wrapping_mul(0x85EB_CA6B) ^ 0x243F_6A88;
504                window_mask.push(rand01(s) < window_fill_probability);
505            }
506
507            let floor_center_y = floor_h * (floor_i as f32) + floor_h * 0.5;
508            spawn_floor(
509                universe,
510                base_t,
511                floor_center_y,
512                footprint_x,
513                footprint_z,
514                floor_h,
515                base_rgb,
516                building_scale,
517                &window_mask,
518                window_height_coeff,
519                floor_seed,
520            );
521        }
522    }
523
524    fn spawn_city(
525        universe: &mut engine::Universe,
526        parent: engine::ecs::ComponentId,
527        columns: u32,
528        spacing: f32,
529        position_scale: f32,
530        building_scale: f32,
531        height_scale: f32,
532        seed_base: u32,
533        y_offset: f32,
534        total_windows_per_floor_per_building_side: u32,
535        window_height_coeff: f32,
536    ) {
537        let columns = columns.max(1);
538        let half = (columns as i32) / 2;
539
540        for gz in -half..=half {
541            for gx in -half..=half {
542                // Hole in the middle.
543                if gx == 0 && gz == 0 {
544                    continue;
545                }
546
547                let seed = seed_base
548                    ^ ((gx + half) as u32).wrapping_mul(0x9E37_79B9)
549                    ^ ((gz + half) as u32).wrapping_mul(0x85EB_CA6B)
550                    ^ 0xB17D_1E55;
551
552                let floors = 3 + (hash_u32(seed) % 10); // 3..=12 floors
553
554                // Centered around the origin.
555                // - position_scale controls how far out the city sits (positions only)
556                // - building_scale controls building footprint/window sizes
557                let x = (gx as f32) * spacing * position_scale;
558                let z = (gz as f32) * spacing * position_scale;
559
560                spawn_building(
561                    universe,
562                    parent,
563                    x,
564                    y_offset,
565                    z,
566                    floors,
567                    seed,
568                    building_scale,
569                    height_scale,
570                    total_windows_per_floor_per_building_side,
571                    window_height_coeff,
572                );
573            }
574        }
575    }
576
577    // // Foreground city: 5x5 (hole in center), scaled out to sit around the floor.
578    // spawn_city(
579    //     &mut universe,
580    //     fg_city_root,
581    //     5,
582    //     7.0,
583    //     2.0,
584    //     1.0,
585    //     1.0,
586    //     0xC170_0001u32,
587    //     3,
588    //     2,
589    //     -0.0
590    // );
591
592    // Background city: larger grid, farther out, taller buildings.
593    spawn_city(
594        &mut universe,
595        bg_city_root,
596        11,
597        3.0,
598        3.0,
599        0.45,
600        1.0,
601        0xC170_0002u32,
602        -2.0,
603        6,
604        0.75,
605    );
606
607    fn spawn_street_light(universe: &mut engine::Universe) -> engine::ecs::ComponentId {
608        // Dimensions (world units).
609        let shaft_height = 6.0;
610        let shaft_thickness = 0.22;
611        let arm_length = shaft_height / 3.0;
612        let arm_thickness = 0.18;
613
614        // Colors.
615        let pole_color = [0.35, 0.35, 0.38, 1.0];
616        let housing_color = [0.18, 0.18, 0.20, 1.0];
617        let diffuser_color = [1.0, 1.0, 1.0, 1.0];
618
619        // Model root at origin.
620        // The caller is expected to position + rotate this model using an external placement transform.
621        // Street light is authored facing +X (arm extends toward +X).
622        let root_t = universe
623            .world
624            .add_component(engine::ecs::component::TransformComponent::new());
625
626        // Vertical shaft (split root/geom so scaling doesn't affect the arm).
627        let shaft_root_t = universe
628            .world
629            .add_component(engine::ecs::component::TransformComponent::new());
630        let shaft_geom_t = universe.world.add_component(
631            engine::ecs::component::TransformComponent::new()
632                .with_position(0.0, shaft_height * 0.5, 0.0)
633                .with_scale(shaft_thickness, shaft_height, shaft_thickness),
634        );
635        let shaft_r = universe
636            .world
637            .add_component(engine::ecs::component::RenderableComponent::cube());
638        let shaft_c = universe
639            .world
640            .add_component(engine::ecs::component::ColorComponent::rgba(
641                pole_color[0],
642                pole_color[1],
643                pole_color[2],
644                pole_color[3],
645            ));
646
647        let _ = universe.attach(root_t, shaft_root_t);
648        let _ = universe.attach(shaft_root_t, shaft_geom_t);
649        let _ = universe.attach(shaft_geom_t, shaft_r);
650        let _ = universe.attach(shaft_r, shaft_c);
651
652        // Horizontal arm at the top; starts at the pole and extends toward +X.
653        let arm_root_t = universe.world.add_component(
654            engine::ecs::component::TransformComponent::new().with_position(0.0, shaft_height, 0.0),
655        );
656        let arm_geom_t = universe.world.add_component(
657            engine::ecs::component::TransformComponent::new()
658                .with_position(arm_length * 0.5, -arm_thickness * 0.5, 0.0)
659                .with_scale(arm_length, arm_thickness, arm_thickness),
660        );
661        let arm_r = universe
662            .world
663            .add_component(engine::ecs::component::RenderableComponent::cube());
664        let arm_c = universe
665            .world
666            .add_component(engine::ecs::component::ColorComponent::rgba(
667                pole_color[0],
668                pole_color[1],
669                pole_color[2],
670                pole_color[3],
671            ));
672
673        let _ = universe.attach(shaft_root_t, arm_root_t);
674        let _ = universe.attach(arm_root_t, arm_geom_t);
675        let _ = universe.attach(arm_geom_t, arm_r);
676        let _ = universe.attach(arm_r, arm_c);
677
678        // Light housing at the end of the arm (+X end).
679        let housing_root_t = universe.world.add_component(
680            engine::ecs::component::TransformComponent::new().with_position(arm_length, 0.0, 0.0),
681        );
682        let housing_geom_t = universe.world.add_component(
683            engine::ecs::component::TransformComponent::new()
684                .with_position(0.5, 0.0, 0.0)
685                // 2x1x1 box in (z, x, y) -> (x, y, z) = (1, 1, 2)
686                .with_scale(2.0, 1.0, 1.0),
687        );
688        let housing_r = universe
689            .world
690            .add_component(engine::ecs::component::RenderableComponent::cube());
691        let housing_c = universe
692            .world
693            .add_component(engine::ecs::component::ColorComponent::rgba(
694                housing_color[0],
695                housing_color[1],
696                housing_color[2],
697                housing_color[3],
698            ));
699
700        let _ = universe.attach(arm_root_t, housing_root_t);
701        let _ = universe.attach(housing_root_t, housing_geom_t);
702        let _ = universe.attach(housing_geom_t, housing_r);
703        let _ = universe.attach(housing_r, housing_c);
704
705        // White diffuser cube (slightly smaller, slightly lower).
706        let diffuser_t = universe.world.add_component(
707            engine::ecs::component::TransformComponent::new()
708                .with_position(0.5, -0.35, 0.0)
709                .with_scale(1.85, 0.55, 0.70),
710        );
711        let diffuser_r = universe
712            .world
713            .add_component(engine::ecs::component::RenderableComponent::cube());
714        let diffuser_c =
715            universe
716                .world
717                .add_component(engine::ecs::component::ColorComponent::rgba(
718                    diffuser_color[0],
719                    diffuser_color[1],
720                    diffuser_color[2],
721                    diffuser_color[3],
722                ));
723
724        let diffuser_emissive = universe
725            .world
726            .add_component(engine::ecs::component::EmissiveComponent::on());
727        let _ = universe.attach(diffuser_r, diffuser_emissive);
728
729        let _ = universe.attach(housing_root_t, diffuser_t);
730        let _ = universe.attach(diffuser_t, diffuser_r);
731        let _ = universe.attach(diffuser_r, diffuser_c);
732
733        // Yellow (slightly red) point light.
734        let light = universe.world.add_component(
735            engine::ecs::component::PointLightComponent::new()
736                .with_color(1.0, 0.82, 0.55)
737                .with_intensity(5.0)
738                .with_distance(40.0),
739        );
740        let _ = universe.attach(diffuser_t, light);
741
742        root_t
743    }
744
745    // Spawn 6 street lights around the arena.
746    // Arms point inward from each side.
747    let light_x = arena_half - 2.0;
748    for z in [-10.0, 0.0, 10.0] {
749        // Left side: street light model faces +X by default, so it points inward.
750        let left_place = universe.world.add_component(
751            engine::ecs::component::TransformComponent::new().with_position(-light_x, 0.0, z),
752        );
753        let left_model = spawn_street_light(&mut universe);
754        let _ = universe.attach(left_place, left_model);
755        universe.add(left_place);
756
757        // Right side: rotate 180° around Y so the arm points toward -X (inward).
758        let right_place = universe.world.add_component(
759            engine::ecs::component::TransformComponent::new()
760                .with_position(light_x, 0.0, z)
761                .with_rotation_euler(0.0, std::f32::consts::PI, 0.0),
762        );
763        let right_model = spawn_street_light(&mut universe);
764        let _ = universe.attach(right_place, right_model);
765        universe.add(right_place);
766    }
767
768    // Big floor.
769    {
770        let floor_half = arena_half;
771        let thickness = 0.20;
772
773        // Split transforms so scaling the floor does not scale any potential children.
774        // Top surface at y=0.
775        let floor_root_t = universe
776            .world
777            .add_component(engine::ecs::component::TransformComponent::new());
778        let floor_geom_t = universe.world.add_component(
779            engine::ecs::component::TransformComponent::new()
780                .with_position(0.0, -thickness, 0.0)
781                .with_scale(floor_half * 2.0, thickness * 2.0, floor_half * 2.0),
782        );
783        let floor_r = universe
784            .world
785            .add_component(engine::ecs::component::RenderableComponent::cube());
786        let floor_color =
787            universe
788                .world
789                .add_component(engine::ecs::component::ColorComponent::rgba(
790                    0.08, 0.08, 0.09, 1.0,
791                ));
792
793        let floor_cn = universe
794            .world
795            .add_component(engine::ecs::component::CollisionComponent::STATIC());
796        let floor_shape =
797            universe
798                .world
799                .add_component(engine::ecs::component::CollisionShapeComponent::new(
800                    engine::ecs::component::CollisionShape::cube_half_extents([
801                        floor_half, thickness, floor_half,
802                    ]),
803                ));
804
805        let _ = universe.attach(floor_root_t, floor_geom_t);
806        let _ = universe.attach(floor_geom_t, floor_r);
807        let _ = universe.attach(floor_r, floor_color);
808        let _ = universe.attach(floor_geom_t, floor_cn);
809        let _ = universe.attach(floor_cn, floor_shape);
810        universe.add(floor_root_t);
811    }
812
813    // Square boundary walls around the floor edges.
814    // Note: STATIC colliders don't need KineticResponse; they still collide with kinematic/rigged.
815    fn spawn_boundary_wall(
816        universe: &mut engine::Universe,
817        x: f32,
818        y: f32,
819        z: f32,
820        half_extents: [f32; 3],
821        color: [f32; 4],
822    ) {
823        let t = universe.world.add_component(
824            engine::ecs::component::TransformComponent::new()
825                .with_position(x, y, z)
826                .with_scale(
827                    half_extents[0] * 2.0,
828                    half_extents[1] * 2.0,
829                    half_extents[2] * 2.0,
830                ),
831        );
832        let r = universe
833            .world
834            .add_component(engine::ecs::component::RenderableComponent::cube());
835        let c = universe
836            .world
837            .add_component(engine::ecs::component::ColorComponent::rgba(
838                color[0], color[1], color[2], color[3],
839            ));
840
841        let cn = universe
842            .world
843            .add_component(engine::ecs::component::CollisionComponent::STATIC());
844        let shape =
845            universe
846                .world
847                .add_component(engine::ecs::component::CollisionShapeComponent::new(
848                    engine::ecs::component::CollisionShape::cube_half_extents(half_extents),
849                ));
850
851        let _ = universe.attach(t, r);
852        let _ = universe.attach(r, c);
853        let _ = universe.attach(t, cn);
854        let _ = universe.attach(cn, shape);
855        universe.add(t);
856    }
857
858    {
859        let wall_half_height = 2.5;
860        let wall_half_thickness = 0.30;
861        let wall_center_y = wall_half_height; // bottom at y=0
862        let wall_half_len = arena_half;
863
864        // Subtle visible walls.
865        let wall_color = [0.10, 0.10, 0.12, 0.30];
866
867        // +Z / -Z
868        spawn_boundary_wall(
869            &mut universe,
870            0.0,
871            wall_center_y,
872            arena_half,
873            [wall_half_len, wall_half_height, wall_half_thickness],
874            wall_color,
875        );
876        spawn_boundary_wall(
877            &mut universe,
878            0.0,
879            wall_center_y,
880            -arena_half,
881            [wall_half_len, wall_half_height, wall_half_thickness],
882            wall_color,
883        );
884
885        // +X / -X
886        spawn_boundary_wall(
887            &mut universe,
888            arena_half,
889            wall_center_y,
890            0.0,
891            [wall_half_thickness, wall_half_height, wall_half_len],
892            wall_color,
893        );
894        spawn_boundary_wall(
895            &mut universe,
896            -arena_half,
897            wall_center_y,
898            0.0,
899            [wall_half_thickness, wall_half_height, wall_half_len],
900            wall_color,
901        );
902    }
903
904    fn spawn_falling_cube(
905        universe: &mut engine::Universe,
906        parent: engine::ecs::ComponentId,
907        x: f32,
908        y: f32,
909        z: f32,
910        s: f32,
911        r: f32,
912        g: f32,
913        b: f32,
914    ) {
915        let t = universe.world.add_component(
916            engine::ecs::component::TransformComponent::new()
917                .with_position(x, y, z)
918                .with_scale(s, s, s),
919        );
920
921        let renderable = universe
922            .world
923            .add_component(engine::ecs::component::RenderableComponent::cube());
924        let color = universe
925            .world
926            .add_component(engine::ecs::component::ColorComponent::rgba(r, g, b, 1.0));
927
928        let cn = universe
929            .world
930            .add_component(engine::ecs::component::CollisionComponent::KINEMATIC());
931
932        let response = universe.world.add_component({
933            let mut r = engine::ecs::component::KineticResponseComponent::push()
934                .with_push_strength(3.0)
935                .with_friction_y(18.0);
936            // Allow higher fall speeds so gravity coefficients are visible.
937            r.max_speed = 80.0;
938            r
939        });
940
941        let shape =
942            universe
943                .world
944                .add_component(engine::ecs::component::CollisionShapeComponent::new(
945                    engine::ecs::component::CollisionShape::cube_half_extents([
946                        0.5 * s,
947                        0.5 * s,
948                        0.5 * s,
949                    ]),
950                ));
951
952        let _ = universe.attach(t, renderable);
953        let _ = universe.attach(renderable, color);
954
955        let _ = universe.attach(t, cn);
956        let _ = universe.attach(cn, response);
957        let _ = universe.attach(cn, shape);
958
959        let _ = universe.attach(parent, t);
960    }
961
962    // Three gravity fields, each with 20 cubes.
963    let field_low = universe
964        .world
965        .add_component(engine::ecs::component::GravityComponent::new().with_coefficient(0.125));
966    let field_mid = universe
967        .world
968        .add_component(engine::ecs::component::GravityComponent::new().with_coefficient(0.5));
969    let field_high = universe
970        .world
971        .add_component(engine::ecs::component::GravityComponent::new().with_coefficient(1.0));
972
973    universe.add(field_low);
974    universe.add(field_mid);
975    universe.add(field_high);
976
977    fn spawn_field(
978        universe: &mut engine::Universe,
979        field: engine::ecs::ComponentId,
980        base_x: f32,
981        base_z: f32,
982        cube_color: [f32; 3],
983    ) {
984        let spacing = 0.7;
985        let start_x = base_x - 2.0 * spacing;
986        let start_z = base_z - 1.5 * spacing;
987
988        for i in 0..20 {
989            let ix = (i % 5) as f32;
990            let iz = (i / 5) as f32;
991
992            let x = start_x + ix * spacing;
993            let z = start_z + iz * spacing;
994            let y = 2.0 + iz * 0.6 + ix * 0.1;
995
996            spawn_falling_cube(
997                universe,
998                field,
999                x,
1000                y,
1001                z,
1002                0.5,
1003                cube_color[0],
1004                cube_color[1],
1005                cube_color[2],
1006            );
1007        }
1008
1009        // Visual marker for the field center.
1010        let marker_t = universe.world.add_component(
1011            engine::ecs::component::TransformComponent::new()
1012                .with_position(base_x, 0.1, base_z)
1013                .with_scale(0.3, 0.02, 0.3),
1014        );
1015        let marker_r = universe
1016            .world
1017            .add_component(engine::ecs::component::RenderableComponent::cube());
1018        let marker_c = universe
1019            .world
1020            .add_component(engine::ecs::component::ColorComponent::rgba(
1021                cube_color[0],
1022                cube_color[1],
1023                cube_color[2],
1024                1.0,
1025            ));
1026        let _ = universe.attach(marker_t, marker_r);
1027        let _ = universe.attach(marker_r, marker_c);
1028        universe.add(marker_t);
1029    }
1030
1031    spawn_field(&mut universe, field_low, -8.0, 0.0, [0.3, 0.6, 1.0]);
1032    spawn_field(&mut universe, field_mid, 0.0, 0.0, [0.4, 1.0, 0.4]);
1033    spawn_field(&mut universe, field_high, 8.0, 0.0, [1.0, 0.5, 0.2]);
1034
1035    // Group-scoped collision behaviors.
1036    // - Low + High fields: cubes turn white after colliding with another cube.
1037    // - Mid field: cubes lose gravity after colliding with another cube.
1038    universe.add_signal_handler(
1039        engine::ecs::SignalKind::CollisionStarted,
1040        field_low,
1041        on_collision_turn_white,
1042    );
1043    universe.add_signal_handler(
1044        engine::ecs::SignalKind::CollisionStarted,
1045        field_high,
1046        on_collision_turn_white,
1047    );
1048    universe.add_signal_handler(
1049        engine::ecs::SignalKind::CollisionStarted,
1050        field_mid,
1051        on_collision_freeze_gravity,
1052    );
1053
1054    universe.systems.process_commands(
1055        &mut universe.world,
1056        &mut universe.visuals,
1057        &mut universe.render_assets,
1058        &mut universe.command_queue,
1059    );
1060
1061    universe.enable_repl();
1062    engine::Windowing::run_app(universe).expect("Windowing failed");
1063}
examples/example_util/mod.rs (line 31)
15pub fn spawn_mms_demo_rig(
16    universe: &mut engine::Universe,
17    cam_pos: [f32; 3],
18) -> engine::ecs::ComponentId {
19    use engine::ecs::component::{
20        BackgroundColorComponent, Camera3DComponent, InputComponent, InputTransformModeComponent,
21        TransformComponent,
22    };
23
24    // Dark blue clear colour.
25    let bg_color = universe
26        .world
27        .add_component(BackgroundColorComponent::new());
28    let bg_color_c = universe
29        .world
30        .add_component(ColorComponent::rgba(0.02, 0.03, 0.10, 1.0));
31    let _ = universe.world.add_child(bg_color, bg_color_c);
32    universe.add(bg_color);
33
34    // Camera rig: Input → Transform → Camera3D.
35    let input = universe
36        .world
37        .add_component(InputComponent::new().with_speed(3.0));
38    let input_mode = universe
39        .world
40        .add_component(InputTransformModeComponent::forward_z().with_roll_axis_y());
41    let _ = universe.attach(input, input_mode);
42
43    let cam_transform = universe
44        .world
45        .add_component(TransformComponent::new().with_position(cam_pos[0], cam_pos[1], cam_pos[2]));
46    let _ = universe.attach(input, cam_transform);
47
48    let camera = universe.world.add_component(Camera3DComponent::new());
49    let _ = universe.attach(cam_transform, camera);
50
51    universe.add(input);
52
53    spawn_desktop_camera_controls_hint(universe, cam_transform);
54
55    cam_transform
56}
examples/diy-panel.rs (line 42)
5fn spawn_runtime_text(
6    universe: &mut engine::Universe,
7    owner: engine::ecs::ComponentId,
8    label: &str,
9) {
10    use engine::ecs::component::{
11        ColorComponent, EdgeInsets, SizeDimension, StyleComponent, TextComponent,
12        TransformComponent,
13    };
14
15    let root = universe.world.add_component_boxed_named(
16        label,
17        Box::new(TransformComponent::new().with_position(0.0, 0.0, 0.2)),
18    );
19    let style = universe.world.add_component_boxed_named(
20        format!("{label}_style"),
21        Box::new({
22            let mut style = StyleComponent::new();
23            style.margin = EdgeInsets::axes(0.25, 0.25);
24            style.padding = EdgeInsets::axes(0.5, 0.5);
25            style.height = SizeDimension::GlyphUnits(2.5);
26            style.width = SizeDimension::Auto;
27            style.background_color = Some([1.0, 0.80, 0.80, 1.0]);
28            style
29        }),
30    );
31    let text_root = universe.world.add_component_boxed_named(
32        format!("{label}_text_root"),
33        Box::new(TransformComponent::new().with_position(0.0, 0.0, 0.2)),
34    );
35    let text = universe
36        .world
37        .add_component_boxed_named(format!("{label}_text"), Box::new(TextComponent::new(label)));
38    let color = universe
39        .world
40        .add_component(ColorComponent::rgba(0.40, 0.05, 0.05, 1.0));
41
42    let _ = universe.world.add_child(root, style);
43    let _ = universe.world.add_child(root, text_root);
44    let _ = universe.world.add_child(text_root, text);
45    let _ = universe.world.add_child(text, color);
46
47    universe.attach(owner, root).expect("attach routed child");
48}
examples/triage/panel-pierce.rs (line 62)
16fn spawn_row(
17    universe: &mut engine::Universe,
18    scrolling: engine::ecs::ComponentId,
19    index: usize,
20) -> engine::ecs::ComponentId {
21    use engine::ecs::component::{
22        ColorComponent, RenderableComponent, TextComponent, TransformComponent,
23    };
24
25    let label = format!("row_{index:02}");
26    let row = universe.world.add_component_boxed_named(
27        label.clone(),
28        Box::new(TransformComponent::new().with_position(0.0, -(index as f32) * 1.15, 0.05)),
29    );
30    let panel = universe.world.add_component_boxed_named(
31        format!("{label}_panel"),
32        Box::new(
33            TransformComponent::new()
34                .with_position(2.3, -0.45, 0.0)
35                .with_scale(4.6, 0.9, 1.0),
36        ),
37    );
38    let panel_renderable = universe.world.add_component_boxed_named(
39        format!("{label}_rend"),
40        Box::new(RenderableComponent::square()),
41    );
42    let panel_color = universe
43        .world
44        .add_component(ColorComponent::rgba(0.98, 0.94, 0.78, 1.0));
45
46    let text_anchor = universe.world.add_component_boxed_named(
47        format!("{label}_text_anchor"),
48        Box::new(
49            TransformComponent::new()
50                .with_position(0.35, -0.38, 0.02)
51                .with_scale(0.11, 0.11, 0.11),
52        ),
53    );
54    let text = universe.world.add_component_boxed_named(
55        format!("{label}_text"),
56        Box::new(TextComponent::new(label.clone())),
57    );
58    let text_color = universe
59        .world
60        .add_component(ColorComponent::rgba(0.20, 0.16, 0.08, 1.0));
61
62    let _ = universe.world.add_child(row, panel);
63    let _ = universe.world.add_child(panel, panel_renderable);
64    let _ = universe.world.add_child(panel_renderable, panel_color);
65    let _ = universe.world.add_child(row, text_anchor);
66    let _ = universe.world.add_child(text_anchor, text);
67    let _ = universe.world.add_child(text, text_color);
68
69    universe.attach(scrolling, row).expect("attach scroll row");
70    row
71}
Source

pub fn set_parent( &mut self, child: ComponentId, new_parent: Option<ComponentId>, ) -> Result<(), &'static str>

Change a component’s parent.

Equivalent to detach_from_parent(child) when new_parent is None.

Source

pub fn detach_from_parent(&mut self, child: ComponentId)

Detach child from its current parent.

This does not delete anything.

Source

pub fn remove_component_leaf( &mut self, c: ComponentId, ) -> Result<(), &'static str>

Remove a component from the world.

This is a leaf-only removal: it fails if the component still has children. Use remove_component_subtree when you want to delete a whole branch.

Source

pub fn remove_component_subtree( &mut self, root: ComponentId, ) -> Result<(), &'static str>

Remove a component and all its descendants.

Source

pub fn init_component_tree( &mut self, root: ComponentId, emit: &mut dyn SignalEmitter, )

Initialize a component tree starting from the given root component.

This recursively initializes the root component and all its descendants by calling Component::init on each component in the tree.

Trait Implementations§

Source§

impl Default for World

Source§

fn default() -> World

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

Auto Trait Implementations§

§

impl !Freeze for World

§

impl !RefUnwindSafe for World

§

impl !Send for World

§

impl !Sync for World

§

impl !UnwindSafe for World

§

impl Unpin for World

§

impl UnsafeUnpin for World

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> 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<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

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, 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