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 std::collections::HashSet;
16use std::f32::consts::PI;
17use super::GfxState;
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) { self.tris.push([a, b, c]); }
37    fn edge(&mut self, a: u32, b: u32)        { self.edges.push([a, b]); }
38
39    /// Add a convex polygon (fan-triangulated) and its perimeter edges.
40    fn face(&mut self, idx: &[u32]) {
41        for k in 1..idx.len() - 1 {
42            self.tris.push([idx[0], idx[k], idx[k + 1]]);
43        }
44        for k in 0..idx.len() {
45            self.edges.push([idx[k], idx[(k + 1) % idx.len()]]);
46        }
47    }
48
49    /// Derive a deduplicated edge list from the triangles (for curved meshes).
50    fn edges_from_tris(&mut self) {
51        let mut seen: HashSet<(u32, u32)> = HashSet::new();
52        for t in &self.tris {
53            for &(a, b) in &[(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
54                let k = if a < b { (a, b) } else { (b, a) };
55                if seen.insert(k) { self.edges.push([k.0, k.1]); }
56            }
57        }
58    }
59
60    /// Compute area-weighted smooth per-vertex normals from the current
61    /// (already transformed) verts + tris — gives continuous shading with no
62    /// faceted edges.
63    fn compute_smooth_normals(&mut self) {
64        let mut n = vec![[0.0f32; 3]; self.verts.len()];
65        for t in &self.tris {
66            let a = self.verts[t[0] as usize];
67            let b = self.verts[t[1] as usize];
68            let c = self.verts[t[2] as usize];
69            let u = [b[0]-a[0], b[1]-a[1], b[2]-a[2]];
70            let v = [c[0]-a[0], c[1]-a[1], c[2]-a[2]];
71            let f = [u[1]*v[2]-u[2]*v[1], u[2]*v[0]-u[0]*v[2], u[0]*v[1]-u[1]*v[0]];
72            for &i in t { let i = i as usize; n[i][0]+=f[0]; n[i][1]+=f[1]; n[i][2]+=f[2]; }
73        }
74        for p in &mut n {
75            let l = (p[0]*p[0]+p[1]*p[1]+p[2]*p[2]).sqrt();
76            if l > 1e-8 { p[0]/=l; p[1]/=l; p[2]/=l; }
77        }
78        self.normals = n;
79    }
80
81    /// scale → rotate(Euler XYZ) → translate, in place.
82    fn transform(&mut self, c: [f32; 9]) {
83        let (cx, cy, cz) = (c[0], c[1], c[2]);
84        let (sx, sy, sz) = (c[3], c[4], c[5]);
85        let (rx, ry, rz) = (c[6], c[7], c[8]);
86        let (srx, crx) = rx.sin_cos();
87        let (sry, cry) = ry.sin_cos();
88        let (srz, crz) = rz.sin_cos();
89        for p in &mut self.verts {
90            let mut x = p[0] * sx;
91            let mut y = p[1] * sy;
92            let mut z = p[2] * sz;
93            // rotate X
94            let (ny, nz) = (y * crx - z * srx, y * srx + z * crx); y = ny; z = nz;
95            // rotate Y
96            let (nx, nz2) = (x * cry + z * sry, -x * sry + z * cry); x = nx; z = nz2;
97            // rotate Z
98            let (nx2, ny2) = (x * crz - y * srz, x * srz + y * crz); x = nx2; y = ny2;
99            *p = [x + cx, y + cy, z + cz];
100        }
101    }
102}
103
104// ── small helpers ───────────────────────────────────────────────────────────
105#[inline] fn iarg(v: f32, default: i32) -> i32 { if v > 0.5 { v.round() as i32 } else { default } }
106#[inline] fn farg(v: f32, default: f32) -> f32 { if v > 1e-6 { v } else { default } }
107
108// ── Platonic / dice solids ───────────────────────────────────────────────────
109
110fn cube() -> Mesh {
111    let mut m = Mesh::default();
112    let s = 1.0;
113    let p = [
114        m.v(-s,-s,-s), m.v(s,-s,-s), m.v(s,s,-s), m.v(-s,s,-s), // back  0..3
115        m.v(-s,-s, s), m.v(s,-s, s), m.v(s,s, s), m.v(-s,s, s), // front 4..7
116    ];
117    m.face(&[p[0],p[1],p[2],p[3]]); // -Z
118    m.face(&[p[5],p[4],p[7],p[6]]); // +Z
119    m.face(&[p[4],p[0],p[3],p[7]]); // -X
120    m.face(&[p[1],p[5],p[6],p[2]]); // +X
121    m.face(&[p[4],p[5],p[1],p[0]]); // -Y
122    m.face(&[p[3],p[2],p[6],p[7]]); // +Y
123    m
124}
125
126fn tetrahedron() -> Mesh {
127    let mut m = Mesh::default();
128    let a = 1.0;
129    let p = [
130        m.v( a, a, a), m.v( a,-a,-a), m.v(-a, a,-a), m.v(-a,-a, a),
131    ];
132    m.face(&[p[0],p[1],p[2]]);
133    m.face(&[p[0],p[3],p[1]]);
134    m.face(&[p[0],p[2],p[3]]);
135    m.face(&[p[1],p[3],p[2]]);
136    m
137}
138
139fn octahedron() -> Mesh {
140    let mut m = Mesh::default();
141    let p = [
142        m.v( 1.0,0.0,0.0), m.v(-1.0,0.0,0.0),
143        m.v(0.0, 1.0,0.0), m.v(0.0,-1.0,0.0),
144        m.v(0.0,0.0, 1.0), m.v(0.0,0.0,-1.0),
145    ];
146    m.face(&[p[0],p[2],p[4]]); m.face(&[p[2],p[1],p[4]]);
147    m.face(&[p[1],p[3],p[4]]); m.face(&[p[3],p[0],p[4]]);
148    m.face(&[p[2],p[0],p[5]]); m.face(&[p[1],p[2],p[5]]);
149    m.face(&[p[3],p[1],p[5]]); m.face(&[p[0],p[3],p[5]]);
150    m
151}
152
153fn icosahedron_raw() -> Mesh {
154    let mut m = Mesh::default();
155    let t = (1.0 + 5.0_f32.sqrt()) / 2.0;
156    let s = 1.0 / (1.0 + t*t).sqrt(); // normalise to unit radius
157    let vs = [
158        [-1., t, 0.],[1., t, 0.],[-1.,-t, 0.],[1.,-t, 0.],
159        [0.,-1., t],[0., 1., t],[0.,-1.,-t],[0., 1.,-t],
160        [ t, 0.,-1.],[ t, 0., 1.],[-t, 0.,-1.],[-t, 0., 1.],
161    ];
162    for v in vs { m.v(v[0]*s, v[1]*s, v[2]*s); }
163    let f = [
164        [0,11,5],[0,5,1],[0,1,7],[0,7,10],[0,10,11],
165        [1,5,9],[5,11,4],[11,10,2],[10,7,6],[7,1,8],
166        [3,9,4],[3,4,2],[3,2,6],[3,6,8],[3,8,9],
167        [4,9,5],[2,4,11],[6,2,10],[8,6,7],[9,8,1],
168    ];
169    for t in f { m.tri(t[0],t[1],t[2]); }
170    m
171}
172
173fn icosahedron() -> Mesh { let mut m = icosahedron_raw(); m.edges_from_tris(); m }
174
175fn icosphere(subdiv: i32) -> Mesh {
176    let mut m = icosahedron_raw();
177    let n = subdiv.clamp(0, 4);
178    for _ in 0..n {
179        let mut nm = Mesh::default();
180        let mut mid: std::collections::HashMap<(u32,u32),u32> = std::collections::HashMap::new();
181        for v in &m.verts { nm.verts.push(*v); }
182        let midpoint = |nm: &mut Mesh, a: u32, b: u32, mid: &mut std::collections::HashMap<(u32,u32),u32>| -> u32 {
183            let key = if a < b { (a,b) } else { (b,a) };
184            if let Some(&i) = mid.get(&key) { return i; }
185            let pa = nm.verts[a as usize]; let pb = nm.verts[b as usize];
186            let mut mp = [(pa[0]+pb[0])/2.0,(pa[1]+pb[1])/2.0,(pa[2]+pb[2])/2.0];
187            let l = (mp[0]*mp[0]+mp[1]*mp[1]+mp[2]*mp[2]).sqrt();
188            mp = [mp[0]/l, mp[1]/l, mp[2]/l];
189            let i = nm.verts.len() as u32; nm.verts.push(mp); mid.insert(key, i); i
190        };
191        for t in &m.tris {
192            let a = midpoint(&mut nm, t[0], t[1], &mut mid);
193            let b = midpoint(&mut nm, t[1], t[2], &mut mid);
194            let c = midpoint(&mut nm, t[2], t[0], &mut mid);
195            nm.tri(t[0],a,c); nm.tri(t[1],b,a); nm.tri(t[2],c,b); nm.tri(a,b,c);
196        }
197        m = nm;
198    }
199    m.edges_from_tris();
200    m
201}
202
203fn dodecahedron() -> Mesh {
204    let mut m = Mesh::default();
205    let phi = (1.0 + 5.0_f32.sqrt()) / 2.0;
206    let b = 1.0 / phi;
207    let c = phi;
208    let r = (3.0_f32).sqrt(); // normalise so |(1,1,1)| family → unit-ish
209    let s = 1.0 / r;
210    let vs = [
211        [ 1., 1., 1.],[ 1., 1.,-1.],[ 1.,-1., 1.],[ 1.,-1.,-1.],
212        [-1., 1., 1.],[-1., 1.,-1.],[-1.,-1., 1.],[-1.,-1.,-1.],
213        [0., b, c],[0., b,-c],[0.,-b, c],[0.,-b,-c],
214        [ b, c, 0.],[ b,-c, 0.],[-b, c, 0.],[-b,-c, 0.],
215        [ c, 0., b],[ c, 0.,-b],[-c, 0., b],[-c, 0.,-b],
216    ];
217    for v in vs { m.v(v[0]*s, v[1]*s, v[2]*s); }
218    let faces: [[u32;5];12] = [
219        [0,8,10,2,16],[0,16,17,1,12],[0,12,14,4,8],
220        [1,9,5,14,12],[1,17,3,11,9],[2,10,6,15,13],
221        [2,13,3,17,16],[3,13,15,7,11],[4,14,5,19,18],
222        [4,18,6,10,8],[5,9,11,7,19],[6,18,19,7,15],
223    ];
224    for f in faces { m.face(&f); }
225    m
226}
227
228// ── round / swept solids ──────────────────────────────────────────────────────
229
230fn uv_sphere(seg: i32, rings: i32) -> Mesh {
231    let mut m = Mesh::default();
232    let seg = seg.clamp(3, 128);
233    let rings = rings.clamp(2, 128);
234    for r in 0..=rings {
235        let v = r as f32 / rings as f32;
236        let theta = v * PI;            // 0..pi
237        let (st, ct) = theta.sin_cos();
238        for s in 0..=seg {
239            let u = s as f32 / seg as f32;
240            let phi = u * 2.0 * PI;
241            let (sp, cp) = phi.sin_cos();
242            m.v(st * cp, ct, st * sp);
243        }
244    }
245    let stride = seg + 1;
246    for r in 0..rings {
247        for s in 0..seg {
248            let a = (r * stride + s) as u32;
249            let b = (r * stride + s + 1) as u32;
250            let cc = ((r + 1) * stride + s) as u32;
251            let d = ((r + 1) * stride + s + 1) as u32;
252            m.tri(a, cc, b); m.tri(b, cc, d);
253        }
254    }
255    m.edges_from_tris();
256    m
257}
258
259fn dome(seg: i32, rings: i32) -> Mesh {
260    // upper hemisphere (y in [0..1]) with a closing base ring
261    let mut m = Mesh::default();
262    let seg = seg.clamp(3, 128);
263    let rings = rings.clamp(1, 128);
264    for r in 0..=rings {
265        let v = r as f32 / rings as f32;
266        let theta = v * (PI / 2.0);    // 0..pi/2
267        let (st, ct) = theta.sin_cos();
268        for s in 0..=seg {
269            let phi = s as f32 / seg as f32 * 2.0 * PI;
270            let (sp, cp) = phi.sin_cos();
271            m.v(st * cp, ct, st * sp);
272        }
273    }
274    let stride = seg + 1;
275    for r in 0..rings {
276        for s in 0..seg {
277            let a = (r*stride+s) as u32; let b=(r*stride+s+1) as u32;
278            let cc=((r+1)*stride+s) as u32; let d=((r+1)*stride+s+1) as u32;
279            m.tri(a, cc, b); m.tri(b, cc, d);
280        }
281    }
282    // base cap
283    let centre = m.v(0.0, 0.0, 0.0);
284    for s in 0..seg {
285        let a = ((rings)*stride+s) as u32; let b=((rings)*stride+s+1) as u32;
286        m.tri(centre, b, a);
287    }
288    m.edges_from_tris();
289    m
290}
291
292fn cylinder(seg: i32) -> Mesh {
293    let mut m = Mesh::default();
294    let seg = seg.clamp(3, 256);
295    // rings at y=-1 (bottom) and y=+1 (top)
296    for s in 0..seg {
297        let phi = s as f32 / seg as f32 * 2.0 * PI;
298        let (sp, cp) = phi.sin_cos();
299        m.v(cp, -1.0, sp);
300        m.v(cp,  1.0, sp);
301    }
302    for s in 0..seg {
303        let b0 = (2*s) as u32; let t0 = (2*s+1) as u32;
304        let b1 = (2*((s+1)%seg)) as u32; let t1 = (2*((s+1)%seg)+1) as u32;
305        m.tri(b0, t0, b1); m.tri(b1, t0, t1);
306        m.edge(b0, b1); m.edge(t0, t1); m.edge(b0, t0);
307    }
308    let cb = m.v(0.0,-1.0,0.0); let ct = m.v(0.0,1.0,0.0);
309    for s in 0..seg {
310        let b0=(2*s) as u32; let b1=(2*((s+1)%seg)) as u32;
311        let t0=(2*s+1) as u32; let t1=(2*((s+1)%seg)+1) as u32;
312        m.tri(cb, b1, b0); m.tri(ct, t0, t1);
313    }
314    m
315}
316
317fn cone(seg: i32) -> Mesh {
318    let mut m = Mesh::default();
319    let seg = seg.clamp(3, 256);
320    let apex = m.v(0.0, 1.0, 0.0);
321    let base0 = m.verts.len() as u32;
322    for s in 0..seg {
323        let phi = s as f32 / seg as f32 * 2.0 * PI;
324        let (sp, cp) = phi.sin_cos();
325        m.v(cp, -1.0, sp);
326    }
327    let centre = m.v(0.0, -1.0, 0.0);
328    for s in 0..seg {
329        let a = base0 + s as u32; let b = base0 + ((s+1)%seg) as u32;
330        m.tri(apex, a, b);   // side
331        m.tri(centre, b, a); // base
332        m.edge(a, b); m.edge(apex, a);
333    }
334    m
335}
336
337fn capsule(seg: i32, rings: i32) -> Mesh {
338    // cylinder body (y -1..1) capped by two hemispheres of radius 1
339    let mut m = Mesh::default();
340    let seg = seg.clamp(3, 128);
341    let rings = rings.clamp(1, 64);
342    let stride = seg + 1;
343    // top hemisphere: theta 0..pi/2 mapped onto y = 1 + cos*? keep radius 1 sphere centred at y=+1
344    let mut ring_start = Vec::new();
345    let total_rows = 2 * rings; // top hemi rows + bottom hemi rows
346    for row in 0..=total_rows {
347        ring_start.push(m.verts.len() as u32);
348        let (cy_off, theta) = if row <= rings {
349            // top hemisphere: row 0 = pole (theta 0)
350            let v = row as f32 / rings as f32;
351            (1.0, v * PI / 2.0)
352        } else {
353            // bottom hemisphere
354            let v = (row - rings) as f32 / rings as f32;
355            (-1.0, PI / 2.0 + v * PI / 2.0)
356        };
357        let (st, ct) = theta.sin_cos();
358        for s in 0..=seg {
359            let phi = s as f32 / seg as f32 * 2.0 * PI;
360            let (sp, cp) = phi.sin_cos();
361            m.v(st * cp, cy_off + ct, st * sp);
362        }
363    }
364    for row in 0..total_rows as usize {
365        for s in 0..seg {
366            let a = ring_start[row] + s as u32;
367            let b = ring_start[row] + s as u32 + 1;
368            let c = ring_start[row + 1] + s as u32;
369            let d = ring_start[row + 1] + s as u32 + 1;
370            m.tri(a, c, b); m.tri(b, c, d);
371        }
372    }
373    let _ = stride;
374    m.edges_from_tris();
375    m
376}
377
378fn torus(seg: i32, sides: i32, tube: f32) -> Mesh {
379    let mut m = Mesh::default();
380    let seg = seg.clamp(3, 256);    // around the ring
381    let sides = sides.clamp(3, 128); // around the tube
382    let tube = tube.clamp(0.02, 0.9);
383    for i in 0..seg {
384        let u = i as f32 / seg as f32 * 2.0 * PI;
385        let (su, cu) = u.sin_cos();
386        for j in 0..sides {
387            let v = j as f32 / sides as f32 * 2.0 * PI;
388            let (sv, cv) = v.sin_cos();
389            let r = 1.0 - tube + tube * cv;
390            m.v(r * cu, tube * sv, r * su);
391        }
392    }
393    for i in 0..seg {
394        for j in 0..sides {
395            let a = (i*sides + j) as u32;
396            let b = (i*sides + (j+1)%sides) as u32;
397            let c = (((i+1)%seg)*sides + j) as u32;
398            let d = (((i+1)%seg)*sides + (j+1)%sides) as u32;
399            m.tri(a, c, b); m.tri(b, c, d);
400        }
401    }
402    m.edges_from_tris();
403    m
404}
405
406// ── prisms / pyramids ─────────────────────────────────────────────────────────
407
408fn pyramid(sides: i32) -> Mesh {
409    let mut m = Mesh::default();
410    let sides = sides.clamp(3, 128);
411    let apex = m.v(0.0, 1.0, 0.0);
412    let base0 = m.verts.len() as u32;
413    let mut ring = Vec::new();
414    for s in 0..sides {
415        let phi = s as f32 / sides as f32 * 2.0 * PI;
416        let (sp, cp) = phi.sin_cos();
417        ring.push(m.v(cp, -1.0, sp));
418    }
419    for s in 0..sides as usize {
420        let a = ring[s]; let b = ring[(s+1)%sides as usize];
421        m.tri(apex, a, b);
422        m.edge(a, b); m.edge(apex, a);
423    }
424    // base face (reversed for outward normal)
425    let mut rev: Vec<u32> = ring.clone(); rev.reverse();
426    for k in 1..rev.len()-1 { m.tri(rev[0], rev[k], rev[k+1]); }
427    let _ = base0;
428    m
429}
430
431fn prism(sides: i32) -> Mesh {
432    let mut m = Mesh::default();
433    let sides = sides.clamp(3, 128);
434    let mut bot = Vec::new(); let mut top = Vec::new();
435    for s in 0..sides {
436        let phi = s as f32 / sides as f32 * 2.0 * PI;
437        let (sp, cp) = phi.sin_cos();
438        bot.push(m.v(cp, -1.0, sp));
439        top.push(m.v(cp,  1.0, sp));
440    }
441    let n = sides as usize;
442    for s in 0..n {
443        let b0=bot[s]; let b1=bot[(s+1)%n]; let t0=top[s]; let t1=top[(s+1)%n];
444        m.tri(b0, t0, b1); m.tri(b1, t0, t1);
445        m.edge(b0,b1); m.edge(t0,t1); m.edge(b0,t0);
446    }
447    for k in 1..n-1 { m.tri(top[0], top[k], top[k+1]); }
448    let mut rb: Vec<u32> = bot.clone(); rb.reverse();
449    for k in 1..rb.len()-1 { m.tri(rb[0], rb[k], rb[k+1]); }
450    m
451}
452
453fn frustum(sides: i32, top_ratio: f32) -> Mesh {
454    let mut m = Mesh::default();
455    let sides = sides.clamp(3, 256);
456    let tr = top_ratio.clamp(0.0, 1.0);
457    let mut bot = Vec::new(); let mut top = Vec::new();
458    for s in 0..sides {
459        let phi = s as f32 / sides as f32 * 2.0 * PI;
460        let (sp, cp) = phi.sin_cos();
461        bot.push(m.v(cp, -1.0, sp));
462        top.push(m.v(cp*tr, 1.0, sp*tr));
463    }
464    let n = sides as usize;
465    for s in 0..n {
466        let b0=bot[s]; let b1=bot[(s+1)%n]; let t0=top[s]; let t1=top[(s+1)%n];
467        m.tri(b0, t0, b1); m.tri(b1, t0, t1);
468        m.edge(b0,b1); m.edge(t0,t1); m.edge(b0,t0);
469    }
470    if tr > 0.001 { for k in 1..n-1 { m.tri(top[0], top[k], top[k+1]); } }
471    let mut rb: Vec<u32> = bot.clone(); rb.reverse();
472    for k in 1..rb.len()-1 { m.tri(rb[0], rb[k], rb[k+1]); }
473    m
474}
475
476// ── mechanical / architectural ────────────────────────────────────────────────
477
478fn gear(teeth: i32, tooth: f32) -> Mesh {
479    // flat gear in the XZ plane, extruded ±1 in Y; `tooth` = radial tooth depth.
480    let mut m = Mesh::default();
481    let teeth = teeth.clamp(3, 96);
482    let tooth = tooth.clamp(0.02, 0.6);
483    let pts = teeth * 4;            // 4 control points per tooth
484    let mut bot = Vec::new(); let mut top = Vec::new();
485    for i in 0..pts {
486        let phi = i as f32 / pts as f32 * 2.0 * PI;
487        // square-ish tooth profile: outer for first half of each tooth, inner for second
488        let phase = (i % 4) as f32;
489        let r = if phase < 2.0 { 1.0 } else { 1.0 - tooth };
490        let (sp, cp) = phi.sin_cos();
491        bot.push(m.v(cp*r, -1.0, sp*r));
492        top.push(m.v(cp*r,  1.0, sp*r));
493    }
494    let n = pts as usize;
495    for s in 0..n {
496        let b0=bot[s]; let b1=bot[(s+1)%n]; let t0=top[s]; let t1=top[(s+1)%n];
497        m.tri(b0, t0, b1); m.tri(b1, t0, t1);   // rim
498        m.edge(b0,b1); m.edge(t0,t1); m.edge(b0,t0);
499    }
500    let cb = m.v(0.0,-1.0,0.0); let ct = m.v(0.0,1.0,0.0);
501    for s in 0..n {
502        let b0=bot[s]; let b1=bot[(s+1)%n]; let t0=top[s]; let t1=top[(s+1)%n];
503        m.tri(cb, b1, b0); m.tri(ct, t0, t1);   // caps
504    }
505    m
506}
507
508fn gyro(rings: i32) -> Mesh {
509    // nested gimbal: `rings` tori on alternating axes at shrinking radius.
510    let mut m = Mesh::default();
511    let rings = rings.clamp(1, 6);
512    for k in 0..rings {
513        let scale = 1.0 - k as f32 * (0.8 / rings as f32);
514        let mut ring = torus(40, 8, 0.06 / scale.max(0.2));
515        // rotate each ring onto a different axis
516        let rot = match k % 3 {
517            0 => [0.0, 0.0, 0.0],
518            1 => [PI/2.0, 0.0, 0.0],
519            _ => [0.0, 0.0, PI/2.0],
520        };
521        ring.transform([0.0,0.0,0.0, scale,scale,scale, rot[0],rot[1],rot[2]]);
522        let base = m.verts.len() as u32;
523        for v in &ring.verts { m.verts.push(*v); }
524        for t in &ring.tris { m.tri(t[0]+base, t[1]+base, t[2]+base); }
525        for e in &ring.edges { m.edge(e[0]+base, e[1]+base); }
526    }
527    m
528}
529
530// ── exotic / compound shapes ──────────────────────────────────────────────────
531
532fn append_mesh(dst: &mut Mesh, src: &Mesh) {
533    let base = dst.verts.len() as u32;
534    for v in &src.verts { dst.verts.push(*v); }
535    for t in &src.tris  { dst.tri(t[0]+base, t[1]+base, t[2]+base); }
536    for e in &src.edges { dst.edge(e[0]+base, e[1]+base); }
537}
538
539fn box_between(x0:f32,x1:f32,y0:f32,y1:f32,z0:f32,z1:f32) -> Mesh {
540    let mut m = Mesh::default();
541    let p=[
542        m.v(x0,y0,z0),m.v(x1,y0,z0),m.v(x1,y1,z0),m.v(x0,y1,z0),
543        m.v(x0,y0,z1),m.v(x1,y0,z1),m.v(x1,y1,z1),m.v(x0,y1,z1),
544    ];
545    m.face(&[p[0],p[1],p[2],p[3]]);
546    m.face(&[p[5],p[4],p[7],p[6]]);
547    m.face(&[p[4],p[0],p[3],p[7]]);
548    m.face(&[p[1],p[5],p[6],p[2]]);
549    m.face(&[p[4],p[5],p[1],p[0]]);
550    m.face(&[p[3],p[2],p[6],p[7]]);
551    m
552}
553
554/// Tube swept along a helix around the Y axis (height −1..1).
555fn helix(turns: i32, tube: f32, sides: i32) -> Mesh {
556    let mut m = Mesh::default();
557    let turns = turns.clamp(1, 24);
558    let sides = sides.clamp(3, 32);
559    let tube  = tube.clamp(0.02, 0.5);
560    let seg_per = 24;
561    let total = turns * seg_per;
562    for i in 0..=total {
563        let ang = (i as f32 / seg_per as f32) * 2.0 * PI;
564        let y = -1.0 + 2.0 * (i as f32 / total as f32);
565        let cen = [ang.cos(), y, ang.sin()];
566        let radial = [ang.cos(), 0.0, ang.sin()];
567        let up = [0.0, 1.0, 0.0];
568        for j in 0..sides {
569            let v = j as f32 / sides as f32 * 2.0 * PI;
570            let (sv, cv) = v.sin_cos();
571            m.v(cen[0] + tube*(cv*radial[0] + sv*up[0]),
572                cen[1] + tube*(cv*radial[1] + sv*up[1]),
573                cen[2] + tube*(cv*radial[2] + sv*up[2]));
574        }
575    }
576    let s = sides;
577    for i in 0..total {
578        for j in 0..sides {
579            let a=(i*s+j) as u32; let b=(i*s+(j+1)%s) as u32;
580            let c=((i+1)*s+j) as u32; let d=((i+1)*s+(j+1)%s) as u32;
581            m.tri(a,c,b); m.tri(b,c,d);
582        }
583    }
584    m.edges_from_tris();
585    m
586}
587
588/// Semicircular archway — circular tube swept over a 180° arc in the XY plane.
589fn arch(segs: i32, tube: f32) -> Mesh {
590    let mut m = Mesh::default();
591    let segs = segs.clamp(6, 128);
592    let sides = 10i32;
593    let tube = tube.clamp(0.05, 0.4);
594    for i in 0..=segs {
595        let a = PI * (i as f32 / segs as f32);     // 0..π
596        let cen = [a.cos(), a.sin(), 0.0];
597        let radial = [a.cos(), a.sin(), 0.0];
598        let binorm = [0.0, 0.0, 1.0];
599        for j in 0..sides {
600            let v = j as f32 / sides as f32 * 2.0 * PI;
601            let (sv, cv) = v.sin_cos();
602            m.v(cen[0] + tube*(cv*radial[0] + sv*binorm[0]),
603                cen[1] + tube*(cv*radial[1] + sv*binorm[1]),
604                cen[2] + tube*(cv*radial[2] + sv*binorm[2]));
605        }
606    }
607    for i in 0..segs {
608        for j in 0..sides {
609            let a=(i*sides+j) as u32; let b=(i*sides+(j+1)%sides) as u32;
610            let c=((i+1)*sides+j) as u32; let d=((i+1)*sides+(j+1)%sides) as u32;
611            m.tri(a,c,b); m.tri(b,c,d);
612        }
613    }
614    m.edges_from_tris();
615    m
616}
617
618/// Staircase of `steps` cuboid steps rising along +Y and +Z.
619fn stairs(steps: i32) -> Mesh {
620    let mut m = Mesh::default();
621    let steps = steps.clamp(2, 40);
622    let sh = 2.0 / steps as f32;
623    let sd = 2.0 / steps as f32;
624    for i in 0..steps {
625        let y0 = -1.0 + i as f32 * sh; let y1 = y0 + sh;
626        let z0 = -1.0 + i as f32 * sd; let zf = z0 + sd;
627        let blk = box_between(-1.0, 1.0, y0, y1, z0, zf);
628        append_mesh(&mut m, &blk);
629    }
630    m
631}
632
633/// Star-shaped prism: an N-point star cross-section extruded along Y.
634fn star_prism(points: i32, inner: f32) -> Mesh {
635    let mut m = Mesh::default();
636    let points = points.clamp(3, 32);
637    let inner = inner.clamp(0.1, 0.95);
638    let n = (points * 2) as usize;
639    let mut bot = Vec::new(); let mut top = Vec::new();
640    for k in 0..n {
641        let ang = k as f32 / n as f32 * 2.0 * PI;
642        let r = if k % 2 == 0 { 1.0 } else { inner };
643        let (s, c) = ang.sin_cos();
644        bot.push(m.v(c*r, -1.0, s*r));
645        top.push(m.v(c*r,  1.0, s*r));
646    }
647    for k in 0..n {
648        let b0=bot[k]; let b1=bot[(k+1)%n]; let t0=top[k]; let t1=top[(k+1)%n];
649        m.tri(b0,t0,b1); m.tri(b1,t0,t1);
650        m.edge(b0,b1); m.edge(t0,t1); m.edge(b0,t0);
651    }
652    for k in 1..n-1 { m.tri(top[0], top[k], top[k+1]); }
653    let mut rb = bot.clone(); rb.reverse();
654    for k in 1..rb.len()-1 { m.tri(rb[0], rb[k], rb[k+1]); }
655    m
656}
657
658/// A row of `count` capsule "beads" along X — a chain / caterpillar.
659fn capsule_chain(count: i32) -> Mesh {
660    let mut m = Mesh::default();
661    let count = count.clamp(1, 12);
662    let step = 2.0 / count as f32;
663    for i in 0..count {
664        let mut c = capsule(12, 4);
665        let cx = -1.0 + (i as f32 + 0.5) * step;
666        c.transform([cx, 0.0, 0.0,  step*0.5, step*0.5, step*0.5,  0.0, 0.0, PI/2.0]);
667        append_mesh(&mut m, &c);
668    }
669    m
670}
671
672/// Möbius strip — a half-twisted band looped once.
673fn mobius(segs: i32, width: f32) -> Mesh {
674    let mut m = Mesh::default();
675    let segs = segs.clamp(8, 240);
676    let w = width.clamp(0.05, 0.6);
677    for i in 0..=segs {
678        let u = i as f32 / segs as f32 * 2.0 * PI;
679        for &vv in &[-1.0f32, 1.0] {
680            let v = vv * w;
681            let x = (1.0 + v/2.0 * (u/2.0).cos()) * u.cos();
682            let y = v/2.0 * (u/2.0).sin();
683            let z = (1.0 + v/2.0 * (u/2.0).cos()) * u.sin();
684            m.v(x, y, z);
685        }
686    }
687    for i in 0..segs {
688        let a=(2*i) as u32; let b=(2*i+1) as u32; let c=(2*(i+1)) as u32; let d=(2*(i+1)+1) as u32;
689        m.tri(a,c,b); m.tri(b,c,d);
690    }
691    m.edges_from_tris();
692    m
693}
694
695/// Resolve a builtin call name (in any supported language) to a canonical
696/// shape kind. Returns `None` if the name is not a 3-D primitive.
697pub fn canon(name: &str) -> Option<&'static str> {
698    Some(match name {
699        // cube / box
700        "cube" | "box" | "立方体" | "方块" | "箱" | "정육면체" | "상자"
701            | "ลูกบาศก์" | "กล่อง" => "cube",
702        // sphere
703        "sphere" | "球体" | "球" | "구" | "ทรงกลม" => "sphere",
704        // icosphere
705        "icosphere" | "二十面球" | "アイコ球" | "아이코구체" | "ทรงกลมเหลี่ยม" => "icosphere",
706        // dome (hemisphere)
707        "dome" | "穹顶" | "ドーム" | "돔" | "โดม" => "dome",
708        // cylinder
709        "cylinder" | "圆柱" | "円柱" | "원기둥" | "ทรงกระบอก" => "cylinder",
710        // cone
711        "cone" | "圆锥" | "円錐" | "원뿔" | "กรวย" => "cone",
712        // capsule
713        "capsule" | "胶囊" | "カプセル" | "캡슐" | "แคปซูล" => "capsule",
714        // torus / ring
715        "torus" | "ring" | "圆环" | "トーラス" | "토러스" | "ทอรัส" => "torus",
716        // pyramid
717        "pyramid" | "金字塔" | "ピラミッド" | "피라미드" | "พีระมิด" => "pyramid",
718        // prism
719        "prism" | "棱柱" | "角柱" | "각기둥" | "ปริซึม" => "prism",
720        // frustum
721        "frustum" | "棱台" | "錐台" | "원뿔대" | "กรวยตัด" => "frustum",
722        // tetrahedron / d4
723        "tetrahedron" | "d4" | "四面体" | "정사면체" | "ทรงสี่หน้า" => "tetrahedron",
724        // octahedron / d8
725        "octahedron" | "d8" | "八面体" | "정팔면체" | "ทรงแปดหน้า" => "octahedron",
726        // dodecahedron / d12
727        "dodecahedron" | "d12" | "十二面体" | "정십이면체" | "ทรงสิบสองหน้า" => "dodecahedron",
728        // icosahedron / d20
729        "icosahedron" | "d20" | "二十面体" | "정이십면체" | "ทรงยี่สิบหน้า" => "icosahedron",
730        // gear / cog
731        "gear" | "cog" | "齿轮" | "歯車" | "톱니바퀴" | "เฟือง" => "gear",
732        // gyro
733        "gyro" | "陀螺" | "ジャイロ" | "자이로" | "ไจโร" => "gyro",
734        // helix
735        "helix" | "螺旋线" | "らせん" | "나선" | "เกลียว" => "helix",
736        // spring
737        "spring" | "弹簧" | "ばね" | "스프링" | "สปริง" => "spring",
738        // arch
739        "arch" | "拱门" | "アーチ" | "아치" | "ซุ้มโค้ง" => "arch",
740        // stairs
741        "stairs" | "楼梯" | "階段" | "계단" | "บันได" => "stairs",
742        // star prism
743        "star_prism" | "star" | "星柱" | "星型柱" | "별기둥" | "แท่งดาว" => "star_prism",
744        // capsule chain
745        "capsule_chain" | "chain" | "胶囊链" | "カプセル鎖" | "캡슐체인" | "โซ่แคปซูล" => "capsule_chain",
746        // mobius
747        "mobius" | "莫比乌斯" | "メビウス" | "뫼비우스" | "เมอบีอุส" => "mobius",
748        _ => return None,
749    })
750}
751
752/// Build a transformed, world-space mesh for `kind`.
753/// `c` = [cx,cy,cz, sx,sy,sz, rx,ry,rz]; `e0..e2` = shape-specific extras.
754pub fn build(kind: &str, c: [f32; 9], e0: f32, e1: f32, e2: f32) -> Option<Mesh> {
755    let mut m = match kind {
756        "cube" | "box"        => cube(),
757        "sphere"              => uv_sphere(iarg(e0,16), iarg(e1,12)),
758        "icosphere"           => icosphere(iarg(e0,1)),
759        "dome"                => dome(iarg(e0,24), iarg(e1,8)),
760        "cylinder"            => cylinder(iarg(e0,24)),
761        "cone"                => cone(iarg(e0,24)),
762        "capsule"             => capsule(iarg(e0,16), iarg(e1,6)),
763        "torus" | "ring"      => torus(iarg(e0,32), iarg(e1,12), farg(e2,0.35)),
764        "pyramid"             => pyramid(iarg(e0,4)),
765        "prism"               => prism(iarg(e0,6)),
766        "frustum"             => frustum(iarg(e0,24), farg(e1,0.5)),
767        "tetrahedron" | "d4"  => { let mut t = tetrahedron(); t.edges = vec![]; t.edges_from_tris(); t }
768        "octahedron"  | "d8"  => { let mut t = octahedron();  t.edges = vec![]; t.edges_from_tris(); t }
769        "dodecahedron"| "d12" => dodecahedron(),
770        "icosahedron" | "d20" => icosahedron(),
771        "gear" | "cog"        => gear(iarg(e0,12), farg(e1,0.25)),
772        "gyro"                => gyro(iarg(e0,3)),
773        "helix"               => helix(iarg(e0,3), farg(e1,0.15), iarg(e2,8)),
774        "spring"              => helix(iarg(e0,6), farg(e1,0.12), iarg(e2,8)),
775        "arch"                => arch(iarg(e0,24), farg(e1,0.18)),
776        "stairs"              => stairs(iarg(e0,5)),
777        "star_prism"          => star_prism(iarg(e0,5), farg(e1,0.5)),
778        "capsule_chain"       => capsule_chain(iarg(e0,3)),
779        "mobius"              => mobius(iarg(e0,60), farg(e1,0.3)),
780        _ => return None,
781    };
782    m.transform(c);
783    m.compute_smooth_normals();
784    Some(m)
785}
786
787/// A flat-shaded, per-triangle-coloured mesh (triangle soup) for fast native-res
788/// model rendering. `pos` holds 3 verts per triangle; `col` one RGB per triangle.
789/// `height` is the model's Y-extent (feet→head), used to weight the deformation.
790#[derive(Default, Clone)]
791pub struct ColorMesh {
792    pub pos: Vec<[f32; 3]>,   // 3 * ntri  (triangle soup)
793    pub col: Vec<[u8; 3]>,    // ntri      (one flat colour per triangle)
794    pub height: f32,
795}
796
797impl GfxState {
798    /// Draw a per-triangle-coloured mesh **unlit** (colours used as-is → ignored by
799    /// the lighting pass, and fast), with the model transform (translate · uniform
800    /// scale · yaw about Y) and a baked procedural deformation: `sway` leans the
801    /// upper body (∝ |y|) and `arm` swings the arms fore/aft in antiphase with an
802    /// elbow-compound bend. Verts are flipped models (feet y≈0, head y≈-height).
803    #[allow(clippy::too_many_arguments)]
804    pub fn draw_color_mesh(&mut self, m: &ColorMesh, cx:f32, cy:f32, cz:f32, sc:f32, yaw:f32, sway:f32, arm:f32, lean:f32, leg:f32, tuck:f32) {
805        let near = -self.camera.zdist + 0.05;
806        let cs = yaw.cos(); let sn = yaw.sin();
807        let h = m.height.max(1e-4);
808        let yc = -0.68 * h;                          // shoulder band centre
809        let torso = 0.13 * h; let elbow = torso + 0.16 * h;
810        let nt = m.col.len();
811        let mut ti = 0usize;
812        while ti < nt {
813            let base = ti * 3;
814            let mut wv = [[0.0f32; 3]; 3];
815            let mut k = 0;
816            while k < 3 {
817                let p = m.pos[base + k];
818                let ax = p[0].abs();
819                let yb = (1.0 - (p[1] - yc).abs() / (0.30 * h)).clamp(0.0, 1.0);   // upper-body band
820                let aw = (((ax - torso) / (0.40 * h)).clamp(0.0, 1.0)) * yb;        // arm weight
821                let ew = (((ax - elbow) / (0.28 * h)).clamp(0.0, 1.0)) * yb;        // elbow/forearm weight
822                let side = if p[0] >= 0.0 { 1.0 } else { -1.0 };
823                // forward bend (running): upper body pitches forward (+z) above the waist,
824                // arms pulled back/tucked relative to the leaning torso.
825                let bw = (((p[1].abs() / h) - 0.40) / 0.60).clamp(0.0, 1.0);        // 0 below waist → 1 head
826                let zlean = lean * bw * bw * h - lean * aw * 0.6 * h;
827                // legs (lower body, not arms): swing fore/aft antiphase L/R; the forward
828                // foot lifts (knee bend). `tuck` raises both knees toward the chest (jump).
829                let lw = (((0.45*h - p[1].abs()) / (0.45*h)).clamp(0.0, 1.0)) * (1.0 - aw);
830                let fw = (((0.16*h - p[1].abs()) / (0.16*h)).clamp(0.0, 1.0)) * (1.0 - aw);
831                let legswing = leg * side * lw;
832                let mut ylift = 0.0f32;
833                if legswing > 0.0 { ylift -= legswing * fw * 0.45 * h; }            // forward foot lifts (up = -Y)
834                ylift -= tuck * lw * 0.22 * h;                                      // jump tuck: knees up
835                let xs = p[0] + sway * p[1].abs();
836                let zs = p[2] + arm * side * (aw + ew * 0.7) + zlean + legswing + tuck * lw * 0.16 * h;
837                wv[k] = [cx + (xs*cs + zs*sn)*sc, cy + (p[1] + ylift)*sc, cz + (zs*cs - xs*sn)*sc];
838                k += 1;
839            }
840            let a = wv[0]; let b = wv[1]; let c = wv[2];
841            let da = self.camera.depth(a[0],a[1],a[2]);
842            let db = self.camera.depth(b[0],b[1],b[2]);
843            let dc = self.camera.depth(c[0],c[1],c[2]);
844            if !(da<=near && db<=near && dc<=near) {
845                let poly = near_clip_poly(&[(a,[0.0;3],da),(b,[0.0;3],db),(c,[0.0;3],dc)], near);
846                if poly.len() >= 3 {
847                    let col = m.col[ti];
848                    let packed = ((col[0] as u32)<<16)|((col[1] as u32)<<8)|(col[2] as u32);
849                    let proj: Vec<(f32,f32,f32)> = poly.iter().map(|(p,_)| self.camera.project(p[0],p[1],p[2])).collect();
850                    let depth = proj.iter().map(|v| v.2).sum::<f32>() / proj.len() as f32;
851                    let mut j = 1;
852                    while j+1 < proj.len() {
853                        self.depth_queue.push_triangle(depth, packed, proj[0].0,proj[0].1, proj[j].0,proj[j].1, proj[j+1].0,proj[j+1].1);
854                        j += 1;
855                    }
856                }
857            }
858            ti += 1;
859        }
860    }
861
862    /// Viscous screen-space distortion of the current framebuffer: warps/puckers/
863    /// bloats in shifting regions and **wraps** at all four edges (toroidal sample).
864    /// Separable (per-row + per-column displacement) so it stays cheap full-screen.
865    /// `amount` = max displacement in pixels; `t` = time (animate the goo).
866    pub fn distort(&mut self, amount: f32, t: f32) {
867        let w = self.width; let h = self.height;
868        if w < 2 || h < 2 || amount <= 0.0 { return; }
869        let src = self.buffer.clone();
870        let a = amount;
871        // per-row horizontal shift + a vertical cross term
872        let mut rdx = vec![0i32; h]; let mut rdy = vec![0i32; h];
873        for y in 0..h {
874            let fy = y as f32;
875            rdx[y] = ((fy*0.018 + t*0.8).sin()*a + (fy*0.005 - t*0.5).sin()*a*0.6) as i32;
876            rdy[y] = ((fy*0.040 + t*1.1).sin()*a*0.4) as i32;
877        }
878        // per-column vertical shift + a horizontal cross term  (the two cross terms
879        // make the warp swirl in 2-D; multi-frequency sines give pucker/bloat zones)
880        let mut cdy = vec![0i32; w]; let mut cdx = vec![0i32; w];
881        for x in 0..w {
882            let fx = x as f32;
883            cdy[x] = ((fx*0.020 + t*0.7).sin()*a + (fx*0.006 + t*0.45).sin()*a*0.6) as i32;
884            cdx[x] = ((fx*0.050 + t*0.9).sin()*a*0.4) as i32;
885        }
886        let wi = w as i32; let hi = h as i32;
887        for y in 0..h {
888            let row = y * w;
889            let ry = rdy[y];
890            for x in 0..w {
891                // branchless small-shift wrap (displacements are a few px → one add wraps)
892                let mut sxi = x as i32 + rdx[y] + cdx[x];
893                if sxi < 0 { sxi += wi; } else if sxi >= wi { sxi -= wi; }
894                let mut syi = y as i32 + cdy[x] + ry;
895                if syi < 0 { syi += hi; } else if syi >= hi { syi -= hi; }
896                self.buffer[row + x] = src[syi as usize * w + sxi as usize];
897            }
898        }
899    }
900
901    /// Render a world-space mesh through the depth queue.
902    /// mode: 0 filled, 1 wireframe, 2 both.
903    pub fn emit_mesh(&mut self, m: &Mesh, mode: i32) {
904        let near = -self.camera.zdist + 0.05;
905
906        let want_fill = mode == 0 || mode == 2;
907        if want_fill {
908            let have_normals = m.normals.len() == m.verts.len() && self.shade_mode != 0;
909            if have_normals {
910                // ── smooth cel / holographic path ─────────────────────────────
911                // Per-vertex coloured lighting (smooth normals) → Gouraud
912                // interpolation → per-pixel posterise. No faceted edges.
913                let base = ling_graphics::shading::unpack(self.color);
914                let eye = [self.camera.tx, self.camera.ty, self.camera.tz];
915                let lights: Vec<ling_graphics::shading::LightS> = self.lights.iter().map(|l| {
916                    ling_graphics::shading::LightS { pos:[l.x,l.y,l.z], color:[l.r,l.g,l.b], intensity:l.intensity, radius:l.radius }
917                }).collect();
918                let mut sp = self.shade;
919                sp.ambient = self.ambient;              // scene ambient drives fill
920                if self.shade_mode == 1 { sp.holo = false; sp.rim *= 0.4; }
921                let bands = sp.bands;
922                for t in &m.tris {
923                    let ia=t[0] as usize; let ib=t[1] as usize; let ic=t[2] as usize;
924                    let a=m.verts[ia]; let b=m.verts[ib]; let c=m.verts[ic];
925                    let da=self.camera.depth(a[0],a[1],a[2]);
926                    let db=self.camera.depth(b[0],b[1],b[2]);
927                    let dc=self.camera.depth(c[0],c[1],c[2]);
928                    if da<=near && db<=near && dc<=near { continue; } // all behind → drop
929                    // Lit colours per vertex (kept unpacked so clipping can lerp them).
930                    let la = ling_graphics::shading::lit_vertex(base, m.normals[ia], a, eye, &lights, &sp);
931                    let lb = ling_graphics::shading::lit_vertex(base, m.normals[ib], b, eye, &lights, &sp);
932                    let lc = ling_graphics::shading::lit_vertex(base, m.normals[ic], c, eye, &lights, &sp);
933                    // Near-plane clip (keeps large straddling tiles instead of dropping them).
934                    let poly = near_clip_poly(&[(a,la,da),(b,lb,db),(c,lc,dc)], near);
935                    if poly.len() < 3 { continue; }
936                    let proj: Vec<(f32,f32,f32,u32)> = poly.iter().map(|(p,col)| {
937                        let (sx,sy,pz)=self.camera.project(p[0],p[1],p[2]);
938                        (sx,sy,pz, ling_graphics::shading::pack(*col))
939                    }).collect();
940                    let depth = proj.iter().map(|v| v.2).sum::<f32>() / proj.len() as f32;
941                    let mut k=1;
942                    while k+1 < proj.len() {
943                        self.depth_queue.push_triangle_g(depth,
944                            proj[0].0,proj[0].1,proj[0].3, proj[k].0,proj[k].1,proj[k].3, proj[k+1].0,proj[k+1].1,proj[k+1].3, bands);
945                        k+=1;
946                    }
947                }
948            } else {
949                // ── flat per-face path (shade_mode 0) ─────────────────────────
950                for t in &m.tris {
951                    let a = m.verts[t[0] as usize];
952                    let b = m.verts[t[1] as usize];
953                    let c = m.verts[t[2] as usize];
954                    let ux=b[0]-a[0]; let uy=b[1]-a[1]; let uz=b[2]-a[2];
955                    let vx=c[0]-a[0]; let vy=c[1]-a[1]; let vz=c[2]-a[2];
956                    let normal = [uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx];
957                    let centroid = [(a[0]+b[0]+c[0])/3.0,(a[1]+b[1]+c[1])/3.0,(a[2]+b[2]+c[2])/3.0];
958                    let lit = crate::gfx::light::compute_lit_color(self.color, normal, centroid, &self.lights, self.ambient);
959                    let da=self.camera.depth(a[0],a[1],a[2]);
960                    let db=self.camera.depth(b[0],b[1],b[2]);
961                    let dc=self.camera.depth(c[0],c[1],c[2]);
962                    if da<=near && db<=near && dc<=near { continue; } // all behind → drop
963                    // Near-plane clip (flat colour, so vertex colour is irrelevant here).
964                    let poly = near_clip_poly(&[(a,[0.0;3],da),(b,[0.0;3],db),(c,[0.0;3],dc)], near);
965                    if poly.len() < 3 { continue; }
966                    let proj: Vec<(f32,f32,f32)> = poly.iter()
967                        .map(|(p,_)| self.camera.project(p[0],p[1],p[2])).collect();
968                    let depth = proj.iter().map(|v| v.2).sum::<f32>() / proj.len() as f32;
969                    let mut k=1;
970                    while k+1 < proj.len() {
971                        self.depth_queue.push_triangle(depth, lit,
972                            proj[0].0,proj[0].1, proj[k].0,proj[k].1, proj[k+1].0,proj[k+1].1);
973                        k+=1;
974                    }
975                }
976            }
977        }
978
979        if mode == 1 || mode == 2 {
980            let color = self.color;
981            // small bias so wireframe paints on top of fills in "both" mode
982            let bias = if mode == 2 { 0.03 } else { 0.0 };
983            for e in &m.edges {
984                let mut a = m.verts[e[0] as usize];
985                let mut b = m.verts[e[1] as usize];
986                let da=self.camera.depth(a[0],a[1],a[2]);
987                let db=self.camera.depth(b[0],b[1],b[2]);
988                if da<=near && db<=near { continue; }
989                if da<=near {
990                    let t=(near-da)/(db-da);
991                    a=[a[0]+t*(b[0]-a[0]), a[1]+t*(b[1]-a[1]), a[2]+t*(b[2]-a[2])];
992                } else if db<=near {
993                    let t=(near-da)/(db-da);
994                    b=[a[0]+t*(b[0]-a[0]), a[1]+t*(b[1]-a[1]), a[2]+t*(b[2]-a[2])];
995                }
996                let (sax,say,pa)=self.camera.project(a[0],a[1],a[2]);
997                let (sbx,sby,pb)=self.camera.project(b[0],b[1],b[2]);
998                let depth=(pa+pb)/2.0 - bias;
999                self.depth_queue.push_line(depth, color, sax,say, sbx,sby);
1000            }
1001        }
1002    }
1003}
1004
1005/// Near-plane clip of a convex polygon (Sutherland–Hodgman). Each input vertex is
1006/// `(world_pos, colour_rgb, camera_depth)`; a vertex is kept when `depth > near`.
1007/// Vertices created on crossing edges interpolate both position and colour, so a
1008/// large floor/wall tile straddling the near plane is trimmed to its in-front
1009/// portion rather than dropped wholesale (which made tiles pop out when close).
1010fn near_clip_poly(vin: &[([f32; 3], [f32; 3], f32)], near: f32) -> Vec<([f32; 3], [f32; 3])> {
1011    let n = vin.len();
1012    let mut out: Vec<([f32; 3], [f32; 3])> = Vec::with_capacity(n + 1);
1013    for i in 0..n {
1014        let a = &vin[i];
1015        let b = &vin[(i + 1) % n];
1016        let ain = a.2 > near;
1017        let bin = b.2 > near;
1018        if ain { out.push((a.0, a.1)); }
1019        if ain != bin {
1020            let t = (near - a.2) / (b.2 - a.2);
1021            let lerp3 = |p: [f32; 3], q: [f32; 3]| {
1022                [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t, p[2] + (q[2] - p[2]) * t]
1023            };
1024            out.push((lerp3(a.0, b.0), lerp3(a.1, b.1)));
1025        }
1026    }
1027    out
1028}