cvkg-core 0.3.1

Cyber Viking Kvasir Graph (CVKG) - High-fidelity agentic UI framework
Documentation
use serde::{Deserialize, Serialize};

/// A 3D mesh containing vertex and index data.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Mesh {
    pub vertices: Vec<[f32; 3]>,
    pub normals: Vec<[f32; 3]>,
    pub indices: Vec<u32>,
    pub tex_coords: Vec<[f32; 2]>,  // ← NEW: UV channel 0
}
impl Mesh {
    pub fn from_obj(data: &[u8]) -> anyhow::Result<Vec<Self>> {
        let mut cursor = std::io::Cursor::new(data);
        let (models, _) = tobj::load_obj_buf(&mut cursor, &tobj::LoadOptions::default(), |_| {
            Ok((Vec::new(), Default::default()))
        })?;
        let mut meshes = Vec::new();
        for m in models {
            let mesh = m.mesh;
            let vertices: Vec<[f32; 3]> = mesh
                .positions
                .chunks_exact(3)
                .map(|c| [c[0], c[1], c[2]])
                .collect();
            let normals = if mesh.normals.is_empty() {
                vec![[0.0, 0.0, 1.0]; vertices.len()]
            } else {
                mesh.normals.chunks(3).map(|c| [c[0], c[1], c[2]]).collect()
            };
            let tex_coords = if mesh.texcoords.is_empty() {
                vec![[0.0, 0.0]; vertices.len()]
            } else {
                mesh.texcoords.chunks(2).map(|c| [c[0], c[1]]).collect()
            };
            meshes.push(Mesh {
                vertices,
                normals,
                indices: mesh.indices,
                tex_coords,
            });
        }
        // Debug invariant: every mesh must have matching vertex/normal/texcoord counts
        for m in &meshes {
            debug_assert_eq!(
                m.vertices.len(),
                m.normals.len(),
                "Mesh vertex/normal count mismatch after normal generation"
            );
            debug_assert_eq!(
                m.vertices.len(),
                m.tex_coords.len(),
                "Mesh vertex/tex_coord count mismatch"
            );
        }
        Ok(meshes)
    }
    pub fn from_stl(data: &[u8]) -> anyhow::Result<Self> {
        let stl = cvkg_stl::parse_bytes(data)
            .map_err(|e| anyhow::anyhow!("STL parse failed: {e}"))?;
        let vertex_count = stl.vertices.len();
        Ok(Self {
            vertices: stl.vertices,
            normals: stl.normals,
            indices: stl.indices,
            tex_coords: vec![[0.0, 0.0]; vertex_count], // STL has no UVs
        })
    }

    /// Compute the axis-aligned bounding box (AABB) of this mesh.
    /// Returns (center, half_extents) in local mesh space.
    pub fn aabb(&self) -> (glam::Vec3, glam::Vec3) {
        if self.vertices.is_empty() {
            return (glam::Vec3::ZERO, glam::Vec3::ZERO);
        }

        let mut min = glam::Vec3::new(f32::MAX, f32::MAX, f32::MAX);
        let mut max = glam::Vec3::new(f32::MIN, f32::MIN, f32::MIN);

        for v in &self.vertices {
            let p = glam::Vec3::new(v[0], v[1], v[2]);
            min = min.min(p);
            max = max.max(p);
        }

        let center = (min + max) * 0.5;
        let half_extents = (max - min) * 0.5;
        (center, half_extents)
    }
}

// ══════════════════════════════════════════════════════════════════════════
// 3D TYPES -- Phase 1: Camera, Transform, and 2.5D layer support
// ══════════════════════════════════════════════════════════════════════════

/// A 3D transform: position, rotation (quaternion), and scale.
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Transform3D {
    pub position: glam::Vec3,
    pub rotation: glam::Quat,
    pub scale: glam::Vec3,
}

impl Default for Transform3D {
    fn default() -> Self {
        Self {
            position: glam::Vec3::ZERO,
            rotation: glam::Quat::IDENTITY,
            scale: glam::Vec3::ONE,
        }
    }
}

impl Transform3D {
    /// Convert this transform to a 4x4 model matrix (TRS order: Translation * Rotation * Scale).
    pub fn to_matrix(&self) -> glam::Mat4 {
        glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
    }

    /// Alias for [`to_matrix`](Self::to_matrix) – convenience for hierarchy crates.
    pub fn to_mat4(&self) -> glam::Mat4 {
        self.to_matrix()
    }

    /// Create a 2D-compatible transform (z=0, no rotation on z axis).
    pub fn from_2d(x: f32, y: f32, rotation: f32) -> Self {
        Self {
            position: glam::Vec3::new(x, y, 0.0),
            rotation: glam::Quat::from_rotation_z(rotation),
            scale: glam::Vec3::ONE,
        }
    }
}

/// Camera definition for 3D rendering.
#[derive(Debug, Clone, Copy)]
pub struct Camera3D {
    /// World-space camera position.
    pub position: glam::Vec3,
    /// World-space point the camera looks at.
    pub target: glam::Vec3,
    /// World-space up vector.
    pub up: glam::Vec3,
    /// Field of view in radians (perspective) or half-height (orthographic).
    pub fov_y: f32,
    /// Near clipping plane distance.
    pub near: f32,
    /// Far clipping plane distance.
    pub far: f32,
    /// If true, use perspective projection. If false, use orthographic.
    pub perspective: bool,
    /// Aspect ratio (width / height). Used for perspective projection.
    pub aspect: f32,
}

/// Material properties for 3D rendering.
#[derive(Debug, Clone, PartialEq)]
pub struct Material3D {
    /// Base color (RGBA).
    pub base_color: [f32; 4],
    /// Optional base color texture name (looked up in Mega-Heim atlas).
    pub base_color_texture: Option<String>,
    /// Optional normal map texture name.
    pub normal_map_texture: Option<String>,
    /// Optional metallic-roughness (ORM) texture name.
    pub metallic_roughness_texture: Option<String>,
    /// Metallic factor (0 = dielectric, 1 = metallic).
    pub metallic: f32,
    /// Roughness factor (0 = mirror, 1 = fully diffuse).
    pub roughness: f32,
    /// Emissive color (RGB) for self-illumination.
    pub emissive: [f32; 3],
    /// Opacity (0 = transparent, 1 = opaque).
    pub opacity: f32,
    /// UV tiling scale.
    pub uv_scale: [f32; 2],
    /// UV offset.
    pub uv_offset: [f32; 2],
}

impl Default for Material3D {
    fn default() -> Self {
        Self {
            base_color: [1.0, 1.0, 1.0, 1.0],
            base_color_texture: None,
            normal_map_texture: None,
            metallic_roughness_texture: None,
            metallic: 0.0,
            roughness: 0.5,
            emissive: [0.0, 0.0, 0.0],
            opacity: 1.0,
            uv_scale: [1.0, 1.0],
            uv_offset: [0.0, 0.0],
        }
    }
}

impl Material3D {
    /// Create a simple unlit material with just a color.
    pub fn unlit(color: [f32; 4]) -> Self {
        Self {
            base_color: color,
            base_color_texture: None,
            normal_map_texture: None,
            metallic_roughness_texture: None,
            metallic: 0.0,
            roughness: 1.0,
            emissive: [0.0, 0.0, 0.0],
            opacity: color[3],
            uv_scale: [1.0, 1.0],
            uv_offset: [0.0, 0.0],
        }
    }

    /// Create a metallic material.
    pub fn metallic(color: [f32; 4], roughness: f32) -> Self {
        Self {
            base_color: color,
            base_color_texture: None,
            normal_map_texture: None,
            metallic_roughness_texture: None,
            metallic: 1.0,
            roughness: roughness.clamp(0.0, 1.0),
            emissive: [0.0, 0.0, 0.0],
            opacity: color[3],
            uv_scale: [1.0, 1.0],
            uv_offset: [0.0, 0.0],
        }
    }
}