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                // Near-clip a triangle → ≤4 verts, on the stack (no per-tri alloc).
1147                let vin = [(a, da), (b, db), (c, dc)];
1148                let mut clip: [[f32; 3]; 4] = [[0.0; 3]; 4];
1149                let mut cn = 0usize;
1150                let mut i = 0;
1151                while i < 3 {
1152                    let (pa, pad) = vin[i];
1153                    let (pb, pbd) = vin[(i + 1) % 3];
1154                    let ain = pad > near;
1155                    let bin = pbd > near;
1156                    if ain && cn < 4 {
1157                        clip[cn] = pa;
1158                        cn += 1;
1159                    }
1160                    if ain != bin && cn < 4 {
1161                        let t = (near - pad) / (pbd - pad);
1162                        clip[cn] = [
1163                            pa[0] + (pb[0] - pa[0]) * t,
1164                            pa[1] + (pb[1] - pa[1]) * t,
1165                            pa[2] + (pb[2] - pa[2]) * t,
1166                        ];
1167                        cn += 1;
1168                    }
1169                    i += 1;
1170                }
1171                if cn >= 3 {
1172                    let col = m.col[ti];
1173                    let packed = ((col[0] as u32) << 16) | ((col[1] as u32) << 8) | (col[2] as u32);
1174                    let mut proj: [(f32, f32, f32); 4] = [(0.0, 0.0, 0.0); 4];
1175                    let mut depth = 0.0f32;
1176                    let mut pi = 0;
1177                    while pi < cn {
1178                        proj[pi] = self.camera.project(clip[pi][0], clip[pi][1], clip[pi][2]);
1179                        depth += proj[pi].2;
1180                        pi += 1;
1181                    }
1182                    depth /= cn as f32;
1183                    let mut j = 1;
1184                    while j + 1 < cn {
1185                        self.depth_queue.push_triangle(
1186                            depth,
1187                            packed,
1188                            proj[0].0,
1189                            proj[0].1,
1190                            proj[j].0,
1191                            proj[j].1,
1192                            proj[j + 1].0,
1193                            proj[j + 1].1,
1194                        );
1195                        j += 1;
1196                    }
1197                }
1198            }
1199            ti += 1;
1200        }
1201    }
1202
1203    /// Viscous screen-space distortion of the current framebuffer: warps/puckers/
1204    /// bloats in shifting regions and **wraps** at all four edges (toroidal sample).
1205    /// Separable (per-row + per-column displacement) so it stays cheap full-screen.
1206    /// `amount` = max displacement in pixels; `t` = time (animate the goo).
1207    pub fn distort(&mut self, amount: f32, t: f32, step: usize) {
1208        let w = self.width;
1209        let h = self.height;
1210        if w < 2 || h < 2 || amount <= 0.0 {
1211            return;
1212        }
1213        let step = step.max(1);
1214        // Source = the current frame. Instead of cloning the framebuffer (an 8 MB
1215        // alloc + memcpy EVERY frame at 1080p), swap a persistent scratch in: now
1216        // `src` holds the rendered frame and `self.buffer` is the old scratch —
1217        // which the gather below overwrites at every pixel, so its stale contents
1218        // don't matter. Net: per-frame distortion drops from (alloc + memcpy +
1219        // gather) to just (gather). `src` is returned to the scratch field at the end.
1220        if self.distort_buf.len() != w * h {
1221            self.distort_buf.clear();
1222            self.distort_buf.resize(w * h, 0);
1223        }
1224        let mut src = std::mem::take(&mut self.distort_buf);
1225        std::mem::swap(&mut self.buffer, &mut src);
1226        let a = amount;
1227        // per-row horizontal shift + a vertical cross term
1228        let mut rdx = vec![0i32; h];
1229        let mut rdy = vec![0i32; h];
1230        for y in 0..h {
1231            let fy = y as f32;
1232            rdx[y] =
1233                ((fy * 0.018 + t * 0.8).sin() * a + (fy * 0.005 - t * 0.5).sin() * a * 0.6) as i32;
1234            rdy[y] = ((fy * 0.040 + t * 1.1).sin() * a * 0.4) as i32;
1235        }
1236        // per-column vertical shift + a horizontal cross term  (the two cross terms
1237        // make the warp swirl in 2-D; multi-frequency sines give pucker/bloat zones)
1238        let mut cdy = vec![0i32; w];
1239        let mut cdx = vec![0i32; w];
1240        for x in 0..w {
1241            let fx = x as f32;
1242            cdy[x] =
1243                ((fx * 0.020 + t * 0.7).sin() * a + (fx * 0.006 + t * 0.45).sin() * a * 0.6) as i32;
1244            cdx[x] = ((fx * 0.050 + t * 0.9).sin() * a * 0.4) as i32;
1245        }
1246        let wi = w as i32;
1247        let hi = h as i32;
1248        if step == 1 {
1249            // full-res per-pixel warp. Each output row is gathered independently
1250            // from the shared source frame, so rows parallelise with no contention.
1251            let warp_row = |y: usize, out: &mut [u32]| {
1252                let rdx_y = rdx[y];
1253                let ry = rdy[y];
1254                for x in 0..w {
1255                    // branchless small-shift wrap (displacements are a few px → one add wraps)
1256                    let mut sxi = x as i32 + rdx_y + cdx[x];
1257                    if sxi < 0 {
1258                        sxi += wi;
1259                    } else if sxi >= wi {
1260                        sxi -= wi;
1261                    }
1262                    let mut syi = y as i32 + cdy[x] + ry;
1263                    if syi < 0 {
1264                        syi += hi;
1265                    } else if syi >= hi {
1266                        syi -= hi;
1267                    }
1268                    out[x] = src[syi as usize * w + sxi as usize];
1269                }
1270            };
1271            #[cfg(not(target_arch = "wasm32"))]
1272            {
1273                use rayon::prelude::*;
1274                self.buffer
1275                    .par_chunks_mut(w)
1276                    .enumerate()
1277                    .for_each(|(y, out)| warp_row(y, out));
1278            }
1279            #[cfg(target_arch = "wasm32")]
1280            for (y, out) in self.buffer.chunks_mut(w).enumerate() {
1281                warp_row(y, out);
1282            }
1283        } else {
1284            // downsampled warp: ONE warped source sample per step×step block,
1285            // filled across the block. The expensive part of the full-res path is
1286            // the W×H scattered gather (memory-bandwidth bound); this does only
1287            // (W/step·H/step) gathers + W×H cheap sequential block-fills → ~step²
1288            // fewer reads/warp-computes. Trade-off: the image is step×step blocky
1289            // (a "performance mode" look, not softer) — gated behind a toggle.
1290            let mut by = 0;
1291            while by < h {
1292                let yend = (by + step).min(h);
1293                let mut bx = 0;
1294                while bx < w {
1295                    let xend = (bx + step).min(w);
1296                    let mut sxi = bx as i32 + rdx[by] + cdx[bx];
1297                    if sxi < 0 {
1298                        sxi += wi;
1299                    } else if sxi >= wi {
1300                        sxi -= wi;
1301                    }
1302                    let mut syi = by as i32 + cdy[bx] + rdy[by];
1303                    if syi < 0 {
1304                        syi += hi;
1305                    } else if syi >= hi {
1306                        syi -= hi;
1307                    }
1308                    let pix = src[syi as usize * w + sxi as usize];
1309                    for y in by..yend {
1310                        let row = y * w;
1311                        for x in bx..xend {
1312                            self.buffer[row + x] = pix;
1313                        }
1314                    }
1315                    bx += step;
1316                }
1317                by += step;
1318            }
1319        }
1320        self.distort_buf = src; // return the scratch buffer for next frame's reuse
1321    }
1322
1323    /// Render a world-space mesh through the depth queue.
1324    /// mode: 0 filled, 1 wireframe, 2 both.
1325    pub fn emit_mesh(&mut self, m: &Mesh, mode: i32) {
1326        let near = -self.camera.zdist + 0.05;
1327
1328        let want_fill = mode == 0 || mode == 2;
1329        if want_fill {
1330            let have_normals = m.normals.len() == m.verts.len() && self.shade_mode != 0;
1331            if have_normals {
1332                // ── smooth cel / holographic path ─────────────────────────────
1333                // Per-vertex coloured lighting (smooth normals) → Gouraud
1334                // interpolation → per-pixel posterise. No faceted edges.
1335                let base = ling_graphics::shading::unpack(self.color);
1336                let eye = [self.camera.tx, self.camera.ty, self.camera.tz];
1337                let lights: Vec<ling_graphics::shading::LightS> = self
1338                    .lights
1339                    .iter()
1340                    .map(|l| ling_graphics::shading::LightS {
1341                        pos: [l.x, l.y, l.z],
1342                        color: [l.r, l.g, l.b],
1343                        intensity: l.intensity,
1344                        radius: l.radius,
1345                    })
1346                    .collect();
1347                let mut sp = self.shade;
1348                sp.ambient = self.ambient; // scene ambient drives fill
1349                if self.shade_mode == 1 {
1350                    sp.holo = false;
1351                    sp.rim *= 0.4;
1352                }
1353                let bands = sp.bands;
1354                for t in &m.tris {
1355                    let ia = t[0] as usize;
1356                    let ib = t[1] as usize;
1357                    let ic = t[2] as usize;
1358                    let a = m.verts[ia];
1359                    let b = m.verts[ib];
1360                    let c = m.verts[ic];
1361                    let da = self.camera.depth(a[0], a[1], a[2]);
1362                    let db = self.camera.depth(b[0], b[1], b[2]);
1363                    let dc = self.camera.depth(c[0], c[1], c[2]);
1364                    if da <= near && db <= near && dc <= near {
1365                        continue;
1366                    } // all behind → drop
1367                      // Lit colours per vertex (kept unpacked so clipping can lerp them).
1368                    let la = ling_graphics::shading::lit_vertex(
1369                        base,
1370                        m.normals[ia],
1371                        a,
1372                        eye,
1373                        &lights,
1374                        &sp,
1375                    );
1376                    let lb = ling_graphics::shading::lit_vertex(
1377                        base,
1378                        m.normals[ib],
1379                        b,
1380                        eye,
1381                        &lights,
1382                        &sp,
1383                    );
1384                    let lc = ling_graphics::shading::lit_vertex(
1385                        base,
1386                        m.normals[ic],
1387                        c,
1388                        eye,
1389                        &lights,
1390                        &sp,
1391                    );
1392                    // Near-plane clip (keeps large straddling tiles instead of dropping them).
1393                    let poly = near_clip_poly(&[(a, la, da), (b, lb, db), (c, lc, dc)], near);
1394                    if poly.len() < 3 {
1395                        continue;
1396                    }
1397                    let proj: Vec<(f32, f32, f32, u32)> = poly
1398                        .iter()
1399                        .map(|(p, col)| {
1400                            let (sx, sy, pz) = self.camera.project(p[0], p[1], p[2]);
1401                            (sx, sy, pz, ling_graphics::shading::pack(*col))
1402                        })
1403                        .collect();
1404                    let mut k = 1;
1405                    while k + 1 < proj.len() {
1406                        self.depth_queue.push_triangle_g_zv(
1407                            proj[0].0,
1408                            proj[0].1,
1409                            proj[0].2,
1410                            proj[0].3,
1411                            proj[k].0,
1412                            proj[k].1,
1413                            proj[k].2,
1414                            proj[k].3,
1415                            proj[k + 1].0,
1416                            proj[k + 1].1,
1417                            proj[k + 1].2,
1418                            proj[k + 1].3,
1419                            bands,
1420                        );
1421                        k += 1;
1422                    }
1423                }
1424            } else {
1425                // ── flat per-face path (shade_mode 0) ─────────────────────────
1426                for t in &m.tris {
1427                    let a = m.verts[t[0] as usize];
1428                    let b = m.verts[t[1] as usize];
1429                    let c = m.verts[t[2] as usize];
1430                    let ux = b[0] - a[0];
1431                    let uy = b[1] - a[1];
1432                    let uz = b[2] - a[2];
1433                    let vx = c[0] - a[0];
1434                    let vy = c[1] - a[1];
1435                    let vz = c[2] - a[2];
1436                    let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
1437                    let centroid = [
1438                        (a[0] + b[0] + c[0]) / 3.0,
1439                        (a[1] + b[1] + c[1]) / 3.0,
1440                        (a[2] + b[2] + c[2]) / 3.0,
1441                    ];
1442                    let lit = if self.flat_shade {
1443                        self.color
1444                    } else {
1445                        crate::gfx::light::compute_lit_color(
1446                            self.color,
1447                            normal,
1448                            centroid,
1449                            &self.lights,
1450                            self.ambient,
1451                        )
1452                    };
1453                    let da = self.camera.depth(a[0], a[1], a[2]);
1454                    let db = self.camera.depth(b[0], b[1], b[2]);
1455                    let dc = self.camera.depth(c[0], c[1], c[2]);
1456                    if da <= near && db <= near && dc <= near {
1457                        continue;
1458                    } // all behind → drop
1459                      // Near-plane clip (flat colour, so vertex colour is irrelevant here).
1460                    let poly = near_clip_poly(
1461                        &[(a, [0.0; 3], da), (b, [0.0; 3], db), (c, [0.0; 3], dc)],
1462                        near,
1463                    );
1464                    if poly.len() < 3 {
1465                        continue;
1466                    }
1467                    let proj: Vec<(f32, f32, f32)> = poly
1468                        .iter()
1469                        .map(|(p, _)| self.camera.project(p[0], p[1], p[2]))
1470                        .collect();
1471                    let mut k = 1;
1472                    while k + 1 < proj.len() {
1473                        self.depth_queue.push_triangle_zv(
1474                            lit,
1475                            proj[0].0,
1476                            proj[0].1,
1477                            proj[0].2,
1478                            proj[k].0,
1479                            proj[k].1,
1480                            proj[k].2,
1481                            proj[k + 1].0,
1482                            proj[k + 1].1,
1483                            proj[k + 1].2,
1484                        );
1485                        k += 1;
1486                    }
1487                }
1488            }
1489        }
1490
1491        if mode == 1 || mode == 2 {
1492            let color = self.color;
1493            // small bias so wireframe paints on top of fills in "both" mode
1494            let bias = if mode == 2 { 0.03 } else { 0.0 };
1495            for e in &m.edges {
1496                let mut a = m.verts[e[0] as usize];
1497                let mut b = m.verts[e[1] as usize];
1498                let da = self.camera.depth(a[0], a[1], a[2]);
1499                let db = self.camera.depth(b[0], b[1], b[2]);
1500                if da <= near && db <= near {
1501                    continue;
1502                }
1503                if da <= near {
1504                    let t = (near - da) / (db - da);
1505                    a = [
1506                        a[0] + t * (b[0] - a[0]),
1507                        a[1] + t * (b[1] - a[1]),
1508                        a[2] + t * (b[2] - a[2]),
1509                    ];
1510                } else if db <= near {
1511                    let t = (near - da) / (db - da);
1512                    b = [
1513                        a[0] + t * (b[0] - a[0]),
1514                        a[1] + t * (b[1] - a[1]),
1515                        a[2] + t * (b[2] - a[2]),
1516                    ];
1517                }
1518                let (sax, say, pa) = self.camera.project(a[0], a[1], a[2]);
1519                let (sbx, sby, pb) = self.camera.project(b[0], b[1], b[2]);
1520                let depth = (pa + pb) / 2.0 - bias;
1521                self.depth_queue.push_line(depth, color, sax, say, sbx, sby);
1522            }
1523        }
1524    }
1525}
1526
1527/// Near-plane clip of a convex polygon (Sutherland–Hodgman). Each input vertex is
1528/// `(world_pos, colour_rgb, camera_depth)`; a vertex is kept when `depth > near`.
1529/// Vertices created on crossing edges interpolate both position and colour, so a
1530/// large floor/wall tile straddling the near plane is trimmed to its in-front
1531/// portion rather than dropped wholesale (which made tiles pop out when close).
1532fn near_clip_poly(vin: &[([f32; 3], [f32; 3], f32)], near: f32) -> Vec<([f32; 3], [f32; 3])> {
1533    let n = vin.len();
1534    let mut out: Vec<([f32; 3], [f32; 3])> = Vec::with_capacity(n + 1);
1535    for i in 0..n {
1536        let a = &vin[i];
1537        let b = &vin[(i + 1) % n];
1538        let ain = a.2 > near;
1539        let bin = b.2 > near;
1540        if ain {
1541            out.push((a.0, a.1));
1542        }
1543        if ain != bin {
1544            let t = (near - a.2) / (b.2 - a.2);
1545            let lerp3 = |p: [f32; 3], q: [f32; 3]| {
1546                [
1547                    p[0] + (q[0] - p[0]) * t,
1548                    p[1] + (q[1] - p[1]) * t,
1549                    p[2] + (q[2] - p[2]) * t,
1550                ]
1551            };
1552            out.push((lerp3(a.0, b.0), lerp3(a.1, b.1)));
1553        }
1554    }
1555    out
1556}