mittens-engine 0.7.0

A Vulkan and OpenXR scene engine with ECS, reactive signals, and Meow Meow scripting
# Normal Visualisation Component

A debug component that reads the normals from a parent renderable's `CpuMesh` and spawns
a child subtree of thin cyan cubes, one per vertex, oriented along each normal.

---

## Usage

Attach `NormalVisualisationComponent` as a child of a `RenderableComponent`. On init it
reads the parent mesh and spawns its visualisation subtree automatically. Removing the
component tears down the subtree via `RemoveSubtree`.

```
TransformComponent
└── RenderableComponent          ← parent renderable (existing)
    └── NormalVisualisationComponent
        └── [spawned subtree]
            ├── TransformComponent + RenderableComponent  ← normal 0
            ├── TransformComponent + RenderableComponent  ← normal 1
            └── ...
```

---

## Spawned Geometry (ノ ᵒ ᵕ ᵒ)ノ

Each vertex in the `CpuMesh` gets one child:

- **Mesh**: `MeshFactory::cube()` (builtin)
- **Color**: cyan — `[0.0, 1.0, 1.0, 1.0]`
- **Emissive**: yes — `EmissiveComponent::on()` so normals are always visible regardless of lighting
- **Scale**: `[t, 10t, t]` where `t` is a configurable thickness (default `0.01`).
  The cube is 10× taller along Y than it is wide, making it read as a line/needle.
- **Position**: `vertex.pos + normal * 5t` — base of the cube sits at the vertex
  surface, tip points away along the normal.
- **Rotation**: Y-axis of the cube aligned to the vertex normal. Computed via the
  standard "rotate [0,1,0] onto normal" quaternion (cross + dot, handle the degenerate
  antiparallel case).

---

## Fields

```rust
pub struct NormalVisualisationComponent {
    /// Thickness of each indicator cube (X and Z scale).
    /// Y scale = thickness * 10.
    pub thickness: f32,

    /// ComponentIds of spawned child transforms, for cleanup.
    spawned_roots: Vec<ComponentId>,
}

impl NormalVisualisationComponent {
    pub fn new() -> Self { ... }
    pub fn with_thickness(t: f32) -> Self { ... }
}
```

---

## Data Flow \(^▽^)/

Spawning is split across two phases because `Component::init` (the intent-handler path) has
access to `World` and `emit` but **not** to `RenderAssets` (GPU-side mesh data). The actual
vertex reads happen later, in the render-preparation path.

### Phase 1 — intent handler (`RenderableSystem::register_normal_vis`)

Triggered by the `RegisterNormalVis { component_ids }` intent emitted in
`NormalVisualisationComponent::init()`.

1. Walk up the parent chain of the `NormalVisualisationComponent` to find the nearest
   ancestor `RenderableComponent`.
2. Read its `CpuMeshHandle` and `thickness` value.
3. Push `(nv_id, renderable_id, base_mesh, thickness)` onto
   `RenderableSystem::pending_normal_vis`**no mesh data read yet**.

### Phase 2 — render preparation (`RenderableSystem::spawn_pending_normal_vis`)

Called from `RenderableSystem::flush_pending`, which is invoked by
`SystemWorld::prepare_render` (the render path). At this point `RenderAssets` is available.

1. Drain `pending_normal_vis`.
2. For each entry:
   - **Skip** if `NormalVisualisationComponent` no longer exists (entity removed between phases).
   - **Skip** if `spawned_roots` is already populated (double-init guard).
   - **Defer** (re-push to `pending_normal_vis`) if `render_assets.cpu_mesh(handle)` returns
     `None` — the mesh upload may not have completed yet; will retry next frame.
3. For each vertex in the `CpuMesh`:
   - Compute position: `vertex.pos + normal * half_height` (base sits at surface).
   - Compute rotation: quaternion that rotates Y-axis onto the vertex normal.
   - Spawn `TransformComponent + RenderableComponent(cube, scale=[t, 10t, t]) + ColorComponent(cyan) + EmissiveComponent`.
   - Call `world.init_component_tree(root_id, queue)` — queues the subtree's `init` signals
     into the provided `CommandQueue`.
4. Write the collected root IDs back into `NormalVisualisationComponent::spawned_roots`.

The `CommandQueue` is threaded through `SystemWorld::prepare_render` →
`RenderableSystem::flush_pending` → `spawn_pending_normal_vis` and is drained on the next
engine tick, completing the two-phase spawn cycle.

---

## Init / Cleanup

**`init()`**:
1. Emit `RegisterNormalVis { component_ids: vec![self_id] }` intent.
2. `RenderableSystem::register_normal_vis` handles it (Phase 1 above).
3. Actual subtree spawning happens in Phase 2 (next `prepare_render` call).

**`cleanup()`**:
- Emit `RemoveSubtree` for each ID in `spawned_roots`.

---

## Notes

- **Duplicate normals**: flat-shaded meshes have identical normals per triangle face (3
  vertices share one normal). The visualisation will stack three cubes on top of each
  other at those positions — visually fine for a debug tool, no need to deduplicate.
- **No update path**: the subtree is spawned once on init. If the parent mesh changes,
  remove and re-add `NormalVisualisationComponent` to refresh.
- **MaterialHandle**: uses `TOON_MESH` (no vertex colors needed — uniform cyan via
  `ColorComponent`). No new pipeline required.
- **Debug only**: not intended for production use. Could be gated behind a feature flag
  or simply left as an opt-in component.