Skip to main content

ling/gfx/
poly.rs

1// src/gfx/poly.rs — Polygon fan triangulation + per-frame shared-edge deduplication.
2//
3// Design:
4//   Quads, pentagons, hexagons (and arbitrary convex n-gons) are split into
5//   (n−2) triangles anchored at vertex 0 — the standard "fan" decomposition.
6//   Zero heap allocation: the caller supplies a fixed-size array.
7//
8//   EdgeSet deduplicates draw_line_3d calls so shared edges between adjacent
9//   faces are drawn exactly once per frame.  Keys are quantised world-space
10//   endpoint pairs → hash-set of sorted (u64, u64) tuples.  Clear at frame
11//   start (clear_screen).
12
13use std::collections::HashSet;
14
15// ── Edge deduplication ────────────────────────────────────────────────────────
16
17/// Canonical edge: always (min, max) so direction doesn't matter.
18#[inline]
19fn canonical(a: u64, b: u64) -> (u64, u64) {
20    if a <= b { (a, b) } else { (b, a) }
21}
22
23/// Encode a 3-D world-space point to a u64 key quantised to 1/64 unit.
24/// Two points within ~0.016 world units hash the same — enough for shared
25/// edges that are geometrically coincident but computed separately.
26#[inline]
27pub fn world_id(x: f32, y: f32, z: f32) -> u64 {
28    let xi = (x * 64.0).round() as i16;
29    let yi = (y * 64.0).round() as i16;
30    let zi = (z * 64.0).round() as i16;
31    ((xi as u16 as u64) << 32) | ((yi as u16 as u64) << 16) | (zi as u16 as u64)
32}
33
34/// Per-frame edge table.  Call `clear()` whenever the screen is cleared.
35pub struct EdgeSet {
36    set: HashSet<(u64, u64)>,
37}
38
39impl Default for EdgeSet {
40    fn default() -> Self {
41        Self { set: HashSet::with_capacity(1024) }
42    }
43}
44
45impl EdgeSet {
46    /// Returns `true` if this world-space edge is new this frame (and marks it).
47    /// Returns `false` when the shared edge has already been emitted — caller skips.
48    #[inline]
49    pub fn try_insert(&mut self, x0: f32, y0: f32, z0: f32, x1: f32, y1: f32, z1: f32) -> bool {
50        let a = world_id(x0, y0, z0);
51        let b = world_id(x1, y1, z1);
52        self.set.insert(canonical(a, b))
53    }
54
55    #[inline]
56    pub fn clear(&mut self) {
57        self.set.clear();
58    }
59}
60
61// ── Fan triangulation — 3-D world space ──────────────────────────────────────
62
63/// Fan-triangulate a 3-D polygon given as a parallel array of world-space
64/// (x, y, z) components and per-vertex lit colours.
65///
66/// Calls `emit(x0,y0,z0,c0, x1,y1,z1,c1, x2,y2,z2,c2)` for each triangle.
67/// At least 3 vertices required; silently does nothing for fewer.
68///
69/// The arrays must all be at least `n` elements; `n` must be ≤ the array length.
70#[inline]
71pub fn fan_emit_3d<F>(
72    xs: &[f32],
73    ys: &[f32],
74    zs: &[f32],
75    cs: &[u32],
76    n: usize,
77    mut emit: F,
78)
79where
80    F: FnMut(f32, f32, f32, u32, f32, f32, f32, u32, f32, f32, f32, u32),
81{
82    if n < 3 {
83        return;
84    }
85    let ax = xs[0]; let ay = ys[0]; let az = zs[0]; let ac = cs[0];
86    for i in 1..n - 1 {
87        emit(
88            ax, ay, az, ac,
89            xs[i],     ys[i],     zs[i],     cs[i],
90            xs[i + 1], ys[i + 1], zs[i + 1], cs[i + 1],
91        );
92    }
93}
94
95// ── Projected-polygon fan triangulation (screen space) ───────────────────────
96
97/// Fan-triangulate an already-projected polygon.  Each element is
98/// `(screen_x, screen_y, camera_z, colour)`.
99///
100/// Calls `emit(sx0,sy0,sz0,c0, sx1,sy1,sz1,c1, sx2,sy2,sz2,c2)`.
101#[inline]
102pub fn fan_emit_proj<F>(poly: &[(f32, f32, f32, u32)], n: usize, mut emit: F)
103where
104    F: FnMut(f32, f32, f32, u32, f32, f32, f32, u32, f32, f32, f32, u32),
105{
106    if n < 3 {
107        return;
108    }
109    let (ax, ay, az, ac) = poly[0];
110    for i in 1..n - 1 {
111        let (bx, by, bz, bc) = poly[i];
112        let (cx, cy, cz, cc) = poly[i + 1];
113        emit(ax, ay, az, ac, bx, by, bz, bc, cx, cy, cz, cc);
114    }
115}
116
117// ── Near-plane Sutherland–Hodgman clip ────────────────────────────────────────
118
119/// Maximum polygon size after near-plane clip: input n-gon → at most n+1 vertices.
120pub const MAX_CLIP_VERTS: usize = 9; // sufficient for hex (6) + 3 extra
121
122/// Clip an n-gon against the near plane `near_depth`.  Input and output are
123/// `(wx, wy, wz, cam_depth, colour)` tuples in a fixed-size stack array.
124/// Returns the number of output vertices (0..=MAX_CLIP_VERTS).
125///
126/// Uses `lerp_color` for interpolated vertex colours at clip edges.
127#[allow(clippy::too_many_arguments)]
128pub fn clip_near(
129    input:      &[(f32, f32, f32, f32, u32)], // (wx, wy, wz, cam_depth, color)
130    n_in:       usize,
131    near:       f32,
132    output:     &mut [(f32, f32, f32, f32, u32); MAX_CLIP_VERTS],
133) -> usize {
134    let mut n_out = 0usize;
135    for ei in 0..n_in {
136        let a = input[ei];
137        let b = input[(ei + 1) % n_in];
138        let a_in = a.3 > near;
139        let b_in = b.3 > near;
140        if a_in && n_out < MAX_CLIP_VERTS {
141            output[n_out] = a;
142            n_out += 1;
143        }
144        if a_in != b_in && n_out < MAX_CLIP_VERTS {
145            let t = (near - a.3) / (b.3 - a.3);
146            output[n_out] = (
147                a.0 + (b.0 - a.0) * t,
148                a.1 + (b.1 - a.1) * t,
149                a.2 + (b.2 - a.2) * t,
150                near,
151                lerp_color(a.4, b.4, t),
152            );
153            n_out += 1;
154        }
155    }
156    n_out
157}
158
159/// Linear-interpolate two 0x00RRGGBB colours.
160#[inline]
161pub fn lerp_color(a: u32, b: u32, t: f32) -> u32 {
162    let ar = ((a >> 16) & 0xFF) as f32;
163    let ag = ((a >> 8) & 0xFF) as f32;
164    let ab = (a & 0xFF) as f32;
165    let br = ((b >> 16) & 0xFF) as f32;
166    let bg = ((b >> 8) & 0xFF) as f32;
167    let bb = (b & 0xFF) as f32;
168    let r = (ar + (br - ar) * t).clamp(0.0, 255.0) as u32;
169    let g = (ag + (bg - ag) * t).clamp(0.0, 255.0) as u32;
170    let bl = (ab + (bb - ab) * t).clamp(0.0, 255.0) as u32;
171    (r << 16) | (g << 8) | bl
172}
173
174// ── Face-normal helper ────────────────────────────────────────────────────────
175
176/// World-space face normal (B−A) × (C−A).  Not normalised.
177#[inline]
178pub fn face_normal(
179    ax: f32, ay: f32, az: f32,
180    bx: f32, by: f32, bz: f32,
181    cx: f32, cy: f32, cz: f32,
182) -> [f32; 3] {
183    let ux = bx - ax; let uy = by - ay; let uz = bz - az;
184    let vx = cx - ax; let vy = cy - ay; let vz = cz - az;
185    [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx]
186}