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 mut 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
787impl GfxState {
788    /// Render a world-space mesh through the depth queue.
789    /// mode: 0 filled, 1 wireframe, 2 both.
790    pub fn emit_mesh(&mut self, m: &Mesh, mode: i32) {
791        let near = -self.camera.zdist + 0.05;
792
793        let want_fill = mode == 0 || mode == 2;
794        if want_fill {
795            let have_normals = m.normals.len() == m.verts.len() && self.shade_mode != 0;
796            if have_normals {
797                // ── smooth cel / holographic path ─────────────────────────────
798                // Per-vertex coloured lighting (smooth normals) → Gouraud
799                // interpolation → per-pixel posterise. No faceted edges.
800                let base = ling_graphics::shading::unpack(self.color);
801                let eye = [self.camera.tx, self.camera.ty, self.camera.tz];
802                let lights: Vec<ling_graphics::shading::LightS> = self.lights.iter().map(|l| {
803                    ling_graphics::shading::LightS { pos:[l.x,l.y,l.z], color:[l.r,l.g,l.b], intensity:l.intensity, radius:l.radius }
804                }).collect();
805                let mut sp = self.shade;
806                sp.ambient = self.ambient;              // scene ambient drives fill
807                if self.shade_mode == 1 { sp.holo = false; sp.rim *= 0.4; }
808                let bands = sp.bands;
809                for t in &m.tris {
810                    let ia=t[0] as usize; let ib=t[1] as usize; let ic=t[2] as usize;
811                    let a=m.verts[ia]; let b=m.verts[ib]; let c=m.verts[ic];
812                    let da=self.camera.depth(a[0],a[1],a[2]);
813                    let db=self.camera.depth(b[0],b[1],b[2]);
814                    let dc=self.camera.depth(c[0],c[1],c[2]);
815                    if da<=near || db<=near || dc<=near { continue; }
816                    let ca = ling_graphics::shading::pack(ling_graphics::shading::lit_vertex(base, m.normals[ia], a, eye, &lights, &sp));
817                    let cb = ling_graphics::shading::pack(ling_graphics::shading::lit_vertex(base, m.normals[ib], b, eye, &lights, &sp));
818                    let cc = ling_graphics::shading::pack(ling_graphics::shading::lit_vertex(base, m.normals[ic], c, eye, &lights, &sp));
819                    let (sax,say,pa)=self.camera.project(a[0],a[1],a[2]);
820                    let (sbx,sby,pb)=self.camera.project(b[0],b[1],b[2]);
821                    let (scx,scy,pc)=self.camera.project(c[0],c[1],c[2]);
822                    let depth=(pa+pb+pc)/3.0;
823                    self.depth_queue.push_triangle_g(depth, sax,say,ca, sbx,sby,cb, scx,scy,cc, bands);
824                }
825            } else {
826                // ── flat per-face path (shade_mode 0) ─────────────────────────
827                for t in &m.tris {
828                    let a = m.verts[t[0] as usize];
829                    let b = m.verts[t[1] as usize];
830                    let c = m.verts[t[2] as usize];
831                    let ux=b[0]-a[0]; let uy=b[1]-a[1]; let uz=b[2]-a[2];
832                    let vx=c[0]-a[0]; let vy=c[1]-a[1]; let vz=c[2]-a[2];
833                    let normal = [uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx];
834                    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];
835                    let lit = crate::gfx::light::compute_lit_color(self.color, normal, centroid, &self.lights, self.ambient);
836                    let da=self.camera.depth(a[0],a[1],a[2]);
837                    let db=self.camera.depth(b[0],b[1],b[2]);
838                    let dc=self.camera.depth(c[0],c[1],c[2]);
839                    if da<=near || db<=near || dc<=near { continue; }
840                    let (sax,say,pa)=self.camera.project(a[0],a[1],a[2]);
841                    let (sbx,sby,pb)=self.camera.project(b[0],b[1],b[2]);
842                    let (scx,scy,pc)=self.camera.project(c[0],c[1],c[2]);
843                    let depth=(pa+pb+pc)/3.0;
844                    self.depth_queue.push_triangle(depth, lit, sax,say, sbx,sby, scx,scy);
845                }
846            }
847        }
848
849        if mode == 1 || mode == 2 {
850            let color = self.color;
851            // small bias so wireframe paints on top of fills in "both" mode
852            let bias = if mode == 2 { 0.03 } else { 0.0 };
853            for e in &m.edges {
854                let mut a = m.verts[e[0] as usize];
855                let mut b = m.verts[e[1] as usize];
856                let da=self.camera.depth(a[0],a[1],a[2]);
857                let db=self.camera.depth(b[0],b[1],b[2]);
858                if da<=near && db<=near { continue; }
859                if da<=near {
860                    let t=(near-da)/(db-da);
861                    a=[a[0]+t*(b[0]-a[0]), a[1]+t*(b[1]-a[1]), a[2]+t*(b[2]-a[2])];
862                } else if db<=near {
863                    let t=(near-da)/(db-da);
864                    b=[a[0]+t*(b[0]-a[0]), a[1]+t*(b[1]-a[1]), a[2]+t*(b[2]-a[2])];
865                }
866                let (sax,say,pa)=self.camera.project(a[0],a[1],a[2]);
867                let (sbx,sby,pb)=self.camera.project(b[0],b[1],b[2]);
868                let depth=(pa+pb)/2.0 - bias;
869                self.depth_queue.push_line(depth, color, sax,say, sbx,sby);
870            }
871        }
872    }
873}