Skip to main content

proof_engine/glyph/
glyph_mesh.rs

1//! Generate 3D extruded meshes from 2D glyph outlines.
2//!
3//! Pipeline: GlyphOutline → triangulate front face → extrude → side faces → GlyphMesh
4//!
5//! Triangulation uses ear clipping with hole bridging. Extrusion duplicates the
6//! front face offset along Z. Side faces connect corresponding edge vertices.
7
8use glam::{Vec2, Vec3};
9use std::collections::HashMap;
10
11use super::font_to_mesh::{GlyphOutline, Contour, OutlineCache, assign_holes_to_outers, signed_area};
12
13// ── Vertex3D ────────────────────────────────────────────────────────────────
14
15#[repr(C)]
16#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
17pub struct Vertex3D {
18    pub position: [f32; 3],
19    pub normal: [f32; 3],
20    pub uv: [f32; 2],
21}
22
23impl Vertex3D {
24    pub fn new(position: Vec3, normal: Vec3, uv: Vec2) -> Self {
25        Self {
26            position: position.to_array(),
27            normal: normal.to_array(),
28            uv: uv.to_array(),
29        }
30    }
31
32    pub fn pos(&self) -> Vec3 { Vec3::from(self.position) }
33    pub fn norm(&self) -> Vec3 { Vec3::from(self.normal) }
34}
35
36// ── GlyphMesh ───────────────────────────────────────────────────────────────
37
38#[derive(Clone, Debug)]
39pub struct GlyphMesh {
40    pub vertices: Vec<Vertex3D>,
41    pub indices: Vec<u32>,
42    pub character: char,
43    pub extrusion_depth: f32,
44    pub triangle_count: u32,
45    pub bounds_min: Vec3,
46    pub bounds_max: Vec3,
47}
48
49impl GlyphMesh {
50    pub fn vertex_count(&self) -> usize { self.vertices.len() }
51    pub fn index_count(&self) -> usize { self.indices.len() }
52
53    pub fn vertex_bytes(&self) -> &[u8] {
54        bytemuck::cast_slice(&self.vertices)
55    }
56
57    pub fn index_bytes(&self) -> &[u8] {
58        bytemuck::cast_slice(&self.indices)
59    }
60}
61
62#[derive(Clone, Debug)]
63pub struct MeshStats {
64    pub vertices: usize,
65    pub triangles: usize,
66    pub bytes: usize,
67}
68
69pub fn mesh_stats(mesh: &GlyphMesh) -> MeshStats {
70    MeshStats {
71        vertices: mesh.vertices.len(),
72        triangles: mesh.triangle_count as usize,
73        bytes: mesh.vertices.len() * std::mem::size_of::<Vertex3D>()
74            + mesh.indices.len() * std::mem::size_of::<u32>(),
75    }
76}
77
78// ── Ear Clipping Triangulation ──────────────────────────────────────────────
79
80/// Check if point p is inside triangle (a, b, c) using barycentric coordinates.
81pub fn point_in_triangle(p: Vec2, a: Vec2, b: Vec2, c: Vec2) -> bool {
82    let v0 = c - a;
83    let v1 = b - a;
84    let v2 = p - a;
85
86    let dot00 = v0.dot(v0);
87    let dot01 = v0.dot(v1);
88    let dot02 = v0.dot(v2);
89    let dot11 = v1.dot(v1);
90    let dot12 = v1.dot(v2);
91
92    let inv_denom = 1.0 / (dot00 * dot11 - dot01 * dot01);
93    let u = (dot11 * dot02 - dot01 * dot12) * inv_denom;
94    let v = (dot00 * dot12 - dot01 * dot02) * inv_denom;
95
96    u >= 0.0 && v >= 0.0 && u + v <= 1.0
97}
98
99/// Cross product of 2D vectors (returns Z component).
100fn cross2d(a: Vec2, b: Vec2) -> f32 {
101    a.x * b.y - a.y * b.x
102}
103
104/// Check if vertex at `curr` is convex (left turn) in CCW polygon.
105fn is_convex(polygon: &[Vec2], prev: usize, curr: usize, next: usize) -> bool {
106    let a = polygon[prev];
107    let b = polygon[curr];
108    let c = polygon[next];
109    cross2d(b - a, c - b) > 0.0
110}
111
112/// Check if the triangle (prev, curr, next) is an ear (no other vertex inside).
113fn is_ear(polygon: &[Vec2], indices: &[usize], prev_idx: usize, curr_idx: usize, next_idx: usize) -> bool {
114    let a = polygon[indices[prev_idx]];
115    let b = polygon[indices[curr_idx]];
116    let c = polygon[indices[next_idx]];
117
118    // Must be convex
119    if cross2d(b - a, c - b) <= 0.0 {
120        return false;
121    }
122
123    // No other vertex inside
124    for (i, &vi) in indices.iter().enumerate() {
125        if i == prev_idx || i == curr_idx || i == next_idx { continue; }
126        if point_in_triangle(polygon[vi], a, b, c) {
127            return false;
128        }
129    }
130
131    true
132}
133
134/// Bridge a hole into the outer contour by finding a mutual visibility edge.
135pub fn bridge_hole(outer: &[Vec2], hole: &[Vec2]) -> Vec<Vec2> {
136    if hole.is_empty() { return outer.to_vec(); }
137
138    // Find rightmost point in hole
139    let mut rightmost_idx = 0;
140    let mut max_x = f32::MIN;
141    for (i, p) in hole.iter().enumerate() {
142        if p.x > max_x {
143            max_x = p.x;
144            rightmost_idx = i;
145        }
146    }
147
148    // Find closest visible point in outer contour
149    let hp = hole[rightmost_idx];
150    let mut best_idx = 0;
151    let mut best_dist = f32::MAX;
152    for (i, p) in outer.iter().enumerate() {
153        let d = (p.x - hp.x).abs() + (p.y - hp.y).abs();
154        if d < best_dist {
155            best_dist = d;
156            best_idx = i;
157        }
158    }
159
160    // Build merged polygon: outer[..best_idx+1] + hole[rightmost..] + hole[..rightmost+1] + outer[best_idx..]
161    let mut result = Vec::with_capacity(outer.len() + hole.len() + 2);
162    for i in 0..=best_idx {
163        result.push(outer[i]);
164    }
165    let n_hole = hole.len();
166    for i in 0..=n_hole {
167        result.push(hole[(rightmost_idx + i) % n_hole]);
168    }
169    // Bridge back
170    result.push(outer[best_idx]);
171    for i in (best_idx + 1)..outer.len() {
172        result.push(outer[i]);
173    }
174
175    result
176}
177
178/// Ear clipping triangulation with hole support.
179/// Returns triangle indices into the polygon array.
180pub fn ear_clip_triangulate(outer: &[Vec2], holes: &[Vec<Vec2>]) -> Vec<[usize; 3]> {
181    // Merge holes into outer contour
182    let mut polygon = outer.to_vec();
183    for hole in holes {
184        polygon = bridge_hole(&polygon, hole);
185    }
186
187    if polygon.len() < 3 { return Vec::new(); }
188
189    // Ensure CCW winding
190    if signed_area(&polygon) < 0.0 {
191        polygon.reverse();
192    }
193
194    let mut indices: Vec<usize> = (0..polygon.len()).collect();
195    let mut triangles = Vec::new();
196    let mut max_iters = polygon.len() * polygon.len();
197
198    while indices.len() > 2 && max_iters > 0 {
199        max_iters -= 1;
200        let n = indices.len();
201        let mut found_ear = false;
202
203        for i in 0..n {
204            let prev = (i + n - 1) % n;
205            let next = (i + 1) % n;
206
207            if is_ear(&polygon, &indices, prev, i, next) {
208                triangles.push([indices[prev], indices[i], indices[next]]);
209                indices.remove(i);
210                found_ear = true;
211                break;
212            }
213        }
214
215        if !found_ear {
216            // Degenerate polygon, force remove a vertex
217            if indices.len() > 2 {
218                let n = indices.len();
219                triangles.push([indices[0], indices[1], indices[2]]);
220                indices.remove(1);
221            } else {
222                break;
223            }
224        }
225    }
226
227    triangles
228}
229
230// ── Extrusion ───────────────────────────────────────────────────────────────
231
232/// Generate a 3D extruded mesh from a glyph outline.
233pub fn extrude_glyph(outline: &GlyphOutline, depth: f32, ch: char) -> GlyphMesh {
234    let assignments = assign_holes_to_outers(&outline.contours);
235
236    let mut all_vertices = Vec::new();
237    let mut all_indices = Vec::new();
238
239    let bounds = outline.bounds;
240    let bw = (bounds.max.x - bounds.min.x).max(0.001);
241    let bh = (bounds.max.y - bounds.min.y).max(0.001);
242
243    for (outer_idx, hole_indices) in &assignments {
244        let outer = &outline.contours[*outer_idx].points;
245        let holes: Vec<Vec<Vec2>> = hole_indices.iter()
246            .map(|&hi| outline.contours[hi].points.clone())
247            .collect();
248        let hole_refs: Vec<&[Vec2]> = holes.iter().map(|h| h.as_slice()).collect();
249
250        // Collect all contour points for this group
251        let mut all_points: Vec<Vec2> = outer.clone();
252        for h in &holes {
253            all_points.extend_from_slice(h);
254        }
255
256        // Triangulate front face
257        let hole_vecs: Vec<Vec<Vec2>> = holes.clone();
258        let tris = ear_clip_triangulate(outer, &hole_vecs);
259
260        // We need a merged polygon to map triangle indices to actual points
261        let mut merged = outer.clone();
262        for h in &holes {
263            merged = bridge_hole(&merged, h);
264        }
265
266        let base_idx = all_vertices.len() as u32;
267
268        // === FRONT FACE (Z = 0, normal = +Z) ===
269        for p in &merged {
270            let u = (p.x - bounds.min.x) / bw;
271            let v = (p.y - bounds.min.y) / bh;
272            all_vertices.push(Vertex3D::new(
273                Vec3::new(p.x, p.y, 0.0),
274                Vec3::Z,
275                Vec2::new(u, v),
276            ));
277        }
278
279        for tri in &tris {
280            all_indices.push(base_idx + tri[0] as u32);
281            all_indices.push(base_idx + tri[1] as u32);
282            all_indices.push(base_idx + tri[2] as u32);
283        }
284
285        // === BACK FACE (Z = -depth, normal = -Z, reversed winding) ===
286        let back_base = all_vertices.len() as u32;
287        for p in &merged {
288            let u = (p.x - bounds.min.x) / bw;
289            let v = (p.y - bounds.min.y) / bh;
290            all_vertices.push(Vertex3D::new(
291                Vec3::new(p.x, p.y, -depth),
292                -Vec3::Z,
293                Vec2::new(u, v),
294            ));
295        }
296
297        for tri in &tris {
298            // Reverse winding for back face
299            all_indices.push(back_base + tri[2] as u32);
300            all_indices.push(back_base + tri[1] as u32);
301            all_indices.push(back_base + tri[0] as u32);
302        }
303
304        // === SIDE FACES (connect front edge to back edge) ===
305        // Use the original outer contour (and each hole contour) for side faces
306        let mut edge_contours: Vec<&Vec<Vec2>> = vec![outer];
307        for h in &holes {
308            edge_contours.push(h);
309        }
310
311        for contour in edge_contours {
312            let n = contour.len();
313            for i in 0..n {
314                let j = (i + 1) % n;
315                let p0 = contour[i];
316                let p1 = contour[j];
317
318                // Edge direction and outward normal
319                let edge = p1 - p0;
320                let normal_2d = Vec2::new(edge.y, -edge.x).normalize_or_zero();
321                let normal = Vec3::new(normal_2d.x, normal_2d.y, 0.0);
322
323                let edge_len = edge.length();
324                let u0 = 0.0;
325                let u1 = edge_len / bw;
326
327                let side_base = all_vertices.len() as u32;
328
329                // Front-left, Front-right, Back-right, Back-left
330                all_vertices.push(Vertex3D::new(Vec3::new(p0.x, p0.y, 0.0), normal, Vec2::new(u0, 0.0)));
331                all_vertices.push(Vertex3D::new(Vec3::new(p1.x, p1.y, 0.0), normal, Vec2::new(u1, 0.0)));
332                all_vertices.push(Vertex3D::new(Vec3::new(p1.x, p1.y, -depth), normal, Vec2::new(u1, 1.0)));
333                all_vertices.push(Vertex3D::new(Vec3::new(p0.x, p0.y, -depth), normal, Vec2::new(u0, 1.0)));
334
335                // Two triangles for the quad
336                all_indices.push(side_base);
337                all_indices.push(side_base + 1);
338                all_indices.push(side_base + 2);
339                all_indices.push(side_base);
340                all_indices.push(side_base + 2);
341                all_indices.push(side_base + 3);
342            }
343        }
344    }
345
346    // If no assignments produced geometry, create a simple box as fallback
347    if all_vertices.is_empty() {
348        let b = outline.bounds;
349        let min = Vec3::new(b.min.x, b.min.y, -depth);
350        let max = Vec3::new(b.max.x, b.max.y, 0.0);
351        return create_box_mesh(min, max, ch, depth);
352    }
353
354    // Compute bounds
355    let mut bmin = Vec3::splat(f32::MAX);
356    let mut bmax = Vec3::splat(f32::MIN);
357    for v in &all_vertices {
358        let p = v.pos();
359        bmin = bmin.min(p);
360        bmax = bmax.max(p);
361    }
362
363    let tri_count = all_indices.len() as u32 / 3;
364
365    GlyphMesh {
366        vertices: all_vertices,
367        indices: all_indices,
368        character: ch,
369        extrusion_depth: depth,
370        triangle_count: tri_count,
371        bounds_min: bmin,
372        bounds_max: bmax,
373    }
374}
375
376pub(crate) fn create_box_mesh(min: Vec3, max: Vec3, ch: char, depth: f32) -> GlyphMesh {
377    let mut vertices = Vec::new();
378    let mut indices = Vec::new();
379
380    let faces: [(Vec3, Vec3, Vec3, Vec3, Vec3); 6] = [
381        (Vec3::new(min.x,min.y,max.z), Vec3::new(max.x,min.y,max.z), Vec3::new(max.x,max.y,max.z), Vec3::new(min.x,max.y,max.z), Vec3::Z),
382        (Vec3::new(max.x,min.y,min.z), Vec3::new(min.x,min.y,min.z), Vec3::new(min.x,max.y,min.z), Vec3::new(max.x,max.y,min.z), -Vec3::Z),
383        (Vec3::new(min.x,max.y,max.z), Vec3::new(max.x,max.y,max.z), Vec3::new(max.x,max.y,min.z), Vec3::new(min.x,max.y,min.z), Vec3::Y),
384        (Vec3::new(min.x,min.y,min.z), Vec3::new(max.x,min.y,min.z), Vec3::new(max.x,min.y,max.z), Vec3::new(min.x,min.y,max.z), -Vec3::Y),
385        (Vec3::new(max.x,min.y,max.z), Vec3::new(max.x,min.y,min.z), Vec3::new(max.x,max.y,min.z), Vec3::new(max.x,max.y,max.z), Vec3::X),
386        (Vec3::new(min.x,min.y,min.z), Vec3::new(min.x,min.y,max.z), Vec3::new(min.x,max.y,max.z), Vec3::new(min.x,max.y,min.z), -Vec3::X),
387    ];
388
389    for (v0, v1, v2, v3, n) in &faces {
390        let base = vertices.len() as u32;
391        vertices.push(Vertex3D::new(*v0, *n, Vec2::new(0.0, 0.0)));
392        vertices.push(Vertex3D::new(*v1, *n, Vec2::new(1.0, 0.0)));
393        vertices.push(Vertex3D::new(*v2, *n, Vec2::new(1.0, 1.0)));
394        vertices.push(Vertex3D::new(*v3, *n, Vec2::new(0.0, 1.0)));
395        indices.extend_from_slice(&[base, base+1, base+2, base, base+2, base+3]);
396    }
397
398    GlyphMesh {
399        vertices, indices, character: ch, extrusion_depth: depth,
400        triangle_count: 12, bounds_min: min, bounds_max: max,
401    }
402}
403
404/// Generate beveled edges for softer appearance.
405pub fn generate_bevel(outline_points: &[Vec2], depth: f32, bevel_size: f32) -> Vec<Vertex3D> {
406    let mut verts = Vec::new();
407    let n = outline_points.len();
408    let steps = 3u32;
409
410    for i in 0..n {
411        let j = (i + 1) % n;
412        let p0 = outline_points[i];
413        let p1 = outline_points[j];
414        let edge = p1 - p0;
415        let normal_2d = Vec2::new(edge.y, -edge.x).normalize_or_zero();
416
417        for s in 0..=steps {
418            let t = s as f32 / steps as f32;
419            let angle = t * std::f32::consts::FRAC_PI_2;
420            let offset_xy = normal_2d * bevel_size * angle.cos();
421            let offset_z = bevel_size * (1.0 - angle.sin());
422
423            // Front bevel
424            let pos = Vec3::new(p0.x + offset_xy.x, p0.y + offset_xy.y, offset_z);
425            let norm = Vec3::new(
426                normal_2d.x * angle.cos(),
427                normal_2d.y * angle.cos(),
428                angle.sin(),
429            ).normalize_or_zero();
430            verts.push(Vertex3D::new(pos, norm, Vec2::new(t, 0.0)));
431
432            // Back bevel
433            let pos_b = Vec3::new(p0.x + offset_xy.x, p0.y + offset_xy.y, -depth - offset_z);
434            let norm_b = Vec3::new(
435                normal_2d.x * angle.cos(),
436                normal_2d.y * angle.cos(),
437                -angle.sin(),
438            ).normalize_or_zero();
439            verts.push(Vertex3D::new(pos_b, norm_b, Vec2::new(t, 1.0)));
440        }
441    }
442
443    verts
444}
445
446// ── Mesh Cache ──────────────────────────────────────────────────────────────
447
448pub struct GlyphMeshCache {
449    pub meshes: HashMap<char, GlyphMesh>,
450}
451
452impl GlyphMeshCache {
453    pub fn build(outline_cache: &OutlineCache, depth: f32) -> Self {
454        let mut meshes = HashMap::new();
455        for (&ch, outline) in &outline_cache.outlines {
456            let mesh = extrude_glyph(outline, depth, ch);
457            meshes.insert(ch, mesh);
458        }
459        Self { meshes }
460    }
461
462    pub fn get(&self, ch: char) -> Option<&GlyphMesh> {
463        self.meshes.get(&ch)
464    }
465
466    pub fn len(&self) -> usize { self.meshes.len() }
467    pub fn is_empty(&self) -> bool { self.meshes.is_empty() }
468
469    pub fn total_triangles(&self) -> u32 {
470        self.meshes.values().map(|m| m.triangle_count).sum()
471    }
472
473    pub fn total_vertices(&self) -> usize {
474        self.meshes.values().map(|m| m.vertices.len()).sum()
475    }
476}
477
478// ── Tests ───────────────────────────────────────────────────────────────────
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use super::super::font_to_mesh::GlyphBounds;
484
485    fn square_outline() -> GlyphOutline {
486        GlyphOutline {
487            contours: vec![Contour {
488                points: vec![
489                    Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0),
490                    Vec2::new(1.0, 1.0), Vec2::new(0.0, 1.0),
491                ],
492                is_hole: false,
493            }],
494            advance_width: 1.0,
495            bounds: GlyphBounds { min: Vec2::ZERO, max: Vec2::ONE },
496        }
497    }
498
499    #[test]
500    fn vertex3d_size() {
501        assert_eq!(std::mem::size_of::<Vertex3D>(), 32);
502    }
503
504    #[test]
505    fn point_in_triangle_basic() {
506        assert!(point_in_triangle(
507            Vec2::new(0.3, 0.3),
508            Vec2::ZERO, Vec2::new(1.0, 0.0), Vec2::new(0.0, 1.0),
509        ));
510        assert!(!point_in_triangle(
511            Vec2::new(2.0, 2.0),
512            Vec2::ZERO, Vec2::new(1.0, 0.0), Vec2::new(0.0, 1.0),
513        ));
514    }
515
516    #[test]
517    fn ear_clip_square() {
518        let sq = vec![
519            Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0),
520            Vec2::new(1.0, 1.0), Vec2::new(0.0, 1.0),
521        ];
522        let tris = ear_clip_triangulate(&sq, &[]);
523        assert_eq!(tris.len(), 2, "Square should produce 2 triangles");
524    }
525
526    #[test]
527    fn extrude_square_produces_geometry() {
528        let outline = square_outline();
529        let mesh = extrude_glyph(&outline, 0.5, '█');
530        assert!(mesh.triangle_count >= 12, "Extruded square: 2 front + 2 back + 8 sides = 12, got {}", mesh.triangle_count);
531        assert!(!mesh.vertices.is_empty());
532        assert!(!mesh.indices.is_empty());
533    }
534
535    #[test]
536    fn extrude_normals_are_unit() {
537        let outline = square_outline();
538        let mesh = extrude_glyph(&outline, 0.5, '█');
539        for v in &mesh.vertices {
540            let n = v.norm();
541            let len = n.length();
542            assert!((len - 1.0).abs() < 0.01 || len < 0.01, "Normal should be unit or zero: {}", len);
543        }
544    }
545
546    #[test]
547    fn extrude_indices_valid() {
548        let outline = square_outline();
549        let mesh = extrude_glyph(&outline, 0.5, '█');
550        let vc = mesh.vertices.len() as u32;
551        for &idx in &mesh.indices {
552            assert!(idx < vc, "Index {} out of range (vertex count = {})", idx, vc);
553        }
554    }
555
556    #[test]
557    fn bridge_hole_increases_count() {
558        let outer = vec![
559            Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0),
560            Vec2::new(10.0, 10.0), Vec2::new(0.0, 10.0),
561        ];
562        let hole = vec![
563            Vec2::new(3.0, 3.0), Vec2::new(7.0, 3.0),
564            Vec2::new(7.0, 7.0), Vec2::new(3.0, 7.0),
565        ];
566        let merged = bridge_hole(&outer, &hole);
567        assert!(merged.len() > outer.len());
568        assert!(merged.len() > hole.len());
569    }
570
571    #[test]
572    fn box_mesh_fallback() {
573        let mesh = create_box_mesh(Vec3::ZERO, Vec3::ONE, 'X', 1.0);
574        assert_eq!(mesh.triangle_count, 12);
575        assert_eq!(mesh.vertices.len(), 24);
576    }
577}