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