dioxus-three 0.0.4

A Three.js 3D model viewer for Dioxus - supports Desktop, Web (WASM), and Mobile
Documentation
# Pointer Events & Selection

Dioxus Three provides pointer event handling and object selection across all platforms.

## Overview

The interaction system consists of:
- **Raycasting**: Hit detection for pointer events
- **Selection**: Click to select, multi-select with Shift
- **Pointer Events**: `on_pointer_down`, `on_pointer_move`, `on_pointer_up`

## Enabling Raycasting

Raycasting is enabled by default. To configure it:

```rust
ThreeView {
    raycast: RaycastConfig {
        enabled: true,
        recursive: true,
        max_distance: 100.0,
        layer_mask: None,
    },
}
```

## Selection

### Basic Selection

```rust
let mut selection = use_signal(|| Selection::empty());

rsx! {
    ThreeView {
        models: models(),
        selection: Some(selection()),
        selection_mode: SelectionMode::Single,
        on_selection_change: move |sel| {
            selection.set(sel);
        },
    }
}
```

### Multi-Selection

```rust
ThreeView {
    selection_mode: SelectionMode::Multiple,
    // Shift+click to add/remove from selection
}
```

### Selection Styling

```rust
ThreeView {
    selection_style: SelectionStyle {
        outline: true,
        outline_color: "#DEC647".to_string(),
        outline_width: 2.0,
        highlight: true,
        highlight_color: "#DEC647".to_string(),
        highlight_opacity: 0.3,
        show_gizmo: true,
    },
}
```

The default selection visual is a wireframe box + inner glow around the selected object. The outline scales with the object.

### Selection API

```rust
let sel = Selection::empty();              // No selection
let sel = Selection::with_mode(SelectionMode::Single);
sel.select(EntityId(0));                   // Select entity 0
let primary = sel.primary();               // Option<EntityId>
let is_selected = sel.is_selected(EntityId(0)); // bool
```

## Pointer Events

### Basic Pointer Events

```rust
ThreeView {
    id: Some("main-view".to_string()),
    on_pointer_down: move |event: PointerEvent| {
        println!("Down at {:?}", event.screen_position);
        if let Some(hit) = event.hit {
            println!("Hit entity: {:?}", hit.entity_id);
        }
    },
    on_pointer_move: move |event: PointerEvent| {
        // Called on hover/drag
    },
    on_pointer_up: move |event: PointerEvent| {
        println!("Up at {:?}", event.screen_position);
    },
}
```

### Pointer Event Structure

```rust
pub struct PointerEvent {
    pub hit: Option<HitInfo>,           // Hitted entity info (if any)
    pub screen_position: Vector2,       // Screen coordinates in pixels
    pub ndc_position: Vector2,          // NDC (-1 to 1)
    pub button: Option<MouseButton>,    // Left | Right | Middle
    pub shift_key: bool,
    pub ctrl_key: bool,
    pub alt_key: bool,
}

pub struct HitInfo {
    pub entity_id: EntityId,
    pub point: Vector3,
    pub normal: Vector3,
    pub uv: Option<Vector2>,
    pub distance: f32,
}
```

### Pointer Drag Events

```rust
ThreeView {
    on_pointer_drag: move |event: PointerDragEvent| {
        println!("Drag delta: {:?}", event.delta);
    },
}
```

## Complete Example: Selection + Transform Readout

```rust
#[component]
fn InteractiveScene() -> Element {
    let mut selection = use_signal(|| Selection::empty());
    let mut gizmo = use_signal(|| None::<Gizmo>);
    let mut transform_overrides = use_signal(|| HashMap::<usize, GizmoTransform>::new());

    let models = use_signal(|| vec![
        ModelConfig::new("model.glb", ModelFormat::Glb)
    ]);

    // Build model configs WITHOUT baking overrides to prevent reloads during drag
    let model_configs: Vec<ModelConfig> = models.read()
        .iter()
        .map(|m| m.config.clone())
        .collect();

    rsx! {
        div {
            ThreeView {
                models: model_configs,
                selection: Some(selection()),
                selection_mode: SelectionMode::Single,
                on_selection_change: move |sel| {
                    selection.set(sel.clone());
                    gizmo.set(sel.primary().map(|id| Gizmo::new(id)));
                },
                gizmo: gizmo(),
                on_gizmo_drag: move |event: GizmoEvent| {
                    transform_overrides.write().insert(event.target.0, event.transform);
                    if event.is_finished {
                        // Persist: write back to your app state
                        println!("Final transform: {:?}", event.transform);
                    }
                },
            }

            // Transform readout UI
            if let Some(primary) = selection().primary() {
                let tf = transform_overrides.read()
                    .get(&primary.0)
                    .cloned()
                    .unwrap_or_else(|| {
                        let m = &models.read()[primary.0].config;
                        GizmoTransform {
                            position: Vector3::new(m.pos_x, m.pos_y, m.pos_z),
                            rotation: Vector3::new(m.rot_x.to_radians(), m.rot_y.to_radians(), m.rot_z.to_radians()),
                            scale: Vector3::new(m.scale, m.scale, m.scale),
                        }
                    });

                div { class: "transform-panel",
                    p { "Position: ({:.2}, {:.2}, {:.2})", tf.position.x, tf.position.y, tf.position.z }
                    p { "Rotation: ({:.2}, {:.2}, {:.2})", tf.rotation.x, tf.rotation.y, tf.rotation.z }
                    p { "Scale: ({:.2}, {:.2}, {:.2})", tf.scale.x, tf.scale.y, tf.scale.z }
                }
            }
        }
    }
}
```

### Important: Transform Persistence Pattern

When using gizmos, the gizmo directly manipulates JS-side Three.js objects. To keep the UI responsive and avoid full scene reloads:

1. **Pass raw configs** to `ThreeView` (without baking `transform_overrides` into `models`)
2. **Store overrides separately** in a `HashMap<EntityId, GizmoTransform>`
3. **Read from overrides** for UI display
4. **Persist on `is_finished`** by writing back to your canonical app state

Baking overrides into `props.models` causes the model config to change every frame during drag, triggering a full scene reload and choppy performance.

## Platform Behavior

### Desktop
- Selection via raycast against scene objects
- Gizmo interaction via `THREE.TransformControls`
- Gizmo handles render on top of objects (no occlusion)
- Events bridged via `document::eval` + `postMessage`

### Web
- Selection via manual raycasting in WASM
- Gizmo interaction via custom-built handles
- Gizmo handles render on top of objects (no occlusion)
- Events bridged via `wasm_bindgen` closures

### Mobile
- Same as Desktop (WebView-based)
- Touch events not yet fully tested