Skip to main content

ling/gfx/
shapes.rs

1// src/gfx/shapes.rs — parametric 3-D primitive mesh library ("Inkscape for 3-D").
2//
3// Each generator returns a `Mesh` in LOCAL space (roughly spanning [-1,1],
4// centred at the origin). `build()` applies a per-axis scale, an Euler
5// rotation (X→Y→Z, radians) and a translation, producing a world-space mesh
6// ready for `GfxState::emit_mesh`.
7//
8// Rendering reuses the engine's existing pipeline: filled triangles are
9// cel-lit + projected + queued exactly like `draw_triangle_3d`; wireframe
10// edges are projected + queued like `draw_line_3d`.
11//
12// Draw modes (the `mode` arg of every shape builtin):
13//   0 = filled      1 = wireframe      2 = both (wire on top of fill)
14
15use super::GfxState;
16use std::collections::HashSet;
17use std::f32::consts::PI;
18
19/// A triangle mesh plus an explicit edge list for clean wireframes.
20#[derive(Default, Clone)]
21pub struct Mesh {
22    pub verts: Vec<[f32; 3]>,
23    pub tris: Vec<[u32; 3]>,
24    pub edges: Vec<[u32; 2]>,
25    /// Smooth (area-weighted averaged) per-vertex normals, world space.
26    /// Populated by `build()` after transform; empty until then.
27    pub normals: Vec<[f32; 3]>,
28}
29
30impl Mesh {
31    fn v(&mut self, x: f32, y: f32, z: f32) -> u32 {
32        let i = self.verts.len() as u32;
33        self.verts.push([x, y, z]);
34        i
35    }
36    fn tri(&mut self, a: u32, b: u32, c: u32) {
37        self.tris.push([a, b, c]);
38    }
39    fn edge(&mut self, a: u32, b: u32) {
40        self.edges.push([a, b]);
41    }
42
43    /// Add a convex polygon (fan-triangulated) and its perimeter edges.
44    fn face(&mut self, idx: &[u32]) {
45        for k in 1..idx.len() - 1 {
46            self.tris.push([idx[0], idx[k], idx[k + 1]]);
47        }
48        for k in 0..idx.len() {
49            self.edges.push([idx[k], idx[(k + 1) % idx.len()]]);
50        }
51    }
52
53    /// Derive a deduplicated edge list from the triangles (for curved meshes).
54    fn edges_from_tris(&mut self) {
55        let mut seen: HashSet<(u32, u32)> = HashSet::new();
56        for t in &self.tris {
57            for &(a, b) in &[(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
58                let k = if a < b { (a, b) } else { (b, a) };
59                if seen.insert(k) {
60                    self.edges.push([k.0, k.1]);
61                }
62            }
63        }
64    }
65
66    /// Compute area-weighted smooth per-vertex normals from the current
67    /// (already transformed) verts + tris — gives continuous shading with no
68    /// faceted edges.
69    fn compute_smooth_normals(&mut self) {
70        let mut n = vec![[0.0f32; 3]; self.verts.len()];
71        for t in &self.tris {
72            let a = self.verts[t[0] as usize];
73            let b = self.verts[t[1] as usize];
74            let c = self.verts[t[2] as usize];
75            let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
76            let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
77            let f = [
78                u[1] * v[2] - u[2] * v[1],
79                u[2] * v[0] - u[0] * v[2],
80                u[0] * v[1] - u[1] * v[0],
81            ];
82            for &i in t {
83                let i = i as usize;
84                n[i][0] += f[0];
85                n[i][1] += f[1];
86                n[i][2] += f[2];
87            }
88        }
89        for p in &mut n {
90            let l = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
91            if l > 1e-8 {
92                p[0] /= l;
93                p[1] /= l;
94                p[2] /= l;
95            }
96        }
97        self.normals = n;
98    }
99
100    /// scale → rotate(Euler XYZ) → translate, in place.
101    fn transform(&mut self, c: [f32; 9]) {
102        let (cx, cy, cz) = (c[0], c[1], c[2]);
103        let (sx, sy, sz) = (c[3], c[4], c[5]);
104        let (rx, ry, rz) = (c[6], c[7], c[8]);
105        let (srx, crx) = rx.sin_cos();
106        let (sry, cry) = ry.sin_cos();
107        let (srz, crz) = rz.sin_cos();
108        for p in &mut self.verts {
109            let mut x = p[0] * sx;
110            let mut y = p[1] * sy;
111            let mut z = p[2] * sz;
112            // rotate X
113            let (ny, nz) = (y * crx - z * srx, y * srx + z * crx);
114            y = ny;
115            z = nz;
116            // rotate Y
117            let (nx, nz2) = (x * cry + z * sry, -x * sry + z * cry);
118            x = nx;
119            z = nz2;
120            // rotate Z
121            let (nx2, ny2) = (x * crz - y * srz, x * srz + y * crz);
122            x = nx2;
123            y = ny2;
124            *p = [x + cx, y + cy, z + cz];
125        }
126    }
127}
128
129// ── small helpers ───────────────────────────────────────────────────────────
130#[inline]
131fn iarg(v: f32, default: i32) -> i32 {
132    if v > 0.5 {
133        v.round() as i32
134    } else {
135        default
136    }
137}
138#[inline]
139fn farg(v: f32, default: f32) -> f32 {
140    if v > 1e-6 {
141        v
142    } else {
143        default
144    }
145}
146
147// ── Platonic / dice solids ───────────────────────────────────────────────────
148
149fn cube() -> Mesh {
150    let mut m = Mesh::default();
151    let s = 1.0;
152    let p = [
153        m.v(-s, -s, -s),
154        m.v(s, -s, -s),
155        m.v(s, s, -s),
156        m.v(-s, s, -s), // back  0..3
157        m.v(-s, -s, s),
158        m.v(s, -s, s),
159        m.v(s, s, s),
160        m.v(-s, s, s), // front 4..7
161    ];
162    m.face(&[p[0], p[1], p[2], p[3]]); // -Z
163    m.face(&[p[5], p[4], p[7], p[6]]); // +Z
164    m.face(&[p[4], p[0], p[3], p[7]]); // -X
165    m.face(&[p[1], p[5], p[6], p[2]]); // +X
166    m.face(&[p[4], p[5], p[1], p[0]]); // -Y
167    m.face(&[p[3], p[2], p[6], p[7]]); // +Y
168    m
169}
170
171fn tetrahedron() -> Mesh {
172    let mut m = Mesh::default();
173    let a = 1.0;
174    let p = [m.v(a, a, a), m.v(a, -a, -a), m.v(-a, a, -a), m.v(-a, -a, a)];
175    m.face(&[p[0], p[1], p[2]]);
176    m.face(&[p[0], p[3], p[1]]);
177    m.face(&[p[0], p[2], p[3]]);
178    m.face(&[p[1], p[3], p[2]]);
179    m
180}
181
182fn octahedron() -> Mesh {
183    let mut m = Mesh::default();
184    let p = [
185        m.v(1.0, 0.0, 0.0),
186        m.v(-1.0, 0.0, 0.0),
187        m.v(0.0, 1.0, 0.0),
188        m.v(0.0, -1.0, 0.0),
189        m.v(0.0, 0.0, 1.0),
190        m.v(0.0, 0.0, -1.0),
191    ];
192    m.face(&[p[0], p[2], p[4]]);
193    m.face(&[p[2], p[1], p[4]]);
194    m.face(&[p[1], p[3], p[4]]);
195    m.face(&[p[3], p[0], p[4]]);
196    m.face(&[p[2], p[0], p[5]]);
197    m.face(&[p[1], p[2], p[5]]);
198    m.face(&[p[3], p[1], p[5]]);
199    m.face(&[p[0], p[3], p[5]]);
200    m
201}
202
203fn icosahedron_raw() -> Mesh {
204    let mut m = Mesh::default();
205    let t = (1.0 + 5.0_f32.sqrt()) / 2.0;
206    let s = 1.0 / (1.0 + t * t).sqrt(); // normalise to unit radius
207    let vs = [
208        [-1., t, 0.],
209        [1., t, 0.],
210        [-1., -t, 0.],
211        [1., -t, 0.],
212        [0., -1., t],
213        [0., 1., t],
214        [0., -1., -t],
215        [0., 1., -t],
216        [t, 0., -1.],
217        [t, 0., 1.],
218        [-t, 0., -1.],
219        [-t, 0., 1.],
220    ];
221    for v in vs {
222        m.v(v[0] * s, v[1] * s, v[2] * s);
223    }
224    let f = [
225        [0, 11, 5],
226        [0, 5, 1],
227        [0, 1, 7],
228        [0, 7, 10],
229        [0, 10, 11],
230        [1, 5, 9],
231        [5, 11, 4],
232        [11, 10, 2],
233        [10, 7, 6],
234        [7, 1, 8],
235        [3, 9, 4],
236        [3, 4, 2],
237        [3, 2, 6],
238        [3, 6, 8],
239        [3, 8, 9],
240        [4, 9, 5],
241        [2, 4, 11],
242        [6, 2, 10],
243        [8, 6, 7],
244        [9, 8, 1],
245    ];
246    for t in f {
247        m.tri(t[0], t[1], t[2]);
248    }
249    m
250}
251
252fn icosahedron() -> Mesh {
253    let mut m = icosahedron_raw();
254    m.edges_from_tris();
255    m
256}
257
258fn icosphere(subdiv: i32) -> Mesh {
259    let mut m = icosahedron_raw();
260    let n = subdiv.clamp(0, 4);
261    for _ in 0..n {
262        let mut nm = Mesh::default();
263        let mut mid: std::collections::HashMap<(u32, u32), u32> = std::collections::HashMap::new();
264        for v in &m.verts {
265            nm.verts.push(*v);
266        }
267        let midpoint = |nm: &mut Mesh,
268                        a: u32,
269                        b: u32,
270                        mid: &mut std::collections::HashMap<(u32, u32), u32>|
271         -> u32 {
272            let key = if a < b { (a, b) } else { (b, a) };
273            if let Some(&i) = mid.get(&key) {
274                return i;
275            }
276            let pa = nm.verts[a as usize];
277            let pb = nm.verts[b as usize];
278            let mut mp = [
279                (pa[0] + pb[0]) / 2.0,
280                (pa[1] + pb[1]) / 2.0,
281                (pa[2] + pb[2]) / 2.0,
282            ];
283            let l = (mp[0] * mp[0] + mp[1] * mp[1] + mp[2] * mp[2]).sqrt();
284            mp = [mp[0] / l, mp[1] / l, mp[2] / l];
285            let i = nm.verts.len() as u32;
286            nm.verts.push(mp);
287            mid.insert(key, i);
288            i
289        };
290        for t in &m.tris {
291            let a = midpoint(&mut nm, t[0], t[1], &mut mid);
292            let b = midpoint(&mut nm, t[1], t[2], &mut mid);
293            let c = midpoint(&mut nm, t[2], t[0], &mut mid);
294            nm.tri(t[0], a, c);
295            nm.tri(t[1], b, a);
296            nm.tri(t[2], c, b);
297            nm.tri(a, b, c);
298        }
299        m = nm;
300    }
301    m.edges_from_tris();
302    m
303}
304
305fn dodecahedron() -> Mesh {
306    let mut m = Mesh::default();
307    let phi = (1.0 + 5.0_f32.sqrt()) / 2.0;
308    let b = 1.0 / phi;
309    let c = phi;
310    let r = (3.0_f32).sqrt(); // normalise so |(1,1,1)| family → unit-ish
311    let s = 1.0 / r;
312    let vs = [
313        [1., 1., 1.],
314        [1., 1., -1.],
315        [1., -1., 1.],
316        [1., -1., -1.],
317        [-1., 1., 1.],
318        [-1., 1., -1.],
319        [-1., -1., 1.],
320        [-1., -1., -1.],
321        [0., b, c],
322        [0., b, -c],
323        [0., -b, c],
324        [0., -b, -c],
325        [b, c, 0.],
326        [b, -c, 0.],
327        [-b, c, 0.],
328        [-b, -c, 0.],
329        [c, 0., b],
330        [c, 0., -b],
331        [-c, 0., b],
332        [-c, 0., -b],
333    ];
334    for v in vs {
335        m.v(v[0] * s, v[1] * s, v[2] * s);
336    }
337    let faces: [[u32; 5]; 12] = [
338        [0, 8, 10, 2, 16],
339        [0, 16, 17, 1, 12],
340        [0, 12, 14, 4, 8],
341        [1, 9, 5, 14, 12],
342        [1, 17, 3, 11, 9],
343        [2, 10, 6, 15, 13],
344        [2, 13, 3, 17, 16],
345        [3, 13, 15, 7, 11],
346        [4, 14, 5, 19, 18],
347        [4, 18, 6, 10, 8],
348        [5, 9, 11, 7, 19],
349        [6, 18, 19, 7, 15],
350    ];
351    for f in faces {
352        m.face(&f);
353    }
354    m
355}
356
357// ── round / swept solids ──────────────────────────────────────────────────────
358
359fn uv_sphere(seg: i32, rings: i32) -> Mesh {
360    let mut m = Mesh::default();
361    let seg = seg.clamp(3, 128);
362    let rings = rings.clamp(2, 128);
363    for r in 0..=rings {
364        let v = r as f32 / rings as f32;
365        let theta = v * PI; // 0..pi
366        let (st, ct) = theta.sin_cos();
367        for s in 0..=seg {
368            let u = s as f32 / seg as f32;
369            let phi = u * 2.0 * PI;
370            let (sp, cp) = phi.sin_cos();
371            m.v(st * cp, ct, st * sp);
372        }
373    }
374    let stride = seg + 1;
375    for r in 0..rings {
376        for s in 0..seg {
377            let a = (r * stride + s) as u32;
378            let b = (r * stride + s + 1) as u32;
379            let cc = ((r + 1) * stride + s) as u32;
380            let d = ((r + 1) * stride + s + 1) as u32;
381            m.tri(a, cc, b);
382            m.tri(b, cc, d);
383        }
384    }
385    m.edges_from_tris();
386    m
387}
388
389fn dome(seg: i32, rings: i32) -> Mesh {
390    // upper hemisphere (y in [0..1]) with a closing base ring
391    let mut m = Mesh::default();
392    let seg = seg.clamp(3, 128);
393    let rings = rings.clamp(1, 128);
394    for r in 0..=rings {
395        let v = r as f32 / rings as f32;
396        let theta = v * (PI / 2.0); // 0..pi/2
397        let (st, ct) = theta.sin_cos();
398        for s in 0..=seg {
399            let phi = s as f32 / seg as f32 * 2.0 * PI;
400            let (sp, cp) = phi.sin_cos();
401            m.v(st * cp, ct, st * sp);
402        }
403    }
404    let stride = seg + 1;
405    for r in 0..rings {
406        for s in 0..seg {
407            let a = (r * stride + s) as u32;
408            let b = (r * stride + s + 1) as u32;
409            let cc = ((r + 1) * stride + s) as u32;
410            let d = ((r + 1) * stride + s + 1) as u32;
411            m.tri(a, cc, b);
412            m.tri(b, cc, d);
413        }
414    }
415    // base cap
416    let centre = m.v(0.0, 0.0, 0.0);
417    for s in 0..seg {
418        let a = ((rings) * stride + s) as u32;
419        let b = ((rings) * stride + s + 1) as u32;
420        m.tri(centre, b, a);
421    }
422    m.edges_from_tris();
423    m
424}
425
426fn cylinder(seg: i32) -> Mesh {
427    let mut m = Mesh::default();
428    let seg = seg.clamp(3, 256);
429    // rings at y=-1 (bottom) and y=+1 (top)
430    for s in 0..seg {
431        let phi = s as f32 / seg as f32 * 2.0 * PI;
432        let (sp, cp) = phi.sin_cos();
433        m.v(cp, -1.0, sp);
434        m.v(cp, 1.0, sp);
435    }
436    for s in 0..seg {
437        let b0 = (2 * s) as u32;
438        let t0 = (2 * s + 1) as u32;
439        let b1 = (2 * ((s + 1) % seg)) as u32;
440        let t1 = (2 * ((s + 1) % seg) + 1) as u32;
441        m.tri(b0, t0, b1);
442        m.tri(b1, t0, t1);
443        m.edge(b0, b1);
444        m.edge(t0, t1);
445        m.edge(b0, t0);
446    }
447    let cb = m.v(0.0, -1.0, 0.0);
448    let ct = m.v(0.0, 1.0, 0.0);
449    for s in 0..seg {
450        let b0 = (2 * s) as u32;
451        let b1 = (2 * ((s + 1) % seg)) as u32;
452        let t0 = (2 * s + 1) as u32;
453        let t1 = (2 * ((s + 1) % seg) + 1) as u32;
454        m.tri(cb, b1, b0);
455        m.tri(ct, t0, t1);
456    }
457    m
458}
459
460fn cone(seg: i32) -> Mesh {
461    let mut m = Mesh::default();
462    let seg = seg.clamp(3, 256);
463    let apex = m.v(0.0, 1.0, 0.0);
464    let base0 = m.verts.len() as u32;
465    for s in 0..seg {
466        let phi = s as f32 / seg as f32 * 2.0 * PI;
467        let (sp, cp) = phi.sin_cos();
468        m.v(cp, -1.0, sp);
469    }
470    let centre = m.v(0.0, -1.0, 0.0);
471    for s in 0..seg {
472        let a = base0 + s as u32;
473        let b = base0 + ((s + 1) % seg) as u32;
474        m.tri(apex, a, b); // side
475        m.tri(centre, b, a); // base
476        m.edge(a, b);
477        m.edge(apex, a);
478    }
479    m
480}
481
482fn capsule(seg: i32, rings: i32) -> Mesh {
483    // cylinder body (y -1..1) capped by two hemispheres of radius 1
484    let mut m = Mesh::default();
485    let seg = seg.clamp(3, 128);
486    let rings = rings.clamp(1, 64);
487    let stride = seg + 1;
488    // top hemisphere: theta 0..pi/2 mapped onto y = 1 + cos*? keep radius 1 sphere centred at y=+1
489    let mut ring_start = Vec::new();
490    let total_rows = 2 * rings; // top hemi rows + bottom hemi rows
491    for row in 0..=total_rows {
492        ring_start.push(m.verts.len() as u32);
493        let (cy_off, theta) = if row <= rings {
494            // top hemisphere: row 0 = pole (theta 0)
495            let v = row as f32 / rings as f32;
496            (1.0, v * PI / 2.0)
497        } else {
498            // bottom hemisphere
499            let v = (row - rings) as f32 / rings as f32;
500            (-1.0, PI / 2.0 + v * PI / 2.0)
501        };
502        let (st, ct) = theta.sin_cos();
503        for s in 0..=seg {
504            let phi = s as f32 / seg as f32 * 2.0 * PI;
505            let (sp, cp) = phi.sin_cos();
506            m.v(st * cp, cy_off + ct, st * sp);
507        }
508    }
509    for row in 0..total_rows as usize {
510        for s in 0..seg {
511            let a = ring_start[row] + s as u32;
512            let b = ring_start[row] + s as u32 + 1;
513            let c = ring_start[row + 1] + s as u32;
514            let d = ring_start[row + 1] + s as u32 + 1;
515            m.tri(a, c, b);
516            m.tri(b, c, d);
517        }
518    }
519    let _ = stride;
520    m.edges_from_tris();
521    m
522}
523
524fn torus(seg: i32, sides: i32, tube: f32) -> Mesh {
525    let mut m = Mesh::default();
526    let seg = seg.clamp(3, 256); // around the ring
527    let sides = sides.clamp(3, 128); // around the tube
528    let tube = tube.clamp(0.02, 0.9);
529    for i in 0..seg {
530        let u = i as f32 / seg as f32 * 2.0 * PI;
531        let (su, cu) = u.sin_cos();
532        for j in 0..sides {
533            let v = j as f32 / sides as f32 * 2.0 * PI;
534            let (sv, cv) = v.sin_cos();
535            let r = 1.0 - tube + tube * cv;
536            m.v(r * cu, tube * sv, r * su);
537        }
538    }
539    for i in 0..seg {
540        for j in 0..sides {
541            let a = (i * sides + j) as u32;
542            let b = (i * sides + (j + 1) % sides) as u32;
543            let c = (((i + 1) % seg) * sides + j) as u32;
544            let d = (((i + 1) % seg) * sides + (j + 1) % sides) as u32;
545            m.tri(a, c, b);
546            m.tri(b, c, d);
547        }
548    }
549    m.edges_from_tris();
550    m
551}
552
553// ── prisms / pyramids ─────────────────────────────────────────────────────────
554
555fn pyramid(sides: i32) -> Mesh {
556    let mut m = Mesh::default();
557    let sides = sides.clamp(3, 128);
558    let apex = m.v(0.0, 1.0, 0.0);
559    let base0 = m.verts.len() as u32;
560    let mut ring = Vec::new();
561    for s in 0..sides {
562        let phi = s as f32 / sides as f32 * 2.0 * PI;
563        let (sp, cp) = phi.sin_cos();
564        ring.push(m.v(cp, -1.0, sp));
565    }
566    for s in 0..sides as usize {
567        let a = ring[s];
568        let b = ring[(s + 1) % sides as usize];
569        m.tri(apex, a, b);
570        m.edge(a, b);
571        m.edge(apex, a);
572    }
573    // base face (reversed for outward normal)
574    let mut rev: Vec<u32> = ring.clone();
575    rev.reverse();
576    for k in 1..rev.len() - 1 {
577        m.tri(rev[0], rev[k], rev[k + 1]);
578    }
579    let _ = base0;
580    m
581}
582
583fn prism(sides: i32) -> Mesh {
584    let mut m = Mesh::default();
585    let sides = sides.clamp(3, 128);
586    let mut bot = Vec::new();
587    let mut top = Vec::new();
588    for s in 0..sides {
589        let phi = s as f32 / sides as f32 * 2.0 * PI;
590        let (sp, cp) = phi.sin_cos();
591        bot.push(m.v(cp, -1.0, sp));
592        top.push(m.v(cp, 1.0, sp));
593    }
594    let n = sides as usize;
595    for s in 0..n {
596        let b0 = bot[s];
597        let b1 = bot[(s + 1) % n];
598        let t0 = top[s];
599        let t1 = top[(s + 1) % n];
600        m.tri(b0, t0, b1);
601        m.tri(b1, t0, t1);
602        m.edge(b0, b1);
603        m.edge(t0, t1);
604        m.edge(b0, t0);
605    }
606    for k in 1..n - 1 {
607        m.tri(top[0], top[k], top[k + 1]);
608    }
609    let mut rb: Vec<u32> = bot.clone();
610    rb.reverse();
611    for k in 1..rb.len() - 1 {
612        m.tri(rb[0], rb[k], rb[k + 1]);
613    }
614    m
615}
616
617fn frustum(sides: i32, top_ratio: f32) -> Mesh {
618    let mut m = Mesh::default();
619    let sides = sides.clamp(3, 256);
620    let tr = top_ratio.clamp(0.0, 1.0);
621    let mut bot = Vec::new();
622    let mut top = Vec::new();
623    for s in 0..sides {
624        let phi = s as f32 / sides as f32 * 2.0 * PI;
625        let (sp, cp) = phi.sin_cos();
626        bot.push(m.v(cp, -1.0, sp));
627        top.push(m.v(cp * tr, 1.0, sp * tr));
628    }
629    let n = sides as usize;
630    for s in 0..n {
631        let b0 = bot[s];
632        let b1 = bot[(s + 1) % n];
633        let t0 = top[s];
634        let t1 = top[(s + 1) % n];
635        m.tri(b0, t0, b1);
636        m.tri(b1, t0, t1);
637        m.edge(b0, b1);
638        m.edge(t0, t1);
639        m.edge(b0, t0);
640    }
641    if tr > 0.001 {
642        for k in 1..n - 1 {
643            m.tri(top[0], top[k], top[k + 1]);
644        }
645    }
646    let mut rb: Vec<u32> = bot.clone();
647    rb.reverse();
648    for k in 1..rb.len() - 1 {
649        m.tri(rb[0], rb[k], rb[k + 1]);
650    }
651    m
652}
653
654// ── mechanical / architectural ────────────────────────────────────────────────
655
656fn gear(teeth: i32, tooth: f32) -> Mesh {
657    // flat gear in the XZ plane, extruded ±1 in Y; `tooth` = radial tooth depth.
658    let mut m = Mesh::default();
659    let teeth = teeth.clamp(3, 96);
660    let tooth = tooth.clamp(0.02, 0.6);
661    let pts = teeth * 4; // 4 control points per tooth
662    let mut bot = Vec::new();
663    let mut top = Vec::new();
664    for i in 0..pts {
665        let phi = i as f32 / pts as f32 * 2.0 * PI;
666        // square-ish tooth profile: outer for first half of each tooth, inner for second
667        let phase = (i % 4) as f32;
668        let r = if phase < 2.0 { 1.0 } else { 1.0 - tooth };
669        let (sp, cp) = phi.sin_cos();
670        bot.push(m.v(cp * r, -1.0, sp * r));
671        top.push(m.v(cp * r, 1.0, sp * r));
672    }
673    let n = pts as usize;
674    for s in 0..n {
675        let b0 = bot[s];
676        let b1 = bot[(s + 1) % n];
677        let t0 = top[s];
678        let t1 = top[(s + 1) % n];
679        m.tri(b0, t0, b1);
680        m.tri(b1, t0, t1); // rim
681        m.edge(b0, b1);
682        m.edge(t0, t1);
683        m.edge(b0, t0);
684    }
685    let cb = m.v(0.0, -1.0, 0.0);
686    let ct = m.v(0.0, 1.0, 0.0);
687    for s in 0..n {
688        let b0 = bot[s];
689        let b1 = bot[(s + 1) % n];
690        let t0 = top[s];
691        let t1 = top[(s + 1) % n];
692        m.tri(cb, b1, b0);
693        m.tri(ct, t0, t1); // caps
694    }
695    m
696}
697
698fn gyro(rings: i32) -> Mesh {
699    // nested gimbal: `rings` tori on alternating axes at shrinking radius.
700    let mut m = Mesh::default();
701    let rings = rings.clamp(1, 6);
702    for k in 0..rings {
703        let scale = 1.0 - k as f32 * (0.8 / rings as f32);
704        let mut ring = torus(40, 8, 0.06 / scale.max(0.2));
705        // rotate each ring onto a different axis
706        let rot = match k % 3 {
707            0 => [0.0, 0.0, 0.0],
708            1 => [PI / 2.0, 0.0, 0.0],
709            _ => [0.0, 0.0, PI / 2.0],
710        };
711        ring.transform([0.0, 0.0, 0.0, scale, scale, scale, rot[0], rot[1], rot[2]]);
712        let base = m.verts.len() as u32;
713        for v in &ring.verts {
714            m.verts.push(*v);
715        }
716        for t in &ring.tris {
717            m.tri(t[0] + base, t[1] + base, t[2] + base);
718        }
719        for e in &ring.edges {
720            m.edge(e[0] + base, e[1] + base);
721        }
722    }
723    m
724}
725
726// ── exotic / compound shapes ──────────────────────────────────────────────────
727
728fn append_mesh(dst: &mut Mesh, src: &Mesh) {
729    let base = dst.verts.len() as u32;
730    for v in &src.verts {
731        dst.verts.push(*v);
732    }
733    for t in &src.tris {
734        dst.tri(t[0] + base, t[1] + base, t[2] + base);
735    }
736    for e in &src.edges {
737        dst.edge(e[0] + base, e[1] + base);
738    }
739}
740
741fn box_between(x0: f32, x1: f32, y0: f32, y1: f32, z0: f32, z1: f32) -> Mesh {
742    let mut m = Mesh::default();
743    let p = [
744        m.v(x0, y0, z0),
745        m.v(x1, y0, z0),
746        m.v(x1, y1, z0),
747        m.v(x0, y1, z0),
748        m.v(x0, y0, z1),
749        m.v(x1, y0, z1),
750        m.v(x1, y1, z1),
751        m.v(x0, y1, z1),
752    ];
753    m.face(&[p[0], p[1], p[2], p[3]]);
754    m.face(&[p[5], p[4], p[7], p[6]]);
755    m.face(&[p[4], p[0], p[3], p[7]]);
756    m.face(&[p[1], p[5], p[6], p[2]]);
757    m.face(&[p[4], p[5], p[1], p[0]]);
758    m.face(&[p[3], p[2], p[6], p[7]]);
759    m
760}
761
762/// Tube swept along a helix around the Y axis (height −1..1).
763fn helix(turns: i32, tube: f32, sides: i32) -> Mesh {
764    let mut m = Mesh::default();
765    let turns = turns.clamp(1, 24);
766    let sides = sides.clamp(3, 32);
767    let tube = tube.clamp(0.02, 0.5);
768    let seg_per = 24;
769    let total = turns * seg_per;
770    for i in 0..=total {
771        let ang = (i as f32 / seg_per as f32) * 2.0 * PI;
772        let y = -1.0 + 2.0 * (i as f32 / total as f32);
773        let cen = [ang.cos(), y, ang.sin()];
774        let radial = [ang.cos(), 0.0, ang.sin()];
775        let up = [0.0, 1.0, 0.0];
776        for j in 0..sides {
777            let v = j as f32 / sides as f32 * 2.0 * PI;
778            let (sv, cv) = v.sin_cos();
779            m.v(
780                cen[0] + tube * (cv * radial[0] + sv * up[0]),
781                cen[1] + tube * (cv * radial[1] + sv * up[1]),
782                cen[2] + tube * (cv * radial[2] + sv * up[2]),
783            );
784        }
785    }
786    let s = sides;
787    for i in 0..total {
788        for j in 0..sides {
789            let a = (i * s + j) as u32;
790            let b = (i * s + (j + 1) % s) as u32;
791            let c = ((i + 1) * s + j) as u32;
792            let d = ((i + 1) * s + (j + 1) % s) as u32;
793            m.tri(a, c, b);
794            m.tri(b, c, d);
795        }
796    }
797    m.edges_from_tris();
798    m
799}
800
801/// Semicircular archway — circular tube swept over a 180° arc in the XY plane.
802fn arch(segs: i32, tube: f32) -> Mesh {
803    let mut m = Mesh::default();
804    let segs = segs.clamp(6, 128);
805    let sides = 10i32;
806    let tube = tube.clamp(0.05, 0.4);
807    for i in 0..=segs {
808        let a = PI * (i as f32 / segs as f32); // 0..π
809        let cen = [a.cos(), a.sin(), 0.0];
810        let radial = [a.cos(), a.sin(), 0.0];
811        let binorm = [0.0, 0.0, 1.0];
812        for j in 0..sides {
813            let v = j as f32 / sides as f32 * 2.0 * PI;
814            let (sv, cv) = v.sin_cos();
815            m.v(
816                cen[0] + tube * (cv * radial[0] + sv * binorm[0]),
817                cen[1] + tube * (cv * radial[1] + sv * binorm[1]),
818                cen[2] + tube * (cv * radial[2] + sv * binorm[2]),
819            );
820        }
821    }
822    for i in 0..segs {
823        for j in 0..sides {
824            let a = (i * sides + j) as u32;
825            let b = (i * sides + (j + 1) % sides) as u32;
826            let c = ((i + 1) * sides + j) as u32;
827            let d = ((i + 1) * sides + (j + 1) % sides) as u32;
828            m.tri(a, c, b);
829            m.tri(b, c, d);
830        }
831    }
832    m.edges_from_tris();
833    m
834}
835
836/// Staircase of `steps` cuboid steps rising along +Y and +Z.
837fn stairs(steps: i32) -> Mesh {
838    let mut m = Mesh::default();
839    let steps = steps.clamp(2, 40);
840    let sh = 2.0 / steps as f32;
841    let sd = 2.0 / steps as f32;
842    for i in 0..steps {
843        let y0 = -1.0 + i as f32 * sh;
844        let y1 = y0 + sh;
845        let z0 = -1.0 + i as f32 * sd;
846        let zf = z0 + sd;
847        let blk = box_between(-1.0, 1.0, y0, y1, z0, zf);
848        append_mesh(&mut m, &blk);
849    }
850    m
851}
852
853/// Star-shaped prism: an N-point star cross-section extruded along Y.
854fn star_prism(points: i32, inner: f32) -> Mesh {
855    let mut m = Mesh::default();
856    let points = points.clamp(3, 32);
857    let inner = inner.clamp(0.1, 0.95);
858    let n = (points * 2) as usize;
859    let mut bot = Vec::new();
860    let mut top = Vec::new();
861    for k in 0..n {
862        let ang = k as f32 / n as f32 * 2.0 * PI;
863        let r = if k % 2 == 0 { 1.0 } else { inner };
864        let (s, c) = ang.sin_cos();
865        bot.push(m.v(c * r, -1.0, s * r));
866        top.push(m.v(c * r, 1.0, s * r));
867    }
868    for k in 0..n {
869        let b0 = bot[k];
870        let b1 = bot[(k + 1) % n];
871        let t0 = top[k];
872        let t1 = top[(k + 1) % n];
873        m.tri(b0, t0, b1);
874        m.tri(b1, t0, t1);
875        m.edge(b0, b1);
876        m.edge(t0, t1);
877        m.edge(b0, t0);
878    }
879    for k in 1..n - 1 {
880        m.tri(top[0], top[k], top[k + 1]);
881    }
882    let mut rb = bot.clone();
883    rb.reverse();
884    for k in 1..rb.len() - 1 {
885        m.tri(rb[0], rb[k], rb[k + 1]);
886    }
887    m
888}
889
890/// A row of `count` capsule "beads" along X — a chain / caterpillar.
891fn capsule_chain(count: i32) -> Mesh {
892    let mut m = Mesh::default();
893    let count = count.clamp(1, 12);
894    let step = 2.0 / count as f32;
895    for i in 0..count {
896        let mut c = capsule(12, 4);
897        let cx = -1.0 + (i as f32 + 0.5) * step;
898        c.transform([
899            cx,
900            0.0,
901            0.0,
902            step * 0.5,
903            step * 0.5,
904            step * 0.5,
905            0.0,
906            0.0,
907            PI / 2.0,
908        ]);
909        append_mesh(&mut m, &c);
910    }
911    m
912}
913
914/// Möbius strip — a half-twisted band looped once.
915fn mobius(segs: i32, width: f32) -> Mesh {
916    let mut m = Mesh::default();
917    let segs = segs.clamp(8, 240);
918    let w = width.clamp(0.05, 0.6);
919    for i in 0..=segs {
920        let u = i as f32 / segs as f32 * 2.0 * PI;
921        for &vv in &[-1.0f32, 1.0] {
922            let v = vv * w;
923            let x = (1.0 + v / 2.0 * (u / 2.0).cos()) * u.cos();
924            let y = v / 2.0 * (u / 2.0).sin();
925            let z = (1.0 + v / 2.0 * (u / 2.0).cos()) * u.sin();
926            m.v(x, y, z);
927        }
928    }
929    for i in 0..segs {
930        let a = (2 * i) as u32;
931        let b = (2 * i + 1) as u32;
932        let c = (2 * (i + 1)) as u32;
933        let d = (2 * (i + 1) + 1) as u32;
934        m.tri(a, c, b);
935        m.tri(b, c, d);
936    }
937    m.edges_from_tris();
938    m
939}
940
941/// Resolve a builtin call name (in any supported language) to a canonical
942/// shape kind. Returns `None` if the name is not a 3-D primitive.
943pub fn canon(name: &str) -> Option<&'static str> {
944    Some(match name {
945        // cube / box
946        "cube" | "box" | "立方体" | "方块" | "箱" | "정육면체" | "상자" | "ลูกบาศก์" | "กล่อง" => {
947            "cube"
948        },
949        // sphere
950        "sphere" | "球体" | "球" | "구" | "ทรงกลม" => "sphere",
951        // icosphere
952        "icosphere" | "二十面球" | "アイコ球" | "아이코구체" | "ทรงกลมเหลี่ยม" => {
953            "icosphere"
954        },
955        // dome (hemisphere)
956        "dome" | "穹顶" | "ドーム" | "돔" | "โดม" => "dome",
957        // cylinder
958        "cylinder" | "圆柱" | "円柱" | "원기둥" | "ทรงกระบอก" => {
959            "cylinder"
960        },
961        // cone
962        "cone" | "圆锥" | "円錐" | "원뿔" | "กรวย" => "cone",
963        // capsule
964        "capsule" | "胶囊" | "カプセル" | "캡슐" | "แคปซูล" => "capsule",
965        // torus / ring
966        "torus" | "ring" | "圆环" | "トーラス" | "토러스" | "ทอรัส" => "torus",
967        // pyramid
968        "pyramid" | "金字塔" | "ピラミッド" | "피라미드" | "พีระมิด" => {
969            "pyramid"
970        },
971        // prism
972        "prism" | "棱柱" | "角柱" | "각기둥" | "ปริซึม" => "prism",
973        // frustum
974        "frustum" | "棱台" | "錐台" | "원뿔대" | "กรวยตัด" => "frustum",
975        // tetrahedron / d4
976        "tetrahedron" | "d4" | "四面体" | "정사면체" | "ทรงสี่หน้า" => {
977            "tetrahedron"
978        },
979        // octahedron / d8
980        "octahedron" | "d8" | "八面体" | "정팔면체" | "ทรงแปดหน้า" => {
981            "octahedron"
982        },
983        // dodecahedron / d12
984        "dodecahedron" | "d12" | "十二面体" | "정십이면체" | "ทรงสิบสองหน้า" => {
985            "dodecahedron"
986        },
987        // icosahedron / d20
988        "icosahedron" | "d20" | "二十面体" | "정이십면체" | "ทรงยี่สิบหน้า" => {
989            "icosahedron"
990        },
991        // gear / cog
992        "gear" | "cog" | "齿轮" | "歯車" | "톱니바퀴" | "เฟือง" => "gear",
993        // gyro
994        "gyro" | "陀螺" | "ジャイロ" | "자이로" | "ไจโร" => "gyro",
995        // helix
996        "helix" | "螺旋线" | "らせん" | "나선" | "เกลียว" => "helix",
997        // spring
998        "spring" | "弹簧" | "ばね" | "스프링" | "สปริง" => "spring",
999        // arch
1000        "arch" | "拱门" | "アーチ" | "아치" | "ซุ้มโค้ง" => "arch",
1001        // stairs
1002        "stairs" | "楼梯" | "階段" | "계단" | "บันได" => "stairs",
1003        // star prism
1004        "star_prism" | "star" | "星柱" | "星型柱" | "별기둥" | "แท่งดาว" => {
1005            "star_prism"
1006        },
1007        // capsule chain
1008        "capsule_chain" | "chain" | "胶囊链" | "カプセル鎖" | "캡슐체인" | "โซ่แคปซูล" => {
1009            "capsule_chain"
1010        },
1011        // mobius
1012        "mobius" | "莫比乌斯" | "メビウス" | "뫼비우스" | "เมอบีอุส" => {
1013            "mobius"
1014        },
1015        _ => return None,
1016    })
1017}
1018
1019/// Build a transformed, world-space mesh for `kind`.
1020/// `c` = [cx,cy,cz, sx,sy,sz, rx,ry,rz]; `e0..e2` = shape-specific extras.
1021pub fn build(kind: &str, c: [f32; 9], e0: f32, e1: f32, e2: f32) -> Option<Mesh> {
1022    let mut m = match kind {
1023        "cube" | "box" => cube(),
1024        "sphere" => uv_sphere(iarg(e0, 16), iarg(e1, 12)),
1025        "icosphere" => icosphere(iarg(e0, 1)),
1026        "dome" => dome(iarg(e0, 24), iarg(e1, 8)),
1027        "cylinder" => cylinder(iarg(e0, 24)),
1028        "cone" => cone(iarg(e0, 24)),
1029        "capsule" => capsule(iarg(e0, 16), iarg(e1, 6)),
1030        "torus" | "ring" => torus(iarg(e0, 32), iarg(e1, 12), farg(e2, 0.35)),
1031        "pyramid" => pyramid(iarg(e0, 4)),
1032        "prism" => prism(iarg(e0, 6)),
1033        "frustum" => frustum(iarg(e0, 24), farg(e1, 0.5)),
1034        "tetrahedron" | "d4" => {
1035            let mut t = tetrahedron();
1036            t.edges = vec![];
1037            t.edges_from_tris();
1038            t
1039        },
1040        "octahedron" | "d8" => {
1041            let mut t = octahedron();
1042            t.edges = vec![];
1043            t.edges_from_tris();
1044            t
1045        },
1046        "dodecahedron" | "d12" => dodecahedron(),
1047        "icosahedron" | "d20" => icosahedron(),
1048        "gear" | "cog" => gear(iarg(e0, 12), farg(e1, 0.25)),
1049        "gyro" => gyro(iarg(e0, 3)),
1050        "helix" => helix(iarg(e0, 3), farg(e1, 0.15), iarg(e2, 8)),
1051        "spring" => helix(iarg(e0, 6), farg(e1, 0.12), iarg(e2, 8)),
1052        "arch" => arch(iarg(e0, 24), farg(e1, 0.18)),
1053        "stairs" => stairs(iarg(e0, 5)),
1054        "star_prism" => star_prism(iarg(e0, 5), farg(e1, 0.5)),
1055        "capsule_chain" => capsule_chain(iarg(e0, 3)),
1056        "mobius" => mobius(iarg(e0, 60), farg(e1, 0.3)),
1057        _ => return None,
1058    };
1059    m.transform(c);
1060    m.compute_smooth_normals();
1061    Some(m)
1062}
1063
1064/// A flat-shaded, per-triangle-coloured mesh (triangle soup) for fast native-res
1065/// model rendering. `pos` holds 3 verts per triangle; `col` one RGB per triangle.
1066/// `height` is the model's Y-extent (feet→head), used to weight the deformation.
1067#[derive(Default, Clone)]
1068pub struct ColorMesh {
1069    pub pos: Vec<[f32; 3]>, // 3 * ntri  (triangle soup)
1070    pub col: Vec<[u8; 3]>,  // ntri      (one flat colour per triangle)
1071    pub height: f32,
1072}
1073
1074impl GfxState {
1075    /// Draw a per-triangle-coloured mesh **unlit** (colours used as-is → ignored by
1076    /// the lighting pass, and fast), with the model transform (translate · uniform
1077    /// scale · yaw about Y) and a baked procedural deformation: `sway` leans the
1078    /// upper body (∝ |y|) and `arm` swings the arms fore/aft in antiphase with an
1079    /// elbow-compound bend. Verts are flipped models (feet y≈0, head y≈-height).
1080    #[allow(clippy::too_many_arguments)]
1081    pub fn draw_color_mesh(
1082        &mut self,
1083        m: &ColorMesh,
1084        cx: f32,
1085        cy: f32,
1086        cz: f32,
1087        sc: f32,
1088        yaw: f32,
1089        sway: f32,
1090        arm: f32,
1091        lean: f32,
1092        leg: f32,
1093        tuck: f32,
1094    ) {
1095        let near = -self.camera.zdist + 0.05;
1096        let cs = yaw.cos();
1097        let sn = yaw.sin();
1098        let h = m.height.max(1e-4);
1099        let yc = -0.68 * h; // shoulder band centre
1100        let torso = 0.13 * h;
1101        let elbow = torso + 0.16 * h;
1102        let nt = m.col.len();
1103        let mut ti = 0usize;
1104        while ti < nt {
1105            let base = ti * 3;
1106            let mut wv = [[0.0f32; 3]; 3];
1107            let mut k = 0;
1108            while k < 3 {
1109                let p = m.pos[base + k];
1110                let ax = p[0].abs();
1111                let yb = (1.0 - (p[1] - yc).abs() / (0.30 * h)).clamp(0.0, 1.0); // upper-body band
1112                let aw = (((ax - torso) / (0.40 * h)).clamp(0.0, 1.0)) * yb; // arm weight
1113                let ew = (((ax - elbow) / (0.28 * h)).clamp(0.0, 1.0)) * yb; // elbow/forearm weight
1114                let side = if p[0] >= 0.0 { 1.0 } else { -1.0 };
1115                // forward bend (running): upper body pitches forward (+z) above the waist,
1116                // arms pulled back/tucked relative to the leaning torso.
1117                let bw = (((p[1].abs() / h) - 0.40) / 0.60).clamp(0.0, 1.0); // 0 below waist → 1 head
1118                let zlean = lean * bw * bw * h - lean * aw * 0.6 * h;
1119                // legs (lower body, not arms): swing fore/aft antiphase L/R; the forward
1120                // foot lifts (knee bend). `tuck` raises both knees toward the chest (jump).
1121                let lw = (((0.45 * h - p[1].abs()) / (0.45 * h)).clamp(0.0, 1.0)) * (1.0 - aw);
1122                let fw = (((0.16 * h - p[1].abs()) / (0.16 * h)).clamp(0.0, 1.0)) * (1.0 - aw);
1123                let legswing = leg * side * lw;
1124                let mut ylift = 0.0f32;
1125                if legswing > 0.0 {
1126                    ylift -= legswing * fw * 0.45 * h;
1127                } // forward foot lifts (up = -Y)
1128                ylift -= tuck * lw * 0.22 * h; // jump tuck: knees up
1129                let xs = p[0] + sway * p[1].abs();
1130                let zs =
1131                    p[2] + arm * side * (aw + ew * 0.7) + zlean + legswing + tuck * lw * 0.16 * h;
1132                wv[k] = [
1133                    cx + (xs * cs + zs * sn) * sc,
1134                    cy + (p[1] + ylift) * sc,
1135                    cz + (zs * cs - xs * sn) * sc,
1136                ];
1137                k += 1;
1138            }
1139            let a = wv[0];
1140            let b = wv[1];
1141            let c = wv[2];
1142            let da = self.camera.depth(a[0], a[1], a[2]);
1143            let db = self.camera.depth(b[0], b[1], b[2]);
1144            let dc = self.camera.depth(c[0], c[1], c[2]);
1145            if !(da <= near && db <= near && dc <= near) {
1146                let poly = near_clip_poly(
1147                    &[(a, [0.0; 3], da), (b, [0.0; 3], db), (c, [0.0; 3], dc)],
1148                    near,
1149                );
1150                if poly.len() >= 3 {
1151                    let col = m.col[ti];
1152                    let packed = ((col[0] as u32) << 16) | ((col[1] as u32) << 8) | (col[2] as u32);
1153                    let proj: Vec<(f32, f32, f32)> = poly
1154                        .iter()
1155                        .map(|(p, _)| self.camera.project(p[0], p[1], p[2]))
1156                        .collect();
1157                    let depth = proj.iter().map(|v| v.2).sum::<f32>() / proj.len() as f32;
1158                    let mut j = 1;
1159                    while j + 1 < proj.len() {
1160                        self.depth_queue.push_triangle(
1161                            depth,
1162                            packed,
1163                            proj[0].0,
1164                            proj[0].1,
1165                            proj[j].0,
1166                            proj[j].1,
1167                            proj[j + 1].0,
1168                            proj[j + 1].1,
1169                        );
1170                        j += 1;
1171                    }
1172                }
1173            }
1174            ti += 1;
1175        }
1176    }
1177
1178    /// Viscous screen-space distortion of the current framebuffer: warps/puckers/
1179    /// bloats in shifting regions and **wraps** at all four edges (toroidal sample).
1180    /// Separable (per-row + per-column displacement) so it stays cheap full-screen.
1181    /// `amount` = max displacement in pixels; `t` = time (animate the goo).
1182    pub fn distort(&mut self, amount: f32, t: f32) {
1183        let w = self.width;
1184        let h = self.height;
1185        if w < 2 || h < 2 || amount <= 0.0 {
1186            return;
1187        }
1188        let src = self.buffer.clone();
1189        let a = amount;
1190        // per-row horizontal shift + a vertical cross term
1191        let mut rdx = vec![0i32; h];
1192        let mut rdy = vec![0i32; h];
1193        for y in 0..h {
1194            let fy = y as f32;
1195            rdx[y] =
1196                ((fy * 0.018 + t * 0.8).sin() * a + (fy * 0.005 - t * 0.5).sin() * a * 0.6) as i32;
1197            rdy[y] = ((fy * 0.040 + t * 1.1).sin() * a * 0.4) as i32;
1198        }
1199        // per-column vertical shift + a horizontal cross term  (the two cross terms
1200        // make the warp swirl in 2-D; multi-frequency sines give pucker/bloat zones)
1201        let mut cdy = vec![0i32; w];
1202        let mut cdx = vec![0i32; w];
1203        for x in 0..w {
1204            let fx = x as f32;
1205            cdy[x] =
1206                ((fx * 0.020 + t * 0.7).sin() * a + (fx * 0.006 + t * 0.45).sin() * a * 0.6) as i32;
1207            cdx[x] = ((fx * 0.050 + t * 0.9).sin() * a * 0.4) as i32;
1208        }
1209        let wi = w as i32;
1210        let hi = h as i32;
1211        for y in 0..h {
1212            let row = y * w;
1213            let ry = rdy[y];
1214            for x in 0..w {
1215                // branchless small-shift wrap (displacements are a few px → one add wraps)
1216                let mut sxi = x as i32 + rdx[y] + cdx[x];
1217                if sxi < 0 {
1218                    sxi += wi;
1219                } else if sxi >= wi {
1220                    sxi -= wi;
1221                }
1222                let mut syi = y as i32 + cdy[x] + ry;
1223                if syi < 0 {
1224                    syi += hi;
1225                } else if syi >= hi {
1226                    syi -= hi;
1227                }
1228                self.buffer[row + x] = src[syi as usize * w + sxi as usize];
1229            }
1230        }
1231    }
1232
1233    /// Render a world-space mesh through the depth queue.
1234    /// mode: 0 filled, 1 wireframe, 2 both.
1235    pub fn emit_mesh(&mut self, m: &Mesh, mode: i32) {
1236        let near = -self.camera.zdist + 0.05;
1237
1238        let want_fill = mode == 0 || mode == 2;
1239        if want_fill {
1240            let have_normals = m.normals.len() == m.verts.len() && self.shade_mode != 0;
1241            if have_normals {
1242                // ── smooth cel / holographic path ─────────────────────────────
1243                // Per-vertex coloured lighting (smooth normals) → Gouraud
1244                // interpolation → per-pixel posterise. No faceted edges.
1245                let base = ling_graphics::shading::unpack(self.color);
1246                let eye = [self.camera.tx, self.camera.ty, self.camera.tz];
1247                let lights: Vec<ling_graphics::shading::LightS> = self
1248                    .lights
1249                    .iter()
1250                    .map(|l| ling_graphics::shading::LightS {
1251                        pos: [l.x, l.y, l.z],
1252                        color: [l.r, l.g, l.b],
1253                        intensity: l.intensity,
1254                        radius: l.radius,
1255                    })
1256                    .collect();
1257                let mut sp = self.shade;
1258                sp.ambient = self.ambient; // scene ambient drives fill
1259                if self.shade_mode == 1 {
1260                    sp.holo = false;
1261                    sp.rim *= 0.4;
1262                }
1263                let bands = sp.bands;
1264                for t in &m.tris {
1265                    let ia = t[0] as usize;
1266                    let ib = t[1] as usize;
1267                    let ic = t[2] as usize;
1268                    let a = m.verts[ia];
1269                    let b = m.verts[ib];
1270                    let c = m.verts[ic];
1271                    let da = self.camera.depth(a[0], a[1], a[2]);
1272                    let db = self.camera.depth(b[0], b[1], b[2]);
1273                    let dc = self.camera.depth(c[0], c[1], c[2]);
1274                    if da <= near && db <= near && dc <= near {
1275                        continue;
1276                    } // all behind → drop
1277                      // Lit colours per vertex (kept unpacked so clipping can lerp them).
1278                    let la = ling_graphics::shading::lit_vertex(
1279                        base,
1280                        m.normals[ia],
1281                        a,
1282                        eye,
1283                        &lights,
1284                        &sp,
1285                    );
1286                    let lb = ling_graphics::shading::lit_vertex(
1287                        base,
1288                        m.normals[ib],
1289                        b,
1290                        eye,
1291                        &lights,
1292                        &sp,
1293                    );
1294                    let lc = ling_graphics::shading::lit_vertex(
1295                        base,
1296                        m.normals[ic],
1297                        c,
1298                        eye,
1299                        &lights,
1300                        &sp,
1301                    );
1302                    // Near-plane clip (keeps large straddling tiles instead of dropping them).
1303                    let poly = near_clip_poly(&[(a, la, da), (b, lb, db), (c, lc, dc)], near);
1304                    if poly.len() < 3 {
1305                        continue;
1306                    }
1307                    let proj: Vec<(f32, f32, f32, u32)> = poly
1308                        .iter()
1309                        .map(|(p, col)| {
1310                            let (sx, sy, pz) = self.camera.project(p[0], p[1], p[2]);
1311                            (sx, sy, pz, ling_graphics::shading::pack(*col))
1312                        })
1313                        .collect();
1314                    let mut k = 1;
1315                    while k + 1 < proj.len() {
1316                        self.depth_queue.push_triangle_g_zv(
1317                            proj[0].0,
1318                            proj[0].1,
1319                            proj[0].2,
1320                            proj[0].3,
1321                            proj[k].0,
1322                            proj[k].1,
1323                            proj[k].2,
1324                            proj[k].3,
1325                            proj[k + 1].0,
1326                            proj[k + 1].1,
1327                            proj[k + 1].2,
1328                            proj[k + 1].3,
1329                            bands,
1330                        );
1331                        k += 1;
1332                    }
1333                }
1334            } else {
1335                // ── flat per-face path (shade_mode 0) ─────────────────────────
1336                for t in &m.tris {
1337                    let a = m.verts[t[0] as usize];
1338                    let b = m.verts[t[1] as usize];
1339                    let c = m.verts[t[2] as usize];
1340                    let ux = b[0] - a[0];
1341                    let uy = b[1] - a[1];
1342                    let uz = b[2] - a[2];
1343                    let vx = c[0] - a[0];
1344                    let vy = c[1] - a[1];
1345                    let vz = c[2] - a[2];
1346                    let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
1347                    let centroid = [
1348                        (a[0] + b[0] + c[0]) / 3.0,
1349                        (a[1] + b[1] + c[1]) / 3.0,
1350                        (a[2] + b[2] + c[2]) / 3.0,
1351                    ];
1352                    let lit = crate::gfx::light::compute_lit_color(
1353                        self.color,
1354                        normal,
1355                        centroid,
1356                        &self.lights,
1357                        self.ambient,
1358                    );
1359                    let da = self.camera.depth(a[0], a[1], a[2]);
1360                    let db = self.camera.depth(b[0], b[1], b[2]);
1361                    let dc = self.camera.depth(c[0], c[1], c[2]);
1362                    if da <= near && db <= near && dc <= near {
1363                        continue;
1364                    } // all behind → drop
1365                      // Near-plane clip (flat colour, so vertex colour is irrelevant here).
1366                    let poly = near_clip_poly(
1367                        &[(a, [0.0; 3], da), (b, [0.0; 3], db), (c, [0.0; 3], dc)],
1368                        near,
1369                    );
1370                    if poly.len() < 3 {
1371                        continue;
1372                    }
1373                    let proj: Vec<(f32, f32, f32)> = poly
1374                        .iter()
1375                        .map(|(p, _)| self.camera.project(p[0], p[1], p[2]))
1376                        .collect();
1377                    let mut k = 1;
1378                    while k + 1 < proj.len() {
1379                        self.depth_queue.push_triangle_zv(
1380                            lit,
1381                            proj[0].0,
1382                            proj[0].1,
1383                            proj[0].2,
1384                            proj[k].0,
1385                            proj[k].1,
1386                            proj[k].2,
1387                            proj[k + 1].0,
1388                            proj[k + 1].1,
1389                            proj[k + 1].2,
1390                        );
1391                        k += 1;
1392                    }
1393                }
1394            }
1395        }
1396
1397        if mode == 1 || mode == 2 {
1398            let color = self.color;
1399            // small bias so wireframe paints on top of fills in "both" mode
1400            let bias = if mode == 2 { 0.03 } else { 0.0 };
1401            for e in &m.edges {
1402                let mut a = m.verts[e[0] as usize];
1403                let mut b = m.verts[e[1] as usize];
1404                let da = self.camera.depth(a[0], a[1], a[2]);
1405                let db = self.camera.depth(b[0], b[1], b[2]);
1406                if da <= near && db <= near {
1407                    continue;
1408                }
1409                if da <= near {
1410                    let t = (near - da) / (db - da);
1411                    a = [
1412                        a[0] + t * (b[0] - a[0]),
1413                        a[1] + t * (b[1] - a[1]),
1414                        a[2] + t * (b[2] - a[2]),
1415                    ];
1416                } else if db <= near {
1417                    let t = (near - da) / (db - da);
1418                    b = [
1419                        a[0] + t * (b[0] - a[0]),
1420                        a[1] + t * (b[1] - a[1]),
1421                        a[2] + t * (b[2] - a[2]),
1422                    ];
1423                }
1424                let (sax, say, pa) = self.camera.project(a[0], a[1], a[2]);
1425                let (sbx, sby, pb) = self.camera.project(b[0], b[1], b[2]);
1426                let depth = (pa + pb) / 2.0 - bias;
1427                self.depth_queue.push_line(depth, color, sax, say, sbx, sby);
1428            }
1429        }
1430    }
1431}
1432
1433/// Near-plane clip of a convex polygon (Sutherland–Hodgman). Each input vertex is
1434/// `(world_pos, colour_rgb, camera_depth)`; a vertex is kept when `depth > near`.
1435/// Vertices created on crossing edges interpolate both position and colour, so a
1436/// large floor/wall tile straddling the near plane is trimmed to its in-front
1437/// portion rather than dropped wholesale (which made tiles pop out when close).
1438fn near_clip_poly(vin: &[([f32; 3], [f32; 3], f32)], near: f32) -> Vec<([f32; 3], [f32; 3])> {
1439    let n = vin.len();
1440    let mut out: Vec<([f32; 3], [f32; 3])> = Vec::with_capacity(n + 1);
1441    for i in 0..n {
1442        let a = &vin[i];
1443        let b = &vin[(i + 1) % n];
1444        let ain = a.2 > near;
1445        let bin = b.2 > near;
1446        if ain {
1447            out.push((a.0, a.1));
1448        }
1449        if ain != bin {
1450            let t = (near - a.2) / (b.2 - a.2);
1451            let lerp3 = |p: [f32; 3], q: [f32; 3]| {
1452                [
1453                    p[0] + (q[0] - p[0]) * t,
1454                    p[1] + (q[1] - p[1]) * t,
1455                    p[2] + (q[2] - p[2]) * t,
1456                ]
1457            };
1458            out.push((lerp3(a.0, b.0), lerp3(a.1, b.1)));
1459        }
1460    }
1461    out
1462}