1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/// A 3D mesh containing vertex and index data.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Mesh {
pub vertices: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub indices: Vec<u32>,
}
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()
};
meshes.push(Mesh {
vertices,
normals,
indices: mesh.indices,
});
}
// Debug invariant: every mesh must have matching vertex/normal counts
for m in &meshes {
debug_assert_eq!(
m.vertices.len(),
m.normals.len(),
"Mesh vertex/normal count mismatch after normal generation"
);
}
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}"))?;
Ok(Self {
vertices: stl.vertices,
normals: stl.normals,
indices: stl.indices,
})
}
}
// ══════════════════════════════════════════════════════════════════════════
// 3D TYPES -- Phase 1: Camera, Transform, and 2.5D layer support
// ══════════════════════════════════════════════════════════════════════════
/// A 3D transform: position, rotation (quaternion), and scale.
#[derive(Debug, Clone, Copy, PartialEq)]
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.
pub fn to_matrix(&self) -> glam::Mat4 {
glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
}
/// 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, Copy, PartialEq)]
pub struct Material3D {
/// Base color (RGBA).
pub base_color: [f32; 4],
/// 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,
}
impl Default for Material3D {
fn default() -> Self {
Self {
base_color: [1.0, 1.0, 1.0, 1.0],
metallic: 0.0,
roughness: 0.5,
emissive: [0.0, 0.0, 0.0],
opacity: 1.0,
}
}
}
impl Material3D {
/// Create a simple unlit material with just a color.
pub fn unlit(color: [f32; 4]) -> Self {
Self {
base_color: color,
metallic: 0.0,
roughness: 1.0,
emissive: [0.0, 0.0, 0.0],
opacity: color[3],
}
}
/// Create a metallic material.
pub fn metallic(color: [f32; 4], roughness: f32) -> Self {
Self {
base_color: color,
metallic: 1.0,
roughness: roughness.clamp(0.0, 1.0),
emissive: [0.0, 0.0, 0.0],
opacity: color[3],
}
}
}