scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use super::visual_patch::{VisualPatchCameraEasedV1, VisualPatchResultV1, VisualPatchV1};
use super::{
    SceneHostCameraState, SceneHostCore, SceneHostEasing, SceneHostError, SceneHostErrorCode,
};
#[cfg(target_arch = "wasm32")]
use crate::OrbitControlAction;
use crate::{
    AssetFetcher, CameraBookmark, CameraKey, FramingOptions, LookupError,
    OrbitControlAction as HostOrbitControlAction, OrbitControls, PointerButton, PointerEvent,
    PointerEventKind, Scene, Vec3,
};

#[cfg(target_arch = "wasm32")]
pub(crate) const fn orbit_action_name(action: OrbitControlAction) -> &'static str {
    match action {
        OrbitControlAction::None => "none",
        OrbitControlAction::BeginOrbit => "begin_orbit",
        OrbitControlAction::Orbit => "orbit",
        OrbitControlAction::Pan => "pan",
        OrbitControlAction::Zoom => "zoom",
        OrbitControlAction::End => "end",
    }
}

impl<F: AssetFetcher> SceneHostCore<F> {
    pub fn camera_state(&self) -> SceneHostCameraState {
        SceneHostCameraState::from_controls(&self.camera_controls)
    }

    pub fn get_camera(&self) -> SceneHostCameraState {
        self.camera_state()
    }

    pub fn camera_json(&self) -> Result<String, SceneHostError> {
        serde_json::to_string(&self.camera_state()).map_err(|error| {
            SceneHostError::new(
                SceneHostErrorCode::Inspect,
                format!("camera state serialization failed: {error}"),
            )
        })
    }

    pub fn set_camera(&mut self, state: SceneHostCameraState) -> Result<(), SceneHostError> {
        self.cancel_camera_transition();
        self.apply_camera_state(state)
    }

    pub(super) fn apply_camera_state(
        &mut self,
        state: SceneHostCameraState,
    ) -> Result<(), SceneHostError> {
        state.validate().map_err(|message| {
            SceneHostError::new(SceneHostErrorCode::InvalidInput, message.to_owned())
        })?;
        let controls = state.into_controls();
        controls.apply_to_scene(&mut self.scene, self.active_camera)?;
        self.camera_controls = controls;
        Ok(())
    }

    pub fn set_camera_json(&mut self, json: &str) -> Result<(), SceneHostError> {
        let state: SceneHostCameraState = serde_json::from_str(json).map_err(|error| {
            SceneHostError::new(
                SceneHostErrorCode::InvalidInput,
                format!("invalid camera JSON: {error}"),
            )
        })?;
        self.set_camera(state)
    }

    pub fn set_camera_bookmark(
        &mut self,
        bookmark: &CameraBookmark,
        duration_seconds: f64,
        easing: SceneHostEasing,
    ) -> Result<VisualPatchResultV1, SceneHostError> {
        self.apply_camera_eased_patch(bookmark.state(), duration_seconds, easing)
    }

    pub fn set_camera_bookmark_json(
        &mut self,
        json: &str,
        duration_seconds: f64,
        easing: SceneHostEasing,
    ) -> Result<String, SceneHostError> {
        let bookmark: CameraBookmark = serde_json::from_str(json).map_err(|error| {
            SceneHostError::new(
                SceneHostErrorCode::InvalidInput,
                format!("invalid camera bookmark JSON: {error}"),
            )
        })?;
        let result = self.set_camera_bookmark(&bookmark, duration_seconds, easing)?;
        serde_json::to_string(&result).map_err(|error| {
            SceneHostError::new(
                SceneHostErrorCode::Inspect,
                format!("camera bookmark result serialization failed: {error}"),
            )
        })
    }

    pub(super) fn apply_camera_eased_patch(
        &mut self,
        camera: SceneHostCameraState,
        duration_seconds: f64,
        easing: SceneHostEasing,
    ) -> Result<VisualPatchResultV1, SceneHostError> {
        self.apply_patch(&VisualPatchV1 {
            camera_eased: Some(VisualPatchCameraEasedV1 {
                camera,
                duration_seconds,
                easing,
            }),
            ..VisualPatchV1::default()
        })
    }

    pub fn camera_pointer_down(
        &mut self,
        x: f32,
        y: f32,
        button: PointerButton,
    ) -> Result<HostOrbitControlAction, SceneHostError> {
        self.apply_camera_pointer(PointerEvent {
            kind: PointerEventKind::Pressed,
            position: (x, y),
            button: Some(button),
            delta: (0.0, 0.0),
            scroll_delta: 0.0,
        })
    }

    pub fn camera_pointer_move(
        &mut self,
        x: f32,
        y: f32,
        delta_x: f32,
        delta_y: f32,
    ) -> Result<HostOrbitControlAction, SceneHostError> {
        self.apply_camera_pointer(PointerEvent::moved(x, y, delta_x, delta_y))
    }

    pub fn camera_pointer_up(
        &mut self,
        x: f32,
        y: f32,
    ) -> Result<HostOrbitControlAction, SceneHostError> {
        self.apply_camera_pointer(PointerEvent::released(x, y))
    }

    pub fn camera_wheel(
        &mut self,
        x: f32,
        y: f32,
        delta_y: f32,
    ) -> Result<HostOrbitControlAction, SceneHostError> {
        self.apply_camera_pointer(PointerEvent::wheel(x, y, delta_y))
    }

    pub fn frame_node(&mut self, node: u64) -> Result<(), SceneHostError> {
        self.ensure_active_camera()?;
        let node = self.resolve_node(node)?;
        let bounds = self
            .scene
            .node_world_bounds(node, &self.assets)?
            .ok_or(LookupError::ImportHasNoBounds)?;
        self.cancel_camera_transition();
        self.scene.frame(self.active_camera, bounds)?;
        self.camera_controls =
            controls_from_scene_camera(&self.scene, self.active_camera, bounds.center())?;
        Ok(())
    }

    pub fn frame_node_product_view(&mut self, node: u64) -> Result<(), SceneHostError> {
        self.frame_node_with_preset(node, "product_viewer_default")
    }

    pub fn frame_node_with_preset(
        &mut self,
        node: u64,
        preset: &str,
    ) -> Result<(), SceneHostError> {
        self.ensure_active_camera()?;
        let node = self.resolve_node(node)?;
        let bounds = self
            .scene
            .node_world_bounds(node, &self.assets)?
            .ok_or(LookupError::ImportHasNoBounds)?;
        let width = self.viewport.logical_width().round().max(1.0) as u32;
        let height = self.viewport.logical_height().round().max(1.0) as u32;
        let (options, fill) = match preset {
            "cell_overview" => (FramingOptions::new().top(), 0.72),
            "operator_review_default" => (
                FramingOptions::new().orbit(35.0_f32.to_radians(), 14.0_f32.to_radians()),
                0.78,
            ),
            "product_viewer_default" => (FramingOptions::new().three_quarter_front_right(), 0.72),
            _ => {
                return Err(SceneHostError::new(
                    SceneHostErrorCode::InvalidInput,
                    format!("unsupported SceneHost camera preset {preset}"),
                ));
            }
        };
        let framing = self.scene.frame_bounds(
            self.active_camera,
            bounds,
            options.fill(fill).margin_px(48.0).viewport(width, height),
        )?;
        self.cancel_camera_transition();
        self.camera_controls = OrbitControls::from_framing(framing);
        Ok(())
    }

    pub fn frame_all(&mut self) -> Result<(), SceneHostError> {
        self.ensure_active_camera()?;
        let bounds = self
            .scene
            .node_world_bounds(self.scene.root(), &self.assets)?
            .ok_or(LookupError::ImportHasNoBounds)?;
        self.cancel_camera_transition();
        self.scene.frame(self.active_camera, bounds)?;
        self.camera_controls =
            controls_from_scene_camera(&self.scene, self.active_camera, bounds.center())?;
        Ok(())
    }

    pub fn frame_all_with_overlays(&mut self) -> Result<(), SceneHostError> {
        self.ensure_active_camera()?;
        let width = self.viewport.logical_width().round().max(1.0) as u32;
        let height = self.viewport.logical_height().round().max(1.0) as u32;
        let framing =
            self.scene
                .frame_all_with_overlays(self.active_camera, &self.assets, width, height)?;
        self.cancel_camera_transition();
        self.camera_controls = OrbitControls::from_framing(framing);
        Ok(())
    }

    fn apply_camera_pointer(
        &mut self,
        event: PointerEvent,
    ) -> Result<HostOrbitControlAction, SceneHostError> {
        let action = self.camera_controls.handle_pointer(event);
        if matches!(
            action,
            HostOrbitControlAction::Orbit
                | HostOrbitControlAction::Pan
                | HostOrbitControlAction::Zoom
        ) {
            self.cancel_camera_transition();
            self.camera_controls
                .apply_to_scene(&mut self.scene, self.active_camera)?;
        }
        Ok(action)
    }
}

pub(super) fn controls_from_scene_camera(
    scene: &Scene,
    camera: CameraKey,
    target: Vec3,
) -> Result<OrbitControls, SceneHostError> {
    let camera_node = scene
        .camera_node(camera)
        .ok_or(LookupError::CameraNotFound(camera))?;
    let camera_world = scene
        .world_transform(camera_node)
        .ok_or(LookupError::NodeNotFound(camera_node))?;
    let offset = camera_world.translation - target;
    let distance = offset.length().max(0.0001);
    let yaw_radians = offset.x.atan2(offset.z);
    let pitch_radians = (offset.y / distance).clamp(-1.0, 1.0).asin();
    Ok(OrbitControls::new(target, distance).with_angles(yaw_radians, pitch_radians))
}