Skip to main content

ling/runtime/
mod.rs

1// src/runtime/mod.rs — tree-walking interpreter with graphics support
2#[cfg(not(target_arch = "wasm32"))]
3mod net;
4#[cfg(not(target_arch = "wasm32"))]
5mod gamepad;
6#[cfg(not(target_arch = "wasm32"))]
7mod ai;
8use std::cell::RefCell;
9use std::collections::HashMap;
10use crate::parser::ast::*;
11use crate::gfx::{GfxState, Light};
12// `raster` is wasm-safe (pure CPU framebuffer), so `draw_line` is available on
13// web too; `fill_triangle` is only reached from native-gated 3-D fill paths.
14use crate::gfx::raster::draw_line;
15#[cfg(not(target_arch = "wasm32"))]
16use crate::gfx::raster::fill_triangle;
17#[cfg(not(target_arch = "wasm32"))]
18use ling_audio::{AudioEngine, ToneParams, Wave};
19
20#[cfg(not(target_arch = "wasm32"))]
21use ling_audio::FftAnalyzer;
22
23#[cfg(not(target_arch = "wasm32"))]
24use ling_mic;
25
26// ─── Values ──────────────────────────────────────────────────────────────────
27
28#[derive(Debug, Clone)]
29pub enum Value {
30    Str(String),
31    Number(f64),
32    Bool(bool),
33    Unit,
34    List(Vec<Value>),
35    Ok(Box<Value>),
36    Err(Box<Value>),
37    Fn(Vec<String>, Vec<Stmt>, Env),
38    /// `form` record instance — ordered named fields.
39    Struct { name: String, fields: Vec<(String, Value)> },
40    /// `choose` enum instance — variant tag plus ordered payload.
41    Variant { enum_name: String, variant: String, payload: Vec<Value> },
42}
43
44type Env = HashMap<String, Value>;
45
46impl std::fmt::Display for Value {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Value::Str(s)    => write!(f, "{s}"),
50            Value::Number(n) => {
51                if n.fract() == 0.0 && n.abs() < 1e15 { write!(f, "{}", *n as i64) }
52                else { write!(f, "{n}") }
53            }
54            Value::Bool(b)   => write!(f, "{b}"),
55            Value::Unit      => write!(f, "()"),
56            Value::List(v)   => {
57                write!(f, "[")?;
58                for (i, x) in v.iter().enumerate() {
59                    if i > 0 { write!(f, ", ")?; }
60                    write!(f, "{x}")?;
61                }
62                write!(f, "]")
63            }
64            Value::Ok(v)     => write!(f, "Ok({v})"),
65            Value::Err(v)    => write!(f, "Err({v})"),
66            Value::Fn(_, _, _) => write!(f, "<fn>"),
67            Value::Struct { name, fields } => {
68                write!(f, "{name} {{ ")?;
69                for (i, (k, v)) in fields.iter().enumerate() {
70                    if i > 0 { write!(f, ", ")?; }
71                    write!(f, "{k}: {v}")?;
72                }
73                write!(f, " }}")
74            }
75            Value::Variant { variant, payload, .. } => {
76                write!(f, "{variant}")?;
77                if !payload.is_empty() {
78                    write!(f, "(")?;
79                    for (i, v) in payload.iter().enumerate() {
80                        if i > 0 { write!(f, ", ")?; }
81                        write!(f, "{v}")?;
82                    }
83                    write!(f, ")")?;
84                }
85                Ok(())
86            }
87        }
88    }
89}
90
91// ─── Control flow ────────────────────────────────────────────────────────────
92
93#[derive(Debug)]
94enum EvalErr {
95    Runtime(String),
96    Return(Value),
97    #[allow(dead_code)] // reserved for future `break` statement support
98    Break,
99}
100
101impl From<String> for EvalErr {
102    fn from(s: String) -> Self { EvalErr::Runtime(s) }
103}
104
105type EvalResult = Result<Value, EvalErr>;
106
107// GfxState is now defined in crate::gfx — see src/gfx/mod.rs.
108
109// ─── SVG writer ───────────────────────────────────────────────────────────────
110
111struct SvgWriter {
112    path:     String,
113    width:    f64,
114    height:   f64,
115    elements: Vec<String>,
116}
117
118impl SvgWriter {
119    fn new(path: String, width: f64, height: f64) -> Self {
120        let bg = format!(
121            "<rect width=\"{width}\" height=\"{height}\" fill=\"#0a0a0a\"/>"
122        );
123        Self { path, width, height, elements: vec![bg] }
124    }
125
126    fn save(&self) -> std::io::Result<()> {
127        let w = self.width;
128        let h = self.height;
129        let mut out = format!(
130            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
131             <svg xmlns=\"http://www.w3.org/2000/svg\" \
132             width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\">\n"
133        );
134        for elem in &self.elements {
135            out.push_str("  ");
136            out.push_str(elem);
137            out.push('\n');
138        }
139        out.push_str("</svg>\n");
140        // Create parent directory if it doesn't exist.
141        if let Some(parent) = std::path::Path::new(&self.path).parent() {
142            if !parent.as_os_str().is_empty() {
143                let _ = std::fs::create_dir_all(parent);
144            }
145        }
146        std::fs::write(&self.path, out.as_bytes())
147    }
148}
149
150fn hsl_to_hex(h: f64, s: f64, l: f64) -> String {
151    let s = s / 100.0;
152    let l = l / 100.0;
153    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
154    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
155    let m = l - c / 2.0;
156    let (r1, g1, b1) = if h < 60.0       { (c, x, 0.0) }
157                       else if h < 120.0  { (x, c, 0.0) }
158                       else if h < 180.0  { (0.0, c, x) }
159                       else if h < 240.0  { (0.0, x, c) }
160                       else if h < 300.0  { (x, 0.0, c) }
161                       else               { (c, 0.0, x) };
162    let r = ((r1 + m) * 255.0).round() as u8;
163    let g = ((g1 + m) * 255.0).round() as u8;
164    let b = ((b1 + m) * 255.0).round() as u8;
165    format!("#{r:02x}{g:02x}{b:02x}")
166}
167
168// ─── Procedural texture helpers ───────────────────────────────────────────────
169
170fn tex_hash(x: i32, y: i32, seed: u32) -> f32 {
171    let mut h = (x as u32).wrapping_add((y as u32).wrapping_mul(2654435769)).wrapping_add(seed.wrapping_mul(1234567891));
172    h ^= h >> 16; h = h.wrapping_mul(0x45d9f3b); h ^= h >> 16;
173    h as f32 / u32::MAX as f32
174}
175
176fn tex_vnoise(x: f32, y: f32, seed: u32) -> f32 {
177    let xi = x.floor() as i32; let yi = y.floor() as i32;
178    let sm = |t: f32| t * t * (3.0 - 2.0 * t);
179    let xf = sm(x - xi as f32); let yf = sm(y - yi as f32);
180    let a = tex_hash(xi, yi, seed); let b = tex_hash(xi+1, yi, seed);
181    let c = tex_hash(xi, yi+1, seed); let d = tex_hash(xi+1, yi+1, seed);
182    a + (b-a)*xf + (c-a)*yf + (a-b-c+d)*xf*yf
183}
184
185fn tex_fbm(x: f32, y: f32, octaves: u32, seed: u32) -> f32 {
186    let mut v = 0.0f32; let mut amp = 0.5f32; let mut f = 1.0f32;
187    for i in 0..octaves {
188        v += tex_vnoise(x * f, y * f, seed.wrapping_add(i * 7919)) * amp;
189        amp *= 0.5; f *= 2.0;
190    }
191    v
192}
193
194fn tex_palette(name: &str, t: f32) -> [f32; 3] {
195    let (a, b, c, d): ([f32;3],[f32;3],[f32;3],[f32;3]) = match name {
196        "fire"        => ([0.8,0.4,0.1],[0.7,0.3,0.1],[1.0,0.5,0.3],[0.0,0.5,0.8]),
197        "ocean"       => ([0.1,0.4,0.7],[0.3,0.3,0.4],[0.8,1.0,0.5],[0.3,0.0,0.6]),
198        "psychedelic" => ([0.5,0.5,0.5],[0.8,0.8,0.8],[1.0,1.3,0.7],[0.0,0.15,0.3]),
199        "neon"        => ([0.5,0.5,0.5],[0.5,0.5,0.5],[2.0,1.0,0.0],[0.5,0.2,0.25]),
200        "forest"      => ([0.3,0.5,0.2],[0.2,0.3,0.1],[1.0,0.5,0.8],[0.1,0.3,0.6]),
201        _             => ([0.5,0.5,0.5],[0.5,0.5,0.5],[1.0,1.0,1.0],[0.0,0.333,0.667]),
202    };
203    [0,1,2].map(|i| (a[i] + b[i] * (std::f32::consts::TAU * (c[i] * t + d[i])).cos()).clamp(0.0, 1.0))
204}
205
206/// Map a physical key to a typed character for ling-ui text input (lowercase).
207#[cfg(not(target_arch = "wasm32"))]
208fn key_char(k: minifb::Key) -> Option<char> {
209    use minifb::Key::*;
210    Some(match k {
211        A=>'a',B=>'b',C=>'c',D=>'d',E=>'e',F=>'f',G=>'g',H=>'h',I=>'i',J=>'j',K=>'k',L=>'l',M=>'m',
212        N=>'n',O=>'o',P=>'p',Q=>'q',R=>'r',S=>'s',T=>'t',U=>'u',V=>'v',W=>'w',X=>'x',Y=>'y',Z=>'z',
213        Key0=>'0',Key1=>'1',Key2=>'2',Key3=>'3',Key4=>'4',Key5=>'5',Key6=>'6',Key7=>'7',Key8=>'8',Key9=>'9',
214        Space=>' ', Minus=>'-', Period=>'.',
215        _ => return None,
216    })
217}
218
219/// Lowercase-hex encode bytes (the wire format for crypto values in Ling).
220fn hex_encode(bytes: &[u8]) -> String {
221    let mut s = String::with_capacity(bytes.len() * 2);
222    for b in bytes { s.push_str(&format!("{b:02x}")); }
223    s
224}
225
226/// Decode a `ling convert` blob: base64 → zlib-inflate → raw little-endian bytes.
227#[cfg(not(target_arch = "wasm32"))]
228fn decode_blob(s: &str) -> Result<Vec<u8>, String> {
229    use base64::Engine as _;
230    use std::io::Read as _;
231    let comp = base64::engine::general_purpose::STANDARD
232        .decode(s.trim())
233        .map_err(|e| format!("base64: {e}"))?;
234    let mut out = Vec::new();
235    flate2::read::ZlibDecoder::new(&comp[..])
236        .read_to_end(&mut out)
237        .map_err(|e| format!("inflate: {e}"))?;
238    Ok(out)
239}
240
241/// Decode a lowercase/uppercase hex string to bytes (ignores malformed tail).
242fn hex_decode(s: &str) -> Vec<u8> {
243    let s = s.trim();
244    (0..s.len() / 2)
245        .filter_map(|i| u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok())
246        .collect()
247}
248
249/// Decode a hex string into a fixed 32-byte key (zero-padded / truncated).
250fn hex_to_32(s: &str) -> [u8; 32] {
251    let v = hex_decode(s);
252    let mut out = [0u8; 32];
253    let n = v.len().min(32);
254    out[..n].copy_from_slice(&v[..n]);
255    out
256}
257
258fn tex_rgb(r: f32, g: f32, b: f32) -> u32 {
259    ((r * 255.0) as u32) << 16 | ((g * 255.0) as u32) << 8 | (b * 255.0) as u32
260}
261
262// ─── 3D Perlin Noise (Improved Perlin 2002) ───────────────────────────────────
263
264const PERM: [u8; 512] = [
265    151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,
266    140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,
267    247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,
268    57,177,33,88,237,149,56,87,174,35,63,189,114,56,42,123,
269    165,38,72,93,69,139,138,78,149,159,56,89,152,78,61,140,
270    63,26,142,76,124,132,72,11,90,44,82,59,96,41,148,126,
271    157,13,49,27,176,33,47,14,97,78,71,40,87,183,4,122,
272    92,7,72,3,246,17,225,87,91,106,203,190,57,74,76,88,
273    207,208,239,170,251,67,77,51,133,69,249,2,127,80,60,159,
274    168,81,163,64,143,146,157,56,245,188,182,218,33,16,255,243,
275    210,205,12,19,236,95,151,68,23,196,167,126,61,100,93,25,
276    115,96,129,79,220,34,42,144,136,70,238,184,20,222,94,11,
277    219,224,50,58,10,73,6,36,92,194,211,172,98,145,149,228,
278    121,231,200,55,109,141,213,78,169,108,86,244,234,101,122,174,
279    8,186,120,37,46,28,166,180,198,232,221,116,31,75,189,139,
280    138,112,62,181,102,72,3,246,14,97,53,87,185,134,193,29,
281    158,225,248,152,17,105,217,142,148,155,30,135,233,206,85,40,
282    223,140,161,137,13,191,230,66,104,153,199,167,147,99,179,92,
283    // Duplicate for wrap-around indexing
284    151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,
285    140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,
286    247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,
287    57,177,33,88,237,149,56,87,174,35,63,189,114,56,42,123,
288    165,38,72,93,69,139,138,78,149,159,56,89,152,78,61,140,
289    63,26,142,76,124,132,72,11,90,44,82,59,96,41,148,126,
290    157,13,49,27,176,33,47,14,97,78,71,40,87,183,4,122,
291    92,7,72,3,246,17,225,87,91,106,203,190,57,74,76,88,
292    207,208,239,170,251,67,77,51,133,69,249,2,127,80,60,159,
293    168,81,163,64,143,146,157,56,245,188,182,218,33,16,255,243,
294    210,205,12,19,236,95,151,68,23,196,167,126,61,100,93,25,
295    115,96,129,79,220,34,42,144,136,70,238,184,20,222,94,11,
296    219,224,50,58,10,73,6,36,92,194,211,172,98,145,149,228,
297    121,231,200,55,109,141,213,78,169,108,86,244,234,101,122,174,
298];
299
300fn fade(t: f32) -> f32 {
301    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
302}
303
304fn grad(hash: u8, x: f32, y: f32, z: f32) -> f32 {
305    let h = hash & 15;
306    let u = if h < 8 { x } else { y };
307    let v = if h < 8 { y } else { z };
308    (if (h & 1) == 0 { u } else { -u }) + (if (h & 2) == 0 { v } else { -v })
309}
310
311fn perlin3(x: f32, y: f32, z: f32) -> f32 {
312    let xi = (x.floor() as i32) & 255;
313    let yi = (y.floor() as i32) & 255;
314    let zi = (z.floor() as i32) & 255;
315
316    let xf = x - x.floor();
317    let yf = y - y.floor();
318    let zf = z - z.floor();
319
320    let u = fade(xf);
321    let v = fade(yf);
322    let w = fade(zf);
323
324    let p0 = PERM[xi as usize] as usize;
325    let p1 = PERM[((xi + 1) & 255) as usize] as usize;
326    let pa = PERM[(p0 + yi as usize) & 255] as usize;
327    let pb = PERM[(p0 + ((yi + 1) & 255) as usize) & 255] as usize;
328    let pc = PERM[(p1 + yi as usize) & 255] as usize;
329    let pd = PERM[(p1 + ((yi + 1) & 255) as usize) & 255] as usize;
330
331    let g000 = grad(PERM[(pa + zi as usize) & 255], xf, yf, zf);
332    let g001 = grad(PERM[(pa + ((zi + 1) & 255) as usize) & 255], xf, yf, zf - 1.0);
333    let g010 = grad(PERM[(pb + zi as usize) & 255], xf, yf - 1.0, zf);
334    let g011 = grad(PERM[(pb + ((zi + 1) & 255) as usize) & 255], xf, yf - 1.0, zf - 1.0);
335    let g100 = grad(PERM[(pc + zi as usize) & 255], xf - 1.0, yf, zf);
336    let g101 = grad(PERM[(pc + ((zi + 1) & 255) as usize) & 255], xf - 1.0, yf, zf - 1.0);
337    let g110 = grad(PERM[(pd + zi as usize) & 255], xf - 1.0, yf - 1.0, zf);
338    let g111 = grad(PERM[(pd + ((zi + 1) & 255) as usize) & 255], xf - 1.0, yf - 1.0, zf - 1.0);
339
340    let l00 = g000 + u * (g100 - g000);
341    let l01 = g001 + u * (g101 - g001);
342    let l10 = g010 + u * (g110 - g010);
343    let l11 = g011 + u * (g111 - g011);
344
345    let l0 = l00 + v * (l10 - l00);
346    let l1 = l01 + v * (l11 - l01);
347
348    l0 + w * (l1 - l0)
349}
350
351fn fast_rand_f64(state: &mut u64) -> f64 {
352    *state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
353    ((*state >> 32) as u32) as f64 / 4294967296.0
354}
355
356// ─── Circle Drawing Primitives ────────────────────────────────────────────────
357
358/// Write one pixel into the framebuffer (normal or additive blend).
359#[inline]
360fn put_px(buf: &mut [u32], idx: usize, color: u32, blend: u8) {
361    if idx >= buf.len() { return; }
362    if blend == 0 {
363        buf[idx] = color;
364    } else {
365        let old = buf[idx];
366        let r = (((old >> 16) & 255) + ((color >> 16) & 255)).min(255);
367        let g = (((old >> 8) & 255) + ((color >> 8) & 255)).min(255);
368        let b = ((old & 255) + (color & 255)).min(255);
369        buf[idx] = (r << 16) | (g << 8) | b;
370    }
371}
372
373fn draw_circle_outline(buf: &mut [u32], w: i32, h: i32, cx: i32, cy: i32, r: i32, color: u32, blend: u8) {
374    let r = r.clamp(0, 20000); // guard against overflow / runaway from tiny depths
375    if r == 0 { return; }
376    let mut x = 0;
377    let mut y = r;
378    let mut d = 3 - 2 * r;
379    while x <= y {
380        plot_circle_points(buf, w, h, cx, cy, x, y, color, blend);
381        if d < 0 {
382            d += 4 * x + 6;
383        } else {
384            d += 4 * (x - y) + 10;
385            y -= 1;
386        }
387        x += 1;
388    }
389}
390
391fn plot_circle_points(buf: &mut [u32], w: i32, h: i32, cx: i32, cy: i32, x: i32, y: i32, color: u32, blend: u8) {
392    let points = [(cx+x, cy+y), (cx-x, cy+y), (cx+x, cy-y), (cx-x, cy-y),
393                  (cx+y, cy+x), (cx-y, cy+x), (cx+y, cy-x), (cx-y, cy-x)];
394    for &(px, py) in &points {
395        if px >= 0 && px < w && py >= 0 && py < h {
396            put_px(buf, (py * w + px) as usize, color, blend);
397        }
398    }
399}
400
401fn draw_circle_filled(buf: &mut [u32], w: i32, h: i32, cx: i32, cy: i32, r: i32, color: u32, blend: u8) {
402    if r <= 0 { return; }
403    for dy in -r..=r {
404        let dx_max = ((r*r - dy*dy) as f64).sqrt() as i32;
405        let py = cy + dy;
406        if py < 0 || py >= h { continue; }
407        for dx in -dx_max..=dx_max {
408            let px = cx + dx;
409            if px >= 0 && px < w {
410                put_px(buf, (py * w + px) as usize, color, blend);
411            }
412        }
413    }
414}
415
416#[cfg(test)]
417mod draw_tests {
418    use super::*;
419
420    #[test]
421    fn filled_circle_actually_writes_pixels() {
422        let mut buf = vec![0u32; 100 * 100];
423        draw_circle_filled(&mut buf, 100, 100, 50, 50, 10, 0xFF00FF, 0);
424        assert_eq!(buf[50 * 100 + 50], 0xFF00FF, "centre pixel must be filled");
425        assert_eq!(buf[0], 0, "far corner must stay clear");
426        let n = buf.iter().filter(|&&p| p != 0).count();
427        assert!(n > 200 && n < 500, "r=10 disc area ≈ 314, got {n}");
428    }
429
430    #[test]
431    fn circle_outline_writes_a_ring() {
432        let mut buf = vec![0u32; 100 * 100];
433        draw_circle_outline(&mut buf, 100, 100, 50, 50, 20, 0x00FF00, 0);
434        assert_eq!(buf[50 * 100 + 50], 0, "outline must NOT fill the centre");
435        assert!(buf.iter().any(|&p| p == 0x00FF00), "outline must draw a ring");
436    }
437
438    #[test]
439    fn additive_blend_accumulates_channels() {
440        let mut buf = vec![0x202020u32; 1];
441        put_px(&mut buf, 0, 0x404040, 1);
442        assert_eq!(buf[0], 0x606060);
443    }
444}
445
446// ─── Interpreter ─────────────────────────────────────────────────────────────
447
448/// Customizable colour palette for the vector UI toolkit (packed 0x00RRGGBB).
449/// `ui_theme(...)` sets it; every widget falls back to these and accepts a
450/// trailing `r,g,b` override.
451#[derive(Clone, Copy)]
452pub struct UiTheme {
453    pub primary: u32,
454    pub accent:  u32,
455    pub track:   u32,
456    pub warn:    u32,
457    pub text:    u32,
458    pub bg:      u32,
459}
460
461impl Default for UiTheme {
462    fn default() -> Self {
463        Self {
464            primary: 0x00D2FF, // holographic cyan
465            accent:  0x28FFB4, // mint
466            track:   0x2C3E64, // dim slate
467            warn:    0xFF5A5A, // alert red
468            text:    0xBEEBFF, // pale cyan
469            bg:      0x0A1018, // near-black panel
470        }
471    }
472}
473
474pub struct Interpreter {
475    globals:   HashMap<String, Expr>,
476    /// Globals evaluated ONCE at program start (immutable after load).
477    /// call_named clones this instead of re-evaluating every global per call.
478    global_seed: Env,
479    functions: HashMap<String, FnDef>,
480    /// `form` definitions: struct name → ordered field names.
481    structs:   HashMap<String, Vec<String>>,
482    /// `choose` variants: variant name (bare and `Enum::Variant`) → (enum name, arity).
483    enum_variants: HashMap<String, (String, usize)>,
484    _modules:  HashMap<String, Vec<FnDef>>,
485    gfx:       RefCell<GfxState>,
486    svg:       RefCell<Option<SvgWriter>>,
487    /// Directory of the primary source file, for relative `use` resolution.
488    pub source_dir: Option<std::path::PathBuf>,
489    /// Files already loaded — prevents circular imports.
490    loaded_files: std::collections::HashSet<String>,
491    /// Optional audio engine — `None` if no audio device is available.
492    #[cfg(not(target_arch = "wasm32"))]
493    audio:     Option<AudioEngine>,
494    #[cfg(not(target_arch = "wasm32"))]
495    fft:       RefCell<FftAnalyzer>,
496    fft_bands_cache: RefCell<Vec<f32>>,
497    /// Real-time clock — initialized at startup
498    start_time: std::time::Instant,
499    /// Frame counter — incremented at each present()
500    frame_num: u64,
501    /// Random state for rand() builtin (xorshift)
502    rand_state: u64,
503    /// Microphone input (Phase 1 audio reactivity)
504    #[cfg(not(target_arch = "wasm32"))]
505    mic: Option<ling_mic::MicInput>,
506    /// Persistent KEM keypairs (knot / hybrid identities), referenced by handle.
507    #[cfg(not(target_arch = "wasm32"))]
508    crypto_ids: Vec<ling_crypto::KnotIdentity>,
509    /// Editable text-input buffer (ling-ui text fields).
510    text_buffer: String,
511    /// Frame counter for record_frame().
512    record_n: u32,
513    /// Accumulated microphone samples (for turning sound into crypto donuts).
514    #[cfg(not(target_arch = "wasm32"))]
515    mic_buffer: Vec<f32>,
516    /// Loaded vector UI fonts, referenced by handle (index) from `font_load`.
517    #[cfg(not(target_arch = "wasm32"))]
518    fonts: Vec<ling_graphics::VectorFont>,
519    /// Customizable UI colour palette (set via `ui_theme`).
520    ui_theme: UiTheme,
521    /// Left-mouse state on the previous frame — for widget click-edge detection.
522    mouse_was_down: bool,
523    /// Live music engine (decode playback + GM synth) — lazily initialised.
524    #[cfg(not(target_arch = "wasm32"))]
525    music: Option<ling_music::MusicEngine>,
526    #[cfg(not(target_arch = "wasm32"))]
527    music_init: bool,
528    /// Decoded tracks (for analysis + playback), by `music_load` handle.
529    #[cfg(not(target_arch = "wasm32"))]
530    tracks: Vec<ling_music::DecodedAudio>,
531    /// Parsed `.lrc` lyrics, by `music_lrc` handle.
532    #[cfg(not(target_arch = "wasm32"))]
533    lyrics: Vec<ling_music::Lyrics>,
534    /// Parsed MIDI songs, by `music_midi_load` handle.
535    #[cfg(not(target_arch = "wasm32"))]
536    midis: Vec<ling_music::MidiSong>,
537    /// Soft bodies (deformable balls), by `soft_ball` handle.
538    soft_bodies: Vec<ling_physics::soft::SoftBody>,
539    /// Rigid-body world (angular dynamics), shared by `rb_*`.
540    rigid_world: ling_physics::rigid::PhysicsWorld,
541    /// Liquid grids (water/oil), by `liquid_new` handle.
542    liquids: Vec<ling_physics::liquid::LiquidGrid>,
543    meshes: Vec<crate::gfx::shapes::ColorMesh>,
544    /// Active cinematic dialog box (Ocarina/Majora-style), if any.
545    dialog: Option<ling_game::dialog::Dialog>,
546    /// Dialog highlight colours by role: text, name, place, item (0x00RRGGBB).
547    dialog_colors: [u32; 4],
548    /// Active user-function call frames (names), for error tracebacks.
549    frames: Vec<String>,
550    /// Snapshot of `frames` captured the moment a runtime error first arose
551    /// (the deepest call). Consumed by `take_error_trace`.
552    error_trace: Option<Vec<String>>,
553    /// Unified input (gamepads/joysticks/VR/touch via the ling-input
554    /// "Sensorium"). Lazily initialised on the first `pad_*` builtin call;
555    /// `None` if no native input backend is available.
556    #[cfg(not(target_arch = "wasm32"))]
557    input: RefCell<Option<InputState>>,
558}
559
560/// Live gamepad input state: a ling-input hub fed by the native `gilrs` backend.
561#[cfg(not(target_arch = "wasm32"))]
562struct InputState {
563    sensorium: ling_input::Sensorium,
564    backend: ling_input::backend::GilrsBackend,
565}
566
567impl Interpreter {
568    pub fn new() -> Self {
569        #[cfg(not(target_arch = "wasm32"))]
570        let audio = AudioEngine::new()
571            .map_err(|e| eprintln!("audio init failed (no sound): {e}"))
572            .ok();
573        Self {
574            globals:   HashMap::new(),
575            global_seed: HashMap::new(),
576            functions: HashMap::new(),
577            structs:   HashMap::new(),
578            enum_variants: HashMap::new(),
579            _modules:  HashMap::new(),
580            gfx:       RefCell::new(GfxState::new()),
581            svg:       RefCell::new(None),
582            source_dir: None,
583            loaded_files: std::collections::HashSet::new(),
584            #[cfg(not(target_arch = "wasm32"))]
585            audio,
586            #[cfg(not(target_arch = "wasm32"))]
587            fft: RefCell::new(FftAnalyzer::new(2048, 44100)),
588            fft_bands_cache: RefCell::new(vec![]),
589            start_time: std::time::Instant::now(),
590            frame_num: 0,
591            rand_state: 0x123456789ABCDEF,
592            #[cfg(not(target_arch = "wasm32"))]
593            mic: None,
594            #[cfg(not(target_arch = "wasm32"))]
595            crypto_ids: Vec::new(),
596            text_buffer: String::new(),
597            record_n: 0,
598            #[cfg(not(target_arch = "wasm32"))]
599            mic_buffer: Vec::new(),
600            #[cfg(not(target_arch = "wasm32"))]
601            fonts: Vec::new(),
602            ui_theme: UiTheme::default(),
603            mouse_was_down: false,
604            #[cfg(not(target_arch = "wasm32"))]
605            music: None,
606            #[cfg(not(target_arch = "wasm32"))]
607            music_init: false,
608            #[cfg(not(target_arch = "wasm32"))]
609            tracks: Vec::new(),
610            #[cfg(not(target_arch = "wasm32"))]
611            lyrics: Vec::new(),
612            #[cfg(not(target_arch = "wasm32"))]
613            midis: Vec::new(),
614            soft_bodies: Vec::new(),
615            rigid_world: ling_physics::rigid::PhysicsWorld::new(),
616            liquids: Vec::new(),
617            meshes: Vec::new(),
618            dialog: None,
619            dialog_colors: [0xE6F2FF, 0xFFD24A, 0x4AD2FF, 0x6CFF8C], // text · name · place · item
620            frames: Vec::new(),
621            error_trace: None,
622            #[cfg(not(target_arch = "wasm32"))]
623            input: RefCell::new(None),
624        }
625    }
626
627    /// Lazily initialise the input system and advance it one frame; returns the
628    /// number of connected gamepads. Call once per game-loop iteration (like a
629    /// window update) before reading `pad_*` state.
630    #[cfg(not(target_arch = "wasm32"))]
631    fn pad_poll(&self) -> usize {
632        let mut slot = self.input.borrow_mut();
633        if slot.is_none() {
634            match ling_input::backend::GilrsBackend::new() {
635                Ok(backend) => {
636                    *slot = Some(InputState { sensorium: ling_input::Sensorium::new(4), backend });
637                },
638                Err(_) => return 0,
639            }
640        }
641        let st = slot.as_mut().unwrap();
642        st.sensorium.begin_frame();
643        st.sensorium.pump(&mut st.backend);
644        st.sensorium.update(1.0 / 60.0);
645        st.sensorium.devices.count()
646    }
647
648    /// Read player `slot`'s gamepad with `f`, or return `default` if there is no
649    /// input system / no such pad.
650    #[cfg(not(target_arch = "wasm32"))]
651    fn with_pad<T>(&self, slot: usize, default: T, f: impl FnOnce(&ling_input::Gamepad) -> T) -> T {
652        let inp = self.input.borrow();
653        match inp.as_ref().and_then(|s| s.sensorium.player(slot)) {
654            Some(p) => f(p),
655            None => default,
656        }
657    }
658
659    /// Take the call-stack snapshot captured at the deepest runtime error, if any.
660    /// Frames are ordered outermost-first (entry point first, failing call last).
661    pub fn take_error_trace(&mut self) -> Vec<String> {
662        self.error_trace.take().unwrap_or_default()
663    }
664
665    /// Run `body`, recording `name` as a call frame and snapshotting the stack on
666    /// the first runtime error so a traceback can be reported.
667    fn framed<T, F>(&mut self, name: &str, body: F) -> Result<T, EvalErr>
668    where
669        F: FnOnce(&mut Self) -> Result<T, EvalErr>,
670    {
671        self.frames.push(name.to_string());
672        let result = body(self);
673        if matches!(result, Err(EvalErr::Runtime(_))) && self.error_trace.is_none() {
674            self.error_trace = Some(self.frames.clone());
675        }
676        self.frames.pop();
677        result
678    }
679
680    /// Render the active dialog box: beveled frame + dark fill, then the visible
681    /// (typewriter-revealed) text word-wrapped with colour-coded runs, plus a
682    /// blinking advance arrow once the page is fully typed.
683    #[cfg(not(target_arch = "wasm32"))]
684    fn render_dialog(&mut self, x: f32, y: f32, w: f32, h: f32, font: i64, t: f32) {
685        let (runs, typing) = match &self.dialog {
686            Some(d) if !d.is_closed() => {
687                let runs: Vec<(String, usize, bool)> = d.visible_runs().into_iter()
688                    .map(|r| (r.text, r.role.index(), r.newline_before)).collect();
689                (runs, d.is_typing())
690            }
691            _ => return,
692        };
693        let colors = self.dialog_colors;
694        // ── frame + fill ──
695        let b = 12.0;
696        let corners: Vec<[f32; 2]> = vec![
697            [x+b,y],[x+w-b,y],[x+w,y+b],[x+w,y+h-b],[x+w-b,y+h],[x+b,y+h],[x,y+h-b],[x,y+b],[x+b,y],
698        ];
699        {
700            let mut gfx = self.gfx.borrow_mut();
701            let (bw, bh) = (gfx.width, gfx.height);
702            crate::gfx::raster::fill_contours_aa(&mut gfx.buffer, bw, bh, 0x0A1018, false, std::slice::from_ref(&corners));
703            for seg in corners.windows(2) {
704                crate::gfx::raster::draw_line_aa(&mut gfx.buffer, bw, bh, 0x00D2FF, false, seg[0][0], seg[0][1], seg[1][0], seg[1][1]);
705            }
706        }
707        // ── word-wrapped, colour-coded text ──
708        let px = 22.0f32;
709        let pad = 20.0f32;
710        let line_h = px * 1.45;
711        let mut cx = x + pad;
712        let mut cy = y + pad;
713        let use_font = font >= 0 && (font as usize) < self.fonts.len();
714        for (text, role, nl) in &runs {
715            if *nl { cx = x + pad; cy += line_h; }
716            for word in text.split_inclusive(' ') {
717                let wpx = if use_font { self.fonts[font as usize].measure(word, px) }
718                          else { ling_ui::holo::text_width(word, px * 0.6, px * 0.24) };
719                if cx + wpx > x + w - pad && cx > x + pad + 1.0 { cx = x + pad; cy += line_h; }
720                if cy + line_h > y + h { break; }
721                let col = colors[(*role).min(3)];
722                if use_font {
723                    let glyphs = self.font_layout_2d_glyphs(font as usize, cx, cy, px, word);
724                    let mut gfx = self.gfx.borrow_mut();
725                    let (bw, bh, add) = (gfx.width, gfx.height, gfx.blend == 1);
726                    for contours in &glyphs {
727                        crate::gfx::raster::fill_contours_aa(&mut gfx.buffer, bw, bh, col, add, contours);
728                    }
729                } else {
730                    let segs = ling_ui::holo::text_lines(word, cx, cy, px * 0.6, px, px * 0.24);
731                    let mut gfx = self.gfx.borrow_mut();
732                    let (bw, bh) = (gfx.width, gfx.height);
733                    for s in segs { draw_line(&mut gfx.buffer, bw, bh, col, s[0], s[1], s[2], s[3]); }
734                }
735                cx += wpx;
736            }
737        }
738        // ── blinking advance arrow when fully typed ──
739        if !typing && (t * 3.0).sin() > 0.0 {
740            let ax = x + w - 26.0; let ay = y + h - 22.0;
741            let mut gfx = self.gfx.borrow_mut();
742            let (bw, bh) = (gfx.width, gfx.height);
743            crate::gfx::raster::fill_contours_aa(&mut gfx.buffer, bw, bh, 0x00D2FF, false,
744                std::slice::from_ref(&vec![[ax-7.0,ay],[ax+7.0,ay],[ax,ay+9.0],[ax-7.0,ay]]));
745        }
746    }
747
748    /// Lazily start the music engine on first use (playback/synth need a device;
749    /// analysis/decoding do not). Returns `false` if no audio device is available.
750    #[cfg(not(target_arch = "wasm32"))]
751    fn ensure_music(&mut self) -> bool {
752        if self.music.is_some() { return true; }
753        if self.music_init { return false; }
754        self.music_init = true;
755        match ling_music::MusicEngine::new() {
756            Ok(m) => { self.music = Some(m); true }
757            Err(e) => { eprintln!("music engine init failed (no music playback): {e}"); false }
758        }
759    }
760
761    /// Lay out `text` for font `id` at size `px`, returning every glyph contour as
762    /// a screen-space polyline (x→right, y→down). `(x, y)` is the text box top-left;
763    /// the baseline is placed `ascent*px` below it. Curves are flattened to 0.3 px.
764    #[cfg(not(target_arch = "wasm32"))]
765    fn font_layout_2d(&mut self, id: usize, x: f32, y: f32, px: f32, text: &str) -> Vec<Vec<[f32; 2]>> {
766        let mut out = Vec::new();
767        for g in self.font_layout_2d_glyphs(id, x, y, px, text) { out.extend(g); }
768        out
769    }
770
771    /// Same as [`font_layout_2d`] but grouped per glyph (so a fill can apply the
772    /// non-zero winding rule within each glyph, preserving interior holes).
773    #[cfg(not(target_arch = "wasm32"))]
774    fn font_layout_2d_glyphs(&mut self, id: usize, x: f32, y: f32, px: f32, text: &str) -> Vec<Vec<Vec<[f32; 2]>>> {
775        let font = &mut self.fonts[id];
776        let asc = font.ascent();
777        let tol = 0.3 / px;
778        let mut pen = 0.0f32;
779        let mut glyphs = Vec::new();
780        for ch in text.chars() {
781            let go = font.glyph_outline(ch, tol);
782            let mut contours = Vec::with_capacity(go.polylines.len());
783            for pl in &go.polylines {
784                let mapped: Vec<[f32; 2]> = pl.iter()
785                    .map(|p| [x + (pen + p[0]) * px, y + (asc - p[1]) * px])
786                    .collect();
787                contours.push(mapped);
788            }
789            glyphs.push(contours);
790            pen += go.advance;
791        }
792        glyphs
793    }
794
795    pub fn run_program(&mut self, program: &Program) -> Result<(), String> {
796        for item in &program.items {
797            self.register_item("", item)?;
798        }
799        let entry = self.find_entry()
800            .ok_or("no entry point — need `bind start = do {...}` or `ผูก เริ่ม = ทำ {...}`")?;
801        // Seed the entry env with non-Do globals so top-level `令` bindings
802        // are visible in the main Do block (same two-pass logic as call_named).
803        let mut env = Env::new();
804        let non_do: Vec<_> = self.globals.iter()
805            .filter(|(_, e)| !matches!(e, Expr::Do(_)))
806            .map(|(k, e)| (k.clone(), e.clone()))
807            .collect();
808        let mut pending: Vec<(String, Expr)> = Vec::new();
809        for (k, expr) in &non_do {
810            let mut tmp = Env::new();
811            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
812                env.insert(k.clone(), v);
813            } else {
814                pending.push((k.clone(), expr.clone()));
815            }
816        }
817        for (k, expr) in &pending {
818            let mut tmp = env.clone();
819            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
820                env.insert(k.clone(), v);
821            }
822        }
823        // Cache the evaluated globals so every user-function call can clone this
824        // seed instead of re-evaluating all globals (see call_named).
825        self.global_seed = env.clone();
826        self.framed("start", |me| me.eval_expr(&entry, &mut env)).map(|_| ()).map_err(|e| match e {
827            EvalErr::Runtime(s) => s,
828            EvalErr::Return(_)  => "unexpected top-level return".to_string(),
829            EvalErr::Break      => "unexpected break at top level".to_string(),
830        })
831    }
832
833    fn register_item(&mut self, ns: &str, item: &Item) -> Result<(), String> {
834        match item {
835            Item::Bind(name, expr) => {
836                let key = if ns.is_empty() { name.clone() } else { format!("{ns}::{name}") };
837                self.globals.insert(key, expr.clone());
838            }
839            Item::Fn(def) => {
840                let key = if ns.is_empty() { def.name.clone() } else { format!("{ns}::{}", def.name) };
841                self.functions.insert(key, def.clone());
842            }
843            Item::Mod(name, body) => {
844                let child_ns = if ns.is_empty() { name.clone() } else { format!("{ns}::{name}") };
845                for child in body {
846                    self.register_item(&child_ns, child)?;
847                }
848            }
849            Item::TypeAlias(_, _) => {}
850            Item::Struct(name, fields) => {
851                self.structs.insert(name.clone(), fields.clone());
852                if !ns.is_empty() { self.structs.insert(format!("{ns}::{name}"), fields.clone()); }
853            }
854            Item::Enum(name, variants) => {
855                for v in variants {
856                    self.enum_variants.insert(v.name.clone(), (name.clone(), v.arity));
857                    self.enum_variants.insert(format!("{name}::{}", v.name), (name.clone(), v.arity));
858                    if !ns.is_empty() {
859                        self.enum_variants.insert(format!("{ns}::{name}::{}", v.name), (name.clone(), v.arity));
860                    }
861                }
862            }
863            Item::Use { path, alias } => {
864                self.load_module(path, alias.as_deref(), ns)?;
865            }
866        }
867        Ok(())
868    }
869
870    /// Resolve `path` relative to `source_dir`, load and parse it, then
871    /// register all its definitions.  If `alias` is given, every name is
872    /// prefixed with `<parent_ns>::<alias>`.  Circular imports are silently
873    /// skipped.
874    fn load_module(&mut self, path: &str, alias: Option<&str>, parent_ns: &str) -> Result<(), String> {
875        // Build candidate file paths (.ling extension variants)
876        let base_dir = self.source_dir.clone().unwrap_or_else(|| std::path::PathBuf::from("."));
877        let raw = std::path::Path::new(path);
878        let candidates: Vec<std::path::PathBuf> = vec![
879            base_dir.join(format!("{}.ling", path)),
880            base_dir.join(format!("{}.灵", path)),
881            base_dir.join(format!("{}.령", path)),
882            base_dir.join(format!("{}.霊", path)),
883            base_dir.join(format!("{}.ลิง", path)),
884            // exact path if already has extension
885            base_dir.join(raw),
886            std::path::PathBuf::from(format!("{}.ling", path)),
887            std::path::PathBuf::from(path),
888        ];
889
890        let resolved = candidates.into_iter().find(|p| p.exists())
891            .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
892
893        let canonical = resolved.canonicalize()
894            .unwrap_or_else(|_| resolved.clone())
895            .to_string_lossy()
896            .to_string();
897
898        // Skip if already loaded (circular import guard)
899        if self.loaded_files.contains(&canonical) {
900            return Ok(());
901        }
902        self.loaded_files.insert(canonical.clone());
903
904        let source = std::fs::read_to_string(&resolved)
905            .map_err(|e| format!("use: failed to read '{path}': {e}"))?;
906
907        // Save/restore source_dir for nested relative imports
908        let prev_dir = self.source_dir.clone();
909        self.source_dir = resolved.parent().map(|p| p.to_path_buf());
910
911        let program = crate::parser::parse(&source)
912            .map_err(|e| format!("use: parse error in '{path}': {e}"))?;
913
914        // Compute target namespace: parent_ns :: alias (or just alias, or just parent_ns)
915        let target_ns = match (parent_ns.is_empty(), alias) {
916            (_, Some(a)) if !parent_ns.is_empty() => format!("{parent_ns}::{a}"),
917            (_, Some(a)) => a.to_string(),
918            (false, None) => parent_ns.to_string(),
919            (true,  None) => String::new(),
920        };
921
922        for item in &program.items {
923            self.register_item(&target_ns, item)?;
924        }
925
926        self.source_dir = prev_dir;
927        Ok(())
928    }
929
930    fn find_entry(&self) -> Option<Expr> {
931        // Try all known entry-point names in multiple human languages
932        for key in &[
933            "start", "main",
934            "启",
935            "เริ่ม",           // Thai
936            "시작",
937            "начать", "начало",
938            "inicio", "comenzar",
939            "début", "commencer",
940            "anfang", "starten",
941            "início",
942            "शुरू",
943            "ابدأ",
944        ] {
945            if let Some(e) = self.globals.get(*key) { return Some(e.clone()); }
946        }
947        self.globals.values().find(|e| matches!(e, Expr::Do(_))).cloned()
948    }
949
950    // ─── Expression evaluation ────────────────────────────────────────────────
951
952    fn eval_expr(&mut self, expr: &Expr, env: &mut Env) -> EvalResult {
953        match expr {
954            Expr::Str(s)    => Ok(Value::Str(s.clone())),
955            Expr::Number(n) => Ok(Value::Number(*n)),
956            Expr::Bool(b)   => Ok(Value::Bool(*b)),
957            Expr::Unit      => Ok(Value::Unit),
958            Expr::Array(elems) => {
959                let vs: Vec<_> = elems.iter()
960                    .map(|e| self.eval_expr(e, env))
961                    .collect::<Result<_,_>>()?;
962                Ok(Value::List(vs))
963            }
964
965            Expr::Ident(name) => self.lookup(name, env),
966
967            Expr::Path(segs) => {
968                if segs.len() == 1 { return self.lookup(&segs[0], env); }
969                Ok(Value::Str(segs.join("::")))
970            }
971
972            Expr::Ref(inner) => self.eval_expr(inner, env),
973            Expr::Await(inner) => self.eval_expr(inner, env),
974
975            Expr::Do(stmts) => {
976                let mut local = env.clone();
977                Ok(self.exec_block(stmts, &mut local)?.unwrap_or(Value::Unit))
978            }
979
980            Expr::BinOp(op, lhs, rhs) => {
981                let l = self.eval_expr(lhs, env)?;
982                let r = self.eval_expr(rhs, env)?;
983                self.apply_binop(op, l, r)
984            }
985
986            Expr::If { cond, then, elseifs, else_body } => {
987                let cond_val = self.eval_expr(cond, env)?;
988                if self.is_truthy(&cond_val) {
989                    return Ok(self.exec_block(then, env)?.unwrap_or(Value::Unit));
990                }
991                for (ei_cond, ei_body) in elseifs {
992                    let ei_cond_val = self.eval_expr(ei_cond, env)?;
993                    if self.is_truthy(&ei_cond_val) {
994                        return Ok(self.exec_block(ei_body, env)?.unwrap_or(Value::Unit));
995                    }
996                }
997                if let Some(eb) = else_body {
998                    return Ok(self.exec_block(eb, env)?.unwrap_or(Value::Unit));
999                }
1000                Ok(Value::Unit)
1001            }
1002
1003            Expr::While { cond, body } => {
1004                // Run the body directly in the *outer* env so that
1005                // `bind counter = counter + 1` persists across iterations,
1006                // which is the expected behaviour in a scripting language.
1007                loop {
1008                    let cv = self.eval_expr(cond, env)?;
1009                    if !self.is_truthy(&cv) { break; }
1010                    match self.exec_block(body, env) {
1011                        Ok(_) => {}
1012                        Err(EvalErr::Break) => break,
1013                        Err(e) => return Err(e),
1014                    }
1015                }
1016                Ok(Value::Unit)
1017            }
1018
1019            Expr::For { var, iter, body } => {
1020                let iter_val = self.eval_expr(iter, env)?;
1021                let items = self.value_to_iter(iter_val)?;
1022                for item in items {
1023                    let mut local = env.clone();
1024                    local.insert(var.clone(), item);
1025                    match self.exec_block(body, &mut local) {
1026                        Ok(_) => {}
1027                        Err(EvalErr::Break) => break,
1028                        Err(e) => return Err(e),
1029                    }
1030                }
1031                Ok(Value::Unit)
1032            }
1033
1034            Expr::Match(subject, arms) => {
1035                let subj = self.eval_expr(subject, env)?;
1036                for arm in arms {
1037                    if let Some(bindings) = self.match_pattern(&arm.pattern, &subj) {
1038                        let mut local = env.clone();
1039                        local.extend(bindings);
1040                        return self.eval_expr(&arm.body, &mut local);
1041                    }
1042                }
1043                Ok(Value::Unit)
1044            }
1045
1046            Expr::Range(lo, hi) => {
1047                let lo_v = self.eval_expr(lo, env)?;
1048                let hi_v = self.eval_expr(hi, env)?;
1049                let lo_n = self.to_number(&lo_v)? as i64;
1050                let hi_n = self.to_number(&hi_v)? as i64;
1051                Ok(Value::List((lo_n..hi_n).map(|i| Value::Number(i as f64)).collect()))
1052            }
1053
1054            Expr::Index(base, idx) => {
1055                let b = self.eval_expr(base, env)?;
1056                let i = self.eval_expr(idx, env)?;
1057                let n = self.to_number(&i)? as usize;
1058                match b {
1059                    Value::List(v) => v.get(n).cloned()
1060                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
1061                    Value::Str(s)  => s.chars().nth(n)
1062                        .map(|c| Value::Str(c.to_string()))
1063                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
1064                    other => Err(EvalErr::from(format!("cannot index {:?}", other))),
1065                }
1066            }
1067
1068            Expr::Call(callee, args) => {
1069                let arg_vals: Vec<Value> = args.iter()
1070                    .map(|a| self.eval_expr(a, env))
1071                    .collect::<Result<_,_>>()?;
1072                match callee.as_ref() {
1073                    Expr::Ident(name) => self.call_named(name, arg_vals, env),
1074                    Expr::Path(segs)  => self.call_named(&segs.join("::"), arg_vals, env),
1075                    _ => {
1076                        let v = self.eval_expr(callee, env)?;
1077                        self.call_value(v, arg_vals)
1078                    }
1079                }
1080            }
1081
1082            Expr::MethodCall { receiver, method, args } => {
1083                let recv = self.eval_expr(receiver, env)?;
1084                let arg_vals: Vec<Value> = args.iter()
1085                    .map(|a| self.eval_expr(a, env))
1086                    .collect::<Result<_,_>>()?;
1087                self.call_method(recv, method, arg_vals)
1088            }
1089
1090            Expr::Closure(params, body) => {
1091                Ok(Value::Fn(params.clone(), vec![Stmt::Expr(*body.clone())], env.clone()))
1092            }
1093        }
1094    }
1095
1096    // ─── Block execution ─────────────────────────────────────────────────────
1097
1098    fn exec_block(&mut self, stmts: &[Stmt], env: &mut Env) -> Result<Option<Value>, EvalErr> {
1099        let mut last: Option<Value> = None;
1100        for stmt in stmts {
1101            match stmt {
1102                Stmt::Bind(name, expr) => {
1103                    let v = self.eval_expr(expr, env)?;
1104                    env.insert(name.clone(), v);
1105                    last = None;
1106                }
1107                Stmt::Return(expr) => {
1108                    let v = self.eval_expr(expr, env)?;
1109                    return Err(EvalErr::Return(v));
1110                }
1111                Stmt::Expr(expr) => {
1112                    last = Some(self.eval_expr(expr, env)?);
1113                }
1114            }
1115        }
1116        Ok(last)
1117    }
1118
1119    // ─── Dispatch helpers ─────────────────────────────────────────────────────
1120
1121    fn lookup(&self, name: &str, env: &Env) -> EvalResult {
1122        if let Some(v) = env.get(name) { return Ok(v.clone()); }
1123        if self.functions.contains_key(name) {
1124            let def = &self.functions[name];
1125            return Ok(Value::Fn(def.params.clone(), def.body.clone(), Env::new()));
1126        }
1127        // Bare nullary enum variant used as a value (e.g. `bind p = Origin`).
1128        if let Some((enum_name, 0)) = self.enum_variants.get(name).cloned() {
1129            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
1130            return Ok(Value::Variant { enum_name, variant, payload: Vec::new() });
1131        }
1132        // Math constants usable as plain identifiers (e.g. `sin(pi)`)
1133        match name {
1134            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" => return Ok(Value::Number(std::f64::consts::PI)),
1135            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว"        => return Ok(Value::Number(std::f64::consts::TAU)),
1136            _ => {}
1137        }
1138        Err(EvalErr::from(format!("undefined: '{name}'")))
1139    }
1140
1141    fn call_named(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
1142        match name {
1143            // ── Print ──
1144            "print" | "println" | "印" | "打印" | "印刷" | "พิมพ์" | "출력" | "вывести" | "imprimir" | "afficher" => {
1145                let s = args.iter().map(|v| v.to_string()).collect::<Vec<_>>().join("");
1146                println!("{s}");
1147                return Ok(Value::Unit);
1148            }
1149            // print_color(colorIdx, text...) — ANSI-coloured console line.
1150            //   colorIdx 0..7 → bright fg (90+idx): 1=red 2=green 3=yellow 4=blue 6=cyan 7=white.
1151            "print_color" | "พิมพ์สี" => {
1152                #[cfg(windows)]
1153                {
1154                    use std::sync::Once;
1155                    static VT: Once = Once::new();
1156                    VT.call_once(|| {
1157                        extern "system" {
1158                            fn GetStdHandle(n: u32) -> *mut std::ffi::c_void;
1159                            fn GetConsoleMode(h: *mut std::ffi::c_void, m: *mut u32) -> i32;
1160                            fn SetConsoleMode(h: *mut std::ffi::c_void, m: u32) -> i32;
1161                        }
1162                        unsafe {
1163                            let h = GetStdHandle(0xFFFF_FFF5u32); // STD_OUTPUT_HANDLE (-11)
1164                            let mut mode = 0u32;
1165                            if GetConsoleMode(h, &mut mode) != 0 {
1166                                SetConsoleMode(h, mode | 0x0004); // ENABLE_VIRTUAL_TERMINAL_PROCESSING
1167                            }
1168                        }
1169                    });
1170                }
1171                let col = self.arg_num(&args, 0, 7.0)? as i64;
1172                let s = args.iter().skip(1).map(|v| v.to_string()).collect::<Vec<_>>().join("");
1173                let code = 90 + col.clamp(0, 7);
1174                println!("\x1b[1;{code}m{s}\x1b[0m");
1175                return Ok(Value::Unit);
1176            }
1177            // ── Format ──
1178            "format" | "格式" | "フォーマット" | "서식" | "รูปแบบ" | "форматировать" | "formatear" | "formater" => {
1179                return Ok(Value::Str(self.builtin_format(&args)?));
1180            }
1181            // ── String join / concatenation ──
1182            "格式::拼接" | "format::join" => {
1183                match args.first() {
1184                    Some(Value::List(items)) => {
1185                        return Ok(Value::Str(items.iter().map(|v| v.to_string()).collect()));
1186                    }
1187                    _ => return Ok(Value::Str(self.builtin_format(&args)?)),
1188                }
1189            }
1190            // ── Result constructors ──
1191            "ok" | "好" | "良し" | "좋아" | "โอเค" => {
1192                let val = args.into_iter().next().unwrap_or(Value::Unit);
1193                return Ok(Value::Ok(Box::new(val)));
1194            }
1195            "bad" | "坏" | "err" | "悪い" | "나쁨" | "ผิด" => {
1196                let val = args.into_iter().next().unwrap_or(Value::Unit);
1197                return Ok(Value::Err(Box::new(val)));
1198            }
1199            // ── Vec constructors ──
1200            "向量::从" | "Vec::from" => {
1201                if let Some(Value::List(v)) = args.first() {
1202                    return Ok(Value::List(v.clone()));
1203                }
1204                return Ok(Value::List(args));
1205            }
1206            "向量::有容量" | "Vec::with_capacity" => return Ok(Value::List(Vec::new())),
1207            // ── Timer stubs ──
1208            "计时::获取当前小时" | "Timer::hour" => return Ok(Value::Number(14.0)),
1209            "计时::现在" | "Timer::now"          => return Ok(Value::Number(1000.0)),
1210            // ── Sleep ──
1211            "sleep" | "หยุด" | "นอน" | "sleep_ms" | "睡眠" | "眠る" | "スリープ" | "잠자기" | "잠" | "流水::睡眠" | "Flow::sleep" => {
1212                if let Some(ms_val) = args.first() {
1213                    if let Ok(ms) = self.to_number(ms_val) {
1214                        std::thread::sleep(std::time::Duration::from_millis(ms as u64));
1215                    }
1216                }
1217                return Ok(Value::Unit);
1218            }
1219            // ── Flow::parallel stub ──
1220            "流水::并行" | "Flow::parallel" => {
1221                if let Some(Value::Fn(params, body, mut cap)) = args.first().cloned() {
1222                    let _ = params;
1223                    match self.exec_block(&body, &mut cap) {
1224                        Ok(Some(v)) => return Ok(v),
1225                        Ok(None) => return Ok(Value::Unit),
1226                        Err(EvalErr::Return(v)) => return Ok(v),
1227                        Err(e) => return Err(e),
1228                    }
1229                }
1230                return Ok(Value::Unit);
1231            }
1232
1233            // ══════════════════════════════════════════════════════════════════
1234            // MATH BUILTINS  (all args and results are f64)
1235            // Thai aliases: ไซน์ โคไซน์ แทนเจนต์ รากที่สอง ค่าสัมบูรณ์
1236            //               ปัดลง ปัดขึ้น ปัดเศษ ตัดทศนิยม ต่ำสุด สูงสุด
1237            //               จำกัด ยกกำลัง ลอการิทึม พาย
1238            // ══════════════════════════════════════════════════════════════════
1239
1240            // ── Trigonometry (input in radians) ──
1241            "sin" | "ไซน์" | "正弦" | "サイン" | "사인" => {
1242                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sin()));
1243            }
1244            "cos" | "โคไซน์" | "余弦" | "コサイン" | "코사인" => {
1245                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cos()));
1246            }
1247
1248            // ── Hyperbolic functions ──
1249            // Hyperbolic tangent
1250            "tanh" | "tanhf" | "双曲正切" | "双曲線正接" | "쌍곡탄젠트" => {
1251                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tanh()));
1252            }
1253
1254
1255            "tan" | "แทนเจนต์" | "正切" | "タンジェント" | "탄젠트" => {
1256                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tan()));
1257            }
1258            "asin" | "arcsin" | "反正弦" | "アークサイン" | "아크사인" | "อาร์กไซน์" => {
1259                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.asin()));
1260            }
1261            "acos" | "arccos" | "反余弦" | "アークコサイン" | "아크코사인" | "อาร์กโคไซน์" => {
1262                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.acos()));
1263            }
1264            "atan" | "arctan" | "反正切" | "アークタンジェント" | "아크탄젠트" | "อาร์กแทนเจนต์" => {
1265                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.atan()));
1266            }
1267            "atan2" | "arctan2" | "反正切2" | "アークタンジェント2" | "아크탄젠트2" => {
1268                let y = self.arg_num(&args, 0, 0.0)?;
1269                let x = self.arg_num(&args, 1, 1.0)?;
1270                return Ok(Value::Number(y.atan2(x)));
1271            }
1272
1273            // ── Roots / powers ──
1274            "sqrt" | "รากที่สอง" | "平方根" | "根" | "제곱근" => {
1275                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sqrt()));
1276            }
1277            "cbrt" | "立方根" | "세제곱근" | "รากที่สาม" => {
1278                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cbrt()));
1279            }
1280            "pow" | "ยกกำลัง" | "幂" | "べき乗" | "거듭제곱" => {
1281                let base = self.arg_num(&args, 0, 0.0)?;
1282                let exp  = self.arg_num(&args, 1, 1.0)?;
1283                return Ok(Value::Number(base.powf(exp)));
1284            }
1285            "exp" | "指数" | "指数関数" | "지수" => {
1286                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.exp()));
1287            }
1288            "hypot" | "斜边" | "斜辺" | "빗변" => {
1289                let x = self.arg_num(&args, 0, 0.0)?;
1290                let y = self.arg_num(&args, 1, 0.0)?;
1291                return Ok(Value::Number(x.hypot(y)));
1292            }
1293
1294            // ── Logarithms ──
1295            "ln" | "log" | "ลอการิทึม" | "对数" | "対数" | "로그" => {
1296                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.ln()));
1297            }
1298            "log2" | "对数2" | "対数2" | "로그2" => {
1299                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log2()));
1300            }
1301            "log10" | "对数10" | "対数10" | "로그10" => {
1302                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log10()));
1303            }
1304
1305            // ── Rounding / truncation ──
1306            "abs" | "ค่าสัมบูรณ์" | "绝对值" | "绝对" | "絶対値" | "절댓값" | "절대값" => {
1307                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.abs()));
1308            }
1309            "floor" | "ปัดลง" | "向下取整" | "下整" | "床関数" | "내림" => {
1310                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.floor()));
1311            }
1312            "ceil" | "ปัดขึ้น" | "向上取整" | "上整" | "天井関数" | "올림" => {
1313                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.ceil()));
1314            }
1315            "round" | "ปัดเศษ" | "四舍五入" | "四舍" | "四捨五入" | "반올림" => {
1316                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.round()));
1317            }
1318            "trunc" | "int" | "ตัดทศนิยม" | "取整" | "整数化" | "整数" | "截整"
1319                    | "정수화" | "정수" | "切り捨て" | "버림" => {
1320                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.trunc()));
1321            }
1322            "fract" | "小数部分" | "小数部" | "소수부" => {
1323                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.fract()));
1324            }
1325
1326            // ── min / max / clamp ──
1327            "min" | "ต่ำสุด" | "最小" | "최솟값" => {
1328                let a = self.arg_num(&args, 0, 0.0)?;
1329                let b = self.arg_num(&args, 1, 0.0)?;
1330                return Ok(Value::Number(a.min(b)));
1331            }
1332            "max" | "สูงสุด" | "最大" | "최댓값" => {
1333                let a = self.arg_num(&args, 0, 0.0)?;
1334                let b = self.arg_num(&args, 1, 0.0)?;
1335                return Ok(Value::Number(a.max(b)));
1336            }
1337            "clamp" | "จำกัด" | "截取" | "範囲制限" | "범위제한" => {
1338                let x  = self.arg_num(&args, 0, 0.0)?;
1339                let lo = self.arg_num(&args, 1, 0.0)?;
1340                let hi = self.arg_num(&args, 2, 1.0)?;
1341                return Ok(Value::Number(x.clamp(lo, hi)));
1342            }
1343
1344            // ── Constants (also accessible as plain identifiers via lookup) ──
1345            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" => return Ok(Value::Number(std::f64::consts::PI)),
1346            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว"        => return Ok(Value::Number(std::f64::consts::TAU)),
1347
1348            // ══════════════════════════════════════════════════════════════════
1349            // PHASE 1: DMT TRIP CODER FEATURES
1350            // ══════════════════════════════════════════════════════════════════
1351
1352            // ── Step 1: Noise Functions ──
1353            "vnoise" | "noise2" | "นอยส์2ดี" | "柏林噪声2D" | "バリューノイズ2D" | "값노이즈2D" => {
1354                let x = self.arg_num(&args, 0, 0.0)? as f32;
1355                let y = self.arg_num(&args, 1, 0.0)? as f32;
1356                let seed = self.arg_num(&args, 2, 0.0)? as u32;
1357                return Ok(Value::Number(tex_vnoise(x, y, seed) as f64));
1358            }
1359
1360            "fbm" | "นอยส์ออร์แกนิก" | "分形噪声" | "フラクタルノイズ" | "프랙탈노이즈" => {
1361                let x = self.arg_num(&args, 0, 0.0)? as f32;
1362                let y = self.arg_num(&args, 1, 0.0)? as f32;
1363                let octaves = self.arg_num(&args, 2, 4.0)? as u32;
1364                let seed = self.arg_num(&args, 3, 0.0)? as u32;
1365                return Ok(Value::Number(tex_fbm(x, y, octaves, seed) as f64));
1366            }
1367
1368            "perlin" | "perlin3" | "เพอร์ลิน3ดี" | "柏林噪声3D" | "パーリンノイズ3D" | "펄린노이즈3D" => {
1369                let x = self.arg_num(&args, 0, 0.0)? as f32;
1370                let y = self.arg_num(&args, 1, 0.0)? as f32;
1371                let z = self.arg_num(&args, 2, 0.0)? as f32;
1372                return Ok(Value::Number(perlin3(x, y, z) as f64));
1373            }
1374
1375            // ── Step 2: Math Ergonomics ──
1376            "lerp" | "ค่าระหว่าง" | "线性插值" | "線形補間" | "선형보간" => {
1377                let a = self.arg_num(&args, 0, 0.0)?;
1378                let b = self.arg_num(&args, 1, 1.0)?;
1379                let t = self.arg_num(&args, 2, 0.0)?;
1380                return Ok(Value::Number(a + (b - a) * t));
1381            }
1382
1383            "smoothstep" | "เปลี่ยนแบบนุ่ม" | "平滑步进" | "スムーズステップ" | "스무스스텝" => {
1384                let lo = self.arg_num(&args, 0, 0.0)?;
1385                let hi = self.arg_num(&args, 1, 1.0)?;
1386                let x = self.arg_num(&args, 2, 0.5)?;
1387                let t = ((x - lo) / (hi - lo)).clamp(0.0, 1.0);
1388                return Ok(Value::Number(t * t * (3.0 - 2.0 * t)));
1389            }
1390
1391            "rand" | "สุ่ม" | "随机" | "乱数" | "난수" => {
1392                let val = fast_rand_f64(&mut self.rand_state);
1393                return Ok(Value::Number(val));
1394            }
1395
1396            "sign" | "เครื่องหมาย" | "符号" | "符号関数" | "부호" => {
1397                let x = self.arg_num(&args, 0, 0.0)?;
1398                return Ok(Value::Number(x.signum()));
1399            }
1400
1401            "hsv_to_rgb" | "เอชเอสวีเป็นRGB" | "HSV转RGB" | "HSV変換RGB" | "HSV변환RGB" => {
1402                let h = self.arg_num(&args, 0, 0.0)?; // 0-360
1403                let s = self.arg_num(&args, 1, 1.0)?; // 0-1
1404                let v = self.arg_num(&args, 2, 1.0)?; // 0-1
1405                let c = v * s;
1406                let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
1407                let m = v - c;
1408                let (r1, g1, b1) = if h < 60.0       { (c, x, 0.0) }
1409                                    else if h < 120.0  { (x, c, 0.0) }
1410                                    else if h < 180.0  { (0.0, c, x) }
1411                                    else if h < 240.0  { (0.0, x, c) }
1412                                    else if h < 300.0  { (x, 0.0, c) }
1413                                    else               { (c, 0.0, x) };
1414                let r = ((r1 + m) * 255.0).round();
1415                let g = ((g1 + m) * 255.0).round();
1416                let b = ((b1 + m) * 255.0).round();
1417                return Ok(Value::List(vec![Value::Number(r), Value::Number(g), Value::Number(b)]));
1418            }
1419
1420            "lerp_color" | "ไล่สี" | "颜色插值" | "色補間" | "색보간" => {
1421                let r1 = self.arg_num(&args, 0, 0.0)?;
1422                let g1 = self.arg_num(&args, 1, 0.0)?;
1423                let b1 = self.arg_num(&args, 2, 0.0)?;
1424                let r2 = self.arg_num(&args, 3, 255.0)?;
1425                let g2 = self.arg_num(&args, 4, 255.0)?;
1426                let b2 = self.arg_num(&args, 5, 255.0)?;
1427                let t = self.arg_num(&args, 6, 0.0)?;
1428                let r = r1 + (r2 - r1) * t;
1429                let g = g1 + (g2 - g1) * t;
1430                let b = b1 + (b2 - b1) * t;
1431                let c = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
1432                self.gfx.borrow_mut().color = c;
1433                return Ok(Value::Unit);
1434            }
1435
1436            // ── Step 3: Real-Time Clock ──
1437            "time_now" | "เวลาปัจจุบัน" | "当前时间" | "経過時間" | "현재시간" => {
1438                return Ok(Value::Number(self.start_time.elapsed().as_secs_f64()));
1439            }
1440
1441            "frame_count" | "เฟรม" | "帧数" | "フレーム数" | "프레임수" => {
1442                return Ok(Value::Number(self.frame_num as f64));
1443            }
1444
1445            // ── Step 4: Microphone Input ──
1446            "mic_open" | "เปิดไมค์" | "开麦克风" | "マイク開く" | "마이크열기" => {
1447                #[cfg(not(target_arch = "wasm32"))]
1448                {
1449                    match ling_mic::MicInput::open(Default::default()) {
1450                        Ok(mic) => {
1451                            let _ = mic.start(|_samples: &[f32]| {});  // No-op callback
1452                            self.mic = Some(mic);
1453                            return Ok(Value::Number(1.0));  // opened
1454                        }
1455                        // No device / permission denied → graceful: don't crash the game loop.
1456                        // Returns 0.0; mic_rms/mic_peak return 0.0 while self.mic is None.
1457                        Err(_e) => { self.mic = None; return Ok(Value::Number(0.0)); }
1458                    }
1459                }
1460                #[cfg(target_arch = "wasm32")]
1461                return Ok(Value::Unit);
1462            }
1463
1464            "mic_rms" | "เสียงRMS" | "麦克风音量" | "マイクRMS" | "마이크RMS" => {
1465                #[cfg(not(target_arch = "wasm32"))]
1466                {
1467                    let rms = self.mic.as_ref().map(|m: &ling_mic::MicInput| m.rms()).unwrap_or(0.0);
1468                    return Ok(Value::Number(rms as f64));
1469                }
1470                #[cfg(target_arch = "wasm32")]
1471                return Ok(Value::Number(0.0));
1472            }
1473
1474            "mic_peak" | "เสียงพีค" | "麦克风峰值" | "マイクピーク" | "마이크피크" => {
1475                #[cfg(not(target_arch = "wasm32"))]
1476                {
1477                    let peak = self.mic.as_ref().map(|m: &ling_mic::MicInput| m.peak()).unwrap_or(0.0);
1478                    return Ok(Value::Number(peak as f64));
1479                }
1480                #[cfg(target_arch = "wasm32")]
1481                return Ok(Value::Number(0.0));
1482            }
1483
1484            "mic_fft" | "วิเคราะห์เสียงสด" | "实时频谱" | "リアルタイムFFT" | "실시간FFT" => {
1485                #[cfg(not(target_arch = "wasm32"))]
1486                {
1487                    let n = self.arg_num(&args, 0, 8.0)? as usize;
1488                    if let Some(mic) = self.mic.as_ref() {
1489                        let samples = mic.latest_samples();
1490                        self.fft.borrow_mut().push_samples(&samples);
1491                    }
1492                    let bands = self.fft.borrow().freq_bands(n);
1493                    let result = bands.iter().map(|&v| Value::Number(v as f64)).collect();
1494                    return Ok(Value::List(result));
1495                }
1496                #[cfg(target_arch = "wasm32")]
1497                return Ok(Value::List(vec![]));
1498            }
1499
1500            // ── Step 5: Additive Blend Mode ──
1501            "set_blend" | "โหมดผสม" | "混合模式" | "ブレンドモード" | "블렌드모드" => {
1502                let mode = self.arg_num(&args, 0, 0.0)? as u8;
1503                self.gfx.borrow_mut().blend = mode;
1504                return Ok(Value::Unit);
1505            }
1506
1507            // ── Step 6: Circle Primitives ──
1508            "draw_circle" | "วาดวงกลม" | "画圆" | "円描画" | "원그리기" => {
1509                let cx = self.arg_num(&args, 0, 0.0)? as i32;
1510                let cy = self.arg_num(&args, 1, 0.0)? as i32;
1511                let r = self.arg_num(&args, 2, 10.0)? as i32;
1512                let mut gfx = self.gfx.borrow_mut();
1513                let (w, h, color, blend) = (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
1514                draw_circle_outline(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
1515                return Ok(Value::Unit);
1516            }
1517
1518            "draw_filled_circle" | "draw_disc" | "วาดวงกลมทึบ" | "画实心圆" | "塗りつぶし円" | "원채우기" => {
1519                let cx = self.arg_num(&args, 0, 0.0)? as i32;
1520                let cy = self.arg_num(&args, 1, 0.0)? as i32;
1521                let r = self.arg_num(&args, 2, 10.0)? as i32;
1522                let mut gfx = self.gfx.borrow_mut();
1523                let (w, h, color, blend) = (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
1524                draw_circle_filled(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
1525                return Ok(Value::Unit);
1526            }
1527
1528            // ══════════════════════════════════════════════════════════════════
1529            // GRAPHICS BUILTINS
1530            // Thai names first, then English aliases.
1531            // ══════════════════════════════════════════════════════════════════
1532
1533            // ── เปิดหน้าต่าง(width, height, title) — open_window ──
1534            "เปิดหน้าต่าง" | "open_window" | "gfx_window" | "开窗" | "ウィンドウ開く" | "창열기" => {
1535                let w = self.arg_num(&args, 0, 800.0)? as usize;
1536                let h = self.arg_num(&args, 1, 600.0)? as usize;
1537                #[cfg(not(target_arch = "wasm32"))]
1538                {
1539                    let title = args.get(2).map(|v| v.to_string()).unwrap_or_else(|| "Ling".into());
1540                    let mut gfx = self.gfx.borrow_mut();
1541                    let mut win = minifb::Window::new(
1542                        &title, w, h,
1543                        minifb::WindowOptions {
1544                            resize: false,
1545                            scale: minifb::Scale::X1,
1546                            ..Default::default()
1547                        },
1548                    ).map_err(|e| EvalErr::from(format!("cannot open window: {e}")))?;
1549                    #[allow(deprecated)]
1550                    win.limit_update_rate(Some(std::time::Duration::from_millis(8)));
1551                    gfx.buffer = vec![0u32; w * h];
1552                    gfx.width  = w;
1553                    gfx.height = h;
1554                    gfx.window = Some(win);
1555                    gfx.sync_projection();
1556                    hide_console_window();
1557                }
1558                #[cfg(target_arch = "wasm32")]
1559                {
1560                    let mut gfx = self.gfx.borrow_mut();
1561                    gfx.width  = w;
1562                    gfx.height = h;
1563                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
1564                    gfx.sync_projection();
1565                    crate::gfx::webgl::resize(w as u32, h as u32);
1566                }
1567                return Ok(Value::Unit);
1568            }
1569
1570            // ── เติม(r, g, b) — fill / clear screen with colour ──
1571            "เติม" | "fill" | "gfx_fill" | "clear" | "填" | "塗り潰し" | "채우기" | "清" | "消去" | "지우기" => {
1572                let r = self.arg_num(&args, 0, 0.0)? as u32;
1573                let g = self.arg_num(&args, 1, 0.0)? as u32;
1574                let b = self.arg_num(&args, 2, 0.0)? as u32;
1575                #[cfg(not(target_arch = "wasm32"))]
1576                {
1577                    let c = (r << 16) | (g << 8) | b;
1578                    self.gfx.borrow_mut().buffer.fill(c);
1579                }
1580                #[cfg(target_arch = "wasm32")]
1581                {
1582                    let mut gfx = self.gfx.borrow_mut();
1583                    gfx.fill_r = r as f32 / 255.0;
1584                    gfx.fill_g = g as f32 / 255.0;
1585                    gfx.fill_b = b as f32 / 255.0;
1586                    // The web path renders into the software framebuffer and blits
1587                    // it at present(), so clear the buffer too (mirrors native).
1588                    let c = (r << 16) | (g << 8) | b;
1589                    gfx.buffer.fill(c);
1590                }
1591                return Ok(Value::Unit);
1592            }
1593
1594            // ── set_color_hsl(h, s, l) — set drawing colour from HSL ──
1595            // h: 0–360 degrees, s: 0–100 saturation, l: 0–100 lightness
1596            "set_color_hsl" | "颜色HSL" | "色相" | "HSL色" | "HSL색설정" | "สีHSLวาด" => {
1597                let h = self.arg_num(&args, 0, 0.0)?;
1598                let s = self.arg_num(&args, 1, 70.0)?;
1599                let l = self.arg_num(&args, 2, 50.0)?;
1600                let hex = hsl_to_hex(h, s, l);
1601                let r = u32::from_str_radix(&hex[1..3], 16).unwrap_or(255);
1602                let g = u32::from_str_radix(&hex[3..5], 16).unwrap_or(255);
1603                let b = u32::from_str_radix(&hex[5..7], 16).unwrap_or(255);
1604                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
1605                return Ok(Value::Unit);
1606            }
1607
1608            // ── สีดินสอ(r, g, b) — set drawing colour ──
1609            "สีดินสอ" | "set_color" | "gfx_color" | "color" | "设色" | "色設定" | "색설정" => {
1610                let r = self.arg_num(&args, 0, 255.0)? as u32;
1611                let g = self.arg_num(&args, 1, 255.0)? as u32;
1612                let b = self.arg_num(&args, 2, 255.0)? as u32;
1613                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
1614                return Ok(Value::Unit);
1615            }
1616
1617            // ── วาดสามเหลี่ยม(x1,y1, x2,y2, x3,y3) — draw filled triangle ──
1618            "วาดสามเหลี่ยม" | "draw_triangle" | "gfx_triangle" | "triangle" | "画三角" | "三角形描画" | "삼각형그리기" => {
1619                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
1620                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
1621                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
1622                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
1623                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
1624                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
1625                let mut gfx = self.gfx.borrow_mut();
1626                let color = gfx.color;
1627                #[cfg(not(target_arch = "wasm32"))]
1628                {
1629                    let w = gfx.width;
1630                    let h = gfx.height;
1631                    fill_triangle(&mut gfx.buffer, w, h, color, x0, y0, x1, y1, x2, y2);
1632                }
1633                #[cfg(target_arch = "wasm32")]
1634                gfx.depth_queue.push_triangle(0.0, color, x0, y0, x1, y1, x2, y2);
1635                return Ok(Value::Unit);
1636            }
1637
1638            // ── วาดเส้น(x1,y1, x2,y2) — draw line ──
1639            "วาดเส้น" | "draw_line" | "gfx_line" | "line" | "画线" | "線描く" | "선그리기" => {
1640                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
1641                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
1642                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
1643                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
1644                let mut gfx = self.gfx.borrow_mut();
1645                let color = gfx.color;
1646                #[cfg(not(target_arch = "wasm32"))]
1647                {
1648                    let w = gfx.width;
1649                    let h = gfx.height;
1650                    draw_line(&mut gfx.buffer, w, h, color, x0, y0, x1, y1);
1651                }
1652                #[cfg(target_arch = "wasm32")]
1653                gfx.depth_queue.push_line(0.0, color, x0, y0, x1, y1);
1654                return Ok(Value::Unit);
1655            }
1656
1657            // ── วาดจุด(x, y) — plot a single pixel ──
1658            "วาดจุด" | "draw_pixel" | "gfx_pixel" | "pixel" | "画点" | "点描く" | "점그리기" => {
1659                let px = self.arg_num(&args, 0, 0.0)? as i32;
1660                let py = self.arg_num(&args, 1, 0.0)? as i32;
1661                #[cfg(not(target_arch = "wasm32"))]
1662                {
1663                    let mut gfx = self.gfx.borrow_mut();
1664                    let color = gfx.color;
1665                    let w = gfx.width;
1666                    let h = gfx.height;
1667                    if px >= 0 && py >= 0 && (px as usize) < w && (py as usize) < h {
1668                        gfx.buffer[py as usize * w + px as usize] = color;
1669                    }
1670                }
1671                #[cfg(target_arch = "wasm32")]
1672                {
1673                    // Render pixel as a 1×1 square via two triangles.
1674                    let mut gfx = self.gfx.borrow_mut();
1675                    let color = gfx.color;
1676                    let x = px as f32; let y = py as f32;
1677                    gfx.depth_queue.push_triangle(0.0, color, x, y, x+1.0, y, x+1.0, y+1.0);
1678                    gfx.depth_queue.push_triangle(0.0, color, x, y, x+1.0, y+1.0, x, y+1.0);
1679                }
1680                return Ok(Value::Unit);
1681            }
1682
1683            // ── แสดงผล() — flush depth queue, then present frame to screen ──
1684            "แสดงผล" | "present" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시" => {
1685                #[cfg(not(target_arch = "wasm32"))]
1686                {
1687                    // Flush depth queue and present — release borrow before reading mouse.
1688                    {
1689                        let mut gfx = self.gfx.borrow_mut();
1690                        if !gfx.depth_queue.is_empty() {
1691                            let w = gfx.width;
1692                            let h = gfx.height;
1693                            let queue = std::mem::take(&mut gfx.depth_queue);
1694                            queue.flush(&mut gfx.buffer, w, h);
1695                        }
1696                        let buf = gfx.buffer.clone();
1697                        let w   = gfx.width;
1698                        let h   = gfx.height;
1699                        if let Some(win) = gfx.window.as_mut() {
1700                            win.update_with_buffer(&buf, w, h)
1701                                .map_err(|e| EvalErr::from(format!("present error: {e}")))?;
1702                        }
1703                    }
1704                    // Read mouse AFTER update_with_buffer so events are processed.
1705                    let mouse_pos = {
1706                        let gfx = self.gfx.borrow();
1707                        gfx.window.as_ref()
1708                            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
1709                    };
1710                    let mut gfx = self.gfx.borrow_mut();
1711                    if gfx.mouse_captured {
1712                        let w = gfx.width as f32;
1713                        let h = gfx.height as f32;
1714                        if let Some((mx, my)) = mouse_pos {
1715                            if gfx.last_mx.is_nan() {
1716                                gfx.mouse_dx = 0.0; gfx.mouse_dy = 0.0;
1717                                gfx.last_mx = mx; gfx.last_my = my;
1718                            } else {
1719                                gfx.mouse_dx = mx - gfx.last_mx;
1720                                gfx.mouse_dy = my - gfx.last_my;
1721                                // Wrap the cursor at every edge (L/R/U/D) → infinite look
1722                                // on both axes, and the cursor is NOT trapped (alt-tab works).
1723                                let margin = 6.0;
1724                                let (mut nx, mut ny, mut warp) = (mx, my, false);
1725                                if mx < margin { nx = w - margin - 2.0; warp = true; }
1726                                else if mx > w - margin { nx = margin + 2.0; warp = true; }
1727                                if my < margin { ny = h - margin - 2.0; warp = true; }
1728                                else if my > h - margin { ny = margin + 2.0; warp = true; }
1729                                if warp {
1730                                    #[cfg(windows)]
1731                                    unsafe {
1732                                        #[repr(C)]
1733                                        struct RECT { left: i32, top: i32, right: i32, bottom: i32 }
1734                                        extern "system" {
1735                                            fn GetForegroundWindow() -> isize;
1736                                            fn GetWindowRect(hwnd: isize, lpRect: *mut RECT) -> i32;
1737                                            fn SetCursorPos(x: i32, y: i32) -> i32;
1738                                        }
1739                                        let hwnd = GetForegroundWindow();
1740                                        let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 };
1741                                        if GetWindowRect(hwnd, &mut rect) != 0 {
1742                                            SetCursorPos(rect.left + nx as i32, rect.top + ny as i32);
1743                                        }
1744                                    }
1745                                    gfx.last_mx = nx; gfx.last_my = ny;
1746                                } else {
1747                                    gfx.last_mx = mx; gfx.last_my = my;
1748                                }
1749                            }
1750                        } else {
1751                            gfx.mouse_dx = 0.0; gfx.mouse_dy = 0.0;
1752                        }
1753                    } else if let Some((mx, my)) = mouse_pos {
1754                        if gfx.last_mx.is_nan() {
1755                            gfx.mouse_dx = 0.0; gfx.mouse_dy = 0.0;
1756                        } else {
1757                            gfx.mouse_dx = mx - gfx.last_mx;
1758                            gfx.mouse_dy = my - gfx.last_my;
1759                        }
1760                        gfx.last_mx = mx; gfx.last_my = my;
1761                    } else {
1762                        gfx.mouse_dx = 0.0; gfx.mouse_dy = 0.0;
1763                    }
1764                }
1765                #[cfg(target_arch = "wasm32")]
1766                {
1767                    // Software-render everything (3-D depth queue + 2-D vtex/ui that
1768                    // already wrote into the buffer) into the framebuffer, exactly
1769                    // like native, then upload that buffer to the canvas in one blit.
1770                    let mut gfx = self.gfx.borrow_mut();
1771                    let w = gfx.width;
1772                    let h = gfx.height;
1773                    if gfx.buffer.len() != w * h {
1774                        gfx.buffer.resize(w * h, 0);
1775                    }
1776                    if !gfx.depth_queue.is_empty() {
1777                        let queue = std::mem::take(&mut gfx.depth_queue);
1778                        queue.flush(&mut gfx.buffer, w, h);
1779                    }
1780                    crate::gfx::webgl::blit_rgb(&gfx.buffer, w, h);
1781                }
1782                // Update the click-edge latch for interactive UI widgets.
1783                #[cfg(not(target_arch = "wasm32"))]
1784                {
1785                    let (_, _, down) = self.mouse_now();
1786                    self.mouse_was_down = down;
1787                }
1788                // Increment frame counter
1789                self.frame_num += 1;
1790                return Ok(Value::Unit);
1791            }
1792
1793            // ── เปิดหน้าต่างเต็มจอ(title) — true native-res fullscreen window ──
1794            "เปิดหน้าต่างเต็มจอ" | "open_fullscreen" | "fullscreen" | "全屏" | "全画面" | "전체화면" => {
1795                // In WASM the canvas defines the viewport; use its current size
1796                // as the default so the projection matches what's actually visible.
1797                #[cfg(target_arch = "wasm32")]
1798                let (default_w, default_h) = {
1799                    let (cw, ch) = crate::gfx::webgl::canvas_size();
1800                    (cw as f64, ch as f64)
1801                };
1802                // On native: query the actual primary monitor resolution.
1803                #[cfg(all(not(target_arch = "wasm32"), windows))]
1804                let (default_w, default_h) = unsafe {
1805                    extern "system" { fn GetSystemMetrics(nIndex: i32) -> i32; }
1806                    (GetSystemMetrics(0) as f64, GetSystemMetrics(1) as f64)
1807                };
1808                #[cfg(all(not(target_arch = "wasm32"), not(windows)))]
1809                let (default_w, default_h) = native_screen_size();
1810
1811                let w = args.get(1).map(|v| self.to_number(v).unwrap_or(default_w) as usize).unwrap_or(default_w as usize);
1812                let h = args.get(2).map(|v| self.to_number(v).unwrap_or(default_h) as usize).unwrap_or(default_h as usize);
1813                #[cfg(not(target_arch = "wasm32"))]
1814                {
1815                    let title = args.get(0).map(|v| v.to_string()).unwrap_or_else(|| "Ling".into());
1816                    let mut gfx = self.gfx.borrow_mut();
1817                    let mut win = minifb::Window::new(
1818                        &title, w, h,
1819                        minifb::WindowOptions {
1820                            borderless: true,
1821                            title:      false,
1822                            resize:     false,
1823                            topmost:    true,
1824                            scale:      minifb::Scale::X1,
1825                            ..Default::default()
1826                        },
1827                    ).map_err(|e| EvalErr::from(format!("cannot open fullscreen: {e}")))?;
1828                    // Drive the loop at the monitor's real refresh rate instead of
1829                    // a hard-coded cap, so a 144 Hz panel runs at 144 fps.
1830                    win.set_target_fps(monitor_info().2.max(30) as usize);
1831                    // Grab the native handle *before* moving the window into gfx.
1832                    #[cfg(windows)]
1833                    let hwnd = win.get_window_handle() as isize;
1834                    gfx.buffer = vec![0u32; w * h];
1835                    gfx.width  = w;
1836                    gfx.height = h;
1837                    gfx.window = Some(win);
1838                    gfx.sync_projection();
1839                    // Strip all chrome and cover the full screen, above the taskbar.
1840                    #[cfg(windows)]
1841                    make_borderless_fullscreen(hwnd, w as i32, h as i32);
1842                    hide_console_window();
1843                }
1844                #[cfg(target_arch = "wasm32")]
1845                {
1846                    let mut gfx = self.gfx.borrow_mut();
1847                    gfx.width  = w;
1848                    gfx.height = h;
1849                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
1850                    gfx.sync_projection();
1851                    crate::gfx::webgl::resize(w as u32, h as u32);
1852                }
1853                return Ok(Value::Unit);
1854            }
1855
1856            // ── ความกว้าง() / ความสูง() — current framebuffer size ──
1857            "get_width" | "ความกว้าง" | "宽" | "幅取得" | "너비" => {
1858                return Ok(Value::Number(self.gfx.borrow().width as f64));
1859            }
1860            "get_height" | "ความสูง" | "高" | "高取得" | "높이" => {
1861                return Ok(Value::Number(self.gfx.borrow().height as f64));
1862            }
1863
1864            // ── monitor detection: physical display, not the framebuffer ──────
1865            // monitor_width() → primary-monitor pixel width
1866            "monitor_width" | "screen_width" | "屏宽" | "画面幅" | "화면너비" | "ความกว้างจอ" => {
1867                return Ok(Value::Number(monitor_info().0 as f64));
1868            }
1869            // monitor_height() → primary-monitor pixel height
1870            "monitor_height" | "screen_height" | "屏高" | "画面高" | "화면높이" | "ความสูงจอ" => {
1871                return Ok(Value::Number(monitor_info().1 as f64));
1872            }
1873            // monitor_refresh() → refresh rate in Hz (a.k.a. the monitor framerate)
1874            "monitor_refresh" | "monitor_hz" | "monitor_fps" | "refresh_rate"
1875                | "刷新率" | "リフレッシュレート" | "주사율" | "อัตรารีเฟรช" => {
1876                return Ok(Value::Number(monitor_info().2 as f64));
1877            }
1878            // monitor_info() → [width, height, refresh_hz]
1879            "monitor_info" | "screen_info" | "屏幕信息" | "画面情報" | "화면정보" | "ข้อมูลจอ" => {
1880                let (w, h, hz) = monitor_info();
1881                return Ok(Value::List(vec![
1882                    Value::Number(w as f64),
1883                    Value::Number(h as f64),
1884                    Value::Number(hz as f64),
1885                ]));
1886            }
1887            // set_fps(n) → cap the render loop at n frames per second
1888            "set_fps" | "set_target_fps" | "target_fps" | "设帧率" | "フレームレート設定" | "프레임설정" | "ตั้งเฟรมเรต" => {
1889                let fps = self.arg_num(&args, 0, 60.0)?.max(1.0) as usize;
1890                #[cfg(not(target_arch = "wasm32"))]
1891                {
1892                    let mut gfx = self.gfx.borrow_mut();
1893                    if let Some(win) = gfx.window.as_mut() {
1894                        win.set_target_fps(fps);
1895                    }
1896                }
1897                return Ok(Value::Unit);
1898            }
1899
1900            // ── หน้าต่างเปิดอยู่() → bool — is the window still open? ──
1901            "หน้าต่างเปิดอยู่" | "window_is_open" | "gfx_is_open" | "is_open" | "窗开" | "開いている" | "창열림" => {
1902                #[cfg(not(target_arch = "wasm32"))]
1903                {
1904                    let gfx = self.gfx.borrow();
1905                    let open = gfx.window.as_ref()
1906                        .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
1907                        .unwrap_or(false);
1908                    return Ok(Value::Bool(open));
1909                }
1910                #[cfg(target_arch = "wasm32")]
1911                return Ok(Value::Bool(true));
1912            }
1913
1914            // ── key_down(name) → bool — is a key held? ──
1915            "key_down" | "กดค้าง" | "按键" | "キー押す" | "키누름" => {
1916                #[cfg(not(target_arch = "wasm32"))]
1917                {
1918                    let name = self.arg_str(&args, 0, "");
1919                    let gfx  = self.gfx.borrow();
1920                    let down = gfx.window.as_ref()
1921                        .and_then(|w| str_to_minifb_key(&name).map(|k| w.is_key_down(k)))
1922                        .unwrap_or(false);
1923                    return Ok(Value::Bool(down));
1924                }
1925                #[cfg(target_arch = "wasm32")]
1926                return Ok(Value::Bool(false));
1927            }
1928
1929            // ── key_pressed(name) → bool — was a key pressed this frame? ──
1930            "key_pressed" | "กดปุ่ม" | "键按" | "キー押した" | "키눌림" => {
1931                #[cfg(not(target_arch = "wasm32"))]
1932                {
1933                    let name = self.arg_str(&args, 0, "");
1934                    let pressed = {
1935                        let gfx = self.gfx.borrow();
1936                        gfx.window.as_ref()
1937                            .and_then(|w| str_to_minifb_key(&name)
1938                                .map(|k| w.is_key_pressed(k, minifb::KeyRepeat::No)))
1939                            .unwrap_or(false)
1940                    };
1941                    // gamepad Start behaves like Enter everywhere
1942                    let pressed = pressed || ((name == "enter" || name == "return") && gamepad::start_edge());
1943                    return Ok(Value::Bool(pressed));
1944                }
1945                #[cfg(target_arch = "wasm32")]
1946                return Ok(Value::Bool(false));
1947            }
1948
1949            // ── mouse_dx() / mouse_dy() → f64 — delta since last frame ──
1950            "mouse_dx" | "เมาส์X" | "鼠ΔX" | "マウスΔX" | "마우스ΔX" => {
1951                #[cfg(not(target_arch = "wasm32"))]
1952                return Ok(Value::Number(self.gfx.borrow().mouse_dx as f64));
1953                #[cfg(target_arch = "wasm32")]
1954                return Ok(Value::Number(0.0));
1955            }
1956            // ── mouse_scroll() → f64 — vertical scroll-wheel delta this frame ──
1957            #[cfg(not(target_arch = "wasm32"))]
1958            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" => {
1959                let gfx = self.gfx.borrow();
1960                let s = gfx.window.as_ref()
1961                    .and_then(|w| w.get_scroll_wheel())
1962                    .map(|(_, y)| y as f64).unwrap_or(0.0);
1963                return Ok(Value::Number(s));
1964            }
1965            "mouse_dy" | "เมาส์Y" | "鼠ΔY" | "マウスΔY" | "마우스ΔY" => {
1966                #[cfg(not(target_arch = "wasm32"))]
1967                return Ok(Value::Number(self.gfx.borrow().mouse_dy as f64));
1968                #[cfg(target_arch = "wasm32")]
1969                return Ok(Value::Number(0.0));
1970            }
1971
1972            // ── Gamepad / joystick input (ling-input "Sensorium" + gilrs) ──
1973            // pad_poll() → number — advance input one frame; returns # connected pads.
1974            "pad_poll" | "手柄轮询" | "パッド更新" | "패드폴링" | "อัปเดตแพด" => {
1975                #[cfg(not(target_arch = "wasm32"))]
1976                return Ok(Value::Number(self.pad_poll() as f64));
1977                #[cfg(target_arch = "wasm32")]
1978                return Ok(Value::Number(0.0));
1979            }
1980            // pad_count() → number — connected gamepads.
1981            "pad_count" | "手柄数" | "パッド数" | "패드수" | "จำนวนแพด" => {
1982                #[cfg(not(target_arch = "wasm32"))]
1983                {
1984                    let inp = self.input.borrow();
1985                    let n = inp.as_ref().map_or(0, |s| s.sensorium.devices.count());
1986                    return Ok(Value::Number(n as f64));
1987                }
1988                #[cfg(target_arch = "wasm32")]
1989                return Ok(Value::Number(0.0));
1990            }
1991            // pad_connected(i) → bool.
1992            "pad_connected" | "手柄连接" | "パッド接続" | "패드연결" | "แพดเชื่อม" => {
1993                #[cfg(not(target_arch = "wasm32"))]
1994                {
1995                    let i = self.arg_num(&args, 0, 0.0)? as usize;
1996                    let inp = self.input.borrow();
1997                    let c = inp.as_ref().is_some_and(|s| {
1998                        s.sensorium.devices.for_player(i as u8).is_some()
1999                    });
2000                    return Ok(Value::Bool(c));
2001                }
2002                #[cfg(target_arch = "wasm32")]
2003                return Ok(Value::Bool(false));
2004            }
2005            // pad_button(i, name) → bool — is the button held?
2006            "pad_button" | "手柄按键" | "パッドボタン" | "패드버튼" | "ปุ่มแพด" => {
2007                #[cfg(not(target_arch = "wasm32"))]
2008                {
2009                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2010                    let name = self.arg_str(&args, 1, "");
2011                    let down = parse_pad_button(&name)
2012                        .is_some_and(|b| self.with_pad(i, false, |p| p.is_down(b)));
2013                    return Ok(Value::Bool(down));
2014                }
2015                #[cfg(target_arch = "wasm32")]
2016                return Ok(Value::Bool(false));
2017            }
2018            // pad_pressed(i, name) → bool — pressed this frame?
2019            "pad_pressed" | "手柄按下" | "パッド押下" | "패드눌림" | "แพดกด" => {
2020                #[cfg(not(target_arch = "wasm32"))]
2021                {
2022                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2023                    let name = self.arg_str(&args, 1, "");
2024                    let p = parse_pad_button(&name)
2025                        .is_some_and(|b| self.with_pad(i, false, |g| g.just_pressed(b)));
2026                    return Ok(Value::Bool(p));
2027                }
2028                #[cfg(target_arch = "wasm32")]
2029                return Ok(Value::Bool(false));
2030            }
2031            // pad_lx(i)/pad_ly(i)/pad_rx(i)/pad_ry(i) → number — stick axes (−1..=1).
2032            "pad_lx" | "手柄左X" | "パッド左X" | "패드왼X" | "แพดซ้ายX" => {
2033                #[cfg(not(target_arch = "wasm32"))]
2034                {
2035                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2036                    return Ok(Value::Number(self.with_pad(i, 0.0, |p| p.left_stick.x as f64)));
2037                }
2038                #[cfg(target_arch = "wasm32")]
2039                return Ok(Value::Number(0.0));
2040            }
2041            "pad_ly" | "手柄左Y" | "パッド左Y" | "패드왼Y" | "แพดซ้ายY" => {
2042                #[cfg(not(target_arch = "wasm32"))]
2043                {
2044                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2045                    return Ok(Value::Number(self.with_pad(i, 0.0, |p| p.left_stick.y as f64)));
2046                }
2047                #[cfg(target_arch = "wasm32")]
2048                return Ok(Value::Number(0.0));
2049            }
2050            "pad_rx" | "手柄右X" | "パッド右X" | "패드오X" | "แพดขวาX" => {
2051                #[cfg(not(target_arch = "wasm32"))]
2052                {
2053                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2054                    return Ok(Value::Number(self.with_pad(i, 0.0, |p| p.right_stick.x as f64)));
2055                }
2056                #[cfg(target_arch = "wasm32")]
2057                return Ok(Value::Number(0.0));
2058            }
2059            "pad_ry" | "手柄右Y" | "パッド右Y" | "패드오Y" | "แพดขวาY" => {
2060                #[cfg(not(target_arch = "wasm32"))]
2061                {
2062                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2063                    return Ok(Value::Number(self.with_pad(i, 0.0, |p| p.right_stick.y as f64)));
2064                }
2065                #[cfg(target_arch = "wasm32")]
2066                return Ok(Value::Number(0.0));
2067            }
2068            // pad_lt(i)/pad_rt(i) → number — analog triggers (0..=1).
2069            "pad_lt" | "手柄左扳机" | "パッド左トリガー" | "패드왼트리거" | "ไกแพดซ้าย" => {
2070                #[cfg(not(target_arch = "wasm32"))]
2071                {
2072                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2073                    return Ok(Value::Number(self.with_pad(i, 0.0, |p| p.left_trigger as f64)));
2074                }
2075                #[cfg(target_arch = "wasm32")]
2076                return Ok(Value::Number(0.0));
2077            }
2078            "pad_rt" | "手柄右扳机" | "パッド右トリガー" | "패드오트리거" | "ไกแพดขวา" => {
2079                #[cfg(not(target_arch = "wasm32"))]
2080                {
2081                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2082                    return Ok(Value::Number(self.with_pad(i, 0.0, |p| p.right_trigger as f64)));
2083                }
2084                #[cfg(target_arch = "wasm32")]
2085                return Ok(Value::Number(0.0));
2086            }
2087            // pad_rumble(i, lo, hi) → unit — set rumble motor amplitudes (0..=1).
2088            "pad_rumble" | "手柄震动" | "パッド振動" | "패드진동" | "แพดสั่น" => {
2089                #[cfg(not(target_arch = "wasm32"))]
2090                {
2091                    use ling_input::backend::InputBackend;
2092                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2093                    let lo = self.arg_num(&args, 1, 0.0)? as f32;
2094                    let hi = self.arg_num(&args, 2, lo as f64)? as f32;
2095                    let mut inp = self.input.borrow_mut();
2096                    if let Some(s) = inp.as_mut() {
2097                        if let Some(dev) = s.sensorium.devices.for_player(i as u8).map(|d| d.id) {
2098                            s.backend.set_rumble(
2099                                dev,
2100                                ling_input::Rumble { low: lo, high: hi, ..Default::default() },
2101                            );
2102                        }
2103                    }
2104                    return Ok(Value::Unit);
2105                }
2106                #[cfg(target_arch = "wasm32")]
2107                return Ok(Value::Unit);
2108            }
2109
2110            // ── set_camera_pos(x, y, z) — move camera to world position ──
2111            "set_camera_pos" | "ตั้งตำแหน่งกล้อง" | "镜坐标" | "カメラ座標" | "카메라좌표" => {
2112                let x = self.arg_num(&args, 0, 0.0)? as f32;
2113                let y = self.arg_num(&args, 1, 0.0)? as f32;
2114                let z = self.arg_num(&args, 2, 0.0)? as f32;
2115                {
2116                    let mut gfx = self.gfx.borrow_mut();
2117                    gfx.camera.tx = x; gfx.camera.ty = y; gfx.camera.tz = z;
2118                }
2119                #[cfg(not(target_arch = "wasm32"))]
2120                if let Some(audio) = &self.audio { audio.set_listener_pos(x, y, z); }
2121                return Ok(Value::Unit);
2122            }
2123
2124            // ── move_camera(dx, dy, dz) — translate camera by delta ──
2125            "move_camera" => {
2126                let dx = self.arg_num(&args, 0, 0.0)? as f32;
2127                let dy = self.arg_num(&args, 1, 0.0)? as f32;
2128                let dz = self.arg_num(&args, 2, 0.0)? as f32;
2129                let mut gfx = self.gfx.borrow_mut();
2130                gfx.camera.tx += dx; gfx.camera.ty += dy; gfx.camera.tz += dz;
2131                return Ok(Value::Unit);
2132            }
2133
2134            // ── set_zdist(d) — set perspective z-offset (field-of-view taper) ──
2135            "set_zdist" | "ตั้งระยะห่าง" | "镜距" | "Z距離設定" | "Z거리설정" => {
2136                let d = self.arg_num(&args, 0, 5.0)? as f32;
2137                self.gfx.borrow_mut().camera.zdist = d;
2138                return Ok(Value::Unit);
2139            }
2140
2141            // ── capture_mouse() — hide cursor and warp to centre each frame ──
2142            "capture_mouse" | "จับเมาส์" | "捕鼠" | "マウス捕捉" | "마우스잡기" => {
2143                #[cfg(not(target_arch = "wasm32"))]
2144                {
2145                    let mut gfx = self.gfx.borrow_mut();
2146                    gfx.mouse_captured = true;
2147                    gfx.last_mx = f32::NAN;
2148                    if let Some(win) = gfx.window.as_mut() {
2149                        win.set_cursor_visibility(false);
2150                    }
2151                }
2152                return Ok(Value::Unit);
2153            }
2154
2155            // ── release_mouse() — restore cursor and remove clip region ──
2156            "release_mouse" => {
2157                #[cfg(not(target_arch = "wasm32"))]
2158                {
2159                    let mut gfx = self.gfx.borrow_mut();
2160                    gfx.mouse_captured = false;
2161                    gfx.last_mx = f32::NAN;
2162                    if let Some(win) = gfx.window.as_mut() {
2163                        win.set_cursor_visibility(true);
2164                    }
2165                    #[cfg(windows)]
2166                    unsafe {
2167                        // Null releases the clip; reuse the RECT-typed declaration above.
2168                        extern "system" { fn ClipCursor(lpRect: *const std::ffi::c_void) -> i32; }
2169                        ClipCursor(std::ptr::null());
2170                    }
2171                }
2172                return Ok(Value::Unit);
2173            }
2174
2175            // ══════════════════════════════════════════════════════════════════
2176            // 3-D / 4-D DRAWING — camera, lights, depth-sorted geometry
2177            // ══════════════════════════════════════════════════════════════════
2178
2179            // ── set_camera(cry, sry, crx, srx) — store precomputed camera trig ──
2180            // Call once per frame after computing cos/sin of your rotation angles.
2181            "set_camera" | "ตั้งกล้อง" | "设镜" | "设置摄像机" | "カメラ設定" | "카메라설정" => {
2182                let cry = self.arg_num(&args, 0, 1.0)? as f32;
2183                let sry = self.arg_num(&args, 1, 0.0)? as f32;
2184                let crx = self.arg_num(&args, 2, 1.0)? as f32;
2185                let srx = self.arg_num(&args, 3, 0.0)? as f32;
2186                let mut gfx = self.gfx.borrow_mut();
2187                gfx.camera.cry = cry; gfx.camera.sry = sry;
2188                gfx.camera.crx = crx; gfx.camera.srx = srx;
2189                return Ok(Value::Unit);
2190            }
2191
2192            // ── set_projection(cx, cy, focal, zdist) — override projection params ──
2193            // Automatically set when the window opens; override only if needed.
2194            "set_projection" | "ตั้งโปรเจกชัน" | "投影" | "投影設定" | "투영설정" => {
2195                let cx    = self.arg_num(&args, 0, 960.0)? as f32;
2196                let cy    = self.arg_num(&args, 1, 540.0)? as f32;
2197                let focal = self.arg_num(&args, 2, 1080.0)? as f32;
2198                let zdist = self.arg_num(&args, 3, 5.0)? as f32;
2199                let mut gfx = self.gfx.borrow_mut();
2200                gfx.camera.cx    = cx;
2201                gfx.camera.cy    = cy;
2202                gfx.camera.focal = focal;
2203                gfx.camera.zdist = zdist;
2204                return Ok(Value::Unit);
2205            }
2206
2207            // ── draw_mesh(pos, idx, ox, oy, oz, scale, mode) ──
2208            //   Native batched triangle mesh. pos = flat [x,y,z,…], idx = flat tri indices.
2209            //   mode 0 = lit with current pen colour; 1 = per-face hue cycle.
2210            //   Vertices are batch-projected via ling-gpu (CPU fallback, or CUDA when the
2211            //   `cuda` feature is on); the per-triangle loop runs natively (not in the
2212            //   interpreter) so dense meshes (imported glTF, grids) stay fast.
2213            "draw_mesh" | "วาดเมช" => {
2214                let pos = match args.first() { Some(Value::List(v)) => v, _ => return Ok(Value::Unit) };
2215                let idx = match args.get(1)   { Some(Value::List(v)) => v, _ => return Ok(Value::Unit) };
2216                let ox = self.arg_num(&args,2,0.0)? as f32;
2217                let oy = self.arg_num(&args,3,0.0)? as f32;
2218                let oz = self.arg_num(&args,4,0.0)? as f32;
2219                let scale = self.arg_num(&args,5,1.0)? as f32;
2220                let mode = self.arg_num(&args,6,0.0)? as i64;
2221                let nv = pos.len() / 3;
2222                if nv == 0 { return Ok(Value::Unit); }
2223                let mut world = vec![0.0f32; nv*3];
2224                for i in 0..nv {
2225                    world[i*3]   = ox + self.to_number(&pos[i*3]).unwrap_or(0.0)   as f32 * scale;
2226                    world[i*3+1] = oy + self.to_number(&pos[i*3+1]).unwrap_or(0.0) as f32 * scale;
2227                    world[i*3+2] = oz + self.to_number(&pos[i*3+2]).unwrap_or(0.0) as f32 * scale;
2228                }
2229                let mut gfx = self.gfx.borrow_mut();
2230                let cp = {
2231                    let c = &gfx.camera;
2232                    ling_gpu::CameraParams { cry:c.cry, sry:c.sry, crx:c.crx, srx:c.srx, cx:c.cx, cy:c.cy, focal:c.focal, zdist:c.zdist, tx:c.tx, ty:c.ty, tz:c.tz }
2233                };
2234                let near = -gfx.camera.zdist + 0.02;
2235                let base = gfx.color;
2236                let ambient = gfx.ambient;
2237                let mut proj = vec![0.0f32; nv*3];   // (sx, sy, depth) per vertex
2238                ling_gpu::backend().project_points(&world, &cp, &mut proj);
2239                let nt = idx.len() / 3;
2240                for t in 0..nt {
2241                    let ia = self.to_number(&idx[t*3]).unwrap_or(0.0)   as usize;
2242                    let ib = self.to_number(&idx[t*3+1]).unwrap_or(0.0) as usize;
2243                    let ic = self.to_number(&idx[t*3+2]).unwrap_or(0.0) as usize;
2244                    if ia>=nv || ib>=nv || ic>=nv { continue; }
2245                    let (da, db, dc) = (proj[ia*3+2], proj[ib*3+2], proj[ic*3+2]);
2246                    if (da+db+dc)/3.0 <= near { continue; }   // near-plane cull (centroid)
2247                    let col = if mode == 1 {
2248                        let h = t as f32 * 0.6;
2249                        let r = ((h.sin()*0.5+0.5)*150.0+55.0) as u32;
2250                        let g = (((h+2.094).sin()*0.5+0.5)*150.0+55.0) as u32;
2251                        let b = (((h+4.189).sin()*0.5+0.5)*150.0+55.0) as u32;
2252                        (r<<16)|(g<<8)|b
2253                    } else {
2254                        let (ax,ay,az)=(world[ia*3],world[ia*3+1],world[ia*3+2]);
2255                        let (bx,by,bz)=(world[ib*3],world[ib*3+1],world[ib*3+2]);
2256                        let (px,py,pz)=(world[ic*3],world[ic*3+1],world[ic*3+2]);
2257                        let (ux,uy,uz)=(bx-ax,by-ay,bz-az);
2258                        let (vx,vy,vz)=(px-ax,py-ay,pz-az);
2259                        let normal=[uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx];
2260                        let centroid=[(ax+bx+px)/3.0,(ay+by+py)/3.0,(az+bz+pz)/3.0];
2261                        crate::gfx::light::compute_lit_color(base, normal, centroid, &gfx.lights, ambient)
2262                    };
2263                    let depth = (da+db+dc)/3.0;
2264                    let col = gfx.fog_apply(col, depth);
2265                    gfx.depth_queue.push_triangle(depth, col, proj[ia*3], proj[ia*3+1], proj[ib*3], proj[ib*3+1], proj[ic*3], proj[ic*3+1]);
2266                }
2267                return Ok(Value::Unit);
2268            }
2269
2270            // ── add_light(x, y, z, r, g, b, intensity, radius) ──
2271            // Adds a point light in world space.  r/g/b in [0..1].
2272            // radius == 0 → no distance falloff.
2273            "add_light" | "เพิ่มแสง" | "加灯" | "ライト追加" | "조명추가" => {
2274                let x   = self.arg_num(&args, 0, 0.0)? as f32;
2275                let y   = self.arg_num(&args, 1, -3.0)? as f32;
2276                let z   = self.arg_num(&args, 2, 3.0)? as f32;
2277                let mut r   = self.arg_num(&args, 3, 1.0)? as f32;
2278                let mut g   = self.arg_num(&args, 4, 1.0)? as f32;
2279                let mut b   = self.arg_num(&args, 5, 1.0)? as f32;
2280                // Forgive 0-255 colour values: if any channel is clearly > 1,
2281                // treat the triple as 0-255 and normalise. Keeps 0-1 callers exact.
2282                if r > 1.5 || g > 1.5 || b > 1.5 { r/=255.0; g/=255.0; b/=255.0; }
2283                let intensity = self.arg_num(&args, 6, 1.0)? as f32;
2284                let radius    = self.arg_num(&args, 7, 0.0)? as f32;
2285                self.gfx.borrow_mut().lights.push(Light { x, y, z, r, g, b, intensity, radius });
2286                return Ok(Value::Unit);
2287            }
2288
2289            // ── clear_lights() — remove all lights ──
2290            "clear_lights" | "ล้างแสง" | "清灯" | "ライト消去" | "조명초기화" => {
2291                self.gfx.borrow_mut().lights.clear();
2292                return Ok(Value::Unit);
2293            }
2294
2295            // ── set_ambient(v) — ambient light level [0..1] ──
2296            "set_ambient" | "ตั้งแสงรอบข้าง" | "环境光" | "環境光設定" | "환경광설정" => {
2297                let v = self.arg_num(&args, 0, 0.15)? as f32;
2298                self.gfx.borrow_mut().ambient = v;
2299                return Ok(Value::Unit);
2300            }
2301
2302            // ── set_fog(r,g,b, start, end) — distance fog toward (r,g,b).
2303            //    triangles/lines fade from `start`..`end` camera depth. end<=0 = off.
2304            "set_fog" | "ตั้งหมอก" | "雾" | "霧設定" | "안개설정" => {
2305                let r = self.arg_num(&args, 0, 0.0)?.clamp(0.0, 255.0) as u32;
2306                let g = self.arg_num(&args, 1, 0.0)?.clamp(0.0, 255.0) as u32;
2307                let b = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 255.0) as u32;
2308                let start = self.arg_num(&args, 3, 0.0)? as f32;
2309                let end   = self.arg_num(&args, 4, 0.0)? as f32;
2310                let mut gfx = self.gfx.borrow_mut();
2311                gfx.fog_color = (r << 16) | (g << 8) | b;
2312                gfx.fog_start = start;
2313                gfx.fog_end   = end;
2314                return Ok(Value::Unit);
2315            }
2316
2317            // ── วาดสามเหลี่ยม3มิติ(ax,ay,az, bx,by,bz, cx,cy,cz) ──
2318            // Computes lighting from world-space normal + active lights (cel shading),
2319            // projects via the stored camera, and pushes to the depth queue.
2320            "วาดสามเหลี่ยม3มิติ" | "draw_triangle_3d" | "triangle3d" => {
2321                let ax = self.arg_num(&args, 0, 0.0)? as f32;
2322                let ay = self.arg_num(&args, 1, 0.0)? as f32;
2323                let az = self.arg_num(&args, 2, 0.0)? as f32;
2324                let bx = self.arg_num(&args, 3, 0.0)? as f32;
2325                let by = self.arg_num(&args, 4, 0.0)? as f32;
2326                let bz = self.arg_num(&args, 5, 0.0)? as f32;
2327                let cx = self.arg_num(&args, 6, 0.0)? as f32;
2328                let cy = self.arg_num(&args, 7, 0.0)? as f32;
2329                let cz = self.arg_num(&args, 8, 0.0)? as f32;
2330
2331                let mut gfx = self.gfx.borrow_mut();
2332
2333                // World-space face normal  N = (B−A) × (C−A)
2334                let ux = bx-ax; let uy = by-ay; let uz = bz-az;
2335                let vx = cx-ax; let vy = cy-ay; let vz = cz-az;
2336                let normal = [
2337                    uy*vz - uz*vy,
2338                    uz*vx - ux*vz,
2339                    ux*vy - uy*vx,
2340                ];
2341                // World-space centroid
2342                let centroid = [
2343                    (ax+bx+cx)/3.0,
2344                    (ay+by+cy)/3.0,
2345                    (az+bz+cz)/3.0,
2346                ];
2347
2348                // Cel-shaded colour
2349                let lit_color = crate::gfx::light::compute_lit_color(
2350                    gfx.color, normal, centroid, &gfx.lights, gfx.ambient,
2351                );
2352
2353                // ── Near-plane CLIP (Sutherland–Hodgman) ──
2354                // A vertex just in front of the eye divides by ~0 in the perspective
2355                // projection and smears the triangle into a screen-filling fan. The old
2356                // "cull if any vertex past near" still let a vertex sitting just inside
2357                // near blow up. Clipping the triangle to the near plane keeps only the
2358                // in-front portion (finite projection — no fan, no over-cull).
2359                let near = -gfx.camera.zdist + 0.05;
2360                let vw = [
2361                    (ax, ay, az, gfx.camera.depth(ax, ay, az)),
2362                    (bx, by, bz, gfx.camera.depth(bx, by, bz)),
2363                    (cx, cy, cz, gfx.camera.depth(cx, cy, cz)),
2364                ];
2365                let mut poly: Vec<(f32, f32, f32)> = Vec::with_capacity(4);
2366                let mut ei = 0;
2367                while ei < 3 {
2368                    let a = vw[ei];
2369                    let b = vw[(ei + 1) % 3];
2370                    let ain = a.3 > near;
2371                    let bin = b.3 > near;
2372                    if ain { poly.push((a.0, a.1, a.2)); }
2373                    if ain != bin {
2374                        let tt = (near - a.3) / (b.3 - a.3);
2375                        poly.push((a.0 + (b.0 - a.0) * tt, a.1 + (b.1 - a.1) * tt, a.2 + (b.2 - a.2) * tt));
2376                    }
2377                    ei += 1;
2378                }
2379                if poly.len() < 3 { return Ok(Value::Unit); }
2380                // Project the clipped polygon, painter-depth = mean, fan-triangulate.
2381                let proj: Vec<(f32, f32, f32)> =
2382                    poly.iter().map(|p| gfx.camera.project(p.0, p.1, p.2)).collect();
2383                let mut dsum = 0.0f32;
2384                for p in &proj { dsum += p.2; }
2385                let depth = dsum / proj.len() as f32;
2386                let lit_color = gfx.fog_apply(lit_color, depth);
2387                let mut fk = 1;
2388                while fk + 1 < proj.len() {
2389                    gfx.depth_queue.push_triangle(
2390                        depth, lit_color,
2391                        proj[0].0, proj[0].1, proj[fk].0, proj[fk].1, proj[fk + 1].0, proj[fk + 1].1,
2392                    );
2393                    fk += 1;
2394                }
2395                return Ok(Value::Unit);
2396            }
2397
2398            // ── วาดเส้น3มิติ(ax,ay,az, bx,by,bz) ──
2399            // Projects two world-space points via the stored camera and pushes
2400            // a line to the depth queue.
2401            "วาดเส้น3มิติ" | "draw_line_3d" | "line3d" | "画3D线" | "3D線描く" | "3D선그리기" => {
2402                let ax = self.arg_num(&args, 0, 0.0)? as f32;
2403                let ay = self.arg_num(&args, 1, 0.0)? as f32;
2404                let az = self.arg_num(&args, 2, 0.0)? as f32;
2405                let bx = self.arg_num(&args, 3, 0.0)? as f32;
2406                let by = self.arg_num(&args, 4, 0.0)? as f32;
2407                let bz = self.arg_num(&args, 5, 0.0)? as f32;
2408
2409                let mut gfx = self.gfx.borrow_mut();
2410                let color = gfx.color;
2411                // Near-plane clip in 3-D before perspective divide
2412                let near = -gfx.camera.zdist + 0.05;
2413                let mut lax = ax; let mut lay = ay; let mut laz = az;
2414                let mut lbx = bx; let mut lby = by; let mut lbz = bz;
2415                let da_raw = gfx.camera.depth(lax, lay, laz);
2416                let db_raw = gfx.camera.depth(lbx, lby, lbz);
2417                if da_raw <= near && db_raw <= near {
2418                    return Ok(Value::Unit);
2419                }
2420                if da_raw <= near {
2421                    let t = (near - da_raw) / (db_raw - da_raw);
2422                    lax += t * (lbx - lax);
2423                    lay += t * (lby - lay);
2424                    laz += t * (lbz - laz);
2425                } else if db_raw <= near {
2426                    let t = (near - da_raw) / (db_raw - da_raw);
2427                    lbx = lax + t * (lbx - lax);
2428                    lby = lay + t * (lby - lay);
2429                    lbz = laz + t * (lbz - laz);
2430                }
2431                let (sax, say, da) = gfx.camera.project(lax, lay, laz);
2432                let (sbx, sby, db) = gfx.camera.project(lbx, lby, lbz);
2433                let depth = (da + db) / 2.0;
2434                let color = gfx.fog_apply(color, depth);
2435                gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
2436                return Ok(Value::Unit);
2437            }
2438
2439            // orb_shell(cx,cy,cz, radius, rot_y, rot_x, density, r,g,b)
2440            //   A single trippy, grayscale, depth-faded vector pattern wound around
2441            //   a sphere — two families of interleaved spherical spirals (a guilloché
2442            //   weave), NOT a lat/long cage. Each segment's brightness follows its
2443            //   facing (front bright, back dim), so it reads as a translucent
2444            //   grayscale "texture" with alpha rather than a hard wireframe; the
2445            //   inner marble shows through. `rot_y`/`rot_x` roll the texture around
2446            //   the orb; `density` = spirals per winding direction. r,g,b tint it
2447            //   (pass a gray like 230,230,230 for pure grayscale).
2448            #[cfg(not(target_arch = "wasm32"))]
2449            "orb_shell" | "球壳" | "オーブ殻" | "오브껍질" | "เปลือกทรงกลม" => {
2450                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32; let cz=self.arg_num(&args,2,0.)? as f32;
2451                let radius=self.arg_num(&args,3,1.0)? as f32;
2452                let ry=self.arg_num(&args,4,0.)? as f32; let rx=self.arg_num(&args,5,0.)? as f32;
2453                let density=(self.arg_num(&args,6,10.)? as i32).clamp(1, 48);
2454                let tr=(self.arg_num(&args,7,230.)? as f32).clamp(0.,255.);
2455                let tg=(self.arg_num(&args,8,230.)? as f32).clamp(0.,255.);
2456                let tb=(self.arg_num(&args,9,235.)? as f32).clamp(0.,255.);
2457                let (cyr, syr) = (ry.cos(), ry.sin());
2458                let (cxr, sxr) = (rx.cos(), rx.sin());
2459                let tau = std::f32::consts::TAU;
2460                let pi = std::f32::consts::PI;
2461                let turns = 6.0_f32;            // how many times each spiral wraps pole→pole
2462                let nseg  = 96;                 // segments per spiral (smoothness)
2463                let inv_r = if radius.abs() > 1e-5 { 1.0 / radius } else { 0.0 };
2464                // a point along a spiral (param u 0..1, start angle theta0, winding dir),
2465                // spun by ry/rx — returns (world point, facing 0..1 where 1 = toward camera)
2466                let pt = |u: f32, theta0: f32, dir: f32| -> ([f32;3], f32) {
2467                    let phi = pi * u;                       // 0..pi  (north → south)
2468                    let th  = dir * turns * tau * u + theta0;
2469                    let (mut x, y, mut z) = (phi.sin()*th.cos()*radius, phi.cos()*radius, phi.sin()*th.sin()*radius);
2470                    let x1 =  x*cyr + z*syr;                // yaw about Y
2471                    let z1 = -x*syr + z*cyr;
2472                    x = x1; z = z1;
2473                    let y2 = y*cxr - z*sxr;                 // pitch about X
2474                    let z2 = y*sxr + z*cxr;
2475                    // facing: camera sits at -zdist looking +z, so smaller z2 = nearer = brighter
2476                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.0, 1.0);
2477                    ([cx + x, cy + y2, cz + z2], facing)
2478                };
2479                let mut gfx = self.gfx.borrow_mut();
2480                let near = -gfx.camera.zdist + 0.05;
2481                // draw one segment (near-clipped) in a grayscale tint scaled by `lum`
2482                let seg = |gfx: &mut crate::gfx::GfxState, a: [f32;3], b: [f32;3], lum: f32| {
2483                    let (mut lax,mut lay,mut laz)=(a[0],a[1],a[2]);
2484                    let (mut lbx,mut lby,mut lbz)=(b[0],b[1],b[2]);
2485                    let da=gfx.camera.depth(lax,lay,laz); let db=gfx.camera.depth(lbx,lby,lbz);
2486                    if da<=near && db<=near { return; }
2487                    if da<=near { let t=(near-da)/(db-da); lax+=t*(lbx-lax); lay+=t*(lby-lay); laz+=t*(lbz-laz); }
2488                    else if db<=near { let t=(near-da)/(db-da); lbx=lax+t*(lbx-lax); lby=lay+t*(lby-lay); lbz=laz+t*(lbz-laz); }
2489                    let (sax,say,da2)=gfx.camera.project(lax,lay,laz);
2490                    let (sbx,sby,db2)=gfx.camera.project(lbx,lby,lbz);
2491                    // grayscale-alpha: front-facing bright, back faded toward black
2492                    let l = (0.12 + 0.88 * lum).clamp(0.0, 1.0);
2493                    let cr=(tr*l) as u32; let cg=(tg*l) as u32; let cb=(tb*l) as u32;
2494                    let color=(cr<<16)|(cg<<8)|cb;
2495                    gfx.depth_queue.push_line((da2+db2)*0.5, color, sax,say, sbx,sby);
2496                };
2497                // two opposite winding directions → a soft guilloché weave (not a cage)
2498                for &dir in &[1.0_f32, -1.0_f32] {
2499                    for s in 0..density {
2500                        let theta0 = s as f32 * tau / density as f32;
2501                        let mut prev = pt(0.0, theta0, dir);
2502                        for k in 1..=nseg {
2503                            let cur = pt(k as f32 / nseg as f32, theta0, dir);
2504                            seg(&mut gfx, prev.0, cur.0, (prev.1 + cur.1) * 0.5);
2505                            prev = cur;
2506                        }
2507                    }
2508                }
2509                return Ok(Value::Unit);
2510            }
2511
2512            // orb_particles(cx,cy,cz, radius, count, t, r,g,b)
2513            //   Fills the VOLUME of a sphere with `count` swirling vector points —
2514            //   like motes suspended inside a snow-globe orb. Points are distributed
2515            //   uniformly through the ball, slowly tumble as a cloud + wobble
2516            //   individually over time `t`, and are depth-shaded (near = bright,
2517            //   far = dim) so the cloud has real volume. Additive, so it layers under
2518            //   a shell / over a liquid marble.
2519            #[cfg(not(target_arch = "wasm32"))]
2520            "orb_particles" | "球内粒子" | "オーブ粒子" | "오브입자" | "อนุภาคทรงกลม" => {
2521                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32; let cz=self.arg_num(&args,2,0.)? as f32;
2522                let radius=self.arg_num(&args,3,1.0)? as f32;
2523                let count=(self.arg_num(&args,4,160.)? as i32).clamp(1, 4000);
2524                let t=self.arg_num(&args,5,0.)? as f32;
2525                let tr=(self.arg_num(&args,6,255.)? as f32).clamp(0.,255.);
2526                let tg=(self.arg_num(&args,7,255.)? as f32).clamp(0.,255.);
2527                let tb=(self.arg_num(&args,8,255.)? as f32).clamp(0.,255.);
2528                let inv_r = if radius.abs() > 1e-5 { 1.0/radius } else { 0.0 };
2529                // cheap deterministic hash → [0,1)
2530                let h = |mut x: u32| -> f32 {
2531                    x = x.wrapping_mul(747796405).wrapping_add(2891336453);
2532                    x = ((x >> ((x >> 28).wrapping_add(4))) ^ x).wrapping_mul(277803737);
2533                    (((x >> 22) ^ x) & 0xFFFFFF) as f32 / 16_777_216.0
2534                };
2535                let tau = std::f32::consts::TAU;
2536                // slow tumble of the whole cloud
2537                let (cyr, syr) = ((t*0.5).cos(), (t*0.5).sin());
2538                let (cxr, sxr) = ((t*0.23).cos(), (t*0.23).sin());
2539                let mut gfx = self.gfx.borrow_mut();
2540                let near = -gfx.camera.zdist + 0.05;
2541                let (sw, sh) = (gfx.width as i32, gfx.height as i32);
2542                for i in 0..count {
2543                    let i = i as u32;
2544                    // uniform-in-volume: r = cbrt(u) * radius; direction from two hashes
2545                    let u  = h(i.wrapping_mul(3) + 1);
2546                    let rr = u.cbrt() * radius * (0.85 + 0.15 * (t*1.3 + i as f32).sin()); // gentle pulse
2547                    let th = h(i.wrapping_mul(3) + 2) * tau + t * (0.3 + 0.5 * h(i*7+5)); // per-mote orbit
2548                    let ph = (h(i.wrapping_mul(3) + 3) * 2.0 - 1.0).acos();               // uniform cos(phi)
2549                    let (mut x, y, mut z) = (rr*ph.sin()*th.cos(), rr*ph.cos(), rr*ph.sin()*th.sin());
2550                    // tumble the cloud (yaw then pitch)
2551                    let x1 = x*cyr + z*syr; let z1 = -x*syr + z*cyr; x = x1; z = z1;
2552                    let y2 = y*cxr - z*sxr; let z2 = y*sxr + z*cxr;
2553                    let (wx, wy, wz) = (cx + x, cy + y2, cz + z2);
2554                    if gfx.camera.depth(wx, wy, wz) <= near { continue; }
2555                    let (sx, sy, dep) = gfx.camera.project(wx, wy, wz);
2556                    let sxi = sx as i32; let syi = sy as i32;
2557                    if sxi < 0 || syi < 0 || sxi >= sw || syi >= sh { continue; }
2558                    // depth-shade: nearer (smaller z2) = brighter
2559                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.15, 1.0);
2560                    let l = facing;
2561                    let cr=(tr*l) as u32; let cg=(tg*l) as u32; let cb=(tb*l) as u32;
2562                    let color=(cr<<16)|(cg<<8)|cb;
2563                    // a 1–2px dot (bigger when near) as a short segment in the depth queue
2564                    let len = if facing > 0.7 { 1.0 } else { 0.0 };
2565                    gfx.depth_queue.push_line(dep, color, sx, sy, sx + len, sy);
2566                }
2567                return Ok(Value::Unit);
2568            }
2569
2570            // project_3d(x,y,z) -> [screen_x, screen_y, depth]; behind the camera
2571            // returns a sentinel ([-99999,-99999, depth]) so scripts can skip it.
2572            // Lets scripts place 2-D overlays (e.g. filled teardrop flames) onto 3-D points.
2573            "project_3d" | "投影3D" | "3D投影" | "3D투영" | "ฉาย3มิติ" => {
2574                let x = self.arg_num(&args,0,0.0)? as f32;
2575                let y = self.arg_num(&args,1,0.0)? as f32;
2576                let z = self.arg_num(&args,2,0.0)? as f32;
2577                let gfx = self.gfx.borrow();
2578                let near = -gfx.camera.zdist + 0.05;
2579                let d = gfx.camera.depth(x, y, z);
2580                if d <= near {
2581                    return Ok(Value::List(vec![Value::Number(-99999.0), Value::Number(-99999.0), Value::Number(d as f64)]));
2582                }
2583                let (sx, sy, depth) = gfx.camera.project(x, y, z);
2584                return Ok(Value::List(vec![Value::Number(sx as f64), Value::Number(sy as f64), Value::Number(depth as f64)]));
2585            }
2586            // draw_poly([x0,y0,x1,y1,…]) — filled 2-D polygon in the current colour,
2587            // honouring the blend mode (additive → translucent glow). Auto-closes.
2588            #[cfg(not(target_arch = "wasm32"))]
2589            "draw_poly" | "填充多边形" | "ポリゴン塗り" | "다각형채우기" | "เติมรูปหลายเหลี่ยม" => {
2590                let mut pts: Vec<[f32; 2]> = Vec::new();
2591                if let Some(Value::List(v)) = args.first() {
2592                    let mut i = 0;
2593                    while i + 1 < v.len() {
2594                        let x = self.to_number(&v[i]).unwrap_or(0.0) as f32;
2595                        let y = self.to_number(&v[i + 1]).unwrap_or(0.0) as f32;
2596                        pts.push([x, y]);
2597                        i += 2;
2598                    }
2599                }
2600                if pts.len() >= 3 {
2601                    if pts[0] != pts[pts.len() - 1] { let p0 = pts[0]; pts.push(p0); } // close
2602                    let mut gfx = self.gfx.borrow_mut();
2603                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
2604                    crate::gfx::raster::fill_contours_aa(&mut gfx.buffer, w, h, color, add, std::slice::from_ref(&pts));
2605                }
2606                return Ok(Value::Unit);
2607            }
2608
2609            // ══════════════════════════════════════════════════════════════════
2610            // VECTOR TEXTURE BUILTINS  (src/gfx/vtex.rs)
2611            // All patterns are depth-biased so they appear on top of surfaces.
2612            // Plane defined by: centre (cx,cy,cz) + U tangent + V tangent.
2613            // Last two args always: fr (frame f32), hue (phase offset f32).
2614            // ══════════════════════════════════════════════════════════════════
2615
2616            // vtex_grid(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cw,ch, fr,hue)
2617            "vtex_grid" | "ลายตาราง" | "纹格" | "格子模様" | "격자무늬" => {
2618                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2619                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2620                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2621                let cols=self.arg_num(&args,9,10.)?as usize; let rows=self.arg_num(&args,10,10.)?as usize;
2622                let cw=self.arg_num(&args,11,1.)?as f32;  let ch=self.arg_num(&args,12,1.)?as f32;
2623                let fr=self.arg_num(&args,13,0.)?as f32;  let hue=self.arg_num(&args,14,0.)?as f32;
2624                let mut gfx = self.gfx.borrow_mut();
2625                let cam = gfx.camera.clone();
2626                crate::gfx::vtex::draw_grid(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows,cw,ch, fr,hue);
2627                return Ok(Value::Unit);
2628            }
2629
2630            // vtex_rings(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_rings,n_sides, max_r,twist, fr,hue)
2631            "vtex_rings" | "ลายวงซ้อน" | "纹环" | "同心円" | "동심원" => {
2632                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2633                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2634                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2635                let nr=self.arg_num(&args,9,6.)?as usize; let ns=self.arg_num(&args,10,6.)?as usize;
2636                let mr=self.arg_num(&args,11,3.)?as f32;  let tw=self.arg_num(&args,12,0.)?as f32;
2637                let fr=self.arg_num(&args,13,0.)?as f32;  let hue=self.arg_num(&args,14,0.)?as f32;
2638                let mut gfx = self.gfx.borrow_mut();
2639                let cam = gfx.camera.clone();
2640                crate::gfx::vtex::draw_rings(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, nr,ns,mr,tw, fr,hue);
2641                return Ok(Value::Unit);
2642            }
2643
2644            // vtex_star(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_pts,r_out,r_in, rot_speed, fr,hue)
2645            "vtex_star" | "ลายดาว" | "纹星" | "星模様" | "별무늬" => {
2646                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2647                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2648                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2649                let np=self.arg_num(&args,9,6.)?as usize;
2650                let ro=self.arg_num(&args,10,2.)?as f32; let ri=self.arg_num(&args,11,1.)?as f32;
2651                let rs=self.arg_num(&args,12,0.01)?as f32;
2652                let fr=self.arg_num(&args,13,0.)?as f32; let hue=self.arg_num(&args,14,0.)?as f32;
2653                let mut gfx = self.gfx.borrow_mut();
2654                let cam = gfx.camera.clone();
2655                crate::gfx::vtex::draw_star(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, np,ro,ri,rs, fr,hue);
2656                return Ok(Value::Unit);
2657            }
2658
2659            // vtex_spiral(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_turns,max_r,steps, fr,hue)
2660            "vtex_spiral" | "ลายเกลียว" | "纹螺" | "螺旋" | "나선" => {
2661                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2662                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2663                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2664                let nt=self.arg_num(&args,9,3.)?as f32; let mr=self.arg_num(&args,10,3.)?as f32;
2665                let st=self.arg_num(&args,11,120.)?as usize;
2666                let fr=self.arg_num(&args,12,0.)?as f32; let hue=self.arg_num(&args,13,0.)?as f32;
2667                let mut gfx = self.gfx.borrow_mut();
2668                let cam = gfx.camera.clone();
2669                crate::gfx::vtex::draw_spiral(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, nt,mr,st, fr,hue);
2670                return Ok(Value::Unit);
2671            }
2672
2673            // vtex_flower(cx,cy,cz, ux,uy,uz, vx,vy,vz, radius,n_sides, fr,hue)
2674            "vtex_flower" | "ลายดอก" | "纹花" | "花模様" | "꽃무늬" => {
2675                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2676                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2677                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2678                let r=self.arg_num(&args,9,1.)?as f32; let ns=self.arg_num(&args,10,24.)?as usize;
2679                let fr=self.arg_num(&args,11,0.)?as f32; let hue=self.arg_num(&args,12,0.)?as f32;
2680                let mut gfx = self.gfx.borrow_mut();
2681                let cam = gfx.camera.clone();
2682                crate::gfx::vtex::draw_flower(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, r,ns, fr,hue);
2683                return Ok(Value::Unit);
2684            }
2685
2686            // vtex_letter_rain(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_cols,n_vis, col_w,row_h, speed, fr,hue)
2687            "vtex_letter_rain" | "ลายอักษรไหล" | "纹字雨" | "文字雨" | "글자비" => {
2688                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2689                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2690                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2691                let nc=self.arg_num(&args,9,16.)?as usize; let nv=self.arg_num(&args,10,14.)?as usize;
2692                let cw=self.arg_num(&args,11,0.65)?as f32; let rh=self.arg_num(&args,12,0.60)?as f32;
2693                let sp=self.arg_num(&args,13,0.025)?as f32;
2694                let fr=self.arg_num(&args,14,0.)?as f32; let hue=self.arg_num(&args,15,0.)?as f32;
2695                let mut gfx = self.gfx.borrow_mut();
2696                let cam = gfx.camera.clone();
2697                crate::gfx::vtex::draw_letter_rain(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, nc,nv,cw,rh,sp, fr,hue);
2698                return Ok(Value::Unit);
2699            }
2700
2701            // vtex_hyperbolic_uv(cx,cy,cz, ux,uy,uz, vx,vy,vz, max_r,n_circles,n_rays, fr,hue)
2702            "vtex_hyperbolic_uv" | "ลายไฮเพอร์โบลิก" | "纹曲面" | "双曲線" | "쌍곡선" => {
2703                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2704                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2705                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2706                let mr=self.arg_num(&args,9,5.)?as f32;
2707                let nc=self.arg_num(&args,10,12.)?as usize; let nr=self.arg_num(&args,11,18.)?as usize;
2708                let fr=self.arg_num(&args,12,0.)?as f32; let hue=self.arg_num(&args,13,0.)?as f32;
2709                let mut gfx = self.gfx.borrow_mut();
2710                let cam = gfx.camera.clone();
2711                crate::gfx::vtex::draw_hyperbolic_uv(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, mr,nc,nr, fr,hue);
2712                return Ok(Value::Unit);
2713            }
2714
2715            // vtex_halftone(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell_w,cell_h, density, fr,hue)
2716            "vtex_halftone" | "ลายจุด" | "纹半调" | "網点模様" | "망점" => {
2717                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2718                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2719                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2720                let cols=self.arg_num(&args,9,16.)?as usize; let rows=self.arg_num(&args,10,12.)?as usize;
2721                let cw=self.arg_num(&args,11,0.5)?as f32; let ch=self.arg_num(&args,12,0.5)?as f32;
2722                let dens=self.arg_num(&args,13,0.4)?as f32;
2723                let fr=self.arg_num(&args,14,0.)?as f32; let hue=self.arg_num(&args,15,0.)?as f32;
2724                let mut gfx = self.gfx.borrow_mut();
2725                let cam = gfx.camera.clone();
2726                crate::gfx::vtex::draw_halftone(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows,cw,ch,dens, fr,hue);
2727                return Ok(Value::Unit);
2728            }
2729
2730            // vtex_tessellated(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell, amplitude,freq, fr,hue)
2731            "vtex_tessellated" | "ลายตาข่าย" | "纹镶嵌" | "網目模様" | "격자망" => {
2732                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2733                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2734                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2735                let cols=self.arg_num(&args,9,14.)?as usize; let rows=self.arg_num(&args,10,10.)?as usize;
2736                let cell=self.arg_num(&args,11,0.6)?as f32;
2737                let amp=self.arg_num(&args,12,0.25)?as f32; let freq=self.arg_num(&args,13,4.)?as f32;
2738                let fr=self.arg_num(&args,14,0.)?as f32; let hue=self.arg_num(&args,15,0.)?as f32;
2739                let mut gfx = self.gfx.borrow_mut();
2740                let cam = gfx.camera.clone();
2741                crate::gfx::vtex::draw_tessellated(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows,cell,amp,freq, fr,hue);
2742                return Ok(Value::Unit);
2743            }
2744
2745            // vtex_lotus(cx,cy,cz, ux,uy,uz, vx,vy,vz, r_inner,r_outer,n_petals, fr,hue)
2746            "vtex_lotus" | "ลายดอกบัว" | "纹莲" | "蓮模様" | "연꽃무늬" => {
2747                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2748                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2749                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2750                let ri=self.arg_num(&args,9,1.)?as f32; let ro=self.arg_num(&args,10,2.)?as f32;
2751                let np=self.arg_num(&args,11,12.)?as usize;
2752                let fr=self.arg_num(&args,12,0.)?as f32; let hue=self.arg_num(&args,13,0.)?as f32;
2753                let mut gfx = self.gfx.borrow_mut();
2754                let cam = gfx.camera.clone();
2755                crate::gfx::vtex::draw_lotus(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, ri,ro,np, fr,hue);
2756                return Ok(Value::Unit);
2757            }
2758
2759            // vtex_chakra(cx,cy,cz, ux,uy,uz, vx,vy,vz, r,n_spokes, fr,hue)
2760            "vtex_chakra" | "ลายจักร" | "纹轮" | "輪模様" | "바퀴무늬" => {
2761                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2762                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2763                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2764                let r=self.arg_num(&args,9,2.)?as f32; let ns=self.arg_num(&args,10,8.)?as usize;
2765                let fr=self.arg_num(&args,11,0.)?as f32; let hue=self.arg_num(&args,12,0.)?as f32;
2766                let mut gfx = self.gfx.borrow_mut();
2767                let cam = gfx.camera.clone();
2768                crate::gfx::vtex::draw_chakra(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, r,ns, fr,hue);
2769                return Ok(Value::Unit);
2770            }
2771
2772            // vtex_yantra(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_layers,max_r, fr,hue)
2773            "vtex_yantra" | "ลายยันต์" | "纹咒" | "護符模様" | "부적무늬" => {
2774                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2775                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2776                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2777                let nl=self.arg_num(&args,9,4.)?as usize; let mr=self.arg_num(&args,10,3.)?as f32;
2778                let fr=self.arg_num(&args,11,0.)?as f32; let hue=self.arg_num(&args,12,0.)?as f32;
2779                let mut gfx = self.gfx.borrow_mut();
2780                let cam = gfx.camera.clone();
2781                crate::gfx::vtex::draw_yantra(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, nl,mr, fr,hue);
2782                return Ok(Value::Unit);
2783            }
2784
2785            // vtex_spiked_cog(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_teeth,r_body,r_spike,r_hub,n_spokes, fr,hue)
2786            "vtex_spiked_cog" | "ฟันเฟืองหนาม" | "纹棘轮" | "歯車模様" | "톱니바퀴" => {
2787                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2788                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2789                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2790                let nt=self.arg_num(&args,9,12.)?as usize; let rb=self.arg_num(&args,10,1.)?as f32;
2791                let rs=self.arg_num(&args,11,1.3)?as f32; let rh=self.arg_num(&args,12,0.2)?as f32;
2792                let ns=self.arg_num(&args,13,6.)?as usize;
2793                let fr=self.arg_num(&args,14,0.)?as f32; let hue=self.arg_num(&args,15,0.)?as f32;
2794                let mut gfx = self.gfx.borrow_mut();
2795                let cam = gfx.camera.clone();
2796                crate::gfx::vtex::draw_spiked_cog(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, nt,rb,rs,rh,ns, fr,hue);
2797                return Ok(Value::Unit);
2798            }
2799
2800            // vtex_torii(cx,cy,cz, ux,uy,uz, vx,vy,vz, width,height, fr,hue)
2801            "vtex_torii" | "ประตูโทริอิ" | "纹鸟居" | "鳥居" | "도리이" => {
2802                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2803                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2804                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2805                let w=self.arg_num(&args,9,4.)?as f32; let h=self.arg_num(&args,10,5.)?as f32;
2806                let fr=self.arg_num(&args,11,0.)?as f32; let hue=self.arg_num(&args,12,0.)?as f32;
2807                let mut gfx = self.gfx.borrow_mut();
2808                let cam = gfx.camera.clone();
2809                crate::gfx::vtex::draw_torii(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, w,h, fr,hue);
2810                return Ok(Value::Unit);
2811            }
2812
2813            // vtex_pagoda(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_tiers,base_w,tier_h,taper,eave_out, fr,hue)
2814            "vtex_pagoda" | "เจดีย์" | "纹塔" | "塔" | "탑" => {
2815                let cx=self.arg_num(&args,0,0.)?as f32; let cy=self.arg_num(&args,1,0.)?as f32; let cz=self.arg_num(&args,2,0.)?as f32;
2816                let ux=self.arg_num(&args,3,1.)?as f32; let uy=self.arg_num(&args,4,0.)?as f32; let uz=self.arg_num(&args,5,0.)?as f32;
2817                let vx=self.arg_num(&args,6,0.)?as f32; let vy=self.arg_num(&args,7,0.)?as f32; let vz=self.arg_num(&args,8,1.)?as f32;
2818                let nt=self.arg_num(&args,9,5.)?as usize; let bw=self.arg_num(&args,10,2.)?as f32;
2819                let th=self.arg_num(&args,11,1.)?as f32; let tp=self.arg_num(&args,12,0.72)?as f32;
2820                let eo=self.arg_num(&args,13,0.28)?as f32;
2821                let fr=self.arg_num(&args,14,0.)?as f32; let hue=self.arg_num(&args,15,0.)?as f32;
2822                let mut gfx = self.gfx.borrow_mut();
2823                let cam = gfx.camera.clone();
2824                crate::gfx::vtex::draw_pagoda(&mut gfx.depth_queue,&cam, cx,cy,cz, ux,uy,uz, vx,vy,vz, nt,bw,th,tp,eo, fr,hue);
2825                return Ok(Value::Unit);
2826            }
2827
2828            // ══════════════════════════════════════════════════════════════════
2829            // AUDIO BUILTINS
2830            // ══════════════════════════════════════════════════════════════════
2831
2832            // audio_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth)
2833            #[cfg(not(target_arch = "wasm32"))]
2834            "audio_tone" | "เสียงโทน" | "音调" | "音調" | "음조" | "空间音" | "空間音" | "공간음" => {
2835                let idx  = self.arg_num(&args, 0, 0.0)? as usize;
2836                let x    = self.arg_num(&args, 1, 0.0)? as f32;
2837                let y    = self.arg_num(&args, 2, 0.0)? as f32;
2838                let z    = self.arg_num(&args, 3, 0.0)? as f32;
2839                let w    = self.arg_num(&args, 4, 1.0)? as f32;
2840                let freq = self.arg_num(&args, 5, 220.0)? as f32;
2841                let amp  = self.arg_num(&args, 6, 0.15)? as f32;
2842                let lfo_rate  = self.arg_num(&args, 7, 0.5)? as f32;
2843                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
2844                if let Some(audio) = &self.audio {
2845                    audio.set_tone(idx, ToneParams { x, y, z, w, freq, amp, lfo_rate, lfo_depth });
2846                }
2847                return Ok(Value::Unit);
2848            }
2849
2850            #[cfg(not(target_arch = "wasm32"))]
2851            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" => {
2852                let cry = self.arg_num(&args, 0, 1.0)? as f32;
2853                let sry = self.arg_num(&args, 1, 0.0)? as f32;
2854                let crx = self.arg_num(&args, 2, 1.0)? as f32;
2855                let srx = self.arg_num(&args, 3, 0.0)? as f32;
2856                if let Some(audio) = &self.audio {
2857                    audio.set_listener(cry, sry, crx, srx);
2858                }
2859                return Ok(Value::Unit);
2860            }
2861
2862            #[cfg(not(target_arch = "wasm32"))]
2863            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" => {
2864                let path = match args.first() {
2865                    Some(Value::Str(s)) => s.clone(),
2866                    _ => return Ok(Value::Unit),
2867                };
2868                let vol = self.arg_num(&args, 1, 0.5)? as f32;
2869                if let Some(audio) = &self.audio {
2870                    audio.load_bgm(&path, vol);
2871                }
2872                return Ok(Value::Unit);
2873            }
2874
2875            #[cfg(not(target_arch = "wasm32"))]
2876            "audio_bgm_volume" | "ระดับเสียงพื้นหลัง" | "ระดับเพลงประกอบ" | "背景乐音量" | "BGM音量" | "배경음악음량" => {
2877                let vol = self.arg_num(&args, 0, 0.5)? as f32;
2878                if let Some(audio) = &self.audio {
2879                    audio.set_bgm_volume(vol);
2880                }
2881                return Ok(Value::Unit);
2882            }
2883
2884            #[cfg(not(target_arch = "wasm32"))]
2885            "audio_volume" | "ระดับเสียง" | "音量" | "음량" => {
2886                let vol = self.arg_num(&args, 0, 0.7)? as f32;
2887                if let Some(audio) = &self.audio {
2888                    audio.set_master_volume(vol);
2889                }
2890                return Ok(Value::Unit);
2891            }
2892
2893            // WASM audio builtins — delegate to Web Audio API
2894            #[cfg(target_arch = "wasm32")]
2895            "audio_tone" | "เสียงโทน" | "音调" | "音調" | "음조" | "空间音" | "空間音" | "공간음" => {
2896                let idx  = self.arg_num(&args, 0, 0.0)? as usize;
2897                let x    = self.arg_num(&args, 1, 0.0)? as f32;
2898                let y    = self.arg_num(&args, 2, 0.0)? as f32;
2899                let z    = self.arg_num(&args, 3, 0.0)? as f32;
2900                let w    = self.arg_num(&args, 4, 1.0)? as f32;
2901                let freq = self.arg_num(&args, 5, 220.0)? as f32;
2902                let amp  = self.arg_num(&args, 6, 0.15)? as f32;
2903                let lfo_rate  = self.arg_num(&args, 7, 0.5)? as f32;
2904                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
2905                crate::gfx::audio_web::set_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth);
2906                return Ok(Value::Unit);
2907            }
2908
2909            #[cfg(target_arch = "wasm32")]
2910            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" => {
2911                let cry = self.arg_num(&args, 0, 1.0)? as f32;
2912                let sry = self.arg_num(&args, 1, 0.0)? as f32;
2913                let crx = self.arg_num(&args, 2, 1.0)? as f32;
2914                let srx = self.arg_num(&args, 3, 0.0)? as f32;
2915                crate::gfx::audio_web::set_listener(cry, sry, crx, srx);
2916                return Ok(Value::Unit);
2917            }
2918
2919            #[cfg(target_arch = "wasm32")]
2920            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" => {
2921                let path = self.arg_str(&args, 0, "");
2922                let vol  = self.arg_num(&args, 1, 0.5)? as f32;
2923                crate::gfx::audio_web::load_bgm(&path, vol);
2924                return Ok(Value::Unit);
2925            }
2926
2927            #[cfg(target_arch = "wasm32")]
2928            "audio_bgm_volume" | "ระดับเสียงพื้นหลัง" | "ระดับเพลงประกอบ" | "背景乐音量" | "BGM音量" | "배경음악음량" => {
2929                let vol = self.arg_num(&args, 0, 0.5)? as f32;
2930                crate::gfx::audio_web::set_bgm_volume(vol);
2931                return Ok(Value::Unit);
2932            }
2933
2934            #[cfg(target_arch = "wasm32")]
2935            "audio_volume" | "ระดับเสียง" | "音量" | "음량" => {
2936                let vol = self.arg_num(&args, 0, 0.7)? as f32;
2937                crate::gfx::audio_web::set_master_volume(vol);
2938                return Ok(Value::Unit);
2939            }
2940
2941            // ── รอหน้าต่าง() — block until window closed / Escape ──
2942            "รอหน้าต่าง" | "wait_window" | "gfx_wait" => {
2943                #[cfg(not(target_arch = "wasm32"))]
2944                loop {
2945                    let still_open = {
2946                        let gfx = self.gfx.borrow();
2947                        gfx.window.as_ref()
2948                            .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
2949                            .unwrap_or(false)
2950                    };
2951                    if !still_open { break; }
2952                    let (buf, w, h) = {
2953                        let gfx = self.gfx.borrow();
2954                        (gfx.buffer.clone(), gfx.width, gfx.height)
2955                    };
2956                    let mut gfx = self.gfx.borrow_mut();
2957                    if let Some(win) = gfx.window.as_mut() {
2958                        if win.update_with_buffer(&buf, w, h).is_err() { break; }
2959                    }
2960                }
2961                return Ok(Value::Unit);
2962            }
2963
2964            // ── File I/O ──────────────────────────────────────────────────────
2965            "read_file" | "อ่านไฟล์" => {
2966                let path = self.arg_str(&args, 0, "");
2967                return std::fs::read_to_string(&path)
2968                    .map(Value::Str)
2969                    .map_err(|e| EvalErr::from(format!("read_file '{path}': {e}")));
2970            }
2971            // ── networking (TCP, 2-peer co-op) ───────────────────────────────
2972            #[cfg(not(target_arch = "wasm32"))]
2973            "net_host" | "เน็ตโฮสต์" => {
2974                let port = self.arg_num(&args, 0, 7777.0)? as u16;
2975                net::host(port);
2976                return Ok(Value::Unit);
2977            }
2978            #[cfg(not(target_arch = "wasm32"))]
2979            "net_join" | "เน็ตจอย" => {
2980                let ip   = self.arg_str(&args, 0, "127.0.0.1");
2981                let port = self.arg_num(&args, 1, 7777.0)? as u16;
2982                net::join(&ip, port);
2983                return Ok(Value::Unit);
2984            }
2985            #[cfg(not(target_arch = "wasm32"))]
2986            "net_send" | "เน็ตส่ง" => {
2987                let s = self.arg_str(&args, 0, "");
2988                net::send(&s);
2989                return Ok(Value::Unit);
2990            }
2991            #[cfg(not(target_arch = "wasm32"))]
2992            "net_recv" | "เน็ตรับ" => {
2993                return Ok(Value::Str(net::recv()));
2994            }
2995            #[cfg(not(target_arch = "wasm32"))]
2996            "net_status" | "เน็ตสถานะ" => {
2997                return Ok(Value::Number(net::status() as f64));
2998            }
2999            // ── LAN lobby discovery (UDP broadcast) ──
3000            #[cfg(not(target_arch = "wasm32"))]
3001            "net_announce" | "เน็ตประกาศ" => {
3002                let port = self.arg_num(&args, 0, 7778.0)? as u16;
3003                let info = self.arg_str(&args, 1, "");
3004                net::announce(port, &info);
3005                return Ok(Value::Unit);
3006            }
3007            #[cfg(not(target_arch = "wasm32"))]
3008            "net_announce_stop" | "เน็ตหยุดประกาศ" => {
3009                net::announce_stop();
3010                return Ok(Value::Unit);
3011            }
3012            #[cfg(not(target_arch = "wasm32"))]
3013            "net_discover" | "เน็ตค้นหา" => {
3014                let port = self.arg_num(&args, 0, 7778.0)? as u16;
3015                return Ok(Value::Str(net::discover(port)));
3016            }
3017            #[cfg(not(target_arch = "wasm32"))]
3018            "net_test" | "เน็ตทดสอบ" => {
3019                let port = self.arg_num(&args, 0, 7777.0)? as u16;
3020                return Ok(Value::Str(net::test_bind(port)));
3021            }
3022            // ── gamepad (gilrs) ──
3023            #[cfg(not(target_arch = "wasm32"))]
3024            "gamepad_poll" | "จอยโพล" => { gamepad::poll(); return Ok(Value::Unit); }
3025            #[cfg(not(target_arch = "wasm32"))]
3026            "gamepad_button" | "จอยปุ่ม" => {
3027                let name = self.arg_str(&args, 0, "");
3028                return Ok(Value::Number(if gamepad::button(&name) { 1.0 } else { 0.0 }));
3029            }
3030            #[cfg(not(target_arch = "wasm32"))]
3031            "gamepad_axis" | "จอยแกน" => {
3032                let name = self.arg_str(&args, 0, "");
3033                return Ok(Value::Number(gamepad::axis(&name) as f64));
3034            }
3035            #[cfg(not(target_arch = "wasm32"))]
3036            "gamepad_rumble" | "จอยสั่น" => {
3037                let low = self.arg_num(&args, 0, 0.0)? as f32;
3038                let high = self.arg_num(&args, 1, 0.0)? as f32;
3039                let ms = self.arg_num(&args, 2, 200.0)? as u32;
3040                gamepad::rumble(low, high, ms);
3041                return Ok(Value::Unit);
3042            }
3043            #[cfg(not(target_arch = "wasm32"))]
3044            "gamepad_list" | "จอยรายการ" => { return Ok(Value::Str(gamepad::list())); }
3045            #[cfg(not(target_arch = "wasm32"))]
3046            "gamepad_any" | "จอยใดๆ" => { return Ok(Value::Number(if gamepad::any_button() { 1.0 } else { 0.0 })); }
3047
3048            // ── game AI: neural networks ─────────────────────────────────────
3049            // nn_new(inputs[, seed]) → handle
3050            #[cfg(not(target_arch = "wasm32"))]
3051            "nn_new" | "建神经网" | "ニューラル作成" | "신경망생성" | "สร้างโครงข่าย" => {
3052                let n_in = self.arg_num(&args, 0, 1.0)?.max(0.0) as usize;
3053                let seed = self.arg_num(&args, 1, 1.0)? as u64;
3054                return Ok(Value::Number(ai::nn_new(n_in, seed) as f64));
3055            }
3056            // nn_dense(handle, units[, activation]) — append a layer
3057            #[cfg(not(target_arch = "wasm32"))]
3058            "nn_dense" | "密集层" | "密層追加" | "밀집층" | "ชั้นหนาแน่น" => {
3059                let id    = self.arg_num(&args, 0, -1.0)? as i64;
3060                let units = self.arg_num(&args, 1, 1.0)?.max(1.0) as usize;
3061                let act   = self.arg_str(&args, 2, "relu");
3062                ai::nn_dense(id, units, &act);
3063                return Ok(Value::Unit);
3064            }
3065            // nn_forward(handle, [inputs]) → [outputs]
3066            #[cfg(not(target_arch = "wasm32"))]
3067            "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" => {
3068                let id = self.arg_num(&args, 0, -1.0)? as i64;
3069                let input = self.arg_list_f32(&args, 1);
3070                let out = ai::nn_forward(id, &input);
3071                return Ok(Value::List(out.into_iter().map(|v| Value::Number(v as f64)).collect()));
3072            }
3073            // nn_train(handle, [inputs], [targets][, lr]) → loss
3074            #[cfg(not(target_arch = "wasm32"))]
3075            "nn_train" | "训练网" | "ニューラル学習" | "신경망학습" | "ฝึกโครงข่าย" => {
3076                let id     = self.arg_num(&args, 0, -1.0)? as i64;
3077                let input  = self.arg_list_f32(&args, 1);
3078                let target = self.arg_list_f32(&args, 2);
3079                let lr     = self.arg_num(&args, 3, 0.01)? as f32;
3080                return Ok(Value::Number(ai::nn_train(id, &input, &target, lr) as f64));
3081            }
3082            // nn_save(handle, path) → bool
3083            #[cfg(not(target_arch = "wasm32"))]
3084            "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" => {
3085                let id   = self.arg_num(&args, 0, -1.0)? as i64;
3086                let path = self.arg_str(&args, 1, "model.lnn");
3087                return Ok(Value::Bool(ai::nn_save(id, &path)));
3088            }
3089            // nn_load(path) → handle (-1 on failure)
3090            #[cfg(not(target_arch = "wasm32"))]
3091            "nn_load" | "载入网" | "網読込" | "신경망불러오기" | "โหลดโครงข่าย" => {
3092                let path = self.arg_str(&args, 0, "model.lnn");
3093                return Ok(Value::Number(ai::nn_load(&path) as f64));
3094            }
3095
3096            // ── game AI: behavior trees ──────────────────────────────────────
3097            // bt_build(dsl_string) → handle
3098            #[cfg(not(target_arch = "wasm32"))]
3099            "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" => {
3100                let spec = self.arg_str(&args, 0, "");
3101                return Ok(Value::Number(ai::bt_build(&spec) as f64));
3102            }
3103            // bt_set(handle, key, value) — set a blackboard fact
3104            #[cfg(not(target_arch = "wasm32"))]
3105            "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" => {
3106                let id  = self.arg_num(&args, 0, -1.0)? as i64;
3107                let key = self.arg_str(&args, 1, "");
3108                let val = self.arg_num(&args, 2, 0.0)? as f32;
3109                ai::bt_set(id, &key, val);
3110                return Ok(Value::Unit);
3111            }
3112            // bt_tick(handle) → chosen action name ("" if none)
3113            #[cfg(not(target_arch = "wasm32"))]
3114            "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" => {
3115                let id = self.arg_num(&args, 0, -1.0)? as i64;
3116                return Ok(Value::Str(ai::bt_tick(id)));
3117            }
3118            // bt_status(handle) → 0 fail / 1 success / 2 running
3119            #[cfg(not(target_arch = "wasm32"))]
3120            "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" => {
3121                let id = self.arg_num(&args, 0, -1.0)? as i64;
3122                return Ok(Value::Number(ai::bt_status(id) as f64));
3123            }
3124
3125            // ── game AI: miniature dialog LLM ────────────────────────────────
3126            // dialog_new([ctx, embed, hidden, seed]) → handle
3127            #[cfg(not(target_arch = "wasm32"))]
3128            "dialog_new" | "建对话模型" | "対話モデル作成" | "대화모델생성" | "สร้างโมเดลสนทนา" => {
3129                let ctx    = self.arg_num(&args, 0, 3.0)?.max(1.0) as usize;
3130                let embed  = self.arg_num(&args, 1, 32.0)?.max(1.0) as usize;
3131                let hidden = self.arg_num(&args, 2, 64.0)?.max(1.0) as usize;
3132                let seed   = self.arg_num(&args, 3, 1.0)? as u64;
3133                return Ok(Value::Number(ai::dialog_new(ctx, embed, hidden, seed) as f64));
3134            }
3135            // dialog_learn(handle, text) — add one utterance to the corpus
3136            #[cfg(not(target_arch = "wasm32"))]
3137            "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" => {
3138                let id   = self.arg_num(&args, 0, -1.0)? as i64;
3139                let text = self.arg_str(&args, 1, "");
3140                ai::dialog_learn(id, &text);
3141                return Ok(Value::Unit);
3142            }
3143            // dialog_load(handle, path) → lines added (-1 on error)
3144            #[cfg(not(target_arch = "wasm32"))]
3145            "dialog_load" | "对话载入" | "対話読込" | "대화불러오기" | "โหลดชุดสนทนา" => {
3146                let id   = self.arg_num(&args, 0, -1.0)? as i64;
3147                let path = self.arg_str(&args, 1, "");
3148                return Ok(Value::Number(ai::dialog_load(id, &path) as f64));
3149            }
3150            // dialog_train(handle[, epochs, lr]) → loss
3151            #[cfg(not(target_arch = "wasm32"))]
3152            "dialog_train" | "对话训练" | "対話訓練" | "대화훈련" | "ฝึกสนทนา" => {
3153                let id     = self.arg_num(&args, 0, -1.0)? as i64;
3154                let epochs = self.arg_num(&args, 1, 20.0)?.max(1.0) as usize;
3155                let lr     = self.arg_num(&args, 2, 0.1)? as f32;
3156                return Ok(Value::Number(ai::dialog_train(id, epochs, lr) as f64));
3157            }
3158            // dialog_say(handle, prompt[, max_tokens, temperature]) → reply text
3159            #[cfg(not(target_arch = "wasm32"))]
3160            "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" => {
3161                let id     = self.arg_num(&args, 0, -1.0)? as i64;
3162                let prompt = self.arg_str(&args, 1, "");
3163                let max    = self.arg_num(&args, 2, 24.0)?.max(1.0) as usize;
3164                let temp   = self.arg_num(&args, 3, 0.8)? as f32;
3165                return Ok(Value::Str(ai::dialog_say(id, &prompt, max, temp)));
3166            }
3167            // dialog_save(handle, path) → bool
3168            #[cfg(not(target_arch = "wasm32"))]
3169            "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" => {
3170                let id   = self.arg_num(&args, 0, -1.0)? as i64;
3171                let path = self.arg_str(&args, 1, "model.llm");
3172                return Ok(Value::Bool(ai::dialog_save(id, &path)));
3173            }
3174            // dialog_load_model(path) → handle (-1 on failure)
3175            #[cfg(not(target_arch = "wasm32"))]
3176            "dialog_load_model" | "对话载模" | "対話モデル読込" | "대화모델불러오기" | "โหลดโมเดลสนทนา" => {
3177                let path = self.arg_str(&args, 0, "model.llm");
3178                return Ok(Value::Number(ai::dialog_load_model(&path) as f64));
3179            }
3180
3181            "write_file" | "เขียนไฟล์" => {
3182                let path    = self.arg_str(&args, 0, "");
3183                let content = self.arg_str(&args, 1, "");
3184                std::fs::write(&path, content.as_bytes())
3185                    .map_err(|e| EvalErr::from(format!("write_file '{path}': {e}")))?;
3186                return Ok(Value::Unit);
3187            }
3188            "print_file" | "พิมพ์ไฟล์" => {
3189                let content = self.arg_str(&args, 0, "");
3190                print!("{content}");
3191                return Ok(Value::Unit);
3192            }
3193
3194            // ── CLI arguments ─────────────────────────────────────────────────
3195            "get_args" | "รับอาร์กิวเมนต์" => {
3196                let v: Vec<Value> = std::env::args().map(Value::Str).collect();
3197                return Ok(Value::List(v));
3198            }
3199
3200            // ── String utilities ──────────────────────────────────────────────
3201            "split" | "str_split" | "แยก" => {
3202                let s   = self.arg_str(&args, 0, "");
3203                let sep = self.arg_str(&args, 1, "\n");
3204                let sep = if sep.is_empty() { "\n".into() } else { sep };
3205                let parts: Vec<Value> = s.split(sep.as_str())
3206                    .map(|p| Value::Str(p.to_string())).collect();
3207                return Ok(Value::List(parts));
3208            }
3209            "trim" | "str_trim" | "ตัดช่องว่าง" => {
3210                let s = self.arg_str(&args, 0, "");
3211                return Ok(Value::Str(s.trim().to_string()));
3212            }
3213            "starts_with" | "str_starts_with" | "เริ่มด้วย" => {
3214                let s      = self.arg_str(&args, 0, "");
3215                let prefix = self.arg_str(&args, 1, "");
3216                return Ok(Value::Bool(s.starts_with(prefix.as_str())));
3217            }
3218            "ends_with" | "str_ends_with" | "ลงท้ายด้วย" => {
3219                let s      = self.arg_str(&args, 0, "");
3220                let suffix = self.arg_str(&args, 1, "");
3221                return Ok(Value::Bool(s.ends_with(suffix.as_str())));
3222            }
3223            "str_replace" | "แทนสตริง" => {
3224                let s    = self.arg_str(&args, 0, "");
3225                let from = self.arg_str(&args, 1, "");
3226                let to   = self.arg_str(&args, 2, "");
3227                return Ok(Value::Str(s.replace(from.as_str(), to.as_str())));
3228            }
3229            "str_find" | "หาในสตริง" => {
3230                let s      = self.arg_str(&args, 0, "");
3231                let needle = self.arg_str(&args, 1, "");
3232                // Return char index (not byte index) for consistency with substr
3233                let pos = s.find(needle.as_str())
3234                    .map(|byte_i| s[..byte_i].chars().count() as f64)
3235                    .unwrap_or(-1.0);
3236                return Ok(Value::Number(pos));
3237            }
3238            "substr" | "str_slice" | "ส่วนสตริง" => {
3239                let s     = self.arg_str(&args, 0, "");
3240                let start = self.arg_num(&args, 1, 0.0)? as usize;
3241                let len   = args.get(2)
3242                    .map(|v| self.to_number(v).unwrap_or(999999.0) as usize)
3243                    .unwrap_or_else(|| s.chars().count().saturating_sub(start));
3244                let chars: Vec<char> = s.chars().collect();
3245                let end   = (start + len).min(chars.len());
3246                let slice: String = chars.get(start..end).unwrap_or(&[]).iter().collect();
3247                return Ok(Value::Str(slice));
3248            }
3249            "to_str" | "str" | "num_str" | "แปลงสตริง" => {
3250                let v = args.into_iter().next().unwrap_or(Value::Unit);
3251                return Ok(Value::Str(v.to_string()));
3252            }
3253            "str_repeat" | "ทำซ้ำสตริง" => {
3254                let s = self.arg_str(&args, 0, "");
3255                let n = self.arg_num(&args, 1, 1.0)? as usize;
3256                return Ok(Value::Str(s.repeat(n)));
3257            }
3258            "str_upper" => {
3259                let s = self.arg_str(&args, 0, "");
3260                return Ok(Value::Str(s.to_uppercase()));
3261            }
3262            "str_lower" => {
3263                let s = self.arg_str(&args, 0, "");
3264                return Ok(Value::Str(s.to_lowercase()));
3265            }
3266            "str_len" | "len" | "ความยาว" | "长度" | "長さ" | "길이" => {
3267                match args.first() {
3268                    Some(Value::Str(s))  => return Ok(Value::Number(s.chars().count() as f64)),
3269                    Some(Value::List(v)) => return Ok(Value::Number(v.len() as f64)),
3270                    _ => return Ok(Value::Number(0.0)),
3271                }
3272            }
3273
3274            // ── FNV-1a hash (deterministic, normalized 0.0–1.0) ──────────────
3275            "hash_str" | "แฮช" => {
3276                let s = self.arg_str(&args, 0, "");
3277                let mut h: u64 = 14695981039346656037_u64;
3278                for b in s.bytes() { h ^= b as u64; h = h.wrapping_mul(1099511628211); }
3279                return Ok(Value::Number((h & 0xFFFFFF) as f64 / 16777215.0));
3280            }
3281            "hash_int" | "แฮชจำนวน" => {
3282                let s = self.arg_str(&args, 0, "");
3283                let n = self.arg_num(&args, 1, 100.0)? as u64;
3284                let mut h: u64 = 14695981039346656037_u64;
3285                for b in s.bytes() { h ^= b as u64; h = h.wrapping_mul(1099511628211); }
3286                return Ok(Value::Number((h % n.max(1)) as f64));
3287            }
3288
3289            // ── List utilities ────────────────────────────────────────────────
3290            "list_new" | "รายการใหม่" | "新建列表" | "新規リスト" | "새목록" => {
3291                return Ok(Value::List(Vec::new()));
3292            }
3293            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가" => {
3294                let lst = args.first().cloned().unwrap_or(Value::List(vec![]));
3295                let val = args.get(1).cloned().unwrap_or(Value::Unit);
3296                if let Value::List(mut v) = lst { v.push(val); return Ok(Value::List(v)); }
3297                return Ok(Value::List(vec![val]));
3298            }
3299            "list_get" | "รับรายการ" | "取元素" | "要素取得" | "요소가져오기" => {
3300                let lst = args.first().cloned().unwrap_or(Value::List(vec![]));
3301                let i   = self.arg_num(&args, 1, 0.0)? as usize;
3302                if let Value::List(v) = lst {
3303                    return Ok(v.get(i).cloned().unwrap_or(Value::Str(String::new())));
3304                }
3305                return Ok(Value::Str(String::new()));
3306            }
3307            "list_join" | "join" | "รวมรายการ" | "连接" | "連結" | "연결" => {
3308                let lst = args.first().cloned().unwrap_or(Value::List(vec![]));
3309                let sep = args.get(1).map(|v| v.to_string()).unwrap_or_default();
3310                if let Value::List(v) = lst {
3311                    return Ok(Value::Str(v.iter().map(|x| x.to_string())
3312                        .collect::<Vec<_>>().join(&sep)));
3313                }
3314                return Ok(Value::Str(String::new()));
3315            }
3316            // blob_f32("<deflate+base64>") / blob_i32(...) — decode an embedded,
3317            // losslessly-compressed numeric blob into a list. Produced by
3318            // `ling convert`; lets converted assets carry geometry/PCM/etc. compactly.
3319            #[cfg(not(target_arch = "wasm32"))]
3320            "blob_f32" | "blob_i32" => {
3321                let s = self.arg_str(&args, 0, "");
3322                let is_i32 = name == "blob_i32";
3323                match decode_blob(&s) {
3324                    Ok(bytes) => {
3325                        let mut out = Vec::with_capacity(bytes.len() / 4);
3326                        for ch in bytes.chunks_exact(4) {
3327                            let arr = [ch[0], ch[1], ch[2], ch[3]];
3328                            let n = if is_i32 {
3329                                i32::from_le_bytes(arr) as f64
3330                            } else {
3331                                f32::from_le_bytes(arr) as f64
3332                            };
3333                            out.push(Value::Number(n));
3334                        }
3335                        return Ok(Value::List(out));
3336                    }
3337                    Err(e) => {
3338                        eprintln!("blob decode failed: {e}");
3339                        return Ok(Value::List(vec![]));
3340                    }
3341                }
3342            }
3343
3344            // ══════════════════════════════════════════════════════════════════
3345            // SVG EXPORT  (svg_begin / svg_rect / svg_circle / svg_line /
3346            //              svg_polyline / svg_text / svg_end / hsl_color)
3347            // Chinese aliases: 开始SVG 结束SVG SVG矩形 SVG圆形 SVG线段 SVG折线 SVG文本 HSL颜色
3348            // Thai aliases:    เริ่มSVG จบSVG SVGสี่เหลี่ยม SVGวงกลม SVGเส้น SVGเส้นหัก SVGข้อความ สีHSL
3349            // ══════════════════════════════════════════════════════════════════
3350
3351            "svg_begin" | "开始SVG" | "เริ่มSVG" => {
3352                let path   = self.arg_str(&args, 0, "output.svg");
3353                let width  = self.arg_num(&args, 1, 800.0)?;
3354                let height = self.arg_num(&args, 2, 600.0)?;
3355                *self.svg.borrow_mut() = Some(SvgWriter::new(path, width, height));
3356                return Ok(Value::Unit);
3357            }
3358
3359            "svg_rect" | "SVG矩形" | "SVGสี่เหลี่ยม" => {
3360                let x    = self.arg_num(&args, 0, 0.0)?;
3361                let y    = self.arg_num(&args, 1, 0.0)?;
3362                let w    = self.arg_num(&args, 2, 10.0)?;
3363                let h    = self.arg_num(&args, 3, 10.0)?;
3364                let fill = self.arg_str(&args, 4, "#ffffff");
3365                if let Some(svg) = self.svg.borrow_mut().as_mut() {
3366                    svg.elements.push(format!(
3367                        "<rect x=\"{x:.1}\" y=\"{y:.1}\" width=\"{w:.1}\" \
3368                         height=\"{h:.1}\" fill=\"{fill}\"/>"));
3369                }
3370                return Ok(Value::Unit);
3371            }
3372
3373            "svg_circle" | "SVG圆形" | "SVGวงกลม" => {
3374                let cx   = self.arg_num(&args, 0, 0.0)?;
3375                let cy   = self.arg_num(&args, 1, 0.0)?;
3376                let r    = self.arg_num(&args, 2, 5.0)?;
3377                let fill = self.arg_str(&args, 3, "#ffffff");
3378                if let Some(svg) = self.svg.borrow_mut().as_mut() {
3379                    svg.elements.push(format!(
3380                        "<circle cx=\"{cx:.1}\" cy=\"{cy:.1}\" r=\"{r:.1}\" fill=\"{fill}\"/>"));
3381                }
3382                return Ok(Value::Unit);
3383            }
3384
3385            "svg_line" | "SVG线段" | "SVGเส้น" => {
3386                let x1     = self.arg_num(&args, 0, 0.0)?;
3387                let y1     = self.arg_num(&args, 1, 0.0)?;
3388                let x2     = self.arg_num(&args, 2, 0.0)?;
3389                let y2     = self.arg_num(&args, 3, 0.0)?;
3390                let stroke = self.arg_str(&args, 4, "#ffffff");
3391                let sw     = self.arg_num(&args, 5, 1.0)?;
3392                if let Some(svg) = self.svg.borrow_mut().as_mut() {
3393                    svg.elements.push(format!(
3394                        "<line x1=\"{x1:.1}\" y1=\"{y1:.1}\" x2=\"{x2:.1}\" y2=\"{y2:.1}\" \
3395                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"));
3396                }
3397                return Ok(Value::Unit);
3398            }
3399
3400            "svg_polyline" | "SVG折线" | "SVGเส้นหัก" => {
3401                let pts    = self.arg_str(&args, 0, "");
3402                let stroke = self.arg_str(&args, 1, "#ffffff");
3403                let sw     = self.arg_num(&args, 2, 1.0)?;
3404                if let Some(svg) = self.svg.borrow_mut().as_mut() {
3405                    svg.elements.push(format!(
3406                        "<polyline points=\"{pts}\" fill=\"none\" \
3407                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"));
3408                }
3409                return Ok(Value::Unit);
3410            }
3411
3412            "svg_text" | "SVG文本" | "SVGข้อความ" => {
3413                let x    = self.arg_num(&args, 0, 0.0)?;
3414                let y    = self.arg_num(&args, 1, 0.0)?;
3415                let text = self.arg_str(&args, 2, "");
3416                let fill = self.arg_str(&args, 3, "#ffffff");
3417                let size = self.arg_num(&args, 4, 12.0)?;
3418                if let Some(svg) = self.svg.borrow_mut().as_mut() {
3419                    let safe = text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;");
3420                    svg.elements.push(format!(
3421                        "<text x=\"{x:.1}\" y=\"{y:.1}\" fill=\"{fill}\" \
3422                         font-family=\"monospace\" font-size=\"{size:.0}\">{safe}</text>"));
3423                }
3424                return Ok(Value::Unit);
3425            }
3426
3427            "svg_end" | "结束SVG" | "จบSVG" => {
3428                {
3429                    let borrow = self.svg.borrow();
3430                    if let Some(svg) = borrow.as_ref() {
3431                        svg.save().map_err(|e| EvalErr::from(format!("svg_end: {e}")))?;
3432                    }
3433                }
3434                *self.svg.borrow_mut() = None;
3435                return Ok(Value::Unit);
3436            }
3437
3438            "hsl_color" | "HSL颜色" | "สีHSL" => {
3439                let h = self.arg_num(&args, 0, 0.0)?;
3440                let s = self.arg_num(&args, 1, 70.0)?;
3441                let l = self.arg_num(&args, 2, 50.0)?;
3442                return Ok(Value::Str(hsl_to_hex(h, s, l)));
3443            }
3444
3445            // ══════════════════════════════════════════════════════════════════
3446            // FFT / AUDIO ANALYSIS BUILTINS  (native only)
3447            // ══════════════════════════════════════════════════════════════════
3448
3449            // fft_push(samples_list) — feed raw audio samples and run FFT
3450            #[cfg(not(target_arch = "wasm32"))]
3451            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" => {
3452                if let Some(Value::List(v)) = args.first() {
3453                    let samples: Vec<f32> = v.iter()
3454                        .filter_map(|x| if let Value::Number(n) = x { Some(*n as f32) } else { None })
3455                        .collect();
3456                    self.fft.borrow_mut().push_samples(&samples);
3457                }
3458                return Ok(Value::Unit);
3459            }
3460
3461            // fft_bands(n) → list of n log-spaced magnitude bands (0..1)
3462            #[cfg(not(target_arch = "wasm32"))]
3463            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" => {
3464                let n = self.arg_num(&args, 0, 32.0)? as usize;
3465                let bands = self.fft.borrow().freq_bands(n);
3466                *self.fft_bands_cache.borrow_mut() = bands.clone();
3467                return Ok(Value::List(bands.into_iter().map(|v| Value::Number(v as f64)).collect()));
3468            }
3469
3470            // fft_beat() → bool
3471            #[cfg(not(target_arch = "wasm32"))]
3472            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" => {
3473                return Ok(Value::Bool(self.fft.borrow().is_beat()));
3474            }
3475
3476            // fft_beat_ratio() → f64  (1.0 = at threshold, >1 = strong beat)
3477            #[cfg(not(target_arch = "wasm32"))]
3478            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" => {
3479                return Ok(Value::Number(self.fft.borrow().beat_ratio() as f64));
3480            }
3481
3482            // fft_rms() → f64
3483            #[cfg(not(target_arch = "wasm32"))]
3484            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" => {
3485                return Ok(Value::Number(self.fft.borrow().rms() as f64));
3486            }
3487
3488            // fft_dominant_freq() → f64  in Hz
3489            #[cfg(not(target_arch = "wasm32"))]
3490            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" => {
3491                return Ok(Value::Number(self.fft.borrow().dominant_freq() as f64));
3492            }
3493
3494            // ── wasm32 stubs: fft builtins are no-ops on web ───────────────
3495            #[cfg(target_arch = "wasm32")]
3496            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" => { return Ok(Value::Unit); }
3497            #[cfg(target_arch = "wasm32")]
3498            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" => {
3499                let n = self.arg_num(&args, 0, 32.0)? as usize;
3500                return Ok(Value::List(vec![Value::Number(0.0); n]));
3501            }
3502            #[cfg(target_arch = "wasm32")]
3503            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" => { return Ok(Value::Bool(false)); }
3504            #[cfg(target_arch = "wasm32")]
3505            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" => { return Ok(Value::Number(1.0)); }
3506            #[cfg(target_arch = "wasm32")]
3507            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" => { return Ok(Value::Number(0.0)); }
3508            #[cfg(target_arch = "wasm32")]
3509            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" => { return Ok(Value::Number(0.0)); }
3510
3511            // ══════════════════════════════════════════════════════════════════
3512            // PROCEDURAL TEXTURE BLIT BUILTINS  (screen-space)
3513            // All: name(dst_x, dst_y, width, height, ...params, palette)
3514            // palette: "rainbow" | "fire" | "ocean" | "psychedelic" | "neon" | "forest"
3515            // ══════════════════════════════════════════════════════════════════
3516
3517            // tex_checkerboard(x, y, w, h, tiles, r1,g1,b1, r2,g2,b2)
3518            "tex_checkerboard" | "ลายตารางหมากรุก" => {
3519                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3520                let tiles = self.arg_num(&args, 4, 8.0)? as u32;
3521                let (r1,g1,b1) = (self.arg_num(&args,5,255.)? as u32, self.arg_num(&args,6,255.)? as u32, self.arg_num(&args,7,255.)? as u32);
3522                let (r2,g2,b2) = (self.arg_num(&args,8,0.)? as u32,   self.arg_num(&args,9,0.)? as u32,   self.arg_num(&args,10,0.)? as u32);
3523                let c1 = (r1<<16)|(g1<<8)|b1; let c2 = (r2<<16)|(g2<<8)|b2;
3524                let mut gfx = self.gfx.borrow_mut();
3525                let (bw, bh) = (gfx.width, gfx.height);
3526                for row in 0..th { for col in 0..tw {
3527                    let cx = col as u32 * tiles / tw as u32;
3528                    let cy = row as u32 * tiles / th as u32;
3529                    let (dx, dy) = (tx+col, ty+row);
3530                    if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = if (cx+cy)%2==0 { c1 } else { c2 }; }
3531                }}
3532                return Ok(Value::Unit);
3533            }
3534
3535            // tex_gradient(x, y, w, h, angle_deg, r1,g1,b1, r2,g2,b2)
3536            "tex_gradient" | "ลายไล่สี" => {
3537                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3538                let angle = self.arg_num(&args, 4, 0.0)? as f32;
3539                let (r1,g1,b1) = (self.arg_num(&args,5,0.)? as f32/255., self.arg_num(&args,6,0.)? as f32/255., self.arg_num(&args,7,0.)? as f32/255.);
3540                let (r2,g2,b2) = (self.arg_num(&args,8,255.)? as f32/255., self.arg_num(&args,9,255.)? as f32/255., self.arg_num(&args,10,255.)? as f32/255.);
3541                let (ca, sa) = (angle.to_radians().cos(), angle.to_radians().sin());
3542                let mut gfx = self.gfx.borrow_mut();
3543                let (bw, bh) = (gfx.width, gfx.height);
3544                for row in 0..th { for col in 0..tw {
3545                    let nx = col as f32/tw as f32 - 0.5; let ny = row as f32/th as f32 - 0.5;
3546                    let t = ((nx*ca + ny*sa + 0.707)/1.414).clamp(0.,1.);
3547                    let (dx, dy) = (tx+col, ty+row);
3548                    if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = tex_rgb(r1+(r2-r1)*t, g1+(g2-g1)*t, b1+(b2-b1)*t); }
3549                }}
3550                return Ok(Value::Unit);
3551            }
3552
3553            // tex_noise(x, y, w, h, scale, octaves, seed, palette)
3554            "tex_noise" | "ลายนอยส์" => {
3555                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3556                let scale   = self.arg_num(&args, 4, 4.0)? as f32;
3557                let octaves = self.arg_num(&args, 5, 4.0)? as u32;
3558                let seed    = self.arg_num(&args, 6, 0.0)? as u32;
3559                let palette = self.arg_str(&args, 7, "rainbow");
3560                let mut gfx = self.gfx.borrow_mut();
3561                let (bw, bh) = (gfx.width, gfx.height);
3562                for row in 0..th { for col in 0..tw {
3563                    let v = tex_fbm(col as f32*scale/tw as f32, row as f32*scale/th as f32, octaves, seed);
3564                    let [r,g,b] = tex_palette(&palette, v);
3565                    let (dx, dy) = (tx+col, ty+row);
3566                    if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = tex_rgb(r, g, b); }
3567                }}
3568                return Ok(Value::Unit);
3569            }
3570
3571            // tex_freq_map(x, y, w, h, time, speed, palette)
3572            // Uses bands written by the last fft_bands() call.
3573            "tex_freq_map" | "ลายความถี่" => {
3574                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3575                let time    = self.arg_num(&args, 4, 0.0)? as f32;
3576                let speed   = self.arg_num(&args, 5, 0.3)? as f32;
3577                let palette = self.arg_str(&args, 6, "rainbow");
3578                let bands: Vec<f32> = {
3579                    let c = self.fft_bands_cache.borrow();
3580                    if c.is_empty() { vec![0.0; 32] } else { c.clone() }
3581                };
3582                let n = bands.len().max(1);
3583                let mut gfx = self.gfx.borrow_mut();
3584                let (bw, bh) = (gfx.width, gfx.height);
3585                for row in 0..th { for col in 0..tw {
3586                    let band_idx = (col * n / tw.max(1)).min(n-1);
3587                    let mag = bands[band_idx].clamp(0.,1.);
3588                    let fill_y = (mag * th as f32) as usize;
3589                    if row >= th.saturating_sub(fill_y) {
3590                        let t = (col as f32/tw as f32 + time*speed) % 1.0;
3591                        let [r,g,b] = tex_palette(&palette, t);
3592                        let bright = mag * (1.0 - row as f32/th as f32 * 0.5);
3593                        let (dx, dy) = (tx+col, ty+row);
3594                        if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = tex_rgb(r*bright, g*bright, b*bright); }
3595                    }
3596                }}
3597                return Ok(Value::Unit);
3598            }
3599
3600            // tex_spiral(x, y, w, h, freq, bands, time, palette)
3601            "tex_spiral" | "ลายเกลียวหมุน" => {
3602                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3603                let freq    = self.arg_num(&args, 4, 5.0)? as f32;
3604                let n_bands = self.arg_num(&args, 5, 8.0)? as f32;
3605                let time    = self.arg_num(&args, 6, 0.0)? as f32;
3606                let palette = self.arg_str(&args, 7, "rainbow");
3607                let mut gfx = self.gfx.borrow_mut();
3608                let (bw, bh) = (gfx.width, gfx.height);
3609                for row in 0..th { for col in 0..tw {
3610                    let nx = col as f32/tw as f32 - 0.5; let ny = row as f32/th as f32 - 0.5;
3611                    let r  = (nx*nx + ny*ny).sqrt();
3612                    let theta = ny.atan2(nx);
3613                    let t = ((r*freq - theta/std::f32::consts::TAU + time*0.5) * n_bands % 1.0).abs();
3614                    let [cr,cg,cb] = tex_palette(&palette, t);
3615                    let (dx, dy) = (tx+col, ty+row);
3616                    if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = tex_rgb(cr, cg, cb); }
3617                }}
3618                return Ok(Value::Unit);
3619            }
3620
3621            // tex_ripple(x, y, w, h, freq, cx, cy, time, palette)
3622            "tex_ripple" | "ลายระลอก" => {
3623                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3624                let freq    = self.arg_num(&args, 4, 10.0)? as f32;
3625                let rcx     = self.arg_num(&args, 5, 0.5)? as f32;
3626                let rcy     = self.arg_num(&args, 6, 0.5)? as f32;
3627                let time    = self.arg_num(&args, 7, 0.0)? as f32;
3628                let palette = self.arg_str(&args, 8, "ocean");
3629                let mut gfx = self.gfx.borrow_mut();
3630                let (bw, bh) = (gfx.width, gfx.height);
3631                for row in 0..th { for col in 0..tw {
3632                    let nx = col as f32/tw as f32 - rcx; let ny = row as f32/th as f32 - rcy;
3633                    let r = (nx*nx + ny*ny).sqrt();
3634                    let t = ((r*freq - time) % 1.0).abs();
3635                    let [cr,cg,cb] = tex_palette(&palette, t);
3636                    let (dx, dy) = (tx+col, ty+row);
3637                    if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = tex_rgb(cr, cg, cb); }
3638                }}
3639                return Ok(Value::Unit);
3640            }
3641
3642            // tex_mandelbrot(x, y, w, h, zoom, cx, cy, max_iter, palette)
3643            "tex_mandelbrot" | "ลายแมนเดลบรอต" => {
3644                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3645                let zoom     = self.arg_num(&args, 4, 1.0)?;
3646                let mcx      = self.arg_num(&args, 5, -0.5)?;
3647                let mcy      = self.arg_num(&args, 6, 0.0)?;
3648                let max_iter = self.arg_num(&args, 7, 64.0)? as u32;
3649                let palette  = self.arg_str(&args, 8, "psychedelic");
3650                let mut gfx = self.gfx.borrow_mut();
3651                let (bw, bh) = (gfx.width, gfx.height);
3652                for row in 0..th { for col in 0..tw {
3653                    let zx0 = (col as f64/tw as f64 - 0.5)/zoom + mcx;
3654                    let zy0 = (row as f64/th as f64 - 0.5)/zoom + mcy;
3655                    let mut x = 0.0f64; let mut y = 0.0f64; let mut i = 0u32;
3656                    while i < max_iter && x*x+y*y < 4.0 { let t=x*x-y*y+zx0; y=2.0*x*y+zy0; x=t; i+=1; }
3657                    let t = if i==max_iter { 0.0f32 } else {
3658                        (i as f32 - (x as f32*x as f32+y as f32*y as f32).ln().ln()/2.0f32.ln()) / max_iter as f32
3659                    };
3660                    let [cr,cg,cb] = tex_palette(&palette, t.clamp(0.,1.));
3661                    let (dx, dy) = (tx+col, ty+row);
3662                    if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = tex_rgb(cr, cg, cb); }
3663                }}
3664                return Ok(Value::Unit);
3665            }
3666
3667            // tex_julia(x, y, w, h, c_re, c_im, max_iter, palette)
3668            "tex_julia" | "ลายจูเลีย" => {
3669                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3670                let c_re     = self.arg_num(&args, 4, -0.7)?;
3671                let c_im     = self.arg_num(&args, 5, 0.27)?;
3672                let max_iter = self.arg_num(&args, 6, 64.0)? as u32;
3673                let palette  = self.arg_str(&args, 7, "neon");
3674                let mut gfx = self.gfx.borrow_mut();
3675                let (bw, bh) = (gfx.width, gfx.height);
3676                for row in 0..th { for col in 0..tw {
3677                    let mut zx = (col as f64/tw as f64 - 0.5)*3.5;
3678                    let mut zy = (row as f64/th as f64 - 0.5)*3.5;
3679                    let mut i = 0u32;
3680                    while i < max_iter && zx*zx+zy*zy < 4.0 { let t=zx*zx-zy*zy+c_re; zy=2.0*zx*zy+c_im; zx=t; i+=1; }
3681                    let t = i as f32 / max_iter as f32;
3682                    let [cr,cg,cb] = tex_palette(&palette, t);
3683                    let (dx, dy) = (tx+col, ty+row);
3684                    if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = tex_rgb(cr, cg, cb); }
3685                }}
3686                return Ok(Value::Unit);
3687            }
3688
3689            // tex_voronoi(x, y, w, h, cells, seed, palette)
3690            "tex_voronoi" | "ลายโวโรนอย" => {
3691                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3692                let cells   = self.arg_num(&args, 4, 16.0)? as u32;
3693                let seed    = self.arg_num(&args, 5, 42.0)? as u32;
3694                let palette = self.arg_str(&args, 6, "rainbow");
3695                let pts: Vec<[f32;2]> = (0..cells).map(|i| [tex_hash(i as i32,0,seed), tex_hash(i as i32,1,seed+999)]).collect();
3696                let mut gfx = self.gfx.borrow_mut();
3697                let (bw, bh) = (gfx.width, gfx.height);
3698                for row in 0..th { for col in 0..tw {
3699                    let (fx, fy) = (col as f32/tw as f32, row as f32/th as f32);
3700                    let (min_d, nearest) = pts.iter().enumerate().fold((f32::MAX,0usize), |(d,idx),(i,&[cx,cy])| {
3701                        let dd = (fx-cx).powi(2)+(fy-cy).powi(2);
3702                        if dd < d { (dd,i) } else { (d,idx) }
3703                    });
3704                    let t = (nearest as f32/cells as f32 + min_d*4.0) % 1.0;
3705                    let [cr,cg,cb] = tex_palette(&palette, t);
3706                    let (dx, dy) = (tx+col, ty+row);
3707                    if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = tex_rgb(cr, cg, cb); }
3708                }}
3709                return Ok(Value::Unit);
3710            }
3711
3712            // tex_halftone(x, y, w, h, dot_size, time, palette)
3713            "tex_halftone" | "ลายฮาล์ฟโทน" => {
3714                let (tx,ty,tw,th) = self.tex_rect(&args)?;
3715                let dot_size = self.arg_num(&args, 4, 0.05)? as f32;
3716                let time     = self.arg_num(&args, 5, 0.0)? as f32;
3717                let palette  = self.arg_str(&args, 6, "rainbow");
3718                let mut gfx = self.gfx.borrow_mut();
3719                let (bw, bh) = (gfx.width, gfx.height);
3720                for row in 0..th { for col in 0..tw {
3721                    let (fx, fy) = (col as f32/tw as f32, row as f32/th as f32);
3722                    let gx = (fx/dot_size).floor(); let gy = (fy/dot_size).floor();
3723                    let lx = (fx/dot_size - gx - 0.5)*2.0; let ly = (fy/dot_size - gy - 0.5)*2.0;
3724                    let r = (lx*lx + ly*ly).sqrt();
3725                    let t = (gx/(1.0/dot_size) + time*0.1) % 1.0;
3726                    let a = if r < 0.7 { ((0.7-r)/0.7).clamp(0.,1.) } else { 0.0 };
3727                    if a > 0.0 {
3728                        let [cr,cg,cb] = tex_palette(&palette, t);
3729                        let (dx, dy) = (tx+col, ty+row);
3730                        if dx < bw && dy < bh { gfx.buffer[dy*bw+dx] = tex_rgb(cr, cg, cb); }
3731                    }
3732                }}
3733                return Ok(Value::Unit);
3734            }
3735
3736            // ══════════════════════════════════════════════════════════════════
3737            // RENDER / LIGHTING MODES  (holographic cel shading)
3738            // ══════════════════════════════════════════════════════════════════
3739            // set_shade_mode(m) — 0 flat · 1 cel · 2 holo (default)
3740            "set_shade_mode" | "设置着色" | "シェード設定" | "셰이드모드" | "ตั้งการแรเงา" => {
3741                let m = self.arg_num(&args, 0, 2.0)? as u8;
3742                self.gfx.borrow_mut().shade_mode = m;
3743                return Ok(Value::Unit);
3744            }
3745            // set_cel_bands(n) — number of posterisation bands (>=2)
3746            "set_cel_bands" | "设置色阶" | "セル段数" | "셀밴드" | "ตั้งระดับสี" => {
3747                let n = (self.arg_num(&args, 0, 4.0)? as u32).max(2);
3748                self.gfx.borrow_mut().shade.bands = n;
3749                return Ok(Value::Unit);
3750            }
3751            // set_shadow_color(r,g,b) — coloured-shadow tint, 0-255
3752            "set_shadow_color" | "设置阴影色" | "影の色" | "그림자색" | "ตั้งสีเงา" => {
3753                let r=self.arg_num(&args,0,26.)? as f32/255.0;
3754                let g=self.arg_num(&args,1,33.)? as f32/255.0;
3755                let b=self.arg_num(&args,2,77.)? as f32/255.0;
3756                self.gfx.borrow_mut().shade.shadow = [r,g,b];
3757                return Ok(Value::Unit);
3758            }
3759            // set_rim(strength, r,g,b) — holographic fresnel edge glow
3760            // ══════════════════════════════════════════════════════════════════
3761            // CRYPTOGRAPHY (ling-crypto) — geo suite, hybrid PQ KEM, holographic
3762            // Bytes cross the language boundary as lowercase hex strings.
3763            // ══════════════════════════════════════════════════════════════════
3764            #[cfg(not(target_arch = "wasm32"))]
3765            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" => {
3766                let s = self.arg_str(&args, 0, "");
3767                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(s.as_bytes()))));
3768            }
3769            // 3-D torus-knot fingerprint of any text/key → flat [x,y,z, x,y,z, …]
3770            #[cfg(not(target_arch = "wasm32"))]
3771            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" => {
3772                let s = self.arg_str(&args, 0, "");
3773                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
3774                let mut out = Vec::with_capacity(shape.points.len() * 3);
3775                for p in &shape.points {
3776                    out.push(Value::Number(p[0] as f64));
3777                    out.push(Value::Number(p[1] as f64));
3778                    out.push(Value::Number(p[2] as f64));
3779                }
3780                return Ok(Value::List(out));
3781            }
3782            #[cfg(not(target_arch = "wasm32"))]
3783            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" => {
3784                let s = self.arg_str(&args, 0, "");
3785                return Ok(Value::Str(ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label()));
3786            }
3787            // KEM keypair (hybrid X25519+ML-KEM-768) → integer handle
3788            #[cfg(not(target_arch = "wasm32"))]
3789            "knot_keygen" | "hybrid_keygen" | "สร้างกุญแจปม" | "生成密钥" | "鍵生成" | "키생성" => {
3790                self.crypto_ids.push(ling_crypto::KnotIdentity::generate());
3791                return Ok(Value::Number((self.crypto_ids.len() - 1) as f64));
3792            }
3793            #[cfg(not(target_arch = "wasm32"))]
3794            "knot_public" | "hybrid_public" | "กุญแจสาธารณะปม" | "公钥" | "公開鍵" | "공개키" => {
3795                let h = self.arg_num(&args, 0, 0.0)? as usize;
3796                let pk = self.crypto_ids.get(h).map(|id| hex_encode(id.public_key())).unwrap_or_default();
3797                return Ok(Value::Str(pk));
3798            }
3799            // encapsulate(pubkey_hex) → [ciphertext_hex, shared_secret_hex]
3800            #[cfg(not(target_arch = "wasm32"))]
3801            "knot_encapsulate" | "hybrid_encapsulate" | "ห่อกุญแจปม" | "封装密钥" | "カプセル化" | "캡슐화" => {
3802                let pk = hex_decode(&self.arg_str(&args, 0, ""));
3803                match ling_crypto::geo::knot_encapsulate(&pk) {
3804                    Ok((ct, ss)) => return Ok(Value::List(vec![Value::Str(hex_encode(&ct)), Value::Str(hex_encode(&ss))])),
3805                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
3806                }
3807            }
3808            // decapsulate(handle, ciphertext_hex) → shared_secret_hex
3809            #[cfg(not(target_arch = "wasm32"))]
3810            "knot_decapsulate" | "hybrid_decapsulate" | "แกะกุญแจปม" | "解封装密钥" | "カプセル解除" | "캡슐해제" => {
3811                let h = self.arg_num(&args, 0, 0.0)? as usize;
3812                let ct = hex_decode(&self.arg_str(&args, 1, ""));
3813                let ss = self.crypto_ids.get(h)
3814                    .and_then(|id| id.decapsulate(&ct).ok())
3815                    .map(|s| hex_encode(&s)).unwrap_or_default();
3816                return Ok(Value::Str(ss));
3817            }
3818            // Authenticated encryption (XChaCha20-Poly1305) — seal(key_hex, text) → ct_hex
3819            #[cfg(not(target_arch = "wasm32"))]
3820            "crypto_seal" | "ผนึก" | "封印" | "封印する" | "봉인" => {
3821                let key = hex_to_32(&self.arg_str(&args, 0, ""));
3822                let pt = self.arg_str(&args, 1, "");
3823                match ling_crypto::geo::holo_seal(key, pt.as_bytes()) {
3824                    Ok(ct) => return Ok(Value::Str(hex_encode(&ct))),
3825                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
3826                }
3827            }
3828            #[cfg(not(target_arch = "wasm32"))]
3829            "crypto_open" | "เปิดผนึก" | "解封" | "封印解除" | "봉인해제" => {
3830                let key = hex_to_32(&self.arg_str(&args, 0, ""));
3831                let ct = hex_decode(&self.arg_str(&args, 1, ""));
3832                match ling_crypto::geo::holo_open(key, &ct) {
3833                    Ok(pt) => return Ok(Value::Str(String::from_utf8_lossy(&pt).into_owned())),
3834                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
3835                }
3836            }
3837            // Holographic all-or-nothing transform — 4-D fragment coords [a,b,c,d, …]
3838            #[cfg(not(target_arch = "wasm32"))]
3839            "holo_points" | "จุดโฮโลแกรม" | "全息点" | "ホログラム点" | "홀로그램점" => {
3840                let s = self.arg_str(&args, 0, "");
3841                let frags = ling_crypto::geo::scatter(s.as_bytes());
3842                let mut out = Vec::with_capacity(frags.len() * 4);
3843                for f in &frags { for c in f.coord { out.push(Value::Number(c as f64)); } }
3844                return Ok(Value::List(out));
3845            }
3846            #[cfg(not(target_arch = "wasm32"))]
3847            "holo_fragment_count" | "จำนวนชิ้นโฮโลแกรม" | "全息碎片数" | "ホログラム断片数" | "홀로그램조각수" => {
3848                let s = self.arg_str(&args, 0, "");
3849                return Ok(Value::Number(ling_crypto::geo::scatter(s.as_bytes()).len() as f64));
3850            }
3851
3852            // ══════════════════════════════════════════════════════════════════
3853            // ling-ui — animation easings + holographic vector widgets + text I/O
3854            // ══════════════════════════════════════════════════════════════════
3855            "ease" => {
3856                let name = self.arg_str(&args, 0, "ease");
3857                let t = self.arg_num(&args, 1, 0.0)? as f32;
3858                return Ok(Value::Number(ling_ui::Easing::from_name(&name).apply(t) as f64));
3859            }
3860
3861            // ══════════════════════════════════════════════════════════════════
3862            // Anima — unified animation drivers (ling-animation). Organic 灵 +
3863            // mechanical 机 scalar drivers, callable per frame from a script.
3864            // ══════════════════════════════════════════════════════════════════
3865            "tween" | "补间" | "補間" | "트윈" | "แทรกค่า" => {
3866                let a = self.arg_num(&args, 0, 0.0)?;
3867                let b = self.arg_num(&args, 1, 0.0)?;
3868                let t = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 1.0);
3869                return Ok(Value::Number(a + (b - a) * t));
3870            }
3871            "tween_ease" | "缓动补间" | "緩和補間" | "이징트윈" | "แทรกนุ่ม" => {
3872                let a = self.arg_num(&args, 0, 0.0)? as f32;
3873                let b = self.arg_num(&args, 1, 0.0)? as f32;
3874                let t = self.arg_num(&args, 2, 0.0)? as f32;
3875                let kind = self.arg_str(&args, 3, "linear");
3876                let e = ling_animation::EaseFunction::from_name(&kind);
3877                return Ok(Value::Number(ling_animation::ease::tween_ease(&a, &b, t, e) as f64));
3878            }
3879            // ── Organic 灵 ──
3880            "breathe" | "呼吸" | "호흡" | "หายใจ" => {
3881                let t = self.arg_num(&args, 0, 0.0)? as f32;
3882                let rate = self.arg_num(&args, 1, 1.0)? as f32;
3883                let depth = self.arg_num(&args, 2, 0.1)? as f32;
3884                return Ok(Value::Number(ling_animation::scalar::breathe(t, rate, depth) as f64));
3885            }
3886            "wobble" | "摆动" | "揺れ" | "흔들림" | "โยก" => {
3887                let t = self.arg_num(&args, 0, 0.0)? as f32;
3888                let freq = self.arg_num(&args, 1, 1.0)? as f32;
3889                let amp = self.arg_num(&args, 2, 1.0)? as f32;
3890                let phase = self.arg_num(&args, 3, 0.0)? as f32;
3891                return Ok(Value::Number(ling_animation::scalar::wobble(t, freq, amp, phase) as f64));
3892            }
3893            "gait_phase" | "步相" | "歩相" | "걸음위상" | "เฟสก้าว" => {
3894                let t = self.arg_num(&args, 0, 0.0)? as f32;
3895                let speed = self.arg_num(&args, 1, 1.0)? as f32;
3896                return Ok(Value::Number(ling_animation::scalar::gait_phase(t, speed) as f64));
3897            }
3898            "gait_swing" | "步摆" | "歩振り" | "걸음흔들" | "ก้าวแกว่ง" => {
3899                let t = self.arg_num(&args, 0, 0.0)? as f32;
3900                let speed = self.arg_num(&args, 1, 1.0)? as f32;
3901                let stride = self.arg_num(&args, 2, 1.0)? as f32;
3902                return Ok(Value::Number(ling_animation::scalar::gait_swing(t, speed, stride) as f64));
3903            }
3904            "gait_lift" | "抬脚" | "足上げ" | "발들기" | "ยกเท้า" => {
3905                let t = self.arg_num(&args, 0, 0.0)? as f32;
3906                let speed = self.arg_num(&args, 1, 1.0)? as f32;
3907                let height = self.arg_num(&args, 2, 1.0)? as f32;
3908                return Ok(Value::Number(ling_animation::scalar::gait_lift(t, speed, height) as f64));
3909            }
3910            "spring_to" | "弹向" | "バネ寄せ" | "스프링이동" | "สปริงไป" => {
3911                let pos = self.arg_num(&args, 0, 0.0)? as f32;
3912                let vel = self.arg_num(&args, 1, 0.0)? as f32;
3913                let target = self.arg_num(&args, 2, 0.0)? as f32;
3914                let stiffness = self.arg_num(&args, 3, 120.0)? as f32;
3915                let damping = self.arg_num(&args, 4, 14.0)? as f32;
3916                let dt = self.arg_num(&args, 5, 1.0 / 60.0)? as f32;
3917                let (np, nv) = ling_animation::scalar::spring_step(pos, vel, target, stiffness, damping, dt);
3918                return Ok(Value::List(vec![Value::Number(np as f64), Value::Number(nv as f64)]));
3919            }
3920            "ik2" | "反解" | "逆運動" | "역운동" | "ไอเค2" => {
3921                let l1 = self.arg_num(&args, 0, 1.0)? as f32;
3922                let l2 = self.arg_num(&args, 1, 1.0)? as f32;
3923                let tx = self.arg_num(&args, 2, 0.0)? as f32;
3924                let ty = self.arg_num(&args, 3, 0.0)? as f32;
3925                let (sh, el) = ling_animation::scalar::two_bone_ik(l1, l2, tx, ty);
3926                return Ok(Value::List(vec![Value::Number(sh as f64), Value::Number(el as f64)]));
3927            }
3928            // ── Mechanical 机 ──
3929            "gear_couple" | "齿轮联动" | "歯車連動" | "기어연동" | "เฟืองทด" => {
3930                let angle = self.arg_num(&args, 0, 0.0)? as f32;
3931                let ti = self.arg_num(&args, 1, 1.0)? as f32;
3932                let to = self.arg_num(&args, 2, 1.0)? as f32;
3933                return Ok(Value::Number(ling_animation::scalar::gear(angle, ti, to) as f64));
3934            }
3935            "gear_train" | "齿轮组" | "歯車列" | "기어열" | "ชุดเฟือง" => {
3936                let angle = self.arg_num(&args, 0, 0.0)? as f32;
3937                let teeth: Vec<f32> = match args.get(1) {
3938                    Some(Value::List(items)) => items.iter()
3939                        .filter_map(|v| if let Value::Number(n) = v { Some(*n as f32) } else { None }).collect(),
3940                    _ => Vec::new(),
3941                };
3942                let out = ling_animation::mechanism::gear_train(angle, &teeth);
3943                return Ok(Value::List(out.into_iter().map(|a| Value::Number(a as f64)).collect()));
3944            }
3945            "cam_lift" | "凸轮升程" | "カム揚程" | "캠리프트" | "ยกลูกเบี้ยว" => {
3946                let angle = self.arg_num(&args, 0, 0.0)? as f32;
3947                let lift = self.arg_num(&args, 1, 1.0)? as f32;
3948                return Ok(Value::Number(ling_animation::scalar::cam_lift(angle, lift) as f64));
3949            }
3950            "piston" | "活塞" | "ピストン" | "피스톤" | "ลูกสูบ" => {
3951                let angle = self.arg_num(&args, 0, 0.0)? as f32;
3952                let crank = self.arg_num(&args, 1, 1.0)? as f32;
3953                let rod = self.arg_num(&args, 2, 2.0)? as f32;
3954                return Ok(Value::Number(ling_animation::scalar::piston(angle, crank, rod) as f64));
3955            }
3956            "rack" | "齿条" | "ラック" | "랙" | "แร็ค" => {
3957                let angle = self.arg_num(&args, 0, 0.0)? as f32;
3958                let radius = self.arg_num(&args, 1, 1.0)? as f32;
3959                return Ok(Value::Number(ling_animation::scalar::rack(angle, radius) as f64));
3960            }
3961            #[cfg(not(target_arch = "wasm32"))]
3962            "mouse_x" => {
3963                let gfx = self.gfx.borrow();
3964                let v = gfx.window.as_ref().and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp)).map(|p| p.0 as f64).unwrap_or(0.0);
3965                return Ok(Value::Number(v));
3966            }
3967            #[cfg(not(target_arch = "wasm32"))]
3968            "mouse_y" => {
3969                let gfx = self.gfx.borrow();
3970                let v = gfx.window.as_ref().and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp)).map(|p| p.1 as f64).unwrap_or(0.0);
3971                return Ok(Value::Number(v));
3972            }
3973            #[cfg(not(target_arch = "wasm32"))]
3974            "mouse_down" => {
3975                let gfx = self.gfx.borrow();
3976                let d = gfx.window.as_ref().map(|w| w.get_mouse_down(minifb::MouseButton::Left)).unwrap_or(false);
3977                return Ok(Value::Bool(d));
3978            }
3979            #[cfg(not(target_arch = "wasm32"))]
3980            "mouse_down_right" | "เมาส์ขวา" => {
3981                let gfx = self.gfx.borrow();
3982                let d = gfx.window.as_ref().map(|w| w.get_mouse_down(minifb::MouseButton::Right)).unwrap_or(false);
3983                return Ok(Value::Bool(d));
3984            }
3985            #[cfg(not(target_arch = "wasm32"))]
3986            "mouse_down_middle" | "เมาส์กลาง" => {
3987                let gfx = self.gfx.borrow();
3988                let d = gfx.window.as_ref().map(|w| w.get_mouse_down(minifb::MouseButton::Middle)).unwrap_or(false);
3989                return Ok(Value::Bool(d));
3990            }
3991            #[cfg(not(target_arch = "wasm32"))]
3992            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" => {
3993                let x = self.arg_num(&args,0,0.0)? as f32;
3994                let y = self.arg_num(&args,1,0.0)? as f32;
3995                let w = self.arg_num(&args,2,0.0)? as f32;
3996                let h = self.arg_num(&args,3,0.0)? as f32;
3997                let gfx = self.gfx.borrow();
3998                let (mx,my) = gfx.window.as_ref().and_then(|win| win.get_mouse_pos(minifb::MouseMode::Clamp)).unwrap_or((0.0,0.0));
3999                return Ok(Value::Bool(ling_ui::holo::hit_rect(mx,my,x,y,w,h)));
4000            }
4001            // ui_text(x, y, scale, "string") — holographic vector text
4002            "ui_text" | "界面文字" | "UI文字" | "UI텍스트" | "ข้อความหน้าจอ" => {
4003                let x = self.arg_num(&args,0,0.0)? as f32;
4004                let y = self.arg_num(&args,1,0.0)? as f32;
4005                let scale = self.arg_num(&args,2,16.0)? as f32;
4006                let s = self.arg_str(&args,3,"");
4007                let segs = ling_ui::holo::text_lines(&s, x, y, scale*0.62, scale, scale*0.24);
4008                let mut gfx = self.gfx.borrow_mut();
4009                let (w,h,color) = (gfx.width, gfx.height, gfx.color);
4010                for sg in segs { draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]); }
4011                return Ok(Value::Unit);
4012            }
4013            // font_load("path.ttf") — load a vector font (outlines cached lazily as
4014            // cache/fonts/<stem>/<codepoint>.ling). Returns a handle, or -1 on failure.
4015            #[cfg(not(target_arch = "wasm32"))]
4016            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" => {
4017                let path = self.arg_str(&args, 0, "");
4018                // Optional 2nd arg: variable-font weight (e.g. 600 for a solid, bold UI).
4019                let weight = match self.arg_num(&args, 1, 0.0)? {
4020                    w if w > 0.0 => Some(w as f32),
4021                    _ => None,
4022                };
4023                // Try the path as given, then relative to the script's directory.
4024                let mut loaded = ling_graphics::VectorFont::from_path_weight(&path, weight);
4025                if loaded.is_err() {
4026                    if let Some(dir) = &self.source_dir {
4027                        let joined = dir.join(&path);
4028                        loaded = ling_graphics::VectorFont::from_path_weight(&joined.to_string_lossy(), weight);
4029                    }
4030                }
4031                match loaded {
4032                    Ok(f) => {
4033                        let id = self.fonts.len();
4034                        self.fonts.push(f);
4035                        return Ok(Value::Number(id as f64));
4036                    }
4037                    Err(e) => {
4038                        eprintln!("font_load failed ({path}): {e}");
4039                        return Ok(Value::Number(-1.0));
4040                    }
4041                }
4042            }
4043            // font_text(handle, x, y, px, "string") — anti-aliased *stroked* vector outline
4044            // in the current set_color / set_blend. (x,y) is the text box top-left.
4045            #[cfg(not(target_arch = "wasm32"))]
4046            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" => {
4047                let id = self.arg_num(&args, 0, 0.0)? as i64;
4048                let x  = self.arg_num(&args, 1, 0.0)? as f32;
4049                let y  = self.arg_num(&args, 2, 0.0)? as f32;
4050                let px = self.arg_num(&args, 3, 16.0)? as f32;
4051                let s  = self.arg_str(&args, 4, "");
4052                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
4053                    let strokes = self.font_layout_2d(id as usize, x, y, px, &s);
4054                    let mut gfx = self.gfx.borrow_mut();
4055                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
4056                    for pl in &strokes {
4057                        for seg in pl.windows(2) {
4058                            crate::gfx::raster::draw_line_aa(&mut gfx.buffer, w, h, color, add,
4059                                seg[0][0], seg[0][1], seg[1][0], seg[1][1]);
4060                        }
4061                    }
4062                }
4063                return Ok(Value::Unit);
4064            }
4065            // font_text_fill(handle, x, y, px, "string") — anti-aliased *filled* vector glyphs.
4066            #[cfg(not(target_arch = "wasm32"))]
4067            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" => {
4068                let id = self.arg_num(&args, 0, 0.0)? as i64;
4069                let x  = self.arg_num(&args, 1, 0.0)? as f32;
4070                let y  = self.arg_num(&args, 2, 0.0)? as f32;
4071                let px = self.arg_num(&args, 3, 16.0)? as f32;
4072                let s  = self.arg_str(&args, 4, "");
4073                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
4074                    // fill each glyph independently so interior holes (winding) stay correct
4075                    let glyphs = self.font_layout_2d_glyphs(id as usize, x, y, px, &s);
4076                    let mut gfx = self.gfx.borrow_mut();
4077                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
4078                    for contours in &glyphs {
4079                        crate::gfx::raster::fill_contours_aa(&mut gfx.buffer, w, h, color, add, contours);
4080                    }
4081                }
4082                return Ok(Value::Unit);
4083            }
4084            // font_text_3d(handle, cx,cy,cz, ux,uy,uz, vx,vy,vz, size, "string")
4085            // — stroked vector text on a 3D plane: u = advance dir, v = up dir, size = world/em.
4086            //   Flows through the depth-sorted line pipeline, so it rotates with the camera (and 4D).
4087            #[cfg(not(target_arch = "wasm32"))]
4088            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" => {
4089                let id = self.arg_num(&args, 0, 0.0)? as i64;
4090                let cx=self.arg_num(&args,1,0.0)? as f32; let cy=self.arg_num(&args,2,0.0)? as f32; let cz=self.arg_num(&args,3,0.0)? as f32;
4091                let ux=self.arg_num(&args,4,1.0)? as f32; let uy=self.arg_num(&args,5,0.0)? as f32; let uz=self.arg_num(&args,6,0.0)? as f32;
4092                let vx=self.arg_num(&args,7,0.0)? as f32; let vy=self.arg_num(&args,8,1.0)? as f32; let vz=self.arg_num(&args,9,0.0)? as f32;
4093                let size=self.arg_num(&args,10,1.0)? as f32;
4094                let s = self.arg_str(&args,11,"");
4095                if id >= 0 && (id as usize) < self.fonts.len() && size > 0.0 {
4096                    // Build world-space polylines: world = C + (pen+ex)*size*U + ey*size*V
4097                    let font = &mut self.fonts[id as usize];
4098                    let asc = font.ascent();
4099                    let mut pen = 0.0f32;
4100                    let mut lines: Vec<[f32; 6]> = Vec::new();
4101                    for ch in s.chars() {
4102                        let go = font.glyph_outline(ch, 0.01);
4103                        for pl in &go.polylines {
4104                            for seg in pl.windows(2) {
4105                                let map = |p: [f32; 2]| {
4106                                    let a = pen + p[0];
4107                                    let b = p[1] - asc; // shift so the top of the cap sits near C
4108                                    [cx + a*size*ux + b*size*vx,
4109                                     cy + a*size*uy + b*size*vy,
4110                                     cz + a*size*uz + b*size*vz]
4111                                };
4112                                let p0 = map(seg[0]); let p1 = map(seg[1]);
4113                                lines.push([p0[0],p0[1],p0[2], p1[0],p1[1],p1[2]]);
4114                            }
4115                        }
4116                        pen += go.advance;
4117                    }
4118                    let mut gfx = self.gfx.borrow_mut();
4119                    let color = gfx.color;
4120                    let near = -gfx.camera.zdist + 0.05;
4121                    for l in &lines {
4122                        let (mut ax, mut ay, mut az) = (l[0], l[1], l[2]);
4123                        let (mut bx, mut by, mut bz) = (l[3], l[4], l[5]);
4124                        let da = gfx.camera.depth(ax, ay, az);
4125                        let db = gfx.camera.depth(bx, by, bz);
4126                        if da <= near && db <= near { continue; }
4127                        if da <= near {
4128                            let t = (near - da) / (db - da);
4129                            ax += t*(bx-ax); ay += t*(by-ay); az += t*(bz-az);
4130                        } else if db <= near {
4131                            let t = (near - da) / (db - da);
4132                            bx = ax + t*(bx-ax); by = ay + t*(by-ay); bz = az + t*(bz-az);
4133                        }
4134                        let (sax, say, da2) = gfx.camera.project(ax, ay, az);
4135                        let (sbx, sby, db2) = gfx.camera.project(bx, by, bz);
4136                        let depth = (da2 + db2) / 2.0;
4137                        gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
4138                    }
4139                }
4140                return Ok(Value::Unit);
4141            }
4142            // font_width(handle, px, "string") — pixel width of a string in a loaded font.
4143            #[cfg(not(target_arch = "wasm32"))]
4144            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" => {
4145                let id = self.arg_num(&args, 0, 0.0)? as i64;
4146                let px = self.arg_num(&args, 1, 16.0)? as f32;
4147                let s  = self.arg_str(&args, 2, "");
4148                if id >= 0 && (id as usize) < self.fonts.len() {
4149                    return Ok(Value::Number(self.fonts[id as usize].measure(&s, px) as f64));
4150                }
4151                return Ok(Value::Number(0.0));
4152            }
4153            // ui_frame(x,y,w,h, bracketLen) — sci-fi corner brackets
4154            "ui_frame" | "边框" | "フレーム枠" | "프레임틀" | "กรอบ" => {
4155                let x=self.arg_num(&args,0,0.0)? as f32; let y=self.arg_num(&args,1,0.0)? as f32;
4156                let w0=self.arg_num(&args,2,0.0)? as f32; let h0=self.arg_num(&args,3,0.0)? as f32;
4157                let l=self.arg_num(&args,4,14.0)? as f32;
4158                let segs = ling_ui::holo::corner_brackets(x,y,w0,h0,l);
4159                let mut gfx = self.gfx.borrow_mut();
4160                let (w,h,color)=(gfx.width,gfx.height,gfx.color);
4161                for sg in segs { draw_line(&mut gfx.buffer, w,h,color, sg[0],sg[1],sg[2],sg[3]); }
4162                return Ok(Value::Unit);
4163            }
4164            // ui_bevel(x,y,w,h, bevel) — beveled holographic panel outline
4165            "ui_bevel" | "斜角框" | "ベベル枠" | "베벨틀" | "กรอบเฉียง" => {
4166                let x=self.arg_num(&args,0,0.0)? as f32; let y=self.arg_num(&args,1,0.0)? as f32;
4167                let w0=self.arg_num(&args,2,0.0)? as f32; let h0=self.arg_num(&args,3,0.0)? as f32;
4168                let bv=self.arg_num(&args,4,10.0)? as f32;
4169                let segs = ling_ui::holo::beveled_rect(x,y,w0,h0,bv);
4170                let mut gfx = self.gfx.borrow_mut();
4171                let (w,h,color)=(gfx.width,gfx.height,gfx.color);
4172                for sg in segs { draw_line(&mut gfx.buffer, w,h,color, sg[0],sg[1],sg[2],sg[3]); }
4173                return Ok(Value::Unit);
4174            }
4175
4176            // ══════════════════════════════════════════════════════════════════
4177            // VECTOR UI TOOLKIT  (crates/ling-ui/src/widgets.rs)
4178            // All widgets are vector + theme-coloured with an optional trailing
4179            // r,g,b override; interactive ones read the mouse and return state.
4180            // ══════════════════════════════════════════════════════════════════
4181            #[cfg(not(target_arch = "wasm32"))]
4182            "ui_theme" | "界面主题" | "UIテーマ" | "인터페이스테마" | "ธีมส่วนติดต่อ" => {
4183                let cur = self.ui_theme;
4184                let primary = self.color_at(&args, 0,  cur.primary);
4185                let accent  = self.color_at(&args, 3,  cur.accent);
4186                let track   = self.color_at(&args, 6,  cur.track);
4187                let warn    = self.color_at(&args, 9,  cur.warn);
4188                let text    = self.color_at(&args, 12, cur.text);
4189                let bg      = self.color_at(&args, 15, cur.bg);
4190                self.ui_theme = UiTheme { primary, accent, track, warn, text, bg };
4191                return Ok(Value::Unit);
4192            }
4193
4194            // ── HUD ──────────────────────────────────────────────────────────
4195            #[cfg(not(target_arch = "wasm32"))]
4196            "ui_radar" | "雷达" | "レーダー" | "레이더" | "เรดาร์" => {
4197                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32;
4198                let r=self.arg_num(&args,2,60.)? as f32; let sweep=self.arg_num(&args,3,0.)? as f32;
4199                let th=self.ui_theme;
4200                let prim=self.color_at(&args,4,th.primary);
4201                self.draw_ui(&ling_ui::widgets::radar(cx,cy,r,sweep, prim, th.accent, th.track));
4202                return Ok(Value::Unit);
4203            }
4204            #[cfg(not(target_arch = "wasm32"))]
4205            "ui_compass" | "罗盘" | "コンパス" | "나침반" | "เข็มทิศ" => {
4206                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4207                let w0=self.arg_num(&args,2,300.)? as f32; let h0=self.arg_num(&args,3,24.)? as f32;
4208                let head=self.arg_num(&args,4,0.)? as f32;
4209                let th=self.ui_theme; let prim=self.color_at(&args,5,th.primary);
4210                self.draw_ui(&ling_ui::widgets::compass(x,y,w0,h0,head, prim, th.track));
4211                return Ok(Value::Unit);
4212            }
4213            #[cfg(not(target_arch = "wasm32"))]
4214            "ui_reticle" | "准星" | "照準" | "조준선" | "เป้าเล็ง" => {
4215                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32;
4216                let r=self.arg_num(&args,2,30.)? as f32; let spread=self.arg_num(&args,3,0.)? as f32;
4217                let th=self.ui_theme; let prim=self.color_at(&args,4,th.primary);
4218                self.draw_ui(&ling_ui::widgets::reticle(cx,cy,r,spread, prim));
4219                return Ok(Value::Unit);
4220            }
4221            #[cfg(not(target_arch = "wasm32"))]
4222            "ui_target" | "锁定框" | "ターゲット" | "표적" | "กรอบเป้า" => {
4223                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4224                let w0=self.arg_num(&args,2,80.)? as f32; let h0=self.arg_num(&args,3,80.)? as f32;
4225                let lock=self.arg_num(&args,4,0.)? as f32;
4226                let th=self.ui_theme; let prim=self.color_at(&args,5,th.primary);
4227                self.draw_ui(&ling_ui::widgets::target(x,y,w0,h0,lock, prim, th.accent));
4228                return Ok(Value::Unit);
4229            }
4230            #[cfg(not(target_arch = "wasm32"))]
4231            "ui_panel" | "面板" | "パネル" | "패널" | "แผง" => {
4232                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4233                let w0=self.arg_num(&args,2,200.)? as f32; let h0=self.arg_num(&args,3,120.)? as f32;
4234                let bv=self.arg_num(&args,4,12.)? as f32;
4235                let th=self.ui_theme; let prim=self.color_at(&args,5,th.primary);
4236                self.draw_ui(&ling_ui::widgets::panel(x,y,w0,h0,bv, prim, th.bg));
4237                return Ok(Value::Unit);
4238            }
4239            #[cfg(not(target_arch = "wasm32"))]
4240            "ui_scanlines" | "扫描线" | "走査線" | "스캔라인" | "เส้นสแกน" => {
4241                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4242                let w0=self.arg_num(&args,2,200.)? as f32; let h0=self.arg_num(&args,3,120.)? as f32;
4243                let dens=self.arg_num(&args,4,24.)? as usize;
4244                let th=self.ui_theme; let line=self.color_at(&args,5,th.track);
4245                self.draw_ui(&ling_ui::widgets::scanlines(x,y,w0,h0,dens, line));
4246                return Ok(Value::Unit);
4247            }
4248
4249            // ── Meters ───────────────────────────────────────────────────────
4250            #[cfg(not(target_arch = "wasm32"))]
4251            "ui_bar" | "进度条" | "バー" | "막대" | "แถบ" => {
4252                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4253                let w0=self.arg_num(&args,2,160.)? as f32; let h0=self.arg_num(&args,3,16.)? as f32;
4254                let val=self.arg_num(&args,4,0.)? as f32; let max=self.arg_num(&args,5,1.)? as f32;
4255                let th=self.ui_theme; let fill=self.color_at(&args,6,th.primary);
4256                self.draw_ui(&ling_ui::widgets::bar(x,y,w0,h0, val/max.max(1e-6), fill, th.track));
4257                return Ok(Value::Unit);
4258            }
4259            #[cfg(not(target_arch = "wasm32"))]
4260            "ui_segbar" | "分段条" | "分割バー" | "분할막대" | "แถบแบ่ง" => {
4261                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4262                let w0=self.arg_num(&args,2,160.)? as f32; let h0=self.arg_num(&args,3,16.)? as f32;
4263                let val=self.arg_num(&args,4,0.)? as f32; let max=self.arg_num(&args,5,1.)? as f32;
4264                let segs=self.arg_num(&args,6,10.)? as usize;
4265                let th=self.ui_theme; let fill=self.color_at(&args,7,th.primary);
4266                self.draw_ui(&ling_ui::widgets::segbar(x,y,w0,h0, val/max.max(1e-6), segs, fill, th.track));
4267                return Ok(Value::Unit);
4268            }
4269            #[cfg(not(target_arch = "wasm32"))]
4270            "ui_gauge" | "仪表" | "ゲージ" | "게이지" | "มาตรวัด" => {
4271                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32;
4272                let r=self.arg_num(&args,2,50.)? as f32;
4273                let val=self.arg_num(&args,3,0.)? as f32; let max=self.arg_num(&args,4,1.)? as f32;
4274                let th=self.ui_theme; let needle=self.color_at(&args,5,th.warn);
4275                self.draw_ui(&ling_ui::widgets::gauge(cx,cy,r, val/max.max(1e-6), needle, th.accent, th.track));
4276                return Ok(Value::Unit);
4277            }
4278            #[cfg(not(target_arch = "wasm32"))]
4279            "ui_ring" | "环表" | "リングメーター" | "링미터" | "วงแหวนวัด" => {
4280                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32;
4281                let r=self.arg_num(&args,2,40.)? as f32;
4282                let val=self.arg_num(&args,3,0.)? as f32; let max=self.arg_num(&args,4,1.)? as f32;
4283                let th=self.ui_theme; let fill=self.color_at(&args,5,th.primary);
4284                self.draw_ui(&ling_ui::widgets::ring(cx,cy,r, val/max.max(1e-6), fill, th.track));
4285                return Ok(Value::Unit);
4286            }
4287            #[cfg(not(target_arch = "wasm32"))]
4288            "ui_vu" | "音量条" | "VUメーター" | "음량막대" | "มาตรเสียง" => {
4289                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4290                let w0=self.arg_num(&args,2,160.)? as f32; let h0=self.arg_num(&args,3,60.)? as f32;
4291                let levels=self.arg_list_f32(&args,4);
4292                let th=self.ui_theme; let fill=self.color_at(&args,5,th.primary);
4293                self.draw_ui(&ling_ui::widgets::vu(x,y,w0,h0, &levels, fill, th.warn));
4294                return Ok(Value::Unit);
4295            }
4296            #[cfg(not(target_arch = "wasm32"))]
4297            "ui_spark" | "迷你图" | "スパークライン" | "스파크라인" | "กราฟจิ๋ว" => {
4298                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4299                let w0=self.arg_num(&args,2,160.)? as f32; let h0=self.arg_num(&args,3,40.)? as f32;
4300                let vals=self.arg_list_f32(&args,4);
4301                let th=self.ui_theme; let line=self.color_at(&args,5,th.accent);
4302                self.draw_ui(&ling_ui::widgets::spark(x,y,w0,h0, &vals, line));
4303                return Ok(Value::Unit);
4304            }
4305            #[cfg(not(target_arch = "wasm32"))]
4306            "ui_battery" | "电池" | "バッテリー" | "배터리" | "แบตเตอรี่" => {
4307                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4308                let w0=self.arg_num(&args,2,50.)? as f32; let h0=self.arg_num(&args,3,22.)? as f32;
4309                let val=self.arg_num(&args,4,1.)? as f32; let max=self.arg_num(&args,5,1.)? as f32;
4310                let th=self.ui_theme; let fill=self.color_at(&args,6,th.accent);
4311                self.draw_ui(&ling_ui::widgets::battery(x,y,w0,h0, val/max.max(1e-6), fill, th.track, th.warn));
4312                return Ok(Value::Unit);
4313            }
4314
4315            // ── Interface controls (interactive → return state) ──────────────
4316            #[cfg(not(target_arch = "wasm32"))]
4317            "ui_button" | "按钮" | "ボタン" | "버튼" | "ปุ่ม" => {
4318                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4319                let w0=self.arg_num(&args,2,120.)? as f32; let h0=self.arg_num(&args,3,40.)? as f32;
4320                let (mx,my,down)=self.mouse_now();
4321                let hover=ling_ui::holo::hit_rect(mx,my,x,y,w0,h0);
4322                let clicked = hover && down && !self.mouse_was_down;
4323                let th=self.ui_theme; let prim=self.color_at(&args,4,th.primary);
4324                self.draw_ui(&ling_ui::widgets::button(x,y,w0,h0, hover, down&&hover, prim, th.bg));
4325                return Ok(Value::Number(if clicked {1.0} else {0.0}));
4326            }
4327            #[cfg(not(target_arch = "wasm32"))]
4328            "ui_toggle" | "开关" | "トグル" | "토글" | "สวิตช์" => {
4329                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4330                let w0=self.arg_num(&args,2,52.)? as f32; let h0=self.arg_num(&args,3,24.)? as f32;
4331                let mut state=self.arg_num(&args,4,0.)? > 0.5;
4332                let (mx,my,down)=self.mouse_now();
4333                let hover=ling_ui::holo::hit_rect(mx,my,x,y,w0,h0);
4334                if hover && down && !self.mouse_was_down { state = !state; }
4335                let th=self.ui_theme; let on=self.color_at(&args,5,th.accent);
4336                self.draw_ui(&ling_ui::widgets::toggle(x,y,w0,h0, state, on, th.track));
4337                return Ok(Value::Number(if state {1.0} else {0.0}));
4338            }
4339            #[cfg(not(target_arch = "wasm32"))]
4340            "ui_slider" | "滑块" | "スライダー" | "슬라이더" | "แถบเลื่อน" => {
4341                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4342                let w0=self.arg_num(&args,2,160.)? as f32;
4343                let mut val=self.arg_num(&args,3,0.)? as f32;
4344                let mn=self.arg_num(&args,4,0.)? as f32; let mx_=self.arg_num(&args,5,1.)? as f32;
4345                let (mx,my,down)=self.mouse_now();
4346                let hover=ling_ui::holo::hit_rect(mx,my,x-8.0,y-10.0,w0+16.0,20.0);
4347                if hover && down {
4348                    let frac=((mx-x)/w0).max(0.0).min(1.0);
4349                    val = mn + (mx_-mn)*frac;
4350                }
4351                let frac=((val-mn)/(mx_-mn).abs().max(1e-6)).max(0.0).min(1.0);
4352                let th=self.ui_theme; let fill=self.color_at(&args,6,th.primary);
4353                self.draw_ui(&ling_ui::widgets::slider(x,y,w0, frac, hover, fill, th.track));
4354                return Ok(Value::Number(val as f64));
4355            }
4356            #[cfg(not(target_arch = "wasm32"))]
4357            "ui_checkbox" | "复选框" | "チェックボックス" | "체크박스" | "ช่องเลือก" => {
4358                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4359                let s=self.arg_num(&args,2,20.)? as f32;
4360                let mut checked=self.arg_num(&args,3,0.)? > 0.5;
4361                let (mx,my,down)=self.mouse_now();
4362                let hover=ling_ui::holo::hit_rect(mx,my,x,y,s,s);
4363                if hover && down && !self.mouse_was_down { checked = !checked; }
4364                let th=self.ui_theme; let prim=self.color_at(&args,4,th.primary);
4365                self.draw_ui(&ling_ui::widgets::checkbox(x,y,s, checked, hover, prim, th.track));
4366                return Ok(Value::Number(if checked {1.0} else {0.0}));
4367            }
4368            #[cfg(not(target_arch = "wasm32"))]
4369            "ui_tabs" | "标签页" | "タブ" | "탭" | "แท็บ" => {
4370                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4371                let w0=self.arg_num(&args,2,240.)? as f32; let h0=self.arg_num(&args,3,28.)? as f32;
4372                let count=self.arg_num(&args,4,3.)? as usize;
4373                let mut active=self.arg_num(&args,5,0.)? as i32;
4374                let (mx,my,down)=self.mouse_now();
4375                let mut hover=-1;
4376                if my>=y && my<=y+h0 && mx>=x && mx<=x+w0 && count>0 {
4377                    hover = (((mx-x)/(w0/count as f32)) as i32).max(0).min(count as i32-1);
4378                    if down && !self.mouse_was_down { active = hover; }
4379                }
4380                let th=self.ui_theme; let prim=self.color_at(&args,6,th.primary);
4381                self.draw_ui(&ling_ui::widgets::tabs(x,y,w0,h0, count, active as usize, hover, prim, th.track));
4382                return Ok(Value::Number(active as f64));
4383            }
4384            #[cfg(not(target_arch = "wasm32"))]
4385            "ui_progress" | "进度" | "プログレス" | "진행바" | "ความคืบหน้า" => {
4386                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4387                let w0=self.arg_num(&args,2,200.)? as f32; let h0=self.arg_num(&args,3,12.)? as f32;
4388                let frac=self.arg_num(&args,4,0.)? as f32;
4389                let th=self.ui_theme; let fill=self.color_at(&args,5,th.accent);
4390                self.draw_ui(&ling_ui::widgets::progress(x,y,w0,h0, frac, fill, th.track));
4391                return Ok(Value::Unit);
4392            }
4393            #[cfg(not(target_arch = "wasm32"))]
4394            "ui_tooltip" | "提示框" | "ツールチップ" | "툴팁" | "คำแนะนำ" => {
4395                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4396                let w0=self.arg_num(&args,2,120.)? as f32; let h0=self.arg_num(&args,3,28.)? as f32;
4397                let th=self.ui_theme; let prim=self.color_at(&args,4,th.primary);
4398                self.draw_ui(&ling_ui::widgets::tooltip(x,y,w0,h0, prim, th.bg));
4399                return Ok(Value::Unit);
4400            }
4401            #[cfg(not(target_arch = "wasm32"))]
4402            "ui_stepper" | "步进器" | "ステッパー" | "스테퍼" | "ตัวปรับค่า" => {
4403                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4404                let w0=self.arg_num(&args,2,120.)? as f32; let h0=self.arg_num(&args,3,28.)? as f32;
4405                let mut val=self.arg_num(&args,4,0.)? as f32; let step=self.arg_num(&args,5,1.)? as f32;
4406                let (mx,my,down)=self.mouse_now();
4407                let hm=ling_ui::holo::hit_rect(mx,my,x,y,h0,h0);
4408                let hp=ling_ui::holo::hit_rect(mx,my,x+w0-h0,y,h0,h0);
4409                if down && !self.mouse_was_down { if hm { val -= step; } if hp { val += step; } }
4410                let th=self.ui_theme; let prim=self.color_at(&args,6,th.primary);
4411                self.draw_ui(&ling_ui::widgets::stepper(x,y,w0,h0, hm, hp, prim, th.track));
4412                return Ok(Value::Number(val as f64));
4413            }
4414
4415            // ── Game UI ──────────────────────────────────────────────────────
4416            #[cfg(not(target_arch = "wasm32"))]
4417            "ui_healthbar" | "血条" | "体力バー" | "체력바" | "แถบพลังชีวิต" => {
4418                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4419                let w0=self.arg_num(&args,2,180.)? as f32; let h0=self.arg_num(&args,3,16.)? as f32;
4420                let val=self.arg_num(&args,4,1.)? as f32; let max=self.arg_num(&args,5,1.)? as f32;
4421                let pulse=self.arg_num(&args,6,0.)? as f32;
4422                let th=self.ui_theme; let full=self.color_at(&args,7,th.accent);
4423                self.draw_ui(&ling_ui::widgets::healthbar(x,y,w0,h0, val/max.max(1e-6), pulse, full, th.warn, th.track));
4424                return Ok(Value::Unit);
4425            }
4426            #[cfg(not(target_arch = "wasm32"))]
4427            "ui_cooldown" | "冷却" | "クールダウン" | "쿨다운" | "คูลดาวน์" => {
4428                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32;
4429                let r=self.arg_num(&args,2,28.)? as f32; let frac=self.arg_num(&args,3,0.)? as f32;
4430                let th=self.ui_theme; let fill=self.color_at(&args,4,th.primary);
4431                self.draw_ui(&ling_ui::widgets::cooldown(cx,cy,r, frac, fill, th.track));
4432                return Ok(Value::Unit);
4433            }
4434            #[cfg(not(target_arch = "wasm32"))]
4435            "ui_counter" | "计数器" | "カウンター" | "카운터" | "ตัวนับ" => {
4436                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4437                let dw=self.arg_num(&args,2,14.)? as f32; let dh=self.arg_num(&args,3,24.)? as f32;
4438                let val=self.arg_num(&args,4,0.)? as i64; let digits=self.arg_num(&args,5,4.)? as usize;
4439                let th=self.ui_theme; let on=self.color_at(&args,6,th.primary);
4440                let off=ling_ui::widgets::shade(th.track,0.5);
4441                self.draw_ui(&ling_ui::widgets::counter(x,y,dw,dh, val, digits, on, off));
4442                return Ok(Value::Unit);
4443            }
4444            #[cfg(not(target_arch = "wasm32"))]
4445            "ui_minimap" | "小地图" | "ミニマップ" | "미니맵" | "แผนที่ย่อ" => {
4446                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4447                let w0=self.arg_num(&args,2,140.)? as f32; let h0=self.arg_num(&args,3,140.)? as f32;
4448                let th=self.ui_theme; let prim=self.color_at(&args,4,th.primary);
4449                self.draw_ui(&ling_ui::widgets::minimap(x,y,w0,h0, prim, th.bg));
4450                return Ok(Value::Unit);
4451            }
4452            #[cfg(not(target_arch = "wasm32"))]
4453            "ui_dpad" | "方向键" | "方向パッド" | "방향패드" | "ปุ่มทิศทาง" => {
4454                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32;
4455                let r=self.arg_num(&args,2,50.)? as f32;
4456                let (mx,my,down)=self.mouse_now();
4457                let mut dir=0;
4458                if down {
4459                    let (dx,dy)=(mx-cx, my-cy);
4460                    if dx*dx+dy*dy <= r*r {
4461                        if dx.abs() > dy.abs() { dir = if dx>0.0 {2} else {4}; }
4462                        else { dir = if dy>0.0 {3} else {1}; }
4463                    }
4464                }
4465                let th=self.ui_theme; let prim=self.color_at(&args,3,th.primary);
4466                self.draw_ui(&ling_ui::widgets::dpad(cx,cy,r, dir, prim, th.track));
4467                return Ok(Value::Number(dir as f64));
4468            }
4469            #[cfg(not(target_arch = "wasm32"))]
4470            "ui_slotgrid" | "物品格" | "スロットグリッド" | "슬롯격자" | "ช่องไอเทม" => {
4471                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4472                let cols=self.arg_num(&args,2,4.)? as usize; let rows=self.arg_num(&args,3,1.)? as usize;
4473                let cell=self.arg_num(&args,4,36.)? as f32; let sel=self.arg_num(&args,5,-1.)? as i32;
4474                let th=self.ui_theme; let prim=self.color_at(&args,6,th.primary);
4475                self.draw_ui(&ling_ui::widgets::slotgrid(x,y,cols,rows,cell, sel, prim, th.track));
4476                return Ok(Value::Unit);
4477            }
4478            #[cfg(not(target_arch = "wasm32"))]
4479            "ui_vignette" | "暗角" | "ビネット" | "비네트" | "ขอบมืด" => {
4480                let intensity=self.arg_num(&args,0,0.5)? as f32;
4481                let (w,h)={ let g=self.gfx.borrow(); (g.width as f32, g.height as f32) };
4482                let th=self.ui_theme; let col=self.color_at(&args,1,th.warn);
4483                self.draw_ui(&ling_ui::widgets::vignette(w,h, intensity, col));
4484                return Ok(Value::Unit);
4485            }
4486
4487            // ── Faux-3D in 2D space ──────────────────────────────────────────
4488            #[cfg(not(target_arch = "wasm32"))]
4489            "ui_gauge3d" | "立体仪表" | "立体ゲージ" | "입체게이지" | "มาตรวัด3มิติ" => {
4490                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32;
4491                let r=self.arg_num(&args,2,50.)? as f32;
4492                let val=self.arg_num(&args,3,0.)? as f32; let max=self.arg_num(&args,4,1.)? as f32;
4493                let spin=self.arg_num(&args,5,0.)? as f32;
4494                let th=self.ui_theme; let fill=self.color_at(&args,6,th.primary);
4495                self.draw_ui(&ling_ui::widgets::gauge3d(cx,cy,r, val/max.max(1e-6), spin, fill, th.track));
4496                return Ok(Value::Unit);
4497            }
4498            #[cfg(not(target_arch = "wasm32"))]
4499            "ui_panel3d" | "立体面板" | "立体パネル" | "입체패널" | "แผง3มิติ" => {
4500                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
4501                let w0=self.arg_num(&args,2,200.)? as f32; let h0=self.arg_num(&args,3,120.)? as f32;
4502                let depth=self.arg_num(&args,4,14.)? as f32;
4503                let th=self.ui_theme; let prim=self.color_at(&args,5,th.primary);
4504                self.draw_ui(&ling_ui::widgets::panel3d(x,y,w0,h0,depth, prim, th.bg));
4505                return Ok(Value::Unit);
4506            }
4507            #[cfg(not(target_arch = "wasm32"))]
4508            "ui_radar3d" | "立体雷达" | "立体レーダー" | "입체레이더" | "เรดาร์3มิติ" => {
4509                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32;
4510                let r=self.arg_num(&args,2,60.)? as f32; let tilt=self.arg_num(&args,3,0.9)? as f32;
4511                let sweep=self.arg_num(&args,4,0.)? as f32;
4512                let th=self.ui_theme; let prim=self.color_at(&args,5,th.primary);
4513                self.draw_ui(&ling_ui::widgets::radar3d(cx,cy,r,tilt,sweep, prim, th.track));
4514                return Ok(Value::Unit);
4515            }
4516
4517            // ── Interface sounds ─────────────────────────────────────────────
4518            #[cfg(not(target_arch = "wasm32"))]
4519            "audio_blip" | "提示音" | "ビープ音" | "효과음" | "เสียงบี๊บ" => {
4520                let freq=self.arg_num(&args,0,660.)? as f32;
4521                let dur=self.arg_num(&args,1,0.08)? as f32;
4522                let wave=Wave::from_name(&self.arg_str(&args,2,"sine"));
4523                let amp=self.arg_num(&args,3,0.25)? as f32;
4524                if let Some(audio)=&self.audio { audio.blip(freq, amp, dur, wave); }
4525                return Ok(Value::Unit);
4526            }
4527            #[cfg(not(target_arch = "wasm32"))]
4528            "ui_sound" | "界面音" | "UI音" | "인터페이스음" | "เสียงปุ่ม" => {
4529                let name=self.arg_str(&args,0,"click");
4530                if let Some(audio)=&self.audio {
4531                    match name.as_str() {
4532                        "hover"   => audio.blip(880.0, 0.10, 0.04, Wave::Sine),
4533                        "confirm" => { audio.blip(660.0, 0.22, 0.07, Wave::Square); audio.blip(990.0, 0.18, 0.10, Wave::Square); }
4534                        "error"   => { audio.blip(180.0, 0.30, 0.16, Wave::Saw); audio.blip(140.0, 0.30, 0.18, Wave::Saw); }
4535                        "toggle"  => audio.blip(520.0, 0.22, 0.05, Wave::Triangle),
4536                        "tick"    => audio.blip(1500.0, 0.12, 0.02, Wave::Square),
4537                        _         => audio.blip(720.0, 0.26, 0.05, Wave::Square), // "click"
4538                    }
4539                }
4540                return Ok(Value::Unit);
4541            }
4542
4543            // ══════════════════════════════════════════════════════════════════
4544            // MUSIC TOOLKIT  (crates/ling-music) — decode · analysis · GM synth ·
4545            // rhythm · karaoke. Analysis/decoding need no audio device; playback
4546            // and synthesis lazily start a dedicated music engine.
4547            // ══════════════════════════════════════════════════════════════════
4548
4549            // music_load(path) -> track handle (decodes WAV/FLAC/OGG/MP3/AAC)
4550            #[cfg(not(target_arch = "wasm32"))]
4551            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" => {
4552                let path = self.arg_str(&args, 0, "");
4553                let resolved = if std::path::Path::new(&path).exists() { path.clone() }
4554                    else if let Some(d) = &self.source_dir { d.join(&path).to_string_lossy().into_owned() }
4555                    else { path.clone() };
4556                match ling_music::load(&resolved) {
4557                    Ok(t) => { let id = self.tracks.len(); self.tracks.push(t); return Ok(Value::Number(id as f64)); }
4558                    Err(e) => { eprintln!("music_load failed ({path}): {e}"); return Ok(Value::Number(-1.0)); }
4559                }
4560            }
4561            #[cfg(not(target_arch = "wasm32"))]
4562            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" => {
4563                let id = self.arg_num(&args,0,0.0)? as i64;
4564                let d = self.tracks.get(id as usize).map(|t| t.duration).unwrap_or(0.0);
4565                return Ok(Value::Number(d as f64));
4566            }
4567            #[cfg(not(target_arch = "wasm32"))]
4568            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" => {
4569                let id = self.arg_num(&args,0,0.0)? as i64;
4570                let b = self.tracks.get(id as usize).map(|t| ling_music::analysis::bpm(&t.mono, t.rate)).unwrap_or(0.0);
4571                return Ok(Value::Number(b as f64));
4572            }
4573            #[cfg(not(target_arch = "wasm32"))]
4574            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" => {
4575                let id = self.arg_num(&args,0,0.0)? as i64;
4576                let k = self.tracks.get(id as usize).map(|t| ling_music::analysis::key_name(&t.mono, t.rate)).unwrap_or_default();
4577                return Ok(Value::Str(k));
4578            }
4579            #[cfg(not(target_arch = "wasm32"))]
4580            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" => {
4581                let id = self.arg_num(&args,0,0.0)? as i64;
4582                let v = self.tracks.get(id as usize).map(|t| ling_music::analysis::onsets(&t.mono, t.rate)).unwrap_or_default();
4583                return Ok(Value::List(v.into_iter().map(|x| Value::Number(x as f64)).collect()));
4584            }
4585            #[cfg(not(target_arch = "wasm32"))]
4586            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" => {
4587                let id = self.arg_num(&args,0,0.0)? as i64;
4588                let beats = self.tracks.get(id as usize).map(|t| {
4589                    let b = ling_music::analysis::bpm(&t.mono, t.rate);
4590                    ling_music::analysis::beat_grid(&t.mono, t.rate, b)
4591                }).unwrap_or_default();
4592                return Ok(Value::List(beats.into_iter().map(|x| Value::Number(x as f64)).collect()));
4593            }
4594
4595            // ── playback ──
4596            #[cfg(not(target_arch = "wasm32"))]
4597            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" => {
4598                let id = self.arg_num(&args,0,0.0)? as i64;
4599                if self.ensure_music() {
4600                    let track = self.tracks.get(id as usize).map(|t| (t.stereo.clone(), t.rate));
4601                    if let (Some((st, rate)), Some(m)) = (track, &self.music) { m.set_track(st, rate); m.play(); }
4602                    else if let Some(m) = &self.music { m.play(); }
4603                }
4604                return Ok(Value::Unit);
4605            }
4606            #[cfg(not(target_arch = "wasm32"))]
4607            "music_pause" | "暂停音乐" | "音楽一時停止" | "음악일시정지" | "หยุดเพลงชั่วคราว" => {
4608                if let Some(m) = &self.music { m.pause(); } return Ok(Value::Unit);
4609            }
4610            #[cfg(not(target_arch = "wasm32"))]
4611            "music_stop" | "停止音乐" | "音楽停止" | "음악정지" | "หยุดเพลง" => {
4612                if let Some(m) = &self.music { m.stop(); } return Ok(Value::Unit);
4613            }
4614            #[cfg(not(target_arch = "wasm32"))]
4615            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" => {
4616                let sec = self.arg_num(&args,0,0.0)? as f32;
4617                if let Some(m) = &self.music { m.seek(sec); } return Ok(Value::Unit);
4618            }
4619            #[cfg(not(target_arch = "wasm32"))]
4620            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" => {
4621                let p = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
4622                return Ok(Value::Number(p as f64));
4623            }
4624            #[cfg(not(target_arch = "wasm32"))]
4625            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" => {
4626                let v = self.arg_num(&args,0,0.8)? as f32;
4627                if self.ensure_music() { if let Some(m) = &self.music { m.set_volume(v); } }
4628                return Ok(Value::Unit);
4629            }
4630
4631            // ── synthesis (GM-capable, patches from .ling files) ──
4632            #[cfg(not(target_arch = "wasm32"))]
4633            "music_patch" | "乐器音色" | "音色読込" | "악기패치" | "แพตช์เครื่องดนตรี" => {
4634                let path = self.arg_str(&args, 0, "");
4635                let resolved = if std::path::Path::new(&path).exists() { path.clone() }
4636                    else if let Some(d) = &self.source_dir { d.join(&path).to_string_lossy().into_owned() }
4637                    else { path.clone() };
4638                if !self.ensure_music() { return Ok(Value::Number(-1.0)); }
4639                match ling_music::patch::from_path(&resolved) {
4640                    Ok(p) => { let id = self.music.as_ref().unwrap().add_patch(p); return Ok(Value::Number(id as f64)); }
4641                    Err(e) => { eprintln!("music_patch failed ({path}): {e}"); return Ok(Value::Number(-1.0)); }
4642                }
4643            }
4644            #[cfg(not(target_arch = "wasm32"))]
4645            "music_note" | "弹音符" | "音符演奏" | "음표연주" | "เล่นโน้ต" => {
4646                let inst = self.arg_num(&args,0,0.0)? as usize;
4647                let midi = self.pitch_arg(&args, 1, 60);
4648                let dur  = self.arg_num(&args,2,0.5)? as f32;
4649                let vel  = self.arg_num(&args,3,0.9)? as f32;
4650                if self.ensure_music() { if let Some(m) = &self.music { m.note(inst, midi, vel, dur); } }
4651                return Ok(Value::Unit);
4652            }
4653            #[cfg(not(target_arch = "wasm32"))]
4654            "music_note_on" | "音符开始" | "音符オン" | "음표켜기" | "โน้ตเริ่ม" => {
4655                let inst = self.arg_num(&args,0,0.0)? as usize;
4656                let midi = self.pitch_arg(&args, 1, 60);
4657                let vel  = self.arg_num(&args,2,0.9)? as f32;
4658                if self.ensure_music() { if let Some(m) = &self.music { m.note_on(inst, midi, vel); } }
4659                return Ok(Value::Unit);
4660            }
4661            #[cfg(not(target_arch = "wasm32"))]
4662            "music_note_off" | "音符结束" | "音符オフ" | "음표끄기" | "โน้ตจบ" => {
4663                let inst = self.arg_num(&args,0,0.0)? as usize;
4664                let midi = self.pitch_arg(&args, 1, 60);
4665                if let Some(m) = &self.music { m.note_off(inst, midi); }
4666                return Ok(Value::Unit);
4667            }
4668
4669            // ── rhythm-game judging ──
4670            #[cfg(not(target_arch = "wasm32"))]
4671            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" => {
4672                let delta_ms = self.arg_num(&args,0,9999.0)? as f32;
4673                return Ok(Value::Number(ling_music::Grade::judge(delta_ms).index() as f64));
4674            }
4675            #[cfg(not(target_arch = "wasm32"))]
4676            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" => {
4677                let idx = self.arg_num(&args,0,4.0)? as i32;
4678                return Ok(Value::Str(ling_music::Grade::from_index(idx).name().to_string()));
4679            }
4680
4681            // ── karaoke ──
4682            #[cfg(not(target_arch = "wasm32"))]
4683            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" => {
4684                let path = self.arg_str(&args, 0, "");
4685                let resolved = if std::path::Path::new(&path).exists() { path.clone() }
4686                    else if let Some(d) = &self.source_dir { d.join(&path).to_string_lossy().into_owned() }
4687                    else { path.clone() };
4688                match std::fs::read_to_string(&resolved) {
4689                    Ok(text) => { let id = self.lyrics.len(); self.lyrics.push(ling_music::Lyrics::parse(&text)); return Ok(Value::Number(id as f64)); }
4690                    Err(e) => { eprintln!("music_lrc failed ({path}): {e}"); return Ok(Value::Number(-1.0)); }
4691                }
4692            }
4693            #[cfg(not(target_arch = "wasm32"))]
4694            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" => {
4695                let id = self.arg_num(&args,0,0.0)? as i64;
4696                let t  = self.arg_num(&args,1,0.0)? as f32;
4697                let line = self.lyrics.get(id as usize).map(|l| l.line_at(t).to_string()).unwrap_or_default();
4698                return Ok(Value::Str(line));
4699            }
4700            #[cfg(not(target_arch = "wasm32"))]
4701            "music_mic_pitch" | "麦克风音高" | "マイク音程" | "마이크음정" | "ระดับเสียงไมค์" => {
4702                let hz = if let Some(mic) = self.mic.as_ref() {
4703                    let s = mic.latest_samples();
4704                    let rate = mic.sample_rate();
4705                    ling_music::pitch::detect(&s, rate).unwrap_or(0.0)
4706                } else { 0.0 };
4707                return Ok(Value::Number(hz as f64));
4708            }
4709            #[cfg(not(target_arch = "wasm32"))]
4710            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" => {
4711                let hz = self.arg_num(&args,0,0.0)? as f32;
4712                return Ok(Value::Str(ling_music::note::hz_to_name(hz)));
4713            }
4714            #[cfg(not(target_arch = "wasm32"))]
4715            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" => {
4716                let midi = self.pitch_arg(&args, 0, 69);
4717                return Ok(Value::Number(ling_music::note::midi_to_hz(midi as f32) as f64));
4718            }
4719            #[cfg(not(target_arch = "wasm32"))]
4720            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" => {
4721                let hz = self.arg_num(&args,0,0.0)? as f32;
4722                let target = self.arg_num(&args,1,0.0)? as f32;
4723                return Ok(Value::Number(ling_music::karaoke::pitch_score(hz, target) as f64));
4724            }
4725
4726            // ── MIDI (inaudible note source: drive coins, cues, etc.) ──
4727            #[cfg(not(target_arch = "wasm32"))]
4728            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" => {
4729                let path = self.arg_str(&args, 0, "");
4730                let resolved = if std::path::Path::new(&path).exists() { path.clone() }
4731                    else if let Some(d) = &self.source_dir { d.join(&path).to_string_lossy().into_owned() }
4732                    else { path.clone() };
4733                match ling_music::midi::load(&resolved) {
4734                    Ok(m) => { let id = self.midis.len(); self.midis.push(m); return Ok(Value::Number(id as f64)); }
4735                    Err(e) => { eprintln!("music_midi_load failed ({path}): {e}"); return Ok(Value::Number(-1.0)); }
4736                }
4737            }
4738            #[cfg(not(target_arch = "wasm32"))]
4739            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" => {
4740                let id = self.arg_num(&args,0,0.0)? as i64;
4741                let n = self.midis.get(id as usize).map(|m| m.notes.len()).unwrap_or(0);
4742                return Ok(Value::Number(n as f64));
4743            }
4744            // music_midi_notes(id) -> flat [time, midi, time, midi, …]
4745            #[cfg(not(target_arch = "wasm32"))]
4746            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" => {
4747                let id = self.arg_num(&args,0,0.0)? as i64;
4748                let mut out = Vec::new();
4749                if let Some(m) = self.midis.get(id as usize) {
4750                    for n in &m.notes { out.push(Value::Number(n.time as f64)); out.push(Value::Number(n.midi as f64)); }
4751                }
4752                return Ok(Value::List(out));
4753            }
4754            // music_midi_bars(id) -> flat [time, midi, dur, …] (for karaoke note bars)
4755            #[cfg(not(target_arch = "wasm32"))]
4756            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" => {
4757                let id = self.arg_num(&args,0,0.0)? as i64;
4758                let mut out = Vec::new();
4759                if let Some(m) = self.midis.get(id as usize) {
4760                    for n in &m.notes {
4761                        out.push(Value::Number(n.time as f64));
4762                        out.push(Value::Number(n.midi as f64));
4763                        out.push(Value::Number(n.dur as f64));
4764                    }
4765                }
4766                return Ok(Value::List(out));
4767            }
4768
4769            // music_fft(track_id, nbands) -> spectrum at the current playback position
4770            #[cfg(not(target_arch = "wasm32"))]
4771            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" => {
4772                let id = self.arg_num(&args,0,0.0)? as i64;
4773                let nbands = self.arg_num(&args,1,16.0)? as usize;
4774                let pos = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
4775                if let Some(t) = self.tracks.get(id as usize) {
4776                    let idx = (pos * t.rate as f32) as usize;
4777                    let end = (idx + 2048).min(t.mono.len());
4778                    if end > idx + 64 {
4779                        self.fft.borrow_mut().push_samples(&t.mono[idx..end]);
4780                    }
4781                }
4782                let bands = self.fft.borrow().freq_bands(nbands);
4783                return Ok(Value::List(bands.into_iter().map(|x| Value::Number(x as f64)).collect()));
4784            }
4785
4786            // ── spatial (2D/3D/4D) one-shot SFX ──
4787            #[cfg(not(target_arch = "wasm32"))]
4788            "audio_sfx" | "音效" | "空間効果音" | "공간효과음" | "เสียงเอฟเฟกต์" => {
4789                let x=self.arg_num(&args,0,0.0)? as f32; let y=self.arg_num(&args,1,0.0)? as f32; let z=self.arg_num(&args,2,0.0)? as f32;
4790                let w=self.arg_num(&args,3,1.0)? as f32; let freq=self.arg_num(&args,4,440.0)? as f32;
4791                let amp=self.arg_num(&args,5,0.3)? as f32; let dur=self.arg_num(&args,6,0.15)? as f32;
4792                let wave=Wave::from_name(&self.arg_str(&args,7,"sine"));
4793                if let Some(a)=&self.audio { a.sfx(x,y,z,w,freq,amp,dur,wave); }
4794                return Ok(Value::Unit);
4795            }
4796            // ── sample load / positional play / loop / stop ──
4797            #[cfg(not(target_arch = "wasm32"))]
4798            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" => {
4799                let path = self.arg_str(&args, 0, "");
4800                let resolved = if std::path::Path::new(&path).exists() { path.clone() }
4801                    else if let Some(d) = &self.source_dir { d.join(&path).to_string_lossy().into_owned() }
4802                    else { path.clone() };
4803                match ling_music::load(&resolved) {
4804                    Ok(t) => {
4805                        if let Some(a)=&self.audio { return Ok(Value::Number(a.add_sample(t.mono, t.rate) as f64)); }
4806                        return Ok(Value::Number(-1.0));
4807                    }
4808                    Err(e) => { eprintln!("audio_sample_load failed ({path}): {e}"); return Ok(Value::Number(-1.0)); }
4809                }
4810            }
4811            #[cfg(not(target_arch = "wasm32"))]
4812            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" => {
4813                let id=self.arg_num(&args,0,0.0)? as usize;
4814                let x=self.arg_num(&args,1,0.0)? as f32; let y=self.arg_num(&args,2,0.0)? as f32; let z=self.arg_num(&args,3,0.0)? as f32;
4815                let w=self.arg_num(&args,4,1.0)? as f32; let vol=self.arg_num(&args,5,1.0)? as f32;
4816                let looping=self.arg_num(&args,6,0.0)? > 0.5;
4817                let v = self.audio.as_ref().map(|a| a.play_sample(id,x,y,z,w,vol,looping)).unwrap_or(0);
4818                return Ok(Value::Number(v as f64));
4819            }
4820            #[cfg(not(target_arch = "wasm32"))]
4821            "audio_sample_stop" | "停止采样" | "サンプル停止" | "샘플정지" | "หยุดตัวอย่างเสียง" => {
4822                let v=self.arg_num(&args,0,0.0)? as u32;
4823                if let Some(a)=&self.audio { a.stop_sample(v); }
4824                return Ok(Value::Unit);
4825            }
4826            // ── master FX: delay / reverb / low-pass (underwater) ──
4827            #[cfg(not(target_arch = "wasm32"))]
4828            "audio_fx_delay" | "回声" | "ディレイ効果" | "딜레이" | "เสียงสะท้อน" => {
4829                let time=self.arg_num(&args,0,0.3)? as f32; let fb=self.arg_num(&args,1,0.3)? as f32; let mix=self.arg_num(&args,2,0.3)? as f32;
4830                if let Some(a)=&self.audio { a.fx_delay(time,fb,mix); }
4831                return Ok(Value::Unit);
4832            }
4833            #[cfg(not(target_arch = "wasm32"))]
4834            "audio_fx_reverb" | "混响" | "リバーブ" | "리버브" | "เสียงก้อง" => {
4835                let mix=self.arg_num(&args,0,0.3)? as f32;
4836                if let Some(a)=&self.audio { a.fx_reverb(mix); }
4837                return Ok(Value::Unit);
4838            }
4839            #[cfg(not(target_arch = "wasm32"))]
4840            "audio_fx_lowpass" | "低通滤波" | "ローパス" | "저역통과" | "กรองความถี่ต่ำ" => {
4841                let cutoff=self.arg_num(&args,0,1.0)? as f32;
4842                if let Some(a)=&self.audio { a.fx_lowpass(cutoff); }
4843                return Ok(Value::Unit);
4844            }
4845
4846            // ══════════════════════════════════════════════════════════════════
4847            // PHYSICS BUILTINS  (crates/ling-physics) — soft bodies, rigid+angular,
4848            // and a fast 2-D water/oil liquid sim mappable onto 3-D surfaces.
4849            // ══════════════════════════════════════════════════════════════════
4850
4851            // ── soft bodies (deformable bouncy balls) ──
4852            #[cfg(not(target_arch = "wasm32"))]
4853            "soft_ball" | "软球" | "ソフトボール" | "소프트볼" | "ลูกบอลนุ่ม" => {
4854                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32; let z=self.arg_num(&args,2,0.)? as f32;
4855                let r=self.arg_num(&args,3,1.0)? as f32;
4856                let b = ling_physics::soft::SoftBody::sphere(ling_physics::Vec3::new(x,y,z), r, 8, 12, 1.0);
4857                let id = self.soft_bodies.len(); self.soft_bodies.push(b);
4858                return Ok(Value::Number(id as f64));
4859            }
4860            #[cfg(not(target_arch = "wasm32"))]
4861            "soft_step" | "软体步进" | "ソフト更新" | "소프트스텝" | "ก้าวนุ่ม" => {
4862                let id=self.arg_num(&args,0,0.)? as usize; let dt=self.arg_num(&args,1,0.016)? as f32;
4863                let gy=self.arg_num(&args,2,15.0)? as f32;
4864                if let Some(b)=self.soft_bodies.get_mut(id) { b.integrate(dt, ling_physics::Vec3::new(0.0,gy,0.0), 4); }
4865                return Ok(Value::Unit);
4866            }
4867            #[cfg(not(target_arch = "wasm32"))]
4868            "soft_bounce" | "软体落地" | "ソフト着地" | "소프트바운스" | "เด้งนุ่ม" => {
4869                let id=self.arg_num(&args,0,0.)? as usize; let fy=self.arg_num(&args,1,0.)? as f32; let rest=self.arg_num(&args,2,0.5)? as f32;
4870                if let Some(b)=self.soft_bodies.get_mut(id) { b.floor_collision(fy, rest); }
4871                return Ok(Value::Unit);
4872            }
4873            #[cfg(not(target_arch = "wasm32"))]
4874            "soft_contain" | "软体边界" | "ソフト箱" | "소프트경계" | "กล่องนุ่ม" => {
4875                let id=self.arg_num(&args,0,0.)? as usize;
4876                let nx=self.arg_num(&args,1,-5.)? as f32; let ny=self.arg_num(&args,2,-5.)? as f32; let nz=self.arg_num(&args,3,-5.)? as f32;
4877                let mx=self.arg_num(&args,4,5.)? as f32; let my=self.arg_num(&args,5,5.)? as f32; let mz=self.arg_num(&args,6,5.)? as f32;
4878                let rest=self.arg_num(&args,7,0.6)? as f32;
4879                if let Some(b)=self.soft_bodies.get_mut(id) { b.contain(ling_physics::Vec3::new(nx,ny,nz), ling_physics::Vec3::new(mx,my,mz), rest); }
4880                return Ok(Value::Unit);
4881            }
4882            #[cfg(not(target_arch = "wasm32"))]
4883            "soft_kick" | "软体踢" | "ソフト衝撃" | "소프트킥" | "เตะนุ่ม" => {
4884                let id=self.arg_num(&args,0,0.)? as usize;
4885                let dx=self.arg_num(&args,1,0.)? as f32; let dy=self.arg_num(&args,2,0.)? as f32; let dz=self.arg_num(&args,3,0.)? as f32;
4886                let s=self.arg_num(&args,4,0.1)? as f32;
4887                if let Some(b)=self.soft_bodies.get_mut(id) { b.kick(ling_physics::Vec3::new(dx,dy,dz), s); }
4888                return Ok(Value::Unit);
4889            }
4890            // soft_spin(id, ax, ay, az, rate) — add angular velocity about the axis
4891            // through the centroid (rate = rad/step; ≈ surface_speed / radius to roll)
4892            #[cfg(not(target_arch = "wasm32"))]
4893            "soft_spin" | "软体自旋" | "ソフト回転" | "소프트회전" | "หมุนนุ่ม" => {
4894                let id=self.arg_num(&args,0,0.)? as usize;
4895                let ax=self.arg_num(&args,1,0.)? as f32; let ay=self.arg_num(&args,2,0.)? as f32; let az=self.arg_num(&args,3,0.)? as f32;
4896                let rate=self.arg_num(&args,4,0.1)? as f32;
4897                if let Some(b)=self.soft_bodies.get_mut(id) { b.spin(ling_physics::Vec3::new(ax,ay,az), rate); }
4898                return Ok(Value::Unit);
4899            }
4900            #[cfg(not(target_arch = "wasm32"))]
4901            "soft_deform" | "形变量" | "変形量" | "변형량" | "ความบิดเบี้ยว" => {
4902                let id=self.arg_num(&args,0,0.)? as usize;
4903                let d=self.soft_bodies.get(id).map(|b| b.deformation()).unwrap_or(0.0);
4904                return Ok(Value::Number(d as f64));
4905            }
4906            // soft_angular_speed(id) -> magnitude of the body's angular velocity
4907            // (how fast it is tumbling/rolling), derived from its node velocities.
4908            #[cfg(not(target_arch = "wasm32"))]
4909            "soft_angular_speed" | "软体角速" | "ソフト角速度" | "소프트각속도" | "ความเร็วเชิงมุมนุ่ม" => {
4910                let id=self.arg_num(&args,0,0.)? as usize;
4911                let w=self.soft_bodies.get(id).map(|b| b.angular_speed()).unwrap_or(0.0);
4912                return Ok(Value::Number(w as f64));
4913            }
4914            #[cfg(not(target_arch = "wasm32"))]
4915            "soft_centroid" | "软体质心" | "ソフト重心" | "소프트중심" | "จุดศูนย์กลางนุ่ม" => {
4916                let id=self.arg_num(&args,0,0.)? as usize;
4917                let c=self.soft_bodies.get(id).map(|b| b.centroid()).unwrap_or(ling_physics::Vec3::ZERO);
4918                return Ok(Value::List(vec![Value::Number(c.x as f64),Value::Number(c.y as f64),Value::Number(c.z as f64)]));
4919            }
4920            // soft_nodes(id) -> flat [x,y,z, x,y,z, …] for rendering the deformed mesh
4921            #[cfg(not(target_arch = "wasm32"))]
4922            "soft_nodes" | "软体节点" | "ソフト節点" | "소프트노드" | "จุดนุ่ม" => {
4923                let id=self.arg_num(&args,0,0.)? as usize;
4924                let mut out=Vec::new();
4925                if let Some(b)=self.soft_bodies.get(id) {
4926                    for n in &b.nodes { out.push(Value::Number(n.pos.x as f64)); out.push(Value::Number(n.pos.y as f64)); out.push(Value::Number(n.pos.z as f64)); }
4927                }
4928                return Ok(Value::List(out));
4929            }
4930
4931            // ── rigid bodies with angular dynamics ──
4932            #[cfg(not(target_arch = "wasm32"))]
4933            "rb_add" | "刚体添加" | "剛体追加" | "강체추가" | "เพิ่มวัตถุแข็ง" => {
4934                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32; let z=self.arg_num(&args,2,0.)? as f32;
4935                let mass=self.arg_num(&args,3,1.0)? as f32;
4936                let mut b = ling_physics::rigid::RigidBody::new(ling_physics::Vec3::new(x,y,z), mass);
4937                b.restitution = 0.6;
4938                return Ok(Value::Number(self.rigid_world.add(b) as f64));
4939            }
4940            #[cfg(not(target_arch = "wasm32"))]
4941            "rb_torque" | "扭矩" | "トルク" | "토크" | "แรงบิด" => {
4942                let i=self.arg_num(&args,0,0.)? as usize;
4943                let tx=self.arg_num(&args,1,0.)? as f32; let ty=self.arg_num(&args,2,0.)? as f32; let tz=self.arg_num(&args,3,0.)? as f32;
4944                if let Some(b)=self.rigid_world.bodies.get_mut(i) { b.apply_torque(ling_physics::Vec3::new(tx,ty,tz)); }
4945                return Ok(Value::Unit);
4946            }
4947            #[cfg(not(target_arch = "wasm32"))]
4948            "rb_spin" | "自旋" | "スピン" | "스핀" | "หมุน" => {
4949                let i=self.arg_num(&args,0,0.)? as usize;
4950                let wx=self.arg_num(&args,1,0.)? as f32; let wy=self.arg_num(&args,2,0.)? as f32; let wz=self.arg_num(&args,3,0.)? as f32;
4951                if let Some(b)=self.rigid_world.bodies.get_mut(i) { b.apply_spin(ling_physics::Vec3::new(wx,wy,wz)); }
4952                return Ok(Value::Unit);
4953            }
4954            #[cfg(not(target_arch = "wasm32"))]
4955            "rb_impulse" | "刚体冲量" | "剛体インパルス" | "강체충격" | "แรงดลแข็ง" => {
4956                let i=self.arg_num(&args,0,0.)? as usize;
4957                let ix=self.arg_num(&args,1,0.)? as f32; let iy=self.arg_num(&args,2,0.)? as f32; let iz=self.arg_num(&args,3,0.)? as f32;
4958                if let Some(b)=self.rigid_world.bodies.get_mut(i) { b.apply_impulse(ling_physics::Vec3::new(ix,iy,iz)); }
4959                return Ok(Value::Unit);
4960            }
4961            #[cfg(not(target_arch = "wasm32"))]
4962            "rb_floor" | "刚体落地" | "剛体着地" | "강체바닥" | "พื้นแข็ง" => {
4963                let i=self.arg_num(&args,0,0.)? as usize; let fy=self.arg_num(&args,1,0.)? as f32;
4964                let rest=self.arg_num(&args,2,0.6)? as f32; let fric=self.arg_num(&args,3,0.6)? as f32;
4965                if let Some(b)=self.rigid_world.bodies.get_mut(i) { b.bounce_floor(fy, rest, fric); }
4966                return Ok(Value::Unit);
4967            }
4968            #[cfg(not(target_arch = "wasm32"))]
4969            "rb_gravity" | "刚体重力" | "剛体重力" | "강체중력" | "แรงโน้มถ่วงแข็ง" => {
4970                let gx=self.arg_num(&args,0,0.)? as f32; let gy=self.arg_num(&args,1,9.81)? as f32; let gz=self.arg_num(&args,2,0.)? as f32;
4971                self.rigid_world.gravity = ling_physics::Vec3::new(gx,gy,gz);
4972                return Ok(Value::Unit);
4973            }
4974            #[cfg(not(target_arch = "wasm32"))]
4975            "rb_step" | "刚体步进" | "剛体更新" | "강체스텝" | "ก้าวแข็ง" => {
4976                let dt=self.arg_num(&args,0,0.016)? as f32;
4977                self.rigid_world.step(dt);
4978                return Ok(Value::Unit);
4979            }
4980            #[cfg(not(target_arch = "wasm32"))]
4981            "rb_pos" | "刚体位置" | "剛体位置" | "강체위치" | "ตำแหน่งแข็ง" => {
4982                let i=self.arg_num(&args,0,0.)? as usize;
4983                let p=self.rigid_world.bodies.get(i).map(|b| b.pos).unwrap_or(ling_physics::Vec3::ZERO);
4984                return Ok(Value::List(vec![Value::Number(p.x as f64),Value::Number(p.y as f64),Value::Number(p.z as f64)]));
4985            }
4986            #[cfg(not(target_arch = "wasm32"))]
4987            "rb_rot" | "刚体旋转" | "剛体回転" | "강체회전" | "การหมุนแข็ง" => {
4988                let i=self.arg_num(&args,0,0.)? as usize;
4989                let q=self.rigid_world.bodies.get(i).map(|b| b.orientation).unwrap_or(ling_physics::Quat::IDENTITY);
4990                return Ok(Value::List(vec![Value::Number(q.x as f64),Value::Number(q.y as f64),Value::Number(q.z as f64),Value::Number(q.w as f64)]));
4991            }
4992
4993            // ── native-res mesh (.lmesh): load once, draw fast (unlit, per-tri colour) ──
4994            #[cfg(not(target_arch = "wasm32"))]
4995            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" => {
4996                let path = self.arg_str(&args, 0, "");
4997                let resolved = if std::path::Path::new(&path).exists() { path.clone() }
4998                    else if let Some(d) = &self.source_dir { d.join(&path).to_string_lossy().into_owned() }
4999                    else { path.clone() };
5000                let bytes = match std::fs::read(&resolved) {
5001                    Ok(b) => b,
5002                    Err(e) => { eprintln!("mesh_load failed ({path}): {e}"); return Ok(Value::Number(-1.0)); }
5003                };
5004                if bytes.len() < 16 || &bytes[0..4] != b"LMSH" { eprintln!("mesh_load: bad header ({path})"); return Ok(Value::Number(-1.0)); }
5005                let rd4 = |o: usize| -> [u8;4] { [bytes[o],bytes[o+1],bytes[o+2],bytes[o+3]] };
5006                let height = f32::from_le_bytes(rd4(8));
5007                let ntri = u32::from_le_bytes(rd4(12)) as usize;
5008                let need = 16usize.saturating_add(ntri.saturating_mul(9*4 + 3));
5009                if bytes.len() < need { eprintln!("mesh_load: truncated ({path})"); return Ok(Value::Number(-1.0)); }
5010                let mut pos = Vec::with_capacity(ntri*3);
5011                let mut col = Vec::with_capacity(ntri);
5012                let mut off = 16usize;
5013                for _ in 0..ntri {
5014                    for _k in 0..3 {
5015                        let x = f32::from_le_bytes(rd4(off)); let y = f32::from_le_bytes(rd4(off+4)); let z = f32::from_le_bytes(rd4(off+8));
5016                        off += 12; pos.push([x,y,z]);
5017                    }
5018                    col.push([bytes[off],bytes[off+1],bytes[off+2]]); off += 3;
5019                }
5020                eprintln!("mesh_load: {} ({} tris, h={:.2})", path, ntri, height);
5021                let id = self.meshes.len();
5022                self.meshes.push(crate::gfx::shapes::ColorMesh{ pos, col, height });
5023                return Ok(Value::Number(id as f64));
5024            }
5025            #[cfg(not(target_arch = "wasm32"))]
5026            "mesh_draw" | "วาดเมชสี" => {        // ('วาดเมช' is taken by draw_mesh — use a distinct Thai alias)
5027                let id = self.arg_num(&args,0,0.)? as usize;
5028                let cx=self.arg_num(&args,1,0.)? as f32; let cy=self.arg_num(&args,2,0.)? as f32; let cz=self.arg_num(&args,3,0.)? as f32;
5029                let sc=self.arg_num(&args,4,1.)? as f32; let yaw=self.arg_num(&args,5,0.)? as f32;
5030                let sway=self.arg_num(&args,6,0.)? as f32; let arm=self.arg_num(&args,7,0.)? as f32;
5031                let lean=self.arg_num(&args,8,0.)? as f32;
5032                let leg=self.arg_num(&args,9,0.)? as f32; let tuck=self.arg_num(&args,10,0.)? as f32;
5033                if id < self.meshes.len() {
5034                    let m = &self.meshes[id];
5035                    let mut gfx = self.gfx.borrow_mut();
5036                    gfx.draw_color_mesh(m, cx,cy,cz, sc, yaw, sway, arm, lean, leg, tuck);
5037                }
5038                return Ok(Value::Unit);
5039            }
5040
5041            // ── liquid sim (water + oil, immiscible) ──
5042            #[cfg(not(target_arch = "wasm32"))]
5043            "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" => {
5044                let w=self.arg_num(&args,0,64.)? as usize; let h=self.arg_num(&args,1,64.)? as usize;
5045                let id=self.liquids.len(); self.liquids.push(ling_physics::liquid::LiquidGrid::new(w,h));
5046                return Ok(Value::Number(id as f64));
5047            }
5048            #[cfg(not(target_arch = "wasm32"))]
5049            "liquid_set_colors" | "液体颜色" | "液体配色" | "액체색상" | "สีของเหลว" => {
5050                let id=self.arg_num(&args,0,0.)? as usize;
5051                let wr=self.arg_num(&args,1,40.)? as f32; let wg=self.arg_num(&args,2,110.)? as f32; let wb=self.arg_num(&args,3,235.)? as f32;
5052                let or_=self.arg_num(&args,4,240.)? as f32; let og=self.arg_num(&args,5,175.)? as f32; let ob=self.arg_num(&args,6,45.)? as f32;
5053                if let Some(g)=self.liquids.get_mut(id) { g.set_colors(wr,wg,wb,or_,og,ob); }
5054                return Ok(Value::Unit);
5055            }
5056            #[cfg(not(target_arch = "wasm32"))]
5057            "liquid_splat" | "液体注入" | "液体追加" | "액체분사" | "หยดของเหลว" => {
5058                let id=self.arg_num(&args,0,0.)? as usize;
5059                let x=self.arg_num(&args,1,0.)? as f32; let y=self.arg_num(&args,2,0.)? as f32;
5060                let kind=self.arg_num(&args,3,0.)? as i32; let amt=self.arg_num(&args,4,1.0)? as f32; let rad=self.arg_num(&args,5,4.0)? as f32;
5061                if let Some(g)=self.liquids.get_mut(id) { g.splat(x,y,kind,amt,rad); }
5062                return Ok(Value::Unit);
5063            }
5064            #[cfg(not(target_arch = "wasm32"))]
5065            "liquid_gravity" | "液体重力" | "液体重力ベクトル" | "액체중력" | "แรงโน้มถ่วงเหลว" => {
5066                let id=self.arg_num(&args,0,0.)? as usize;
5067                let gx=self.arg_num(&args,1,0.)? as f32; let gy=self.arg_num(&args,2,60.)? as f32;
5068                if let Some(g)=self.liquids.get_mut(id) { g.set_gravity(gx,gy); }
5069                return Ok(Value::Unit);
5070            }
5071            #[cfg(not(target_arch = "wasm32"))]
5072            "liquid_step" | "液体步进" | "液体更新" | "액체스텝" | "ก้าวของเหลว" => {
5073                let id=self.arg_num(&args,0,0.)? as usize; let dt=self.arg_num(&args,1,0.016)? as f32;
5074                if let Some(g)=self.liquids.get_mut(id) { g.step(dt); }
5075                return Ok(Value::Unit);
5076            }
5077            // liquid_rainbow(id, on) — colour the fluid as a flowing ROYGBIV marble
5078            #[cfg(not(target_arch = "wasm32"))]
5079            "liquid_rainbow" | "液体彩虹" | "液体虹" | "액체무지개" | "ของเหลวสายรุ้ง" => {
5080                let id=self.arg_num(&args,0,0.)? as usize;
5081                let on=self.arg_num(&args,1,1.0)? > 0.5;
5082                if let Some(g)=self.liquids.get_mut(id) { g.rainbow = on; }
5083                return Ok(Value::Unit);
5084            }
5085            // liquid_mix(id) -> 0 (oil/water separated) .. 1 (fully intermixed)
5086            #[cfg(not(target_arch = "wasm32"))]
5087            "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" => {
5088                let id=self.arg_num(&args,0,0.)? as usize;
5089                let m=self.liquids.get(id).map(|g| g.mix_amount()).unwrap_or(0.0);
5090                return Ok(Value::Number(m as f64));
5091            }
5092            // liquid_draw(id, sx, sy, scale) — fast flat 2-D blit of the colour field
5093            #[cfg(not(target_arch = "wasm32"))]
5094            "liquid_draw" | "绘制液体" | "液体描画" | "액체그리기" | "วาดของเหลว" => {
5095                let id=self.arg_num(&args,0,0.)? as usize;
5096                let sx=self.arg_num(&args,1,0.)? as i32; let sy=self.arg_num(&args,2,0.)? as i32;
5097                let scale=(self.arg_num(&args,3,4.)? as i32).max(1);
5098                if id < self.liquids.len() {
5099                    let (gw,gh)={ let g=&self.liquids[id]; (g.w,g.h) };
5100                    let mut gfx=self.gfx.borrow_mut(); let (w,h)=(gfx.width as i32, gfx.height as i32);
5101                    let g=&self.liquids[id];
5102                    for cy in 0..gh { for cx in 0..gw {
5103                        let col=g.sample_rgb(cx,cy);
5104                        let bx=sx + cx as i32*scale; let by=sy + cy as i32*scale;
5105                        for dy in 0..scale { for dx in 0..scale {
5106                            let px=bx+dx; let py=by+dy;
5107                            if px>=0 && py>=0 && px<w && py<h { gfx.buffer[(py*w+px) as usize]=col; }
5108                        }}
5109                    }}
5110                }
5111                return Ok(Value::Unit);
5112            }
5113            // liquid_draw_surface(id, kind, cx,cy,cz, radius, height)
5114            //   kind: 0 plane · 1 sphere · 2 cylinder · 3 cone · 4 dome
5115            #[cfg(not(target_arch = "wasm32"))]
5116            "liquid_draw_surface" | "液体贴面" | "液体曲面" | "액체곡면" | "ของเหลวบนพื้นผิว" => {
5117                let id=self.arg_num(&args,0,0.)? as usize;
5118                let kind=self.arg_num(&args,1,1.)? as i32;
5119                let cx=self.arg_num(&args,2,0.)? as f32; let cy=self.arg_num(&args,3,0.)? as f32; let cz=self.arg_num(&args,4,0.)? as f32;
5120                let radius=self.arg_num(&args,5,2.0)? as f32; let height=self.arg_num(&args,6,3.0)? as f32;
5121                if id < self.liquids.len() {
5122                    let (gw,gh)={ let g=&self.liquids[id]; (g.w,g.h) };
5123                    let mut gfx=self.gfx.borrow_mut();
5124                    let (w,h,add)=(gfx.width, gfx.height, gfx.blend==1);
5125                    let cam=gfx.camera.clone();
5126                    let near = -cam.zdist + 0.05;
5127                    let g=&self.liquids[id];
5128                    let tau=std::f32::consts::TAU; let pi=std::f32::consts::PI;
5129                    // surface point for a (u,v) in [0,1] on the chosen primitive
5130                    let sp = |u:f32, v:f32| -> [f32;3] {
5131                        if kind==0 { [cx+(u-0.5)*2.0*radius, cy, cz+(v-0.5)*2.0*radius] }
5132                        else if kind==2 { let th=u*tau; [cx+th.cos()*radius, cy+(v-0.5)*height, cz+th.sin()*radius] }
5133                        else if kind==3 { let th=u*tau; let rr=radius*(1.0-v); [cx+th.cos()*rr, cy+(v-0.5)*height, cz+th.sin()*rr] }
5134                        else if kind==4 { let th=u*tau; let ph=v*pi*0.5; [cx+ph.sin()*th.cos()*radius, cy-ph.cos()*radius, cz+ph.sin()*th.sin()*radius] }
5135                        else { let th=u*tau; let ph=v*pi; [cx+ph.sin()*th.cos()*radius, cy+ph.cos()*radius, cz+ph.sin()*th.sin()*radius] }
5136                    };
5137                    let nrm = |u:f32, v:f32| -> [f32;3] {
5138                        if kind==0 { [0.0,-1.0,0.0] }
5139                        else if kind==2 { let th=u*tau; [th.cos(),0.0,th.sin()] }
5140                        else if kind==3 { let th=u*tau; let s=(radius/height.max(0.01)).atan(); [th.cos()*s.cos(), s.sin(), th.sin()*s.cos()] }
5141                        else if kind==4 { let th=u*tau; let ph=v*pi*0.5; [ph.sin()*th.cos(),-ph.cos(),ph.sin()*th.sin()] }
5142                        else { let th=u*tau; let ph=v*pi; [ph.sin()*th.cos(),ph.cos(),ph.sin()*th.sin()] }
5143                    };
5144                    let gwf=gw as f32; let ghf=gh as f32;
5145                    let mut cyc=0usize;
5146                    while cyc<gh {
5147                        let mut cxc=0usize;
5148                        while cxc<gw {
5149                            // cull by the cell centre's outward normal
5150                            let uc=(cxc as f32+0.5)/gwf; let vc=(cyc as f32+0.5)/ghf;
5151                            let c=sp(uc,vc); let n=nrm(uc,vc);
5152                            let dc=cam.depth(c[0],c[1],c[2]);
5153                            if dc>near {
5154                                let cull = kind!=0 && cam.depth(c[0]+n[0]*0.06,c[1]+n[1]*0.06,c[2]+n[2]*0.06) > dc;
5155                                if !cull {
5156                                    // project the 4 cell corners → a filled AA vector quad
5157                                    let u0=cxc as f32/gwf; let u1=(cxc+1) as f32/gwf;
5158                                    let v0=cyc as f32/ghf; let v1=(cyc+1) as f32/ghf;
5159                                    let q=[sp(u0,v0),sp(u1,v0),sp(u1,v1),sp(u0,v1)];
5160                                    let mut poly: Vec<[f32;2]> = Vec::with_capacity(5);
5161                                    let mut ok=true;
5162                                    for p in &q { if cam.depth(p[0],p[1],p[2])<=near { ok=false; break; } let (sx,sy,_)=cam.project(p[0],p[1],p[2]); poly.push([sx,sy]); }
5163                                    if ok { let p0=poly[0]; poly.push(p0);
5164                                        let col=g.sample_rgb(cxc,cyc);
5165                                        crate::gfx::raster::fill_contours_aa(&mut gfx.buffer, w, h, col, add, std::slice::from_ref(&poly));
5166                                    }
5167                                }
5168                            }
5169                            cxc+=1;
5170                        }
5171                        cyc+=1;
5172                    }
5173                }
5174                return Ok(Value::Unit);
5175            }
5176            // sparkle(x, y, w, h, count [, t]) — scatter twinkling vector star-sparkles
5177            // in a rect (snowglobe effect) in the current colour + blend mode.
5178            #[cfg(not(target_arch = "wasm32"))]
5179            "sparkle" | "闪光" | "きらめき" | "반짝임" | "ประกาย" => {
5180                let x=self.arg_num(&args,0,0.)? as f32; let y=self.arg_num(&args,1,0.)? as f32;
5181                let ww=self.arg_num(&args,2,200.)? as f32; let hh=self.arg_num(&args,3,200.)? as f32;
5182                let count=self.arg_num(&args,4,40.)? as i32;
5183                let t=self.arg_num(&args,5,0.)? as f32;
5184                let mut gfx=self.gfx.borrow_mut();
5185                let (w,h,add,color)=(gfx.width, gfx.height, gfx.blend==1, gfx.color);
5186                let (cr,cg,cb)=((color>>16&0xFF) as f32,(color>>8&0xFF) as f32,(color&0xFF) as f32);
5187                let mut n=0i32;
5188                while n<count {
5189                    let hsh=(n as u32).wrapping_mul(2654435761).wrapping_add(0x9E3779B9);
5190                    let u=((hsh>>8)&1023) as f32/1023.0;
5191                    let v=((hsh>>18)&1023) as f32/1023.0;
5192                    let phase=(hsh&255) as f32/255.0;
5193                    let tw=(t*3.0 + phase*6.2831 + n as f32).sin()*0.5+0.5;
5194                    let sz=1.5+tw*5.0;
5195                    let px=x+u*ww; let py=y+v*hh;
5196                    let b=tw*tw; // sharp twinkle
5197                    let col=(((cr*b)as u32)<<16)|(((cg*b)as u32)<<8)|((cb*b)as u32);
5198                    crate::gfx::raster::draw_line_aa(&mut gfx.buffer,w,h,col,add, px-sz,py, px+sz,py);
5199                    crate::gfx::raster::draw_line_aa(&mut gfx.buffer,w,h,col,add, px,py-sz, px,py+sz);
5200                    let d=sz*0.55;
5201                    crate::gfx::raster::draw_line_aa(&mut gfx.buffer,w,h,col,add, px-d,py-d, px+d,py+d);
5202                    crate::gfx::raster::draw_line_aa(&mut gfx.buffer,w,h,col,add, px-d,py+d, px+d,py-d);
5203                    n+=1;
5204                }
5205                return Ok(Value::Unit);
5206            }
5207
5208            // ══════════════════════════════════════════════════════════════════
5209            // DIALOG BUILTINS  (crates/ling-game/src/dialog.rs) — cinematic,
5210            // typed-out, colour-coded text boxes. Markup: {n}name{/} {p}place{/}
5211            // {i}item{/}, \n newline, || page break.
5212            // ══════════════════════════════════════════════════════════════════
5213            #[cfg(not(target_arch = "wasm32"))]
5214            "dialog_show" | "对话显示" | "会話表示" | "대화표시" | "แสดงบทสนทนา" => {
5215                let text = self.arg_str(&args, 0, "");
5216                let cps = self.arg_num(&args, 1, 32.0)? as f32;
5217                self.dialog = Some(ling_game::dialog::Dialog::new(&text, cps));
5218                return Ok(Value::Unit);
5219            }
5220            #[cfg(not(target_arch = "wasm32"))]
5221            "dialog_step" | "对话步进" | "会話更新" | "대화스텝" | "ก้าวบทสนทนา" => {
5222                let dt = self.arg_num(&args, 0, 0.016)? as f32;
5223                if let Some(d) = self.dialog.as_mut() { d.update(dt); }
5224                return Ok(Value::Unit);
5225            }
5226            #[cfg(not(target_arch = "wasm32"))]
5227            "dialog_advance" | "对话推进" | "会話送り" | "대화진행" | "เลื่อนบทสนทนา" => {
5228                if let Some(d) = self.dialog.as_mut() { d.advance(); }
5229                return Ok(Value::Unit);
5230            }
5231            #[cfg(not(target_arch = "wasm32"))]
5232            "dialog_active" | "对话激活" | "会話中" | "대화중" | "บทสนทนาทำงาน" => {
5233                let a = self.dialog.as_ref().map(|d| !d.is_closed()).unwrap_or(false);
5234                return Ok(Value::Bool(a));
5235            }
5236            #[cfg(not(target_arch = "wasm32"))]
5237            "dialog_typing" | "对话打字" | "会話タイプ中" | "대화타이핑" | "กำลังพิมพ์บทสนทนา" => {
5238                use ling_game::dialog::Dialog;
5239                
5240                let a = self.dialog.as_ref().map(|d: &Dialog | !d.is_closed() && d.is_typing()).unwrap_or(false);
5241                return Ok(Value::Bool(a))
5242            }
5243            #[cfg(not(target_arch = "wasm32"))]
5244            "dialog_close" | "对话关闭" | "会話閉じる" | "대화닫기" | "ปิดบทสนทนา" => {
5245                self.dialog = None;
5246                return Ok(Value::Unit);
5247            }
5248            // dialog_color(role, r, g, b) — role: 0 text · 1 name · 2 place · 3 item
5249            #[cfg(not(target_arch = "wasm32"))]
5250            "dialog_color" | "对话颜色" | "会話色" | "대화색" | "สีบทสนทนา" => {
5251                let role = (self.arg_num(&args,0,0.0)? as usize).min(3);
5252                let r = self.arg_num(&args,1,255.0)? as u32 & 0xFF;
5253                let g = self.arg_num(&args,2,255.0)? as u32 & 0xFF;
5254                let b = self.arg_num(&args,3,255.0)? as u32 & 0xFF;
5255                self.dialog_colors[role] = (r<<16)|(g<<8)|b;
5256                return Ok(Value::Unit);
5257            }
5258            // dialog_draw(x, y, w, h [, font_handle]) — draw the box + typed text
5259            #[cfg(not(target_arch = "wasm32"))]
5260            "dialog_draw" | "对话绘制" | "会話描画" | "대화그리기" | "วาดบทสนทนา" => {
5261                let x=self.arg_num(&args,0,40.0)? as f32; let y=self.arg_num(&args,1,0.0)? as f32;
5262                let ww=self.arg_num(&args,2,720.0)? as f32; let hh=self.arg_num(&args,3,150.0)? as f32;
5263                let font = self.arg_num(&args,4,-1.0)? as i64;
5264                let t = self.start_time.elapsed().as_secs_f32();
5265                self.render_dialog(x, y, ww, hh, font, t);
5266                return Ok(Value::Unit);
5267            }
5268
5269            // text_poll() — fold newly-typed keys into the input buffer, return it
5270            #[cfg(not(target_arch = "wasm32"))]
5271            "text_poll" => {
5272                let keys = { let gfx = self.gfx.borrow(); gfx.window.as_ref().map(|w| w.get_keys_pressed(minifb::KeyRepeat::No)).unwrap_or_default() };
5273                for k in keys {
5274                    if k == minifb::Key::Backspace { self.text_buffer.pop(); }
5275                    else if let Some(c) = key_char(k) { self.text_buffer.push(c); }
5276                }
5277                return Ok(Value::Str(self.text_buffer.clone()));
5278            }
5279            "text_get"   => return Ok(Value::Str(self.text_buffer.clone())),
5280            "text_set"   => { self.text_buffer = self.arg_str(&args,0,""); return Ok(Value::Unit); }
5281            "text_clear" => { self.text_buffer.clear(); return Ok(Value::Unit); }
5282            // record_frame() — append the current framebuffer as a PPM, return frame #
5283            #[cfg(not(target_arch = "wasm32"))]
5284            "record_frame" => {
5285                let n = self.record_n;
5286                let (buf, w, h) = { let gfx = self.gfx.borrow(); (gfx.buffer.clone(), gfx.width, gfx.height) };
5287                let _ = std::fs::create_dir_all("recordings");
5288                let mut out = Vec::with_capacity(w*h*3 + 32);
5289                out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes());
5290                for px in &buf { let p = *px; out.push((p>>16) as u8); out.push((p>>8) as u8); out.push(p as u8); }
5291                let _ = std::fs::write(format!("recordings/frame_{n:05}.ppm"), out);
5292                self.record_n += 1;
5293                return Ok(Value::Number(n as f64));
5294            }
5295            "record_count" => return Ok(Value::Number(self.record_n as f64)),
5296            // ── screenshot(mode) → PNG in ./screenshots/ with timestamp + mode + size ──
5297            #[cfg(not(target_arch = "wasm32"))]
5298            "screenshot" | "บันทึกภาพ" => {
5299                let mode = self.arg_str(&args, 0, "game");
5300                let (buf, w, h) = { let gfx = self.gfx.borrow(); (gfx.buffer.clone(), gfx.width, gfx.height) };
5301                let _ = std::fs::create_dir_all("screenshots");
5302                let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
5303                let safe: String = mode.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect();
5304                let path = format!("screenshots/ss_{ts}_{safe}_{w}x{h}.png");
5305                let mut rgb = Vec::with_capacity(w * h * 3);
5306                for px in &buf { let p = *px; rgb.push((p >> 16) as u8); rgb.push((p >> 8) as u8); rgb.push(p as u8); }
5307                if let Some(img) = image::RgbImage::from_raw(w as u32, h as u32, rgb) {
5308                    let _ = img.save(&path);
5309                }
5310                return Ok(Value::Str(path));
5311            }
5312            // ── microphone → crypto donut ──
5313            // mic_capture() — append the latest mic samples to the record buffer
5314            // (call each frame while recording). Returns the buffer length.
5315            #[cfg(not(target_arch = "wasm32"))]
5316            "mic_capture" => {
5317                if let Some(mic) = self.mic.as_ref() {
5318                    let s = mic.latest_samples();
5319                    self.mic_buffer.extend_from_slice(&s);
5320                    let cap = 96_000usize; // ~2 s @ 48 kHz
5321                    if self.mic_buffer.len() > cap {
5322                        let drop = self.mic_buffer.len() - cap;
5323                        self.mic_buffer.drain(0..drop);
5324                    }
5325                }
5326                return Ok(Value::Number(self.mic_buffer.len() as f64));
5327            }
5328            // mic_seed() — SHA3-256 hex of the recorded audio, usable as a donut seed
5329            #[cfg(not(target_arch = "wasm32"))]
5330            "mic_seed" => {
5331                let mut bytes = Vec::with_capacity(self.mic_buffer.len() * 4);
5332                for f in &self.mic_buffer { bytes.extend_from_slice(&f.to_le_bytes()); }
5333                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(&bytes))));
5334            }
5335            #[cfg(not(target_arch = "wasm32"))]
5336            "mic_clear" => { self.mic_buffer.clear(); return Ok(Value::Number(0.0)); }
5337            // flush the 3-D depth queue onto the framebuffer WITHOUT presenting,
5338            // so 2-D UI drawn afterwards overlays the 3-D scene.
5339            #[cfg(not(target_arch = "wasm32"))]
5340            "flush_3d" | "render_3d" => {
5341                let mut gfx = self.gfx.borrow_mut();
5342                if !gfx.depth_queue.is_empty() {
5343                    let w = gfx.width; let h = gfx.height;
5344                    let queue = std::mem::take(&mut gfx.depth_queue);
5345                    queue.flush(&mut gfx.buffer, w, h);
5346                }
5347                return Ok(Value::Unit);
5348            }
5349
5350            // Viscous full-screen distortion (warp/pucker/bloat, edge-wrapped). Call
5351            // after the 3-D flush and before the UI so only the world layer warps.
5352            #[cfg(not(target_arch = "wasm32"))]
5353            "screen_distort" | "บิดจอ" | "屏幕扭曲" | "画面歪み" | "화면왜곡" => {
5354                let amount = self.arg_num(&args, 0, 8.0)? as f32;
5355                let t = self.arg_num(&args, 1, 0.0)? as f32;
5356                self.gfx.borrow_mut().distort(amount, t);
5357                return Ok(Value::Unit);
5358            }
5359
5360            "set_rim" | "设置边缘光" | "リム設定" | "림라이트" | "ตั้งขอบเรือง" => {
5361                let s=self.arg_num(&args,0,0.6)? as f32;
5362                let r=self.arg_num(&args,1,115.)? as f32/255.0;
5363                let g=self.arg_num(&args,2,217.)? as f32/255.0;
5364                let b=self.arg_num(&args,3,255.)? as f32/255.0;
5365                let mut gfx=self.gfx.borrow_mut();
5366                gfx.shade.rim = s; gfx.shade.rim_color = [r,g,b];
5367                return Ok(Value::Unit);
5368            }
5369
5370            // ══════════════════════════════════════════════════════════════════
5371            // 3-D PRIMITIVES  (src/gfx/shapes.rs)  — "Inkscape for 3-D"
5372            //   shape(cx,cy,cz,  sx,sy,sz,  rx,ry,rz,  mode,  e0,e1,e2)
5373            //     centre (cx,cy,cz), per-axis scale, Euler rotation (radians),
5374            //     mode: 0 filled · 1 wireframe · 2 both,
5375            //     e0..e2: shape-specific (segments / sides / ratio …).
5376            //   Pen colour (set_color) drives fill lighting and wireframe colour.
5377            // ══════════════════════════════════════════════════════════════════
5378            n if crate::gfx::shapes::canon(n).is_some() => {
5379                let kind = crate::gfx::shapes::canon(n).unwrap();
5380                let cx=self.arg_num(&args,0,0.)? as f32; let cy=self.arg_num(&args,1,0.)? as f32; let cz=self.arg_num(&args,2,0.)? as f32;
5381                let sx=self.arg_num(&args,3,1.)? as f32; let sy=self.arg_num(&args,4,1.)? as f32; let sz=self.arg_num(&args,5,1.)? as f32;
5382                let rx=self.arg_num(&args,6,0.)? as f32; let ry=self.arg_num(&args,7,0.)? as f32; let rz=self.arg_num(&args,8,0.)? as f32;
5383                let mode=self.arg_num(&args,9,0.)? as i32;
5384                let e0=self.arg_num(&args,10,0.)? as f32; let e1=self.arg_num(&args,11,0.)? as f32; let e2=self.arg_num(&args,12,0.)? as f32;
5385                if let Some(mesh)=crate::gfx::shapes::build(kind,[cx,cy,cz,sx,sy,sz,rx,ry,rz],e0,e1,e2){
5386                    let mut gfx=self.gfx.borrow_mut();
5387                    gfx.emit_mesh(&mesh,mode);
5388                }
5389                return Ok(Value::Unit);
5390            }
5391
5392            _ => {}
5393        }
5394
5395        // User-defined function
5396        if let Some(def) = self.functions.get(name).cloned() {
5397            // Clone the pre-evaluated global seed (built once in run_program) and
5398            // add the params. Globals are immutable after load, so re-evaluating
5399            // them on every call was pure waste — this is the hot-path fix.
5400            let mut call_env = self.global_seed.clone();
5401            let _ = env; // call-site locals are intentionally NOT visible to fns
5402            for (param, arg) in def.params.iter().zip(args) {
5403                call_env.insert(param.clone(), arg);
5404            }
5405            return match self.framed(name, |me| me.exec_block(&def.body, &mut call_env)) {
5406                Ok(v) => Ok(v.unwrap_or(Value::Unit)),
5407                Err(EvalErr::Return(v)) => Ok(v),
5408                Err(e) => Err(e),
5409            };
5410        }
5411
5412        // `form` struct constructor: positional `Name(v0, v1, ...)`.
5413        if let Some(field_names) = self.structs.get(name).cloned() {
5414            if args.len() != field_names.len() {
5415                return Err(EvalErr::from(format!(
5416                    "{name} expects {} field(s), got {}", field_names.len(), args.len())));
5417            }
5418            let fields = field_names.into_iter().zip(args).collect();
5419            return Ok(Value::Struct { name: name.to_string(), fields });
5420        }
5421
5422        // `choose` enum variant constructor: `Variant(...)` or `Enum::Variant(...)`.
5423        if let Some((enum_name, arity)) = self.enum_variants.get(name).cloned() {
5424            if args.len() != arity {
5425                return Err(EvalErr::from(format!(
5426                    "{name} expects {arity} value(s), got {}", args.len())));
5427            }
5428            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
5429            return Ok(Value::Variant { enum_name, variant, payload: args });
5430        }
5431
5432        Err(EvalErr::from(format!("unknown function '{name}'")))
5433    }
5434
5435    fn call_value(&mut self, v: Value, args: Vec<Value>) -> EvalResult {
5436        match v {
5437            Value::Fn(params, body, mut captured) => {
5438                for (p, a) in params.iter().zip(args) {
5439                    captured.insert(p.clone(), a);
5440                }
5441                match self.framed("<closure>", |me| me.exec_block(&body, &mut captured)) {
5442                    Ok(v) => Ok(v.unwrap_or(Value::Unit)),
5443                    Err(EvalErr::Return(v)) => Ok(v),
5444                    Err(e) => Err(e),
5445                }
5446            }
5447            other => Err(EvalErr::from(format!("cannot call {:?}", other))),
5448        }
5449    }
5450
5451    fn call_method(&self, recv: Value, method: &str, args: Vec<Value>) -> EvalResult {
5452        match (&recv, method) {
5453            (Value::Str(s), "is_empty" | "是空") => Ok(Value::Bool(s.is_empty())),
5454            (Value::Str(s), "len" | "长")        => Ok(Value::Number(s.len() as f64)),
5455            (Value::Str(s), "to_string" | "转文") => Ok(Value::Str(s.clone())),
5456            (Value::Str(s), "contains" | "包含") => {
5457                if let Some(Value::Str(sub)) = args.first() {
5458                    Ok(Value::Bool(s.contains(sub.as_str())))
5459                } else { Ok(Value::Bool(false)) }
5460            }
5461            (Value::Str(s), "push_str" | "推_文") => {
5462                let mut s2 = s.clone();
5463                if let Some(Value::Str(a)) = args.first() { s2.push_str(a); }
5464                Ok(Value::Str(s2))
5465            }
5466            (Value::List(v), "len" | "长") => Ok(Value::Number(v.len() as f64)),
5467            (Value::List(v), "push" | "推") => {
5468                let mut v2 = v.clone();
5469                if let Some(a) = args.first() { v2.push(a.clone()); }
5470                Ok(Value::List(v2))
5471            }
5472            // `form` field access: `point.x` (no-arg method == field read).
5473            (Value::Struct { fields, .. }, _) if args.is_empty() => {
5474                fields.iter().find(|(k, _)| k == method).map(|(_, v)| v.clone())
5475                    .ok_or_else(|| EvalErr::from(format!("no field '{method}' on {recv}")))
5476            }
5477            // Enum introspection: `.tag` → variant name, `.is(Name)` not needed for now.
5478            (Value::Variant { variant, .. }, "tag" | "标签" | "タグ" | "태그" | "ป้าย") if args.is_empty() =>
5479                Ok(Value::Str(variant.clone())),
5480            (Value::Ok(inner), _) | (Value::Err(inner), _) => Ok(*inner.clone()),
5481            _ => Err(EvalErr::from(format!("no method '{method}' on {recv}"))),
5482        }
5483    }
5484
5485    // ─── Pattern matching ─────────────────────────────────────────────────────
5486
5487    fn match_pattern(&self, pat: &Pattern, val: &Value) -> Option<Env> {
5488        match (pat, val) {
5489            (Pattern::Wildcard, _) => Some(Env::new()),
5490            (Pattern::Str(s), Value::Str(v)) if s == v => Some(Env::new()),
5491            (Pattern::Number(n), Value::Number(v)) if (n - v).abs() < 1e-12 => Some(Env::new()),
5492            (Pattern::Bool(b), Value::Bool(v)) if b == v => Some(Env::new()),
5493            (Pattern::Ident(name), _) => {
5494                let mut e = Env::new();
5495                e.insert(name.clone(), val.clone());
5496                Some(e)
5497            }
5498            (Pattern::Constructor(ctor, inner_pat), _) => {
5499                let (matches, inner_val) = match (ctor.as_str(), val) {
5500                    ("ok"  | "好", Value::Ok(v))  => (true, Some(v.as_ref().clone())),
5501                    ("bad" | "坏", Value::Err(v)) => (true, Some(v.as_ref().clone())),
5502                    ("ok"  | "好", v) if !matches!(v, Value::Err(_)) => (true, Some(v.clone())),
5503                    _ => (false, None),
5504                };
5505                if !matches { return None; }
5506                match (inner_pat, inner_val) {
5507                    (Some(p), Some(v)) => self.match_pattern(p, &v),
5508                    (None, _)          => Some(Env::new()),
5509                    (Some(p), None)    => self.match_pattern(p, &Value::Unit),
5510                }
5511            }
5512            // User enum variant pattern: `Circle(r)`, `Pair(a, b)`, nullary `Origin()`.
5513            (Pattern::Variant(vname, sub_pats), Value::Variant { variant, payload, .. }) => {
5514                if vname != variant || sub_pats.len() != payload.len() { return None; }
5515                let mut bindings = Env::new();
5516                for (p, v) in sub_pats.iter().zip(payload.iter()) {
5517                    bindings.extend(self.match_pattern(p, v)?);
5518                }
5519                Some(bindings)
5520            }
5521            // A zero-payload variant pattern also matches the bare result-style `ok`/`bad`
5522            // values so `Ok()`-style patterns keep working uniformly.
5523            (Pattern::Variant(vname, sub), Value::Ok(v)) if (vname == "ok" || vname == "好") => {
5524                match sub.as_slice() {
5525                    []  => Some(Env::new()),
5526                    [p] => self.match_pattern(p, v),
5527                    _   => None,
5528                }
5529            }
5530            (Pattern::Variant(vname, sub), Value::Err(v)) if (vname == "bad" || vname == "坏" || vname == "err") => {
5531                match sub.as_slice() {
5532                    []  => Some(Env::new()),
5533                    [p] => self.match_pattern(p, v),
5534                    _   => None,
5535                }
5536            }
5537            _ => None,
5538        }
5539    }
5540
5541    // ─── Utilities ───────────────────────────────────────────────────────────
5542
5543    fn value_to_iter(&self, val: Value) -> Result<Vec<Value>, EvalErr> {
5544        match val {
5545            Value::List(v)   => Ok(v),
5546            Value::Str(s)    => Ok(s.chars().map(|c| Value::Str(c.to_string())).collect()),
5547            Value::Number(n) => Ok((0..n as i64).map(|i| Value::Number(i as f64)).collect()),
5548            other => Err(EvalErr::from(format!("cannot iterate over {:?}", other))),
5549        }
5550    }
5551
5552    fn is_truthy(&self, val: &Value) -> bool {
5553        match val {
5554            Value::Bool(b)     => *b,
5555            Value::Unit        => false,
5556            Value::Number(n)   => *n != 0.0,
5557            Value::Str(s)      => !s.is_empty(),
5558            Value::List(v)     => !v.is_empty(),
5559            Value::Ok(_)       => true,
5560            Value::Err(_)      => false,
5561            Value::Fn(_, _, _) => true,
5562            Value::Struct { .. }  => true,
5563            Value::Variant { .. } => true,
5564        }
5565    }
5566
5567    fn to_number(&self, val: &Value) -> Result<f64, EvalErr> {
5568        match val {
5569            Value::Number(n) => Ok(*n),
5570            Value::Str(s)    => s.parse().map_err(|_| EvalErr::from(format!("cannot convert '{s}' to number"))),
5571            other => Err(EvalErr::from(format!("expected number, got {:?}", other))),
5572        }
5573    }
5574
5575    /// Get the n-th argument as f64, falling back to `default` if missing.
5576    fn arg_num(&self, args: &[Value], n: usize, default: f64) -> Result<f64, EvalErr> {
5577        match args.get(n) {
5578            Some(v) => self.to_number(v),
5579            None    => Ok(default),
5580        }
5581    }
5582
5583    fn arg_str(&self, args: &[Value], n: usize, default: &str) -> String {
5584        args.get(n).map(|v| v.to_string()).unwrap_or_else(|| default.to_string())
5585    }
5586
5587    /// Read a list-of-numbers argument as `Vec<f32>` (empty if absent/not a list).
5588    #[allow(dead_code)]
5589    fn arg_list_f32(&self, args: &[Value], n: usize) -> Vec<f32> {
5590        match args.get(n) {
5591            Some(Value::List(v)) => v.iter().filter_map(|x| match x {
5592                Value::Number(n) => Some(*n as f32),
5593                _ => None,
5594            }).collect(),
5595            _ => Vec::new(),
5596        }
5597    }
5598
5599    /// Optional `r,g,b` colour override starting at arg `i` → packed 0x00RRGGBB,
5600    /// or `default` if those three numeric args aren't present.
5601    #[cfg(not(target_arch = "wasm32"))]
5602    fn color_at(&self, args: &[Value], i: usize, default: u32) -> u32 {
5603        match (args.get(i), args.get(i + 1), args.get(i + 2)) {
5604            (Some(a), Some(b), Some(c)) => match (self.to_number(a), self.to_number(b), self.to_number(c)) {
5605                (Ok(r), Ok(g), Ok(bl)) =>
5606                    ((r as u32 & 0xFF) << 16) | ((g as u32 & 0xFF) << 8) | (bl as u32 & 0xFF),
5607                _ => default,
5608            },
5609            _ => default,
5610        }
5611    }
5612
5613    /// A pitch argument: a note-name string (`"C4"`, `"A#3"`) or a numeric MIDI value.
5614    #[cfg(not(target_arch = "wasm32"))]
5615    fn pitch_arg(&self, args: &[Value], i: usize, default: i32) -> i32 {
5616        match args.get(i) {
5617            Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(default),
5618            Some(Value::Number(n)) => *n as i32,
5619            _ => default,
5620        }
5621    }
5622
5623    /// Current mouse position + left-button-down (native window only).
5624    #[cfg(not(target_arch = "wasm32"))]
5625    fn mouse_now(&self) -> (f32, f32, bool) {
5626        let gfx = self.gfx.borrow();
5627        let (mx, my) = gfx.window.as_ref()
5628            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp)).unwrap_or((0.0, 0.0));
5629        let down = gfx.window.as_ref()
5630            .map(|w| w.get_mouse_down(minifb::MouseButton::Left)).unwrap_or(false);
5631        (mx, my, down)
5632    }
5633
5634    /// Rasterize a UI [`ling_ui::widgets::Draw`] into the framebuffer: filled
5635    /// polygons via the AA scanline fill, polylines via AA lines, honouring the
5636    /// current blend mode.
5637    #[cfg(not(target_arch = "wasm32"))]
5638    fn draw_ui(&self, d: &ling_ui::widgets::Draw) {
5639        let mut gfx = self.gfx.borrow_mut();
5640        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
5641        for (c, poly) in &d.fills {
5642            crate::gfx::raster::fill_contours_aa(&mut gfx.buffer, w, h, *c, add, std::slice::from_ref(poly));
5643        }
5644        for (c, pl) in &d.strokes {
5645            for s in pl.windows(2) {
5646                crate::gfx::raster::draw_line_aa(&mut gfx.buffer, w, h, *c, add, s[0][0], s[0][1], s[1][0], s[1][1]);
5647            }
5648        }
5649    }
5650
5651    /// Parse (dst_x, dst_y, width, height) from the first four args of a tex_* builtin.
5652    fn tex_rect(&self, args: &[Value]) -> Result<(usize, usize, usize, usize), EvalErr> {
5653        let tx = self.arg_num(args, 0, 0.0)? as usize;
5654        let ty = self.arg_num(args, 1, 0.0)? as usize;
5655        let tw = self.arg_num(args, 2, 256.0)? as usize;
5656        let th = self.arg_num(args, 3, 256.0)? as usize;
5657        Ok((tx, ty, tw.max(1), th.max(1)))
5658    }
5659
5660    fn apply_binop(&self, op: &BinOp, l: Value, r: Value) -> EvalResult {
5661        match op {
5662            BinOp::Add => match (l, r) {
5663                (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
5664                (Value::Str(a), Value::Str(b))       => Ok(Value::Str(a + &b)),
5665                (Value::Str(a), b)                   => Ok(Value::Str(a + &b.to_string())),
5666                (a, Value::Str(b))                   => Ok(Value::Str(a.to_string() + &b)),
5667                (a, b) => Err(EvalErr::from(format!("cannot add {:?} and {:?}", a, b))),
5668            },
5669            BinOp::Sub => Ok(Value::Number(self.to_number(&l)? - self.to_number(&r)?)),
5670            BinOp::Mul => Ok(Value::Number(self.to_number(&l)? * self.to_number(&r)?)),
5671            BinOp::Div => Ok(Value::Number(self.to_number(&l)? / self.to_number(&r)?)),
5672            BinOp::Rem => Ok(Value::Number(self.to_number(&l)? % self.to_number(&r)?)),
5673            BinOp::Eq  => Ok(Value::Bool(values_equal(&l, &r))),
5674            BinOp::Ne  => Ok(Value::Bool(!values_equal(&l, &r))),
5675            BinOp::Lt  => Ok(Value::Bool(self.to_number(&l)? < self.to_number(&r)?)),
5676            BinOp::Gt  => Ok(Value::Bool(self.to_number(&l)? > self.to_number(&r)?)),
5677            BinOp::Le  => Ok(Value::Bool(self.to_number(&l)? <= self.to_number(&r)?)),
5678            BinOp::Ge  => Ok(Value::Bool(self.to_number(&l)? >= self.to_number(&r)?)),
5679            BinOp::And => Ok(Value::Bool(self.is_truthy(&l) && self.is_truthy(&r))),
5680            BinOp::Or  => Ok(Value::Bool(self.is_truthy(&l) || self.is_truthy(&r))),
5681        }
5682    }
5683
5684    fn builtin_format(&self, args: &[Value]) -> Result<String, EvalErr> {
5685        if args.is_empty() { return Ok(String::new()); }
5686        let fmt = match &args[0] {
5687            Value::Str(s) => s.clone(),
5688            other => return Ok(other.to_string()),
5689        };
5690
5691        let mut result = String::new();
5692        let mut arg_idx = 1usize;
5693        let mut chars = fmt.chars().peekable();
5694        while let Some(c) = chars.next() {
5695            if c == '{' {
5696                if chars.peek() == Some(&'}') {
5697                    chars.next();
5698                    if arg_idx < args.len() {
5699                        result.push_str(&args[arg_idx].to_string());
5700                        arg_idx += 1;
5701                    }
5702                } else {
5703                    let mut spec = String::new();
5704                    for ch in chars.by_ref() {
5705                        if ch == '}' { break; }
5706                        spec.push(ch);
5707                    }
5708                    if arg_idx < args.len() {
5709                        if spec.starts_with(":.") {
5710                            if let Value::Number(n) = &args[arg_idx] {
5711                                let prec: usize = spec[2..].trim_end_matches('f')
5712                                    .parse().unwrap_or(2);
5713                                result.push_str(&format!("{:.prec$}", n));
5714                                arg_idx += 1;
5715                                continue;
5716                            }
5717                        }
5718                        result.push_str(&args[arg_idx].to_string());
5719                        arg_idx += 1;
5720                    }
5721                }
5722            } else {
5723                result.push(c);
5724            }
5725        }
5726        Ok(result)
5727    }
5728}
5729
5730#[cfg(not(target_arch = "wasm32"))]
5731/// Map a friendly button name (any vendor / d-pad alias) to a gamepad button.
5732#[cfg(not(target_arch = "wasm32"))]
5733fn parse_pad_button(name: &str) -> Option<ling_input::GamepadButton> {
5734    use ling_input::GamepadButton as B;
5735    Some(match name.to_ascii_lowercase().as_str() {
5736        "a" | "south" | "cross" => B::South,
5737        "b" | "east" | "circle" => B::East,
5738        "x" | "west" | "square" => B::West,
5739        "y" | "north" | "triangle" => B::North,
5740        "lb" | "l1" | "left_shoulder" => B::LeftShoulder,
5741        "rb" | "r1" | "right_shoulder" => B::RightShoulder,
5742        "lt" | "l2" | "left_trigger" => B::LeftTrigger,
5743        "rt" | "r2" | "right_trigger" => B::RightTrigger,
5744        "start" | "menu" | "options" => B::Start,
5745        "select" | "back" | "share" | "view" => B::Select,
5746        "guide" | "home" => B::Guide,
5747        "l3" | "left_stick" => B::LeftStick,
5748        "r3" | "right_stick" => B::RightStick,
5749        "up" | "dpad_up" => B::DpadUp,
5750        "down" | "dpad_down" => B::DpadDown,
5751        "left" | "dpad_left" => B::DpadLeft,
5752        "right" | "dpad_right" => B::DpadRight,
5753        _ => return None,
5754    })
5755}
5756
5757#[cfg(not(target_arch = "wasm32"))]
5758fn str_to_minifb_key(name: &str) -> Option<minifb::Key> {
5759    use minifb::Key;
5760    Some(match name {
5761        "numpad0" | "kp0" => Key::NumPad0,
5762        "numpad1" | "kp1" => Key::NumPad1,
5763        "numpad2" | "kp2" => Key::NumPad2,
5764        "numpad3" | "kp3" => Key::NumPad3,
5765        "numpad4" | "kp4" => Key::NumPad4,
5766        "numpad5" | "kp5" => Key::NumPad5,
5767        "numpad6" | "kp6" => Key::NumPad6,
5768        "numpad7" | "kp7" => Key::NumPad7,
5769        "numpad8" | "kp8" => Key::NumPad8,
5770        "numpad9" | "kp9" => Key::NumPad9,
5771        "numpad+" | "kp+" => Key::NumPadPlus,
5772        "numpad-" | "kp-" => Key::NumPadMinus,
5773        "numpad*" | "kp*" => Key::NumPadAsterisk,
5774        "numpad/" | "kp/" => Key::NumPadSlash,
5775        "left"   => Key::Left,
5776        "right"  => Key::Right,
5777        "up"     => Key::Up,
5778        "down"   => Key::Down,
5779        "space"  => Key::Space,
5780        "enter"  => Key::Enter,
5781        "escape" => Key::Escape,
5782        "pageup" => Key::PageUp,
5783        "pagedown" => Key::PageDown,
5784        "lshift" | "leftshift"  => Key::LeftShift,
5785        "rshift" | "rightshift" => Key::RightShift,
5786        "lctrl"  | "leftctrl"   => Key::LeftCtrl,
5787        "rctrl"  | "rightctrl"  => Key::RightCtrl,
5788        "lalt"   | "leftalt"    => Key::LeftAlt,
5789        "ralt"   | "rightalt"   => Key::RightAlt,
5790        "tab"    => Key::Tab,
5791        "backspace" => Key::Backspace,
5792        "delete" => Key::Delete,
5793        "insert" => Key::Insert,
5794        "home"   => Key::Home,
5795        "end"    => Key::End,
5796        "a" => Key::A, "b" => Key::B, "c" => Key::C, "d" => Key::D,
5797        "e" => Key::E, "f" => Key::F, "g" => Key::G, "h" => Key::H,
5798        "i" => Key::I, "j" => Key::J, "k" => Key::K, "l" => Key::L,
5799        "m" => Key::M, "n" => Key::N, "o" => Key::O, "p" => Key::P,
5800        "q" => Key::Q, "r" => Key::R, "s" => Key::S, "t" => Key::T,
5801        "u" => Key::U, "v" => Key::V, "w" => Key::W, "x" => Key::X,
5802        "y" => Key::Y, "z" => Key::Z,
5803        "0" => Key::Key0, "1" => Key::Key1, "2" => Key::Key2,
5804        "3" => Key::Key3, "4" => Key::Key4, "5" => Key::Key5,
5805        "6" => Key::Key6, "7" => Key::Key7, "8" => Key::Key8,
5806        "9" => Key::Key9,
5807        _ => return None,
5808    })
5809}
5810
5811fn values_equal(a: &Value, b: &Value) -> bool {
5812    match (a, b) {
5813        (Value::Number(x), Value::Number(y)) => (x - y).abs() < 1e-12,
5814        (Value::Str(x), Value::Str(y))       => x == y,
5815        (Value::Bool(x), Value::Bool(y))     => x == y,
5816        (Value::Unit, Value::Unit)            => true,
5817        _ => false,
5818    }
5819}
5820
5821// Rasteriser functions live in crate::gfx::raster — imported at top of file.
5822
5823// ── Window platform helpers ────────────────────────────────────────────────────
5824
5825/// Hide the console window that the OS auto-attaches to console-subsystem
5826/// processes. No-op on non-Windows and when no console is present.
5827#[cfg(not(target_arch = "wasm32"))]
5828fn hide_console_window() {
5829    #[cfg(windows)]
5830    unsafe {
5831        extern "system" {
5832            fn GetConsoleWindow() -> isize;
5833            fn ShowWindow(hwnd: isize, nCmdShow: i32) -> i32;
5834        }
5835        let hwnd = GetConsoleWindow();
5836        if hwnd != 0 {
5837            ShowWindow(hwnd, 0); // SW_HIDE = 0
5838        }
5839    }
5840}
5841
5842/// Strip *all* window chrome from `hwnd` and make it cover the whole primary
5843/// monitor (0,0 → screen_w × screen_h), above the taskbar. This turns the
5844/// minifb window into a true borderless-fullscreen surface: no title bar, no
5845/// frame, no resize grips — there is no visible window "handle" left.
5846#[cfg(all(not(target_arch = "wasm32"), windows))]
5847fn make_borderless_fullscreen(hwnd: isize, screen_w: i32, screen_h: i32) {
5848    if hwnd == 0 {
5849        return;
5850    }
5851    unsafe {
5852        extern "system" {
5853            fn SetWindowLongPtrW(hwnd: isize, index: i32, new: isize) -> isize;
5854            fn SetWindowPos(hwnd: isize, insert_after: isize,
5855                            x: i32, y: i32, cx: i32, cy: i32,
5856                            flags: u32) -> i32;
5857            fn ShowWindow(hwnd: isize, cmd: i32) -> i32;
5858        }
5859        const GWL_STYLE:   i32 = -16;
5860        const GWL_EXSTYLE: i32 = -20;
5861        // WS_POPUP (0x80000000) | WS_VISIBLE (0x10000000) — a bare top-level
5862        // window with no caption, border, or system menu.
5863        SetWindowLongPtrW(hwnd, GWL_STYLE, 0x9000_0000isize);
5864        // Clear extended edges (WS_EX_WINDOWEDGE / CLIENTEDGE / DLGMODALFRAME).
5865        SetWindowLongPtrW(hwnd, GWL_EXSTYLE, 0);
5866        // HWND_TOPMOST = -1; SWP_FRAMECHANGED (0x0020) | SWP_SHOWWINDOW (0x0040).
5867        SetWindowPos(hwnd, -1isize, 0, 0, screen_w, screen_h, 0x0020 | 0x0040);
5868        ShowWindow(hwnd, 3); // SW_MAXIMIZE-equivalent paint; 3 = SW_SHOWMAXIMIZED
5869    }
5870}
5871
5872/// Primary-monitor resolution and refresh rate as `(width, height, hz)`.
5873/// `hz` falls back to 60 when the driver reports an unknown/`default` rate.
5874#[cfg(all(not(target_arch = "wasm32"), windows))]
5875fn monitor_info() -> (i32, i32, i32) {
5876    unsafe {
5877        extern "system" {
5878            fn GetSystemMetrics(index: i32) -> i32;
5879            fn GetDC(hwnd: isize) -> isize;
5880            fn ReleaseDC(hwnd: isize, hdc: isize) -> i32;
5881            fn GetDeviceCaps(hdc: isize, index: i32) -> i32;
5882        }
5883        let w = GetSystemMetrics(0).max(1); // SM_CXSCREEN
5884        let h = GetSystemMetrics(1).max(1); // SM_CYSCREEN
5885        let hdc = GetDC(0);
5886        let mut hz = if hdc != 0 { GetDeviceCaps(hdc, 116) } else { 0 }; // VREFRESH
5887        if hdc != 0 {
5888            ReleaseDC(0, hdc);
5889        }
5890        if hz <= 1 {
5891            hz = 60; // 0 or 1 means "device default" → assume 60 Hz
5892        }
5893        (w, h, hz)
5894    }
5895}
5896
5897/// Non-Windows native fallback: resolution from [`native_screen_size`], 60 Hz.
5898#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
5899fn monitor_info() -> (i32, i32, i32) {
5900    let (w, h) = native_screen_size();
5901    (w as i32, h as i32, 60)
5902}
5903
5904/// WASM fallback: the canvas is the display surface; assume 60 Hz.
5905#[cfg(target_arch = "wasm32")]
5906fn monitor_info() -> (i32, i32, i32) {
5907    let (w, h) = crate::gfx::webgl::canvas_size();
5908    (w as i32, h as i32, 60)
5909}
5910
5911/// Query the primary display resolution on non-Windows platforms.
5912/// Falls back to 1920×1080 if the size cannot be determined.
5913#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
5914fn native_screen_size() -> (f64, f64) {
5915    // On Linux/macOS we don't have an easy dependency-free call; return a
5916    // sensible default. Callers can always pass explicit dimensions.
5917    (1920.0, 1080.0)
5918}