pub struct World { /* private fields */ }Expand description
World: owns all global components.
Implementations§
Source§impl World
impl World
Sourcepub fn component_id_by_guid(&self, guid: Uuid) -> Option<ComponentId>
pub fn component_id_by_guid(&self, guid: Uuid) -> Option<ComponentId>
Fast GUID -> ComponentId lookup.
Sourcepub fn set_component_guid(
&mut self,
id: ComponentId,
new_guid: Uuid,
) -> Result<(), String>
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).
Sourcepub fn add_component<T: Component>(&mut self, c: T) -> ComponentId
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?
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
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}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}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}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}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}- examples/pointer-events.rs
- examples/diy-panel.rs
- examples/util/mod.rs
- examples/vr-input.rs
- examples/vtuber-mirror-example.rs
- examples/vtuber-editor-example.rs
- examples/triage/panel-pierce.rs
- examples/scrolling.rs
- examples/audio-clip-instance-demo.rs
- examples/audio-music-demo.rs
- examples/button-press.rs
- examples/text-animation.rs
- examples/vtuber-example.rs
- examples/openxr.rs
- examples/text-example.rs
- examples/gestures-and-gizmos.rs
- examples/background-example.rs
- examples/mesh-factory-example.rs
- examples/bisket-vr-demo.rs
- examples/background-occlusion-example.rs
- examples/raycast-topology-animation.rs
- examples/font-example.rs
- examples/vtuber-joints-example.rs
- examples/animation-example.rs
- examples/folder-text.rs
- examples/collision-perimeter.rs
- examples/audio-graph-example.rs
- examples/gravity-fields.rs
Sourcepub fn register<T: Component>(&mut self, c: T) -> ComponentId
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?
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
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}Sourcepub fn is_initialized(&self, c: ComponentId) -> bool
pub fn is_initialized(&self, c: ComponentId) -> bool
Whether this component has had Component::init invoked.
Sourcepub fn add_component_boxed(&mut self, c: Box<dyn Component>) -> ComponentId
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.
Sourcepub fn add_component_boxed_named(
&mut self,
name: impl Into<String>,
c: Box<dyn Component>,
) -> ComponentId
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?
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
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}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}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}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}Sourcepub fn add_component_boxed_with_guid_named(
&mut self,
guid: Uuid,
name: impl Into<String>,
c: Box<dyn Component>,
) -> ComponentId
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.
Sourcepub fn spawn_component_boxed(&mut self, c: Box<dyn Component>) -> ComponentId
pub fn spawn_component_boxed(&mut self, c: Box<dyn Component>) -> ComponentId
Temporary alias during migration.
Sourcepub fn get_component_record(&self, id: ComponentId) -> Option<&ComponentNode>
pub fn get_component_record(&self, id: ComponentId) -> Option<&ComponentNode>
Examples found in repository?
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}Sourcepub fn get_component_node(&self, id: ComponentId) -> Option<&ComponentNode>
pub fn get_component_node(&self, id: ComponentId) -> Option<&ComponentNode>
Alias for get_component_record.
pub fn get_component_record_mut( &mut self, id: ComponentId, ) -> Option<&mut ComponentNode>
Sourcepub fn component_name(&self, id: ComponentId) -> Option<&str>
pub fn component_name(&self, id: ComponentId) -> Option<&str>
Returns the engine type identifier for this component (e.g. "transform").
Examples found in repository?
More examples
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}Sourcepub fn component_label(&self, id: ComponentId) -> Option<&str>
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?
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
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}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}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}Sourcepub fn parent_of(&self, c: ComponentId) -> Option<ComponentId>
pub fn parent_of(&self, c: ComponentId) -> Option<ComponentId>
Examples found in repository?
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
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}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}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}Sourcepub fn all_components(&self) -> impl Iterator<Item = ComponentId> + '_
pub fn all_components(&self) -> impl Iterator<Item = ComponentId> + '_
Iterator over all component IDs in the world.
Examples found in repository?
More examples
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}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}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 ©_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}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}pub fn component_count(&self) -> usize
Sourcepub fn children_of(&self, c: ComponentId) -> &[ComponentId]
pub fn children_of(&self, c: ComponentId) -> &[ComponentId]
Examples found in repository?
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
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}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}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}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}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}Sourcepub fn get_component_by_id_as<T: 'static>(&self, c: ComponentId) -> Option<&T>
pub fn get_component_by_id_as<T: 'static>(&self, c: ComponentId) -> Option<&T>
Examples found in repository?
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
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}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}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}Sourcepub fn get_component_by_id_as_mut<T: 'static>(
&mut self,
c: ComponentId,
) -> Option<&mut T>
pub fn get_component_by_id_as_mut<T: 'static>( &mut self, c: ComponentId, ) -> Option<&mut T>
Examples found in repository?
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
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}Sourcepub fn find_component(
&self,
root: ComponentId,
selector: &str,
) -> Option<ComponentId>
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.
Sourcepub fn find_all_components(
&self,
root: ComponentId,
selector: &str,
) -> Vec<ComponentId>
pub fn find_all_components( &self, root: ComponentId, selector: &str, ) -> Vec<ComponentId>
Find all components under root matching selector (DFS pre-order).
pub fn component_matches_selector( &self, component: ComponentId, selector: &str, ) -> bool
pub fn world_roots(&self) -> Vec<ComponentId>
pub fn get_parent_as<T: 'static>( &self, c: ComponentId, ) -> Option<(ComponentId, &T)>
pub fn get_parent_as_mut<T: 'static>( &mut self, c: ComponentId, ) -> Option<(ComponentId, &mut T)>
Sourcepub fn add_child(
&mut self,
parent: ComponentId,
child: ComponentId,
) -> Result<(), &'static str>
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.
childis detached from its current parent first.- Cycles are rejected.
Examples found in repository?
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
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}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}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}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}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
- examples/audio-clip-instance-demo.rs
- examples/audio-music-demo.rs
- examples/opacity-example.rs
- examples/pointer-events.rs
- examples/button-press.rs
- examples/vtuber-example.rs
- examples/openxr.rs
- examples/transparent-cutout-example.rs
- examples/gestures-and-gizmos.rs
- examples/background-example.rs
- examples/bisket-vr-demo.rs
- examples/background-occlusion-example.rs
- examples/animation-for-topology.rs
- examples/vr-input.rs
- examples/font-example.rs
- examples/vtuber-joints-example.rs
- examples/animation-example.rs
- examples/folder-text.rs
- examples/collision-perimeter.rs
- examples/audio-graph-example.rs
Sourcepub fn set_parent(
&mut self,
child: ComponentId,
new_parent: Option<ComponentId>,
) -> Result<(), &'static str>
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.
Sourcepub fn detach_from_parent(&mut self, child: ComponentId)
pub fn detach_from_parent(&mut self, child: ComponentId)
Detach child from its current parent.
This does not delete anything.
Sourcepub fn remove_component_leaf(
&mut self,
c: ComponentId,
) -> Result<(), &'static str>
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.
Sourcepub fn remove_component_subtree(
&mut self,
root: ComponentId,
) -> Result<(), &'static str>
pub fn remove_component_subtree( &mut self, root: ComponentId, ) -> Result<(), &'static str>
Remove a component and all its descendants.
Sourcepub fn init_component_tree(
&mut self,
root: ComponentId,
emit: &mut dyn SignalEmitter,
)
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§
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> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.