Skip to main content

PointerComponent

Struct PointerComponent 

Source
pub struct PointerComponent {
    pub enabled: bool,
    /* private fields */
}
Expand description

User-facing pointer component.

Attach this under the pose-driving part of the topology (for example a desktop camera rig transform, an XR camera, or a controller-driven transform). At init time the engine spawns and owns a child RayCastComponent, so authoring only needs to describe the pointer itself.

Fields§

§enabled: bool

Implementations§

Source§

impl PointerComponent

Source

pub fn new() -> Self

Examples found in repository?
examples/pointer-events.rs (line 404)
349fn main() {
350    mittens_engine::example_support::ensure_model_assets();
351    utils::logger::init();
352
353    let world = engine::ecs::World::default();
354    let mut universe = engine::Universe::new(world);
355
356    use engine::ecs::component::{
357        AmbientLightComponent, BackgroundColorComponent, Camera3DComponent, ColorComponent,
358        DirectionalLightComponent, InputComponent, InputTransformModeComponent, PointerComponent,
359        TransformComponent,
360    };
361
362    // Background.
363    let bg = universe
364        .world
365        .add_component(BackgroundColorComponent::new());
366    let bg_c = universe
367        .world
368        .add_component(ColorComponent::rgba(0.12, 0.12, 0.16, 1.0));
369    let _ = universe.world.add_child(bg, bg_c);
370    universe.add(bg);
371
372    // Lighting.
373    let ambient = universe
374        .world
375        .add_component(AmbientLightComponent::rgb(0.30, 0.30, 0.32));
376    universe.add(ambient);
377
378    let sun_t = universe
379        .world
380        .add_component(TransformComponent::new().with_position(2.0, 4.0, 3.0));
381    let sun = universe
382        .world
383        .add_component(DirectionalLightComponent::new());
384    let _ = universe.attach(sun_t, sun);
385    universe.add(sun_t);
386
387    // Camera + pointer rig.
388    let input = universe
389        .world
390        .add_component(InputComponent::new().with_speed(3.0));
391    let input_mode = universe.world.add_component(
392        InputTransformModeComponent::forward_z()
393            .with_fps_rotation()
394            .with_roll_axis_y(),
395    );
396    let _ = universe.attach(input, input_mode);
397
398    let cam_t = universe
399        .world
400        .add_component(TransformComponent::new().with_position(0.0, 0.5, 4.5));
401    let cam = universe
402        .world
403        .add_component(Camera3DComponent::new().with_fov(70.0));
404    let pointer = universe.world.add_component(PointerComponent::new());
405
406    let _ = universe.attach(input, cam_t);
407    let _ = universe.attach(cam_t, cam);
408    let _ = universe.attach(cam, pointer);
409
410    example_util::spawn_desktop_camera_controls_hint(&mut universe, cam_t);
411    universe.add(input);
412
413    // Scene root for all interactive objects.
414    let scene = universe.world.add_component(TransformComponent::new());
415    universe.add(scene);
416
417    // --- LEFT: click-only cubes ---
418    let click_group = universe
419        .world
420        .add_component(TransformComponent::new().with_position(-2.2, 0.0, 0.0));
421    let _ = universe.attach(scene, click_group);
422
423    for (i, dy) in [-0.55_f32, 0.0, 0.55].iter().enumerate() {
424        spawn_click_cube(&mut universe, click_group, [0.0, *dy, 0.0]);
425        let _ = i;
426    }
427    spawn_label(
428        &mut universe,
429        click_group,
430        [-0.25, -1.05, 0.0],
431        "click\ncycles color",
432    );
433
434    // --- MID: drag-only cube ---
435    let drag_cube_root = universe
436        .world
437        .add_component(TransformComponent::new().with_position(0.0, 0.0, 0.0));
438    let _ = universe.attach(scene, drag_cube_root);
439
440    spawn_drag_cube(
441        &mut universe,
442        drag_cube_root,
443        [0.0, 0.0, 0.0],
444        [0.35, 0.85, 0.55, 1.0],
445    );
446    spawn_label(
447        &mut universe,
448        drag_cube_root,
449        [-0.25, -0.80, 0.0],
450        "drag\nto move",
451    );
452
453    // --- RIGHT: drag + click cube ---
454    let both_root = universe
455        .world
456        .add_component(TransformComponent::new().with_position(2.2, 0.0, 0.0));
457    let _ = universe.attach(scene, both_root);
458
459    spawn_both_cube(&mut universe, both_root, [0.0, 0.0, 0.0]);
460    spawn_label(
461        &mut universe,
462        both_root,
463        [-0.30, -0.80, 0.0],
464        "drag to move\nclick for color",
465    );
466
467    universe.systems.process_commands(
468        &mut universe.world,
469        &mut universe.visuals,
470        &mut universe.render_assets,
471        &mut universe.command_queue,
472    );
473
474    universe.enable_repl();
475    engine::Windowing::run_app(universe).expect("Windowing failed");
476}
More examples
Hide additional examples
examples/button-press.rs (line 498)
439fn main() {
440    mittens_engine::example_support::ensure_model_assets();
441    utils::logger::init();
442
443    let world = engine::ecs::World::default();
444    let mut universe = engine::Universe::new(world);
445
446    // Minimal lit scene + pointer raycaster.
447    use engine::ecs::component::{
448        AmbientLightComponent, BackgroundColorComponent, Camera3DComponent,
449        DirectionalLightComponent, InputComponent, InputTransformModeComponent, PointerComponent,
450        TransformComponent,
451    };
452
453    let bg = universe
454        .world
455        .add_component(BackgroundColorComponent::new());
456    let bg_c = universe
457        .world
458        .add_component(engine::ecs::component::ColorComponent::rgba(
459            0.92, 0.92, 0.96, 1.0,
460        ));
461    let _ = universe.world.add_child(bg, bg_c);
462    universe.add(bg);
463
464    let ambient = universe
465        .world
466        .add_component(AmbientLightComponent::rgb(0.35, 0.35, 0.35));
467    universe.add(ambient);
468
469    let sun_t = universe
470        .world
471        .add_component(TransformComponent::new().with_position(1.0, 1.0, 1.0));
472    let sun = universe
473        .world
474        .add_component(DirectionalLightComponent::new());
475    let _ = universe.attach(sun_t, sun);
476    universe.add(sun_t);
477
478    let input = universe
479        .world
480        .add_component(InputComponent::new().with_speed(2.5));
481    let input_mode = universe.world.add_component(
482        InputTransformModeComponent::forward_z()
483            .with_fps_rotation()
484            .with_roll_axis_y(),
485    );
486    let _ = universe.attach(input, input_mode);
487
488    let rig_t = universe
489        .world
490        .add_component(TransformComponent::new().with_position(0.0, 0.0, 2.5));
491    let _ = universe.attach(input, rig_t);
492
493    let cam = universe
494        .world
495        .add_component(Camera3DComponent::new().with_far(600.0).with_fov(70.0));
496    let _ = universe.attach(rig_t, cam);
497
498    let pointer = universe.world.add_component(PointerComponent::new());
499    let _ = universe.attach(cam, pointer);
500
501    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_t);
502    universe.add(input);
503
504    // The button.
505    let button_root = spawn_button(
506        &mut universe,
507        [0.0, 0.0, 0.0],
508        [1.20, 0.45],
509        ButtonBorder::new(0.025, 0.025, 0.025, 0.025),
510        "click me",
511        0.18,
512        [0.15, 0.15, 0.20, 1.0],
513        [0.85, 0.85, 0.92, 1.0],
514    );
515
516    // A small 3-cube stack to the left of the button for gizmo testing:
517    //   [cyan]
518    // [yellow][light brown]
519    //
520    // These live under an EditorComponent subtree so clicking attaches the editor gizmo.
521    {
522        use engine::ecs::component::{EditorComponent, TransformComponent};
523
524        let editor_root = universe.world.add_component(EditorComponent::new());
525
526        // Position the stack in world space (left of the button).
527        let stack_root = universe
528            .world
529            .add_component(TransformComponent::new().with_position(-1.55, 0.0, 0.0));
530        let _ = universe.attach(editor_root, stack_root);
531
532        let cube = 0.22_f32;
533        let gap = 0.04_f32;
534        let step = cube + gap;
535        let y_bottom = -0.5 * step;
536        let y_top = 0.5 * step;
537        let x_left = -0.5 * step;
538        let x_right = 0.5 * step;
539
540        let yellow = [1.0, 0.92, 0.22, 1.0];
541        let light_brown = [0.80, 0.66, 0.46, 1.0];
542        let cyan = [0.20, 0.95, 1.0, 1.0];
543
544        let _bottom_left = spawn_raycastable_colored_cube(
545            &mut universe,
546            stack_root,
547            [x_left, y_bottom, 0.0],
548            [cube, cube, 0.06],
549            yellow,
550        );
551        let _bottom_right = spawn_raycastable_colored_cube(
552            &mut universe,
553            stack_root,
554            [x_right, y_bottom, 0.0],
555            [cube, cube, 0.06],
556            light_brown,
557        );
558        let _top = spawn_raycastable_colored_cube(
559            &mut universe,
560            stack_root,
561            [0.0, y_top, 0.0],
562            [cube, cube, 0.06],
563            cyan,
564        );
565
566        universe.add(editor_root);
567    }
568
569    universe.add_signal_handler(
570        engine::ecs::SignalKind::DragStart,
571        button_root,
572        button_press_handler,
573    );
574    universe.add_signal_handler(
575        engine::ecs::SignalKind::DragEnd,
576        button_root,
577        button_press_handler,
578    );
579
580    universe.systems.process_commands(
581        &mut universe.world,
582        &mut universe.visuals,
583        &mut universe.render_assets,
584        &mut universe.command_queue,
585    );
586
587    universe.enable_repl();
588    engine::Windowing::run_app(universe).expect("Windowing failed");
589}
examples/gestures-and-gizmos.rs (line 117)
13fn build_gestures_and_gizmos_scene(universe: &mut engine::Universe) -> Scene {
14    use engine::ecs::component::{
15        BackgroundColorComponent, BackgroundComponent, Camera3DComponent, ColorComponent,
16        DirectionalLightComponent, InputComponent, InputTransformModeComponent, PointerComponent,
17        RaycastableComponent, RenderableComponent, TransformComponent, TransformGizmoComponent,
18    };
19    use engine::graphics::BuiltinMeshType;
20    use engine::graphics::primitives::{MaterialHandle, Renderable};
21
22    let tri_mesh = universe.render_assets.get_mesh(BuiltinMeshType::Triangle2D);
23    let cube_mesh = universe.render_assets.get_mesh(BuiltinMeshType::Cube);
24    let tetra_mesh = universe
25        .render_assets
26        .get_mesh(BuiltinMeshType::Tetrahedron);
27
28    // BackgroundColor { C.rgba }
29    let bg_color = universe
30        .world
31        .add_component(BackgroundColorComponent::new());
32    let bg_color_c = universe
33        .world
34        .add_component(ColorComponent::rgba(0.90, 0.90, 0.90, 1.0));
35    let _ = universe.world.add_child(bg_color, bg_color_c);
36    universe.add(bg_color);
37
38    // ambient light
39    let ambient = universe
40        .world
41        .add_component(AmbientLightComponent::rgb(0.25, 0.25, 0.25));
42    universe.add(ambient);
43
44    // ground plane
45    let ground_tx = universe.world.add_component(
46        TransformComponent::new()
47            .with_position(0.0, -2.5, 0.0)
48            .with_scale(20.0, 1.0, 20.0),
49    );
50    let ground_r = universe
51        .world
52        .add_component(RenderableComponent::new(Renderable::new(
53            universe.render_assets.get_mesh(BuiltinMeshType::Cube),
54            MaterialHandle::TOON_MESH,
55        )));
56    let ground_c = universe
57        .world
58        .add_component(ColorComponent::rgba(0.75, 0.75, 0.75, 1.0));
59    let _ = universe.attach(ground_tx, ground_r);
60    let _ = universe.attach(ground_r, ground_c);
61    let _ = universe.add(ground_tx);
62
63    // Background {
64    //     with_occlusion_and_lighting()
65    //     // using the example utils to add clouds to the background
66    // }
67    let bg_root = universe
68        .world
69        .add_component(BackgroundComponent::new().with_occlusion_and_lighting());
70    universe.add(bg_root);
71
72    // DirectionalLight {
73    //     T { translate [1, 1, 1] }
74    // }
75    // Directional lights encode their direction in the node's world position.
76    let sun_t = universe
77        .world
78        .add_component(TransformComponent::new().with_position(1.0, 1.0, 1.0));
79    let sun = universe
80        .world
81        .add_component(DirectionalLightComponent::new());
82    let _ = universe.attach(sun_t, sun);
83    universe.add(sun_t);
84
85    // i = input
86    // t = transform
87    // c3d = camera3d
88    //
89    // I {
90    //     T {
91    //         C3D { with_fps_rotation().with_roll_axis_y() }
92    //     }
93    // }
94    let input = universe
95        .world
96        .add_component(InputComponent::new().with_speed(2.5));
97    let input_mode = universe.world.add_component(
98        InputTransformModeComponent::forward_z()
99            .with_fps_rotation()
100            .with_roll_axis_y(),
101    );
102
103    let _ = universe.attach(input, input_mode);
104
105    // Forward is -Z, so put the camera at +Z looking toward the origin.
106    let rig_t = universe
107        .world
108        .add_component(TransformComponent::new().with_position(0.0, 0.0, 3.5));
109    let _ = universe.attach(input, rig_t);
110
111    let cam = universe
112        .world
113        .add_component(Camera3DComponent::new().with_far(600.0).with_fov(70.0));
114    let _ = universe.attach(rig_t, cam);
115
116    // Opt-in: treat this camera rig as a pointer source.
117    let pointer = universe.world.add_component(PointerComponent::new());
118    let _ = universe.attach(cam, pointer);
119
120    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
121    example_util::spawn_desktop_camera_controls_hint(universe, rig_t);
122
123    fn spawn_shape_with_gizmo(
124        universe: &mut engine::Universe,
125        mesh: engine::graphics::primitives::CpuMeshHandle,
126        pos: [f32; 3],
127        scale: [f32; 3],
128        rot_euler: [f32; 3],
129        color: [f32; 4],
130    ) {
131        let t = universe.world.add_component(
132            TransformComponent::new()
133                .with_position(pos[0], pos[1], pos[2])
134                .with_scale(scale[0], scale[1], scale[2])
135                .with_rotation_euler(rot_euler[0], rot_euler[1], rot_euler[2]),
136        );
137        let r = universe
138            .world
139            .add_component(RenderableComponent::new(Renderable::new(
140                mesh,
141                MaterialHandle::TOON_MESH,
142            )));
143        let c = universe
144            .world
145            .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
146        let rc = universe
147            .world
148            .add_component(RaycastableComponent::enabled());
149        let g = universe.world.add_component(TransformGizmoComponent::new());
150
151        let _ = universe.attach(t, r);
152        let _ = universe.attach(r, c);
153        let _ = universe.attach(r, rc);
154        let _ = universe.attach(t, g);
155
156        universe.add(t);
157    }
158
159    fn spawn_shape_raycastable_no_gizmo(
160        universe: &mut engine::Universe,
161        mesh: engine::graphics::primitives::CpuMeshHandle,
162        pos: [f32; 3],
163        scale: [f32; 3],
164        rot_euler: [f32; 3],
165        color: [f32; 4],
166    ) {
167        let t = universe.world.add_component(
168            TransformComponent::new()
169                .with_position(pos[0], pos[1], pos[2])
170                .with_scale(scale[0], scale[1], scale[2])
171                .with_rotation_euler(rot_euler[0], rot_euler[1], rot_euler[2]),
172        );
173        let r = universe
174            .world
175            .add_component(RenderableComponent::new(Renderable::new(
176                mesh,
177                MaterialHandle::TOON_MESH,
178            )));
179        let c = universe
180            .world
181            .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
182        let rc = universe
183            .world
184            .add_component(RaycastableComponent::enabled());
185
186        let _ = universe.attach(t, r);
187        let _ = universe.attach(r, c);
188        let _ = universe.attach(r, rc);
189
190        universe.add(t);
191    }
192
193    spawn_shape_with_gizmo(
194        universe,
195        tri_mesh,
196        [-1.2, 0.0, 0.0],
197        [0.65, 0.65, 0.65],
198        [0.0, 0.0, 0.0],
199        [0.2, 0.9, 0.25, 1.0],
200    );
201    spawn_shape_with_gizmo(
202        universe,
203        cube_mesh,
204        [0.0, 0.0, 0.0],
205        [0.55, 0.55, 0.55],
206        [0.0, 0.0, 0.0],
207        [0.95, 0.25, 0.2, 1.0],
208    );
209    spawn_shape_with_gizmo(
210        universe,
211        tetra_mesh,
212        [1.2, 0.0, 0.0],
213        [0.7, 0.7, 0.7],
214        [0.0, 0.0, 0.0],
215        [0.2, 0.55, 1.0, 1.0],
216    );
217
218    // Standalone tetrahedron: no gizmo, but explicitly raycastable.
219    // This helps isolate tetra picking vs gizmo-handle interception.
220    spawn_shape_raycastable_no_gizmo(
221        universe,
222        tetra_mesh,
223        [2.6, -0.6, 0.0],
224        [0.95, 0.95, 0.95],
225        [0.0, 0.0, 0.0],
226        [0.85, 0.85, 1.0, 1.0],
227    );
228
229    universe.add(input);
230
231    Scene { bg_root }
232}
examples/vr-input.rs (line 278)
186fn main() {
187    mittens_engine::example_support::ensure_model_assets();
188    utils::logger::init();
189
190    let options = match parse_options() {
191        Ok(options) => options,
192        Err(message) => {
193            eprintln!("{message}");
194            std::process::exit(2);
195        }
196    };
197
198    println!(
199        "[vr-input] xr controller rotation filter pipeline: {}",
200        if options.xr_controller_rotation_filter {
201            "enabled"
202        } else {
203            "disabled"
204        }
205    );
206
207    let world = engine::ecs::World::default();
208    let mut universe = engine::Universe::new(world);
209
210    let renderer_settings = universe
211        .world
212        .add_component(RendererSettingsComponent::msaa_off().with_window_size(320, 240));
213    universe.add(renderer_settings);
214
215    let render_graph = universe.world.add_component(RenderGraphComponent::new());
216    let emissive_pass = universe.world.add_component(EmissivePassComponent::new());
217    let blur_pass = universe.world.add_component(
218        BlurPassComponent::new()
219            .with_radius_ndc(0.06)
220            .with_half_res(true),
221    );
222    let bloom = universe.world.add_component(
223        BloomComponent::new()
224            .with_intensity(0.95)
225            .with_emissive_scale(1.2),
226    );
227    let _ = universe.attach(emissive_pass, blur_pass);
228    let _ = universe.attach(render_graph, emissive_pass);
229    let _ = universe.attach(render_graph, bloom);
230    universe.add(render_graph);
231
232    // Sky base.
233    let background = universe
234        .world
235        .add_component(BackgroundColorComponent::new());
236    let background_c = universe
237        .world
238        .add_component(ColorComponent::rgba(0.62, 0.80, 1.00, 1.0));
239    let _ = universe.world.add_child(background, background_c);
240    universe.add(background);
241
242    // Lighting for the model.
243    let ambient = universe
244        .world
245        .add_component(AmbientLightComponent::rgb(0.18, 0.18, 0.22));
246    universe.add(ambient);
247
248    let sun = universe.world.add_component(
249        DirectionalLightComponent::new()
250            .with_intensity(1.1)
251            .with_color(1.0, 0.98, 0.95),
252    );
253    let sun_dir = universe
254        .world
255        .add_component(TransformComponent::new().with_position(0.15, -0.45, 1.0));
256    let _ = universe.attach(sun_dir, sun);
257    universe.add(sun_dir);
258
259    // --- Desktop camera rig (for debugging while in VR) ---
260    let input = universe
261        .world
262        .add_component(InputComponent::new().with_speed(1.5));
263    let input_mode = universe.world.add_component(
264        InputTransformModeComponent::forward_z()
265            .with_fps_rotation()
266            .with_roll_axis_y(),
267    );
268    let _ = universe.attach(input, input_mode);
269
270    let desktop_rig = universe
271        .world
272        .add_component(TransformComponent::new().with_position(0.0, 1.2, 3.5));
273    let _ = universe.attach(input, desktop_rig);
274
275    let camera3d = universe.world.add_component(Camera3DComponent::new());
276    let _ = universe.attach(desktop_rig, camera3d);
277
278    let pointer = universe.world.add_component(PointerComponent::new());
279    let _ = universe.attach(camera3d, pointer);
280
281    example_util::spawn_desktop_camera_controls_hint(&mut universe, desktop_rig);
282    universe.add(input);
283
284    // --- XR rig (Aim controller debug cubes only; camera has moved to AVC) ---
285    let xr_input = universe.world.add_component(InputXRComponent::on());
286    let xr_gamepad = universe.world.add_component(
287        mittens_engine::engine::ecs::component::InputXRGamepadComponent::new().speed(1.5),
288    );
289    let xr_rig = universe.world.add_component(TransformComponent::new());
290    let _ = universe.attach(xr_input, xr_rig);
291    let _ = universe.attach(xr_input, xr_gamepad);
292
293    // renderer stats
294    let renderer_stats = universe
295        .world
296        .add_component(RendererStatsComponent::new().with_camera_target(CameraTarget::Xr));
297    let render_stats_rig = universe
298        .world
299        .add_component(TransformComponent::new().with_position(0.0, 1.85, 0.6));
300    let _ = universe.attach(render_stats_rig, renderer_stats);
301    let _ = universe.attach(xr_rig, render_stats_rig);
302
303    universe.add(xr_input);
304
305    // Background "skybox" content.
306    spawn_sun_background(&mut universe);
307
308    // --- VTuber model — single-input topology ---
309    //
310    // InputXRComponent drives body translation and head rotation through AvatarControlSystem.
311    // AvatarControlSystem:
312    //   - Splices a TransformComponent under J_Bip_C_Head's parent (the neck) to drive
313    //     head rotation directly. Rotating the head — not the neck — isolates the spine
314    //     so the torso doesn't twist with HMD yaw.
315    //   - Strips rotation from model_root (body faces body_yaw, not raw HMD yaw).
316    //   - Bakes the π Y handedness correction into the head rotation math.
317    //   - Smoothly rotates body to follow head when yaw delta exceeds threshold.
318    //   - Measures J_Bip_C_Head local Y in the rest pose and sets model_root.y = -bone_local_y,
319    //     so the head bone sits at driven_t world Y (= HMD height) with no hardcoded constant.
320    //   - Re-parents CameraXRComponent under J_Bip_C_Head for first-person alignment.
321    //
322    // Topology (after AVC init):
323    //   editor_root
324    //     └── avatar_input_xr (InputXRComponent)
325    //           └── avatar_driven_t (TransformComponent)
326    //                 └── AvatarControlComponent
327    //                       ├── body_pipeline → pipeline_output
328    //                       │     └── model_root (y auto-calibrated from J_Bip_C_Head)
329    //                       │           └── GLTFComponent → ... → J_Bip_C_Head
330    //                       │                                           └── CameraXRComponent
331    //                       ├── CTLXR(Left, Grip) → re-parented to lower_arm
332    //                       └── CTLXR(Right, Grip) → re-parented to lower_arm
333
334    let editor_root = universe.world.add_component(EditorComponent::new());
335
336    let avatar_input_xr = universe.world.add_component(InputXRComponent::on());
337    let avatar_xr_gamepad = universe.world.add_component(
338        mittens_engine::engine::ecs::component::InputXRGamepadComponent::new().speed(1.5),
339    );
340    let avatar_driven_t = universe.world.add_component(TransformComponent::new());
341    let _ = universe.attach(avatar_input_xr, avatar_driven_t);
342    let _ = universe.attach(avatar_input_xr, avatar_xr_gamepad);
343
344    // AvatarControlComponent: -Z forward (OpenXR default), body starts facing -Z (π yaw).
345    // camera_bone triggers auto-calibration of model_root.y from J_Bip_C_Head rest pose height,
346    // and causes any CameraXR/Camera3D direct children of AVC to be re-parented to that bone.
347    let avatar_control = universe.world.add_component(
348        AvatarControlComponent::new()
349            .with_head_bone("J_Bip_C_Head")
350            .with_camera_bone("J_Bip_C_Head")
351            .with_left_hand_bone("J_Bip_L_Hand")
352            .with_right_hand_bone("J_Bip_R_Hand")
353            .with_initial_yaw(std::f32::consts::PI)
354            .with_hand_rotation_smoothing(220.0),
355    );
356    let _ = universe.attach(avatar_driven_t, avatar_control);
357
358    // CameraXR as a direct child of AVC — discovered and re-parented to J_Bip_C_Head at init.
359    let camera_xr = universe.world.add_component(CameraXRComponent::on());
360    let _ = universe.attach(avatar_control, camera_xr);
361    let head_pointer = universe.world.add_component(PointerComponent::new());
362    let _ = universe.attach(camera_xr, head_pointer);
363
364    // Grip controllers for hand bone splicing — children of AvatarControlComponent so
365    // AvatarControlSystem discovers them by topology. Each needs a TransformComponent
366    // child (driven_t) that OpenXRSystem writes each tick.
367    let left_grip = universe.world.add_component(ControllerXRComponent::new(
368        true,
369        ControllerHand::Left,
370        ControllerPoseKind::Grip,
371    ));
372    let left_grip_t = universe.world.add_component(TransformComponent::new());
373    let _ = universe.attach(left_grip, left_grip_t);
374    let left_pointer = universe.world.add_component(PointerComponent::new());
375    let _ = universe.attach(left_grip_t, left_pointer);
376    let _ = universe.attach(avatar_control, left_grip);
377
378    let right_grip = universe.world.add_component(ControllerXRComponent::new(
379        true,
380        ControllerHand::Right,
381        ControllerPoseKind::Grip,
382    ));
383    let right_grip_t = universe.world.add_component(TransformComponent::new());
384    let _ = universe.attach(right_grip, right_grip_t);
385    let right_pointer = universe.world.add_component(PointerComponent::new());
386    let _ = universe.attach(right_grip_t, right_pointer);
387    let _ = universe.attach(avatar_control, right_grip);
388
389    // model_root: no explicit Y offset — AvatarControlSystem calibrates it from J_Bip_C_Head.
390    let model_root = universe.world.add_component(TransformComponent::new());
391    let model = universe
392        .world
393        .add_component(GLTFComponent::new("assets/models/pc-rei.hoodie.glb"));
394    let emissive = universe.world.add_component(EmissiveComponent::on());
395    let _ = universe.attach(model, emissive);
396
397    let _ = universe.attach(editor_root, avatar_input_xr);
398    let _ = universe.attach(avatar_control, model_root);
399    let _ = universe.attach(model_root, model);
400    universe.add(editor_root);
401
402    // --- Controller debug cubes (tracked poses) ---
403    let _left = spawn_controller_cube(
404        &mut universe,
405        xr_rig,
406        ControllerHand::Left,
407        (0.10, 0.90, 1.00, 1.0),
408        220.0,
409        options.xr_controller_rotation_filter,
410    );
411    let _right = spawn_controller_cube(
412        &mut universe,
413        xr_rig,
414        ControllerHand::Right,
415        (1.00, 0.35, 0.35, 1.0),
416        220.0,
417        options.xr_controller_rotation_filter,
418    );
419
420    // Enable OpenXR runtime.
421    let xr_root = universe.world.add_component(XrComponent::on());
422    universe.add(xr_root);
423
424    universe.systems.process_commands(
425        &mut universe.world,
426        &mut universe.visuals,
427        &mut universe.render_assets,
428        &mut universe.command_queue,
429    );
430
431    // Force the glTF subtree to spawn so we can query the armature for bone markers.
432    {
433        let systems = &mut universe.systems;
434        systems.gltf.tick_with_queue(
435            &mut universe.world,
436            &mut universe.visuals,
437            &mut systems.skinned_mesh,
438            &mut universe.command_queue,
439            0.0,
440        );
441    }
442    universe.systems.process_commands(
443        &mut universe.world,
444        &mut universe.visuals,
445        &mut universe.render_assets,
446        &mut universe.command_queue,
447    );
448
449    // Bone markers for editor inspection.
450    let marker_joints: &[(&str, (f32, f32, f32, f32))] = &[
451        ("[name='J_Bip_C_Head']", (0.85, 0.20, 0.85, 0.9)),
452        ("[name='J_Bip_C_Neck']", (0.20, 0.85, 0.85, 0.9)),
453        ("[name='J_Bip_C_UpperChest']", (0.20, 0.20, 0.85, 0.9)),
454        ("[name='J_Bip_L_UpperArm']", (0.85, 0.85, 0.20, 0.9)),
455        ("[name='J_Bip_R_UpperArm']", (0.85, 0.60, 0.20, 0.9)),
456    ];
457
458    for &(selector, color) in marker_joints {
459        let Some(bone) = universe.find_component(model_root, selector) else {
460            continue;
461        };
462        let marker_t = universe
463            .world
464            .add_component(TransformComponent::new().with_scale(0.025, 0.025, 0.025));
465        let marker_r = universe.world.add_component(RenderableComponent::cube());
466        let marker_c = universe
467            .world
468            .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
469        let marker_rcast = universe
470            .world
471            .add_component(RaycastableComponent::enabled());
472        let _ = universe.world.add_child(marker_r, marker_c);
473        let _ = universe.world.add_child(marker_r, marker_rcast);
474        let _ = universe.world.add_child(marker_t, marker_r);
475        let _ = universe.attach(bone, marker_t);
476    }
477
478    universe.enable_repl();
479    engine::Windowing::run_app(universe).expect("Windowing failed");
480}
examples/font-example.rs (line 83)
13fn main() {
14    mittens_engine::example_support::ensure_model_assets();
15    utils::logger::init();
16
17    let world = engine::ecs::World::default();
18    let mut universe = engine::Universe::new(world);
19
20    // Dark background so the font texture pops.
21    let background = universe
22        .world
23        .add_component(BackgroundColorComponent::new());
24    let background_c = universe
25        .world
26        .add_component(ColorComponent::rgba(0.20, 0.2, 0.20, 1.0));
27    let _ = universe.world.add_child(background, background_c);
28    universe.add(background);
29
30    // Ambient so text is readable even without explicit lights.
31    let ambient = universe
32        .world
33        .add_component(AmbientLightComponent::rgb(0.50, 0.50, 0.50));
34    universe.add(ambient);
35
36    let directional_tx = universe
37        .world
38        .add_component(TransformComponent::new().with_position(0.0, 0.5, 1.0));
39    let directional_light = universe.world.add_component(
40        engine::ecs::component::DirectionalLightComponent::new()
41            .with_color(1.0, 1.0, 1.0)
42            .with_intensity(0.8),
43    );
44    let _ = universe.attach(directional_tx, directional_light);
45    universe.add(directional_tx);
46
47    // --- background clouds ---
48    // Background stage (occluded + lit) so the cloud volume self-occludes but won't occlude
49    // the foreground text (renderer clears depth before foreground).
50    let bg_root = universe
51        .world
52        .add_component(BackgroundComponent::new().with_occlusion_and_lighting());
53    universe.add(bg_root);
54
55    let mut bg_cloud_params = example_util::CloudRingParams::default();
56    bg_cloud_params.cloud_count = 6;
57    bg_cloud_params.seed = 0xF0_17_C10u32;
58    example_util::spawn_cloud_ring(&mut universe, bg_root, bg_cloud_params);
59
60    // I {
61    //   // not fps rotation, just relative rotation
62    //   with_forward_z()
63    //   with_roll_axis_y()
64    //   C3D {}
65    // }
66    let input = universe
67        .world
68        .add_component(InputComponent::new().with_speed(2.0));
69    let input_mode = universe
70        .world
71        .add_component(InputTransformModeComponent::forward_z().with_roll_axis_y());
72    let _ = universe.attach(input, input_mode);
73
74    let rig_transform = universe
75        .world
76        .add_component(TransformComponent::new().with_position(1.8, -0.5, 2.5));
77    let _ = universe.attach(input, rig_transform);
78
79    let camera = universe.world.add_component(Camera3DComponent::new());
80    let _ = universe.attach(rig_transform, camera);
81
82    // Click-to-pick: treat this camera rig as a pointer source.
83    let pointer = universe.world.add_component(PointerComponent::new());
84    let _ = universe.attach(camera, pointer);
85
86    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
87    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
88
89    universe.add(input);
90
91    // --- foreground clouds ---
92    // Normal foreground renderables (not background stage).
93    // Offset the ring forward (negative Z) so several clusters are in view.
94    let fg_cloud_root = universe
95        .world
96        .add_component(TransformComponent::new().with_position(0.0, -6.0, -10.0));
97    universe.add(fg_cloud_root);
98
99    let mut fg_cloud_params = example_util::CloudRingParams::default();
100    fg_cloud_params.cloud_count = 4;
101    fg_cloud_params.radius = 9.0;
102    fg_cloud_params.center_y = 1.0;
103    fg_cloud_params.puffs_per_cloud = 22;
104    fg_cloud_params.angle_jitter = 0.35;
105    fg_cloud_params.high_y_probability = 0.35;
106    fg_cloud_params.high_y_multiplier = 1.4;
107    fg_cloud_params.seed = 0xF0_17_C102u32;
108    example_util::spawn_cloud_ring(&mut universe, fg_cloud_root, fg_cloud_params);
109
110    // T {
111    //   with_translation(0,0, -2)
112    //   TXT {
113    //     "ababaabbaabbaaabbbaaabbbaaaabbbbaaaaabbbbbababababa"
114    //     TextureComponent { assets/images/test.font_system.png }
115    //   }
116    // }
117    fn estimate_text_height_world(text: &str, scale: f32) -> f32 {
118        let line_count = text.lines().count().max(1) as f32;
119        // Text quads are ~1 unit tall per line in text-space.
120        // Add some padding so blocks don't feel cramped.
121        let pad_lines = 1.25;
122        (line_count + pad_lines) * scale
123    }
124
125    fn spawn_text_block(
126        universe: &mut engine::Universe,
127        position: (f32, f32, f32),
128        scale: f32,
129        text: &str,
130        font_texture_uri: Option<&str>,
131        text_color_rgba: Option<[f32; 4]>,
132        shadow: Option<TextShadowComponent>,
133        filtering: TextureFilteringComponent,
134    ) -> f32 {
135        // T_root { T_scale { [Color] { TXT { shadow, filtering } } } }
136        let text_root = universe.world.add_component(
137            TransformComponent::new().with_position(position.0, position.1, position.2),
138        );
139
140        let text_scale = universe
141            .world
142            .add_component(TransformComponent::new().with_scale(scale, scale, 1.0));
143        let _ = universe.attach(text_root, text_scale);
144
145        let text_parent = if let Some([r, g, b, a]) = text_color_rgba {
146            let color = universe
147                .world
148                .add_component(ColorComponent::rgba(r, g, b, a));
149            let _ = universe.attach(text_scale, color);
150            color
151        } else {
152            text_scale
153        };
154
155        let text_c = universe.world.add_component(TextComponent::new(text));
156        let _ = universe.attach(text_parent, text_c);
157
158        // Explicit opt-in: make the glyph renderables pickable.
159        // TextSystem will propagate this to all spawned glyph quads.
160        let raycastable = universe
161            .world
162            .add_component(RaycastableComponent::enabled());
163        let _ = universe.attach(text_c, raycastable);
164
165        // Route glyph quads into the alpha-to-coverage cutout pass.
166        let cutout = universe
167            .world
168            .add_component(TransparentCutoutComponent::new());
169        let _ = universe.attach(text_c, cutout);
170
171        if let Some(shadow) = shadow {
172            let shadow_id = universe.world.add_component(shadow);
173            let _ = universe.attach(text_c, shadow_id);
174        }
175
176        // Optional: override the font atlas for this text block.
177        // TextSystem will propagate this to all glyph renderables.
178        if let Some(uri) = font_texture_uri {
179            let tex = universe
180                .world
181                .add_component(TextureComponent::with_uri(uri));
182            let _ = universe.attach(text_c, tex);
183        }
184
185        let filtering_id = universe.world.add_component(filtering);
186        let _ = universe.attach(text_c, filtering_id);
187
188        universe.add(text_root);
189
190        estimate_text_height_world(text, scale)
191    }
192
193    // --- text blocks ---
194    // Multi-line samples at different scales.
195    // (Text literals omitted in the README/snippet; see constants below.)
196    const TEXT_BIG: &str = "CAT ENGINE\nfont example\nBIG TEXT";
197    const TEXT_MED: &str = "multi-line\ntext block\nmedium";
198    const TEXT_SMALL: &str = "small\ntext";
199    const TEXT_TINY: &str = "tiny\ntext\n(zoom in)";
200
201    // Stack vertically; advance by (measured height + gap) so big text gets more room.
202    let x = -1.2;
203    let z = -2.0;
204    let mut y = 1.2;
205    let gap = 0.15;
206
207    let shadow_crisp = Some(
208        TextShadowComponent::new()
209            .with_scale(1.35)
210            .with_offset([0.06, -0.06, 0.0015]),
211    );
212
213    y -= spawn_text_block(
214        &mut universe,
215        (x, y, z),
216        0.55,
217        TEXT_BIG,
218        None,
219        Some([1.0, 1.0, 1.0, 1.0]),
220        shadow_crisp,
221        TextureFilteringComponent::nearest_magnification(),
222    ) + gap;
223    y -= spawn_text_block(
224        &mut universe,
225        (x, y, z),
226        0.25,
227        TEXT_MED,
228        None,
229        Some([0.60, 0.95, 1.00, 1.0]),
230        Some(
231            TextShadowComponent::new()
232                .with_rgba([0.0, 0.0, 0.15, 1.0])
233                .with_scale(1.20)
234                .with_offset([0.05, -0.04, 0.0015]),
235        ),
236        TextureFilteringComponent::linear(),
237    ) + gap;
238    y -= spawn_text_block(
239        &mut universe,
240        (x, y, z),
241        0.14,
242        TEXT_SMALL,
243        None,
244        Some([1.0, 0.88, 0.35, 1.0]),
245        Some(
246            TextShadowComponent::new()
247                .with_rgba([0.15, 0.0, 0.0, 1.0])
248                .with_scale(1.55)
249                .with_offset([0.08, -0.08, 0.0015]),
250        ),
251        TextureFilteringComponent::nearest(),
252    ) + gap;
253    let _ = spawn_text_block(
254        &mut universe,
255        (x, y, z),
256        0.08,
257        TEXT_TINY,
258        None,
259        Some([0.90, 1.0, 0.70, 1.0]),
260        shadow_crisp,
261        TextureFilteringComponent::nearest_magnification(),
262    );
263
264    // Left block: explicit multi-line text using the default font_system atlas.
265    const TEXT_LEFT: &str = "even though there's hexes\nto the solar plexus in my lexus\ni'm feelin' reckless,\nwhen i'm eating breakfast";
266    let _ = spawn_text_block(
267        &mut universe,
268        (x - 8.1, 1.1, z),
269        0.22,
270        TEXT_LEFT,
271        Some("assets/textures/font_system.dds"),
272        Some([0.95, 0.95, 0.95, 1.0]),
273        Some(
274            TextShadowComponent::new()
275                .with_scale(1.25)
276                .with_offset([0.05, -0.05, 0.0015]),
277        ),
278        TextureFilteringComponent::nearest_magnification(),
279    );
280
281    // Alt atlas: put it *behind* the original stack (slightly farther from the camera)
282    // and tint it dark grey.
283    let alt_atlas = Some("assets/textures/font_system.0.0.dds");
284    let alt_z = z - 0.05;
285    let alt_grey = Some([0.25, 0.25, 0.25, 1.0]);
286
287    let mut y_alt = 1.2;
288    y_alt -= spawn_text_block(
289        &mut universe,
290        (x, y_alt, alt_z),
291        0.55,
292        TEXT_BIG,
293        alt_atlas,
294        alt_grey,
295        Some(
296            TextShadowComponent::new()
297                .with_rgba([0.0, 0.0, 0.0, 1.0])
298                .with_scale(1.15)
299                .with_offset([0.03, -0.03, 0.0015]),
300        ),
301        TextureFilteringComponent::linear(),
302    ) + gap;
303    y_alt -= spawn_text_block(
304        &mut universe,
305        (x, y_alt, alt_z),
306        0.25,
307        TEXT_MED,
308        alt_atlas,
309        alt_grey,
310        Some(
311            TextShadowComponent::new()
312                .with_rgba([0.0, 0.0, 0.0, 1.0])
313                .with_scale(1.15)
314                .with_offset([0.03, -0.03, 0.0015]),
315        ),
316        TextureFilteringComponent::linear(),
317    ) + gap;
318    y_alt -= spawn_text_block(
319        &mut universe,
320        (x, y_alt, alt_z),
321        0.14,
322        TEXT_SMALL,
323        alt_atlas,
324        alt_grey,
325        Some(
326            TextShadowComponent::new()
327                .with_rgba([0.0, 0.0, 0.0, 1.0])
328                .with_scale(1.15)
329                .with_offset([0.03, -0.03, 0.0015]),
330        ),
331        TextureFilteringComponent::linear(),
332    ) + gap;
333    let _ = spawn_text_block(
334        &mut universe,
335        (x, y_alt, alt_z),
336        0.08,
337        TEXT_TINY,
338        alt_atlas,
339        alt_grey,
340        Some(
341            TextShadowComponent::new()
342                .with_rgba([0.0, 0.0, 0.0, 1.0])
343                .with_scale(1.15)
344                .with_offset([0.03, -0.03, 0.0015]),
345        ),
346        TextureFilteringComponent::linear(),
347    );
348
349    universe.enable_repl();
350
351    let xr_input = universe.world.add_component(InputXRComponent::on());
352    let xr_gamepad = universe
353        .world
354        .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
355    let xr_head = universe.world.add_component(TransformComponent::new());
356    let xr_camera = universe.world.add_component(CameraXRComponent::on());
357    let _ = universe.attach(xr_input, xr_head);
358    let _ = universe.attach(xr_input, xr_gamepad);
359    let _ = universe.attach(xr_head, xr_camera);
360    universe.add(xr_input);
361
362    // Add an OpenXR component so OpenXRSystem initializes and starts polling events.
363    let xr_root = universe
364        .world
365        .add_component(engine::ecs::component::XrComponent::on());
366    universe.add(xr_root);
367
368    // Process init-time registrations (Text expands into glyph subtrees here).
369    universe.systems.process_commands(
370        &mut universe.world,
371        &mut universe.visuals,
372        &mut universe.render_assets,
373        &mut universe.command_queue,
374    );
375
376    engine::Windowing::run_app(universe).expect("Windowing failed");
377}
examples/vtuber-joints-example.rs (line 64)
14fn main() {
15    mittens_engine::example_support::ensure_model_assets();
16    utils::logger::init();
17
18    let world = engine::ecs::World::default();
19    let mut universe = engine::Universe::new(world);
20
21    // Slow the global beat clock so beat-based animations run half as fast.
22    let clock = universe
23        .world
24        .add_component(ClockComponent::new().with_bpm(60.0));
25    universe.add(clock);
26
27    // Light pink background.
28    let background = universe
29        .world
30        .add_component(BackgroundColorComponent::new());
31    let background_c = universe
32        .world
33        .add_component(ColorComponent::rgba(1.0, 0.82, 0.90, 1.0));
34    let _ = universe.world.add_child(background, background_c);
35    universe.add(background);
36
37    // Small ambient so shadowed areas aren't pure black.
38    let ambient = universe
39        .world
40        .add_component(AmbientLightComponent::rgb(0.10, 0.10, 0.12));
41    universe.add(ambient);
42
43    // --- Camera rig (WASD + mouse) ---
44    let input = universe
45        .world
46        .add_component(InputComponent::new().with_speed(1.5));
47    let input_mode = universe.world.add_component(
48        InputTransformModeComponent::forward_z()
49            .with_fps_rotation()
50            .with_roll_axis_y(),
51    );
52    let _ = universe.attach(input, input_mode);
53
54    // Start slightly pulled back looking towards the origin.
55    let rig_transform = universe
56        .world
57        .add_component(TransformComponent::new().with_position(0.0, 0.0, 6.0));
58    let _ = universe.attach(input, rig_transform);
59
60    let camera3d = universe.world.add_component(Camera3DComponent::new());
61    let _ = universe.attach(rig_transform, camera3d);
62
63    // Pointer so gizmos can be interacted with.
64    let pointer = universe.world.add_component(PointerComponent::new());
65    let _ = universe.attach(camera3d, pointer);
66
67    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
68    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
69
70    universe.add(input);
71
72    // --- lighting ---
73    let sun = universe.world.add_component(
74        DirectionalLightComponent::new()
75            .with_intensity(1.2)
76            .with_color(1.0, 0.98, 0.94),
77    );
78    let sun_dir = universe
79        .world
80        .add_component(TransformComponent::new().with_position(0.0, -0.35, 1.0));
81    let _ = universe.attach(sun_dir, sun);
82    universe.add(sun_dir);
83
84    let light_transform = universe.world.add_component(
85        TransformComponent::new()
86            .with_position(1.0, 6.0, 3.0)
87            .with_scale(0.1, 0.1, 0.1),
88    );
89    let light = universe.world.add_component(
90        engine::ecs::component::PointLightComponent::new()
91            .with_distance(120.0)
92            .with_color(1.0, 1.0, 1.0),
93    );
94    let _ = universe.attach(light_transform, light);
95    universe.add(light_transform);
96
97    // --- VTuber model ---
98    let model_uri = "assets/models/pc-rei.hoodie.glb";
99
100    // Wrap the model subtree in an editor root so transform-only glTF nodes can be visualized
101    // (and thus raycasted/selected) without affecting non-editor scenes.
102    let editor_root = universe.world.add_component(EditorComponent::new());
103
104    let model_root = universe.world.add_component(TransformComponent::new());
105    let model = universe.world.add_component(GLTFComponent::new(model_uri));
106
107    // emissive for pc-rei
108    let emissive = universe.world.add_component(EmissiveComponent::on());
109    let _ = universe.attach(model, emissive);
110
111    let xr_input = universe.world.add_component(InputXRComponent::on());
112    let xr_gamepad = universe
113        .world
114        .add_component(engine::ecs::component::InputXRGamepadComponent::new().speed(1.5));
115    let xr_head = universe.world.add_component(TransformComponent::new());
116    let xr_camera = universe.world.add_component(CameraXRComponent::on());
117    let _ = universe.attach(xr_input, xr_head);
118    let _ = universe.attach(xr_input, xr_gamepad);
119    let _ = universe.attach(xr_head, xr_camera);
120    let xr_pointer = universe.world.add_component(PointerComponent::new());
121    let _ = universe.attach(xr_camera, xr_pointer);
122    let _ = universe.attach(xr_head, editor_root);
123
124    let _ = universe.attach(model_root, model);
125
126    let _ = universe.attach(editor_root, model_root);
127
128    // Initialize the editor root so GLTFComponent gets registered.
129    universe.add(xr_input);
130    universe.add(editor_root);
131
132    // --- Background clouds (occluded + lit) ---
133    let bg_root = universe.world.add_component(
134        engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
135    );
136    universe.add(bg_root);
137    let mut cloud_params = example_util::CloudRingParams::default();
138    cloud_params.cloud_count = 8; // +3 clusters
139    cloud_params.angle_jitter = 0.35;
140    cloud_params.high_y_probability = 0.5;
141    cloud_params.high_y_multiplier = 1.5;
142    cloud_params.seed = 0x57_55_B0_01u32;
143    example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
144
145    // --- Simple environment ---
146    let spawn_cube = |universe: &mut engine::Universe,
147                      position: (f32, f32, f32),
148                      scale: (f32, f32, f32),
149                      color: (f32, f32, f32, f32)| {
150        let transform = universe.world.add_component(
151            TransformComponent::new()
152                .with_position(position.0, position.1, position.2)
153                .with_scale(scale.0, scale.1, scale.2),
154        );
155        let renderable = universe.world.add_component(RenderableComponent::cube());
156        let color = universe
157            .world
158            .add_component(ColorComponent::rgba(color.0, color.1, color.2, color.3));
159
160        let _ = universe.attach(transform, renderable);
161        let _ = universe.attach(renderable, color);
162
163        universe.add(transform);
164    };
165
166    // floor
167    spawn_cube(
168        &mut universe,
169        (0.0, -0.05, 0.0),
170        (10.0, 0.1, 10.0),
171        (0.92, 0.92, 0.92, 1.0),
172    );
173
174    // back wall
175    spawn_cube(
176        &mut universe,
177        (-3.0, 1.5, -5.0),
178        (3.0, 3.0, 1.0),
179        (0.95, 0.94, 0.96, 1.0),
180    );
181
182    // desk
183    spawn_cube(
184        &mut universe,
185        (0.0, 0.35, 1.0),
186        (1.0, 0.75, 0.5),
187        (0.75, 0.70, 0.65, 1.0),
188    );
189
190    // --- Editor-side stacked cubes (inside the editor subtree for picking/gizmos) ---
191    {
192        let spawn_editor_cube = |universe: &mut engine::Universe,
193                                 editor_root: engine::ecs::ComponentId,
194                                 name: &str,
195                                 position: (f32, f32, f32),
196                                 scale: (f32, f32, f32),
197                                 color: (f32, f32, f32, f32)| {
198            let transform = universe.world.add_component_boxed_named(
199                format!("{name}_t"),
200                Box::new(
201                    TransformComponent::new()
202                        .with_position(position.0, position.1, position.2)
203                        .with_scale(scale.0, scale.1, scale.2),
204                ),
205            );
206            let renderable = universe.world.add_component_boxed_named(
207                format!("{name}_r"),
208                Box::new(RenderableComponent::cube()),
209            );
210            let color_comp = universe.world.add_component_boxed_named(
211                format!("{name}_color"),
212                Box::new(ColorComponent::rgba(color.0, color.1, color.2, color.3)),
213            );
214            let raycastable = universe.world.add_component_boxed_named(
215                format!("{name}_raycastable"),
216                Box::new(RaycastableComponent::enabled()),
217            );
218
219            let _ = universe.world.add_child(transform, renderable);
220            let _ = universe.world.add_child(renderable, color_comp);
221            let _ = universe.world.add_child(renderable, raycastable);
222
223            // One attach into the initialized editor subtree triggers init for the new subtree.
224            let _ = universe.attach(editor_root, transform);
225        };
226
227        // Place the stack beside the desk (a bit to the right).
228        let stack_x = 1.35;
229        let stack_z = 1.0;
230        let s = 0.25;
231        let half = 0.5 * s;
232        let light_brown = (0.80, 0.72, 0.55, 1.0);
233        let cyan = (0.20, 1.00, 1.00, 1.0);
234
235        spawn_editor_cube(
236            &mut universe,
237            editor_root,
238            "editor_stack_0",
239            (stack_x, half, stack_z),
240            (s, s, s),
241            light_brown,
242        );
243        spawn_editor_cube(
244            &mut universe,
245            editor_root,
246            "editor_stack_1",
247            (stack_x, half + 1.0 * s, stack_z),
248            (s, s, s),
249            light_brown,
250        );
251        spawn_editor_cube(
252            &mut universe,
253            editor_root,
254            "editor_stack_2",
255            (stack_x, half + 2.0 * s, stack_z),
256            (s, s, s),
257            light_brown,
258        );
259        spawn_editor_cube(
260            &mut universe,
261            editor_root,
262            "editor_stack_top",
263            (stack_x, half + 3.0 * s, stack_z),
264            (s, s, s),
265            cyan,
266        );
267    }
268
269    let xr_root = universe
270        .world
271        .add_component(engine::ecs::component::XrComponent::on());
272    universe.add(xr_root);
273
274    universe.systems.process_commands(
275        &mut universe.world,
276        &mut universe.visuals,
277        &mut universe.render_assets,
278        &mut universe.command_queue,
279    );
280
281    // Spawn the glTF subtree once up-front so joint ComponentIds exist.
282    {
283        let systems = &mut universe.systems;
284        systems.gltf.tick_with_queue(
285            &mut universe.world,
286            &mut universe.visuals,
287            &mut systems.skinned_mesh,
288            &mut universe.command_queue,
289            0.0,
290        );
291    }
292    universe.systems.process_commands(
293        &mut universe.world,
294        &mut universe.visuals,
295        &mut universe.render_assets,
296        &mut universe.command_queue,
297    );
298
299    // Register imported meshes into RenderAssets early so we can inspect skin weights
300    // (textures will still be uploaded later during normal rendering).
301    universe
302        .systems
303        .gltf
304        .flush_mesh_imports_only(&mut universe.render_assets);
305
306    // --- Joint printout + animation binding (in the example) ---
307    let all_joints = collect_joint_transforms(
308        &universe.world,
309        &universe.systems.skinned_mesh,
310        &universe.visuals,
311        model,
312        model_root,
313    );
314    println!("[vtuber-joints-example] joints found: {}", all_joints.len());
315    for (i, (node_index, joint_tx)) in all_joints.iter().enumerate() {
316        println!("  joint[{i:03}] node_index={node_index} transform={joint_tx:?}");
317    }
318
319    let node_index_to_transform: HashMap<usize, engine::ecs::ComponentId> =
320        all_joints.iter().copied().collect();
321
322    // Example settings are hardcoded to keep this example simple.
323    let joint_offset: usize = 0;
324    let wiggle_count: usize = 16;
325
326    let target_mesh_key = "pc-rei.hoodie:Body_(merged).baked:prim0".to_string();
327    let target_joint_names: Vec<String> = vec![
328        "J_Bip_L_UpperArm".to_string(),
329        "J_Bip_R_UpperArm".to_string(),
330    ];
331
332    let print_transform_updates: bool = false;
333
334    let selected_joint_transforms: Vec<(usize, engine::ecs::ComponentId)> =
335        select_named_joints(&universe.world, &all_joints, &target_joint_names)
336            .or_else(|| {
337                select_body_prim0_influencers(
338                    &universe,
339                    model_root,
340                    &target_mesh_key,
341                    wiggle_count,
342                    &node_index_to_transform,
343                )
344            })
345            .unwrap_or_else(|| select_joint_range(&all_joints, joint_offset, wiggle_count));
346    println!(
347        "[vtuber-joints-example] wiggle selection: target_mesh_key='{}' offset={} count={} selected={}",
348        target_mesh_key,
349        joint_offset,
350        wiggle_count,
351        selected_joint_transforms.len()
352    );
353
354    debug_print_selected_joint_influence(
355        &universe,
356        &target_mesh_key,
357        model_root,
358        &selected_joint_transforms,
359    );
360
361    println!("[vtuber-joints-example] selected joints:");
362    for (i, (node_index, joint_tx)) in selected_joint_transforms.iter().enumerate() {
363        let name = universe
364            .world
365            .get_component_record(*joint_tx)
366            .map(|n| n.name.as_str())
367            .unwrap_or("<unknown>");
368        println!("  sel[{i:02}] node_index={node_index} name={name} transform={joint_tx:?}");
369    }
370
371    if print_transform_updates {
372        println!("[vtuber-joints-example] note: joint animation disabled");
373    }
374
375    universe.systems.process_commands(
376        &mut universe.world,
377        &mut universe.visuals,
378        &mut universe.render_assets,
379        &mut universe.command_queue,
380    );
381
382    universe.enable_repl();
383    engine::Windowing::run_app(universe).expect("Windowing failed");
384}
Source

pub fn disabled() -> Self

Source

pub fn with_enabled(self, enabled: bool) -> Self

Trait Implementations§

Source§

impl Clone for PointerComponent

Source§

fn clone(&self) -> PointerComponent

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

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

Performs copy-assignment from source. Read more
Source§

impl Component for PointerComponent

Source§

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

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

fn set_id(&mut self, component: ComponentId)

Source§

fn as_any(&self) -> &dyn Any

Source§

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

Source§

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

Called when component is added to the World
Source§

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

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

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

Called when component is removed from the World.
Source§

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

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

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

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

impl Copy for PointerComponent

Source§

impl Debug for PointerComponent

Source§

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

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

impl Default for PointerComponent

Source§

fn default() -> Self

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

Auto Trait Implementations§

Blanket Implementations§

Source§

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

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

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

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

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

Source§

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

Mutably borrows from an owned value. Read more
Source§

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

Source§

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

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

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

Source§

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

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

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

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

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

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

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

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

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

Source§

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

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

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

Source§

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

Source§

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

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

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

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

fn in_current_span(self) -> Instrumented<Self>

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

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

Source§

fn into(self) -> U

Calls U::from(self).

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

Source§

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

Source§

fn into_sample(self) -> T

Source§

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

Source§

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

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

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

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

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

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

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

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

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

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

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

Source§

fn to_sample_(self) -> U

Source§

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

Source§

type Error = Infallible

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

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

Performs the conversion.
Source§

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

Source§

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

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

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

Performs the conversion.
Source§

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

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

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

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

fn with_current_subscriber(self) -> WithDispatch<Self>

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