scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use serde::{Deserialize, Serialize};

use super::inputs::validate_transform;
use super::visual_patch::VisualPatchResultV1;
use super::{SceneHostCore, SceneHostError, SceneHostErrorCode};
use crate::{
    AssetFetcher, GizmoAxis, GizmoConstraint, GizmoMode, GizmoRay, GizmoSpace, Transform,
    TransformGizmo, Vec3,
};

pub const SCENE_HOST_GIZMO_DRAG_SCHEMA_V1: &str = "scena.scene_host_gizmo_drag.v1";

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SceneHostGizmoDragV1 {
    pub schema: String,
    pub mode: SceneHostGizmoModeV1,
    #[serde(default)]
    pub space: SceneHostGizmoSpaceV1,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub constraint: Option<SceneHostGizmoConstraintV1>,
    pub start_transform: Transform,
    pub start_ray: SceneHostGizmoRayV1,
    pub current_ray: SceneHostGizmoRayV1,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SceneHostGizmoModeV1 {
    Translate,
    Rotate,
    Scale,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SceneHostGizmoAxisV1 {
    X,
    Y,
    Z,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SceneHostGizmoSpaceV1 {
    #[default]
    World,
    Local,
    ViewAligned,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SceneHostGizmoConstraintV1 {
    Axis { axis: SceneHostGizmoAxisV1 },
    Plane { axis: SceneHostGizmoAxisV1 },
    ViewPlane,
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SceneHostGizmoRayV1 {
    pub origin: [f32; 3],
    pub direction: [f32; 3],
}

impl<F: AssetFetcher> SceneHostCore<F> {
    pub fn apply_gizmo_drag_json(
        &mut self,
        target: u64,
        request_json: &str,
    ) -> Result<String, SceneHostError> {
        let request: SceneHostGizmoDragV1 =
            serde_json::from_str(request_json).map_err(|error| {
                SceneHostError::new(
                    SceneHostErrorCode::InvalidInput,
                    format!("invalid SceneHost gizmo drag JSON: {error}"),
                )
            })?;
        let result = self.apply_gizmo_drag(target, &request)?;
        serde_json::to_string(&result).map_err(|error| {
            SceneHostError::new(
                SceneHostErrorCode::InvalidInput,
                format!("SceneHost gizmo drag result serialization failed: {error}"),
            )
        })
    }

    pub fn apply_gizmo_drag(
        &mut self,
        target: u64,
        request: &SceneHostGizmoDragV1,
    ) -> Result<VisualPatchResultV1, SceneHostError> {
        request.validate_schema()?;
        let start = validate_transform(request.start_transform)?;
        let gizmo = request.gizmo();
        let transformed = gizmo
            .drag_transform(
                start,
                request.start_ray.to_gizmo_ray("start_ray")?,
                request.current_ray.to_gizmo_ray("current_ray")?,
            )
            .ok_or_else(|| {
                invalid_input(
                    "gizmo drag did not resolve a finite transform; check rays and constraint",
                )
            })?;
        let patch = gizmo.to_visual_patch(target, transformed);
        self.apply_patch(&patch)
    }
}

impl SceneHostGizmoDragV1 {
    fn validate_schema(&self) -> Result<(), SceneHostError> {
        if self.schema == SCENE_HOST_GIZMO_DRAG_SCHEMA_V1 {
            return Ok(());
        }
        Err(invalid_input(format!(
            "unsupported SceneHost gizmo drag schema {}; expected {}",
            self.schema, SCENE_HOST_GIZMO_DRAG_SCHEMA_V1
        )))
    }

    fn gizmo(&self) -> TransformGizmo {
        let mut gizmo = TransformGizmo::new(self.mode.into()).with_space(self.space.into());
        if let Some(constraint) = self.constraint {
            gizmo = gizmo.with_constraint(constraint.into());
        }
        gizmo
    }
}

impl SceneHostGizmoRayV1 {
    fn to_gizmo_ray(self, field: &str) -> Result<GizmoRay, SceneHostError> {
        GizmoRay::new(
            Vec3::from_array(self.origin),
            Vec3::from_array(self.direction),
        )
        .ok_or_else(|| {
            invalid_input(format!(
                "{field} must have finite origin and non-zero finite direction"
            ))
        })
    }
}

impl From<SceneHostGizmoModeV1> for GizmoMode {
    fn from(value: SceneHostGizmoModeV1) -> Self {
        match value {
            SceneHostGizmoModeV1::Translate => Self::Translate,
            SceneHostGizmoModeV1::Rotate => Self::Rotate,
            SceneHostGizmoModeV1::Scale => Self::Scale,
        }
    }
}

impl From<SceneHostGizmoAxisV1> for GizmoAxis {
    fn from(value: SceneHostGizmoAxisV1) -> Self {
        match value {
            SceneHostGizmoAxisV1::X => Self::X,
            SceneHostGizmoAxisV1::Y => Self::Y,
            SceneHostGizmoAxisV1::Z => Self::Z,
        }
    }
}

impl From<SceneHostGizmoSpaceV1> for GizmoSpace {
    fn from(value: SceneHostGizmoSpaceV1) -> Self {
        match value {
            SceneHostGizmoSpaceV1::World => Self::World,
            SceneHostGizmoSpaceV1::Local => Self::Local,
            SceneHostGizmoSpaceV1::ViewAligned => Self::ViewAligned,
        }
    }
}

impl From<SceneHostGizmoConstraintV1> for GizmoConstraint {
    fn from(value: SceneHostGizmoConstraintV1) -> Self {
        match value {
            SceneHostGizmoConstraintV1::Axis { axis } => Self::Axis(axis.into()),
            SceneHostGizmoConstraintV1::Plane { axis } => Self::Plane(axis.into()),
            SceneHostGizmoConstraintV1::ViewPlane => Self::ViewPlane,
        }
    }
}

fn invalid_input(message: impl Into<String>) -> SceneHostError {
    SceneHostError::new(SceneHostErrorCode::InvalidInput, message.into())
}