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