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 ai;
4#[cfg(not(target_arch = "wasm32"))]
5mod gamepad;
6#[cfg(target_arch = "wasm32")]
7mod input_web;
8#[cfg(not(target_arch = "wasm32"))]
9pub(crate) mod jit_abi;
10
11/// Initialize the AOT/JIT runtime. Must be called before any AOT-compiled code.
12/// Creates a new interpreter instance for runtime function dispatch.
13#[cfg(not(target_arch = "wasm32"))]
14pub fn init_aot_runtime() {
15    let interp = Interpreter::new();
16    jit_abi::init(interp);
17}
18
19/// Returns seconds since Unix epoch. On wasm32 uses `js_sys::Date::now()`
20/// (milliseconds / 1000); on native uses `SystemTime`.
21pub fn now_secs() -> f64 {
22    #[cfg(target_arch = "wasm32")]
23    {
24        js_sys::Date::now() / 1000.0
25    }
26    #[cfg(not(target_arch = "wasm32"))]
27    {
28        std::time::SystemTime::now()
29            .duration_since(std::time::UNIX_EPOCH)
30            .map(|d| d.as_secs_f64())
31            .unwrap_or(0.0)
32    }
33}
34
35// Wasm-only module registry: seeded by JS before `run_program` is called so
36// that `use "path"` statements resolve without a real filesystem.
37#[cfg(target_arch = "wasm32")]
38thread_local! {
39    static WASM_MODULES: std::cell::RefCell<std::collections::HashMap<String, String>> =
40        std::cell::RefCell::new(std::collections::HashMap::new());
41}
42
43/// Register a module source for wasm32 `use` resolution.
44/// Called from JS via `wasm_bindgen` before `run_program`.
45#[cfg(target_arch = "wasm32")]
46pub fn register_wasm_module(path: &str, source: &str) {
47    WASM_MODULES.with(|m| m.borrow_mut().insert(path.to_string(), source.to_string()));
48}
49
50/// Look up a registered module source on wasm32.
51#[cfg(target_arch = "wasm32")]
52pub(crate) fn get_wasm_module(path: &str) -> Option<String> {
53    WASM_MODULES.with(|m| m.borrow().get(path).cloned())
54}
55
56#[cfg(target_arch = "wasm32")]
57#[inline]
58fn wasm_sleep_ms(ms: i32) {
59    if ms <= 0 {
60        return;
61    }
62
63    // Keep this in Rust/js_sys so wasm-bindgen doesn't emit `require(...)`
64    // snippets, which break worker/no-modules output.
65    let global = js_sys::global();
66    let has_sab = js_sys::Reflect::has(&global, &wasm_bindgen::JsValue::from_str("SharedArrayBuffer"))
67        .unwrap_or(false);
68    let has_atomics =
69        js_sys::Reflect::has(&global, &wasm_bindgen::JsValue::from_str("Atomics")).unwrap_or(false);
70    if has_sab && has_atomics {
71        let sab = js_sys::SharedArrayBuffer::new(4);
72        let i32a = js_sys::Int32Array::new(&sab);
73        if js_sys::Atomics::wait_with_timeout(&i32a, 0, 0, ms as f64).is_ok() {
74            return;
75        }
76    }
77
78    let end = js_sys::Date::now() + ms as f64;
79    while js_sys::Date::now() < end {}
80}
81
82#[cfg(target_arch = "wasm32")]
83fn wasm_fetch_sync(path: &str, response_type: &str, return_expr: &str) -> Result<wasm_bindgen::JsValue, String> {
84    let quoted = js_sys::JSON::stringify(&wasm_bindgen::JsValue::from_str(path))
85        .ok()
86        .and_then(|s| s.as_string())
87        .unwrap_or_else(|| "\"\"".to_string());
88
89    let script = format!(
90        "(function(){{\n  var xhr = new XMLHttpRequest();\n  xhr.open('GET', {quoted}, false);\n  xhr.responseType = '{response_type}';\n  xhr.send(null);\n  if ((xhr.status|0) !== 200 && (xhr.status|0) !== 0) {{ throw new Error('HTTP ' + xhr.status + ' for ' + {quoted}); }}\n  return {return_expr};\n}})()"
91    );
92
93    js_sys::eval(&script)
94        .map_err(|e| e.as_string().unwrap_or_else(|| format!("JS eval failed: {:?}", e)))
95}
96
97#[cfg(target_arch = "wasm32")]
98fn wasm_fetch_bytes(path: &str) -> Result<Vec<u8>, String> {
99    let value = wasm_fetch_sync(path, "arraybuffer", "new Uint8Array(xhr.response || new ArrayBuffer(0))")?;
100    let arr = js_sys::Uint8Array::new(&value);
101    let mut out = vec![0u8; arr.length() as usize];
102    arr.copy_to(&mut out);
103    Ok(out)
104}
105
106#[cfg(target_arch = "wasm32")]
107fn wasm_fetch_text(path: &str) -> Result<String, String> {
108    let value = wasm_fetch_sync(path, "text", "String(xhr.responseText || '')")?;
109    Ok(value.as_string().unwrap_or_default())
110}
111
112#[cfg(not(target_arch = "wasm32"))]
113mod net;
114#[cfg(target_arch = "wasm32")]
115use js_sys;
116use crate::gfx::{GfxState, Light};
117use crate::parser::ast::*;
118use std::cell::RefCell;
119use std::collections::HashMap;
120use std::rc::Rc;
121// `raster` is wasm-safe (pure CPU framebuffer), so `draw_line` is available on
122// web too; `fill_triangle` is only reached from native-gated 3-D fill paths.
123use crate::gfx::raster::draw_line;
124#[cfg(not(target_arch = "wasm32"))]
125use crate::gfx::raster::fill_triangle;
126#[cfg(not(target_arch = "wasm32"))]
127use ling_audio::{AudioEngine, ToneParams, Wave};
128
129#[cfg(not(target_arch = "wasm32"))]
130use ling_audio::FftAnalyzer;
131
132#[cfg(not(target_arch = "wasm32"))]
133use ling_mic;
134
135// ─── Values ──────────────────────────────────────────────────────────────────
136
137#[derive(Debug, Clone)]
138pub enum Value {
139    Str(String),
140    Number(f64),
141    Bool(bool),
142    Unit,
143    List(Rc<Vec<Value>>),
144    Ok(Box<Value>),
145    Err(Box<Value>),
146    Fn(Vec<String>, Vec<Stmt>, Env),
147    /// `form` record instance — ordered named fields.
148    Struct {
149        name: String,
150        fields: Vec<(String, Value)>,
151    },
152    /// `choose` enum instance — variant tag plus ordered payload.
153    Variant {
154        enum_name: String,
155        variant: String,
156        payload: Vec<Value>,
157    },
158}
159
160// Interpreter-hot maps use a fast non-crypto hasher: short Thai identifier keys
161// are hashed on every variable access and builtin dispatch, where SipHash dominates.
162use rustc_hash::FxHashMap;
163type Env = FxHashMap<String, Value>;
164
165#[inline]
166fn new_env() -> Env {
167    FxHashMap::default()
168}
169
170impl std::fmt::Display for Value {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            Value::Str(s) => write!(f, "{s}"),
174            Value::Number(n) => {
175                if n.fract() == 0.0 && n.abs() < 1e15 {
176                    write!(f, "{}", *n as i64)
177                } else {
178                    write!(f, "{n}")
179                }
180            },
181            Value::Bool(b) => write!(f, "{b}"),
182            Value::Unit => write!(f, "()"),
183            Value::List(v) => {
184                write!(f, "[")?;
185                for (i, x) in v.iter().enumerate() {
186                    if i > 0 {
187                        write!(f, ", ")?;
188                    }
189                    write!(f, "{x}")?;
190                }
191                write!(f, "]")
192            },
193            Value::Ok(v) => write!(f, "Ok({v})"),
194            Value::Err(v) => write!(f, "Err({v})"),
195            Value::Fn(_, _, _) => write!(f, "<fn>"),
196            Value::Struct { name, fields } => {
197                write!(f, "{name} {{ ")?;
198                for (i, (k, v)) in fields.iter().enumerate() {
199                    if i > 0 {
200                        write!(f, ", ")?;
201                    }
202                    write!(f, "{k}: {v}")?;
203                }
204                write!(f, " }}")
205            },
206            Value::Variant { variant, payload, .. } => {
207                write!(f, "{variant}")?;
208                if !payload.is_empty() {
209                    write!(f, "(")?;
210                    for (i, v) in payload.iter().enumerate() {
211                        if i > 0 {
212                            write!(f, ", ")?;
213                        }
214                        write!(f, "{v}")?;
215                    }
216                    write!(f, ")")?;
217                }
218                Ok(())
219            },
220        }
221    }
222}
223
224#[cfg(target_arch = "wasm32")]
225fn wasm_unsupported_builtin(name: &str) -> Option<Value> {
226    Some(match name {
227        // interface blips (native-only today)
228        "audio_blip" | "提示音" | "ビープ音" | "효과음" | "เสียงบี๊บ"
229        | "ui_sound" | "界面音" | "UI音" | "인터페이스음" | "เสียงปุ่ม"
230        | "audio_stop_sfx" | "停止音效" | "効果音停止" | "효과음정지" | "หยุดเอฟเฟกต์ทั้งหมด" => {
231            Value::Unit
232        },
233
234        // music loading / analysis / playback / midi (native-only today)
235        "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง"
236        | "music_patch" | "乐器音色" | "音色読込" | "악기패치" | "แพตช์เครื่องดนตรี"
237        | "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง"
238        | "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" => {
239            Value::Number(-1.0)
240        },
241
242        "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง"
243        | "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที"
244        | "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง"
245        | "music_mic_pitch" | "麦克风音高" | "マイク音程" | "마이크음정" | "ระดับเสียงไมค์"
246        | "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต"
247        | "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง"
248        | "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ"
249        | "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" => {
250            Value::Number(0.0)
251        },
252
253        "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง"
254        | "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน"
255        | "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต"
256        | "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" => {
257            Value::Str(String::new())
258        },
259
260        "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง"
261        | "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ"
262        | "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี"
263        | "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี"
264        | "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" => {
265            Value::List(Vec::new())
266        },
267
268        "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง"
269        | "music_pause" | "暂停音乐" | "音楽一時停止" | "음악일시정지" | "หยุดเพลงชั่วคราว"
270        | "music_stop" | "停止音乐" | "音楽停止" | "음악정지" | "หยุดเพลง"
271        | "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง"
272        | "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง"
273        | "music_note" | "弹音符" | "音符演奏" | "음표연주" | "เล่นโน้ต"
274        | "music_note_on" | "音符开始" | "音符オン" | "음표켜기" | "โน้ตเริ่ม"
275        | "music_note_off" | "音符结束" | "音符オフ" | "음표끄기" | "โน้ตจบ" => {
276            Value::Unit
277        },
278
279        // liquid sim — return handle 0 for new, Unit for everything else
280        "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" => {
281            Value::Number(0.0)
282        },
283        "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" => {
284            Value::Number(0.0)
285        },
286        "liquid_set_colors" | "液体颜色" | "液体配色" | "액체색상" | "สีของเหลว"
287        | "liquid_splat" | "液体注入" | "液体追加" | "액체분사" | "หยดของเหลว"
288        | "liquid_gravity" | "液体重力" | "液体重力ベクトル" | "액체중력" | "แรงโน้มถ่วงเหลว"
289        | "liquid_step" | "液体步进" | "液体更新" | "액체스텝" | "ก้าวของเหลว"
290        | "liquid_step_all" | "液体全步进" | "液体全更新" | "전체액체스텝" | "ก้าวของเหลวทั้งหมด"
291        | "liquid_rainbow" | "液体彩虹" | "液体虹" | "액체무지개" | "ของเหลวสายรุ้ง"
292        | "liquid_draw" | "绘制液体" | "液体描画" | "액체그리기" | "วาดของเหลว"
293        | "liquid_draw_surface" | "液体贴面" | "液体曲面" | "액체곡면" | "ของเหลวบนพื้นผิว" => {
294            Value::Unit
295        },
296
297        // ── game AI: neural networks ─────────────────────────────────────────
298        "nn_new" | "建神经网" | "ニューラル作成" | "신경망생성" | "สร้างโครงข่าย"
299        | "nn_load" | "载入网" | "網読込" | "신경망불러오기" | "โหลดโครงข่าย" => {
300            Value::Number(-1.0)
301        },
302        "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" => {
303            Value::List(Vec::new())
304        },
305        "nn_train" | "训练网" | "ニューラル学習" | "신경망학습" | "ฝึกโครงข่าย"
306        | "nn_dense" | "密集层" | "密層追加" | "밀집층" | "ชั้นหนาแน่น" => {
307            Value::Number(0.0)
308        },
309        "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" => {
310            Value::Bool(false)
311        },
312
313        // ── game AI: behavior trees ─────────────────────────────────────────
314        "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" => {
315            Value::Number(-1.0)
316        },
317        "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" => {
318            Value::Str(String::new())
319        },
320        "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" => {
321            Value::Number(0.0)
322        },
323        "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" => Value::Unit,
324
325        // ── game AI: dialog LLM ─────────────────────────────────────────────
326        "dialog_new" | "建对话模型" | "対話モデル作成" | "대화모델생성" | "สร้างโมเดลสนทนา"
327        | "dialog_load_model" | "对话载模" | "対話モデル読込" | "대화모델불러오기"
328            | "โหลดโมเดลสนทนา"
329        | "dialog_train" | "对话训练" | "対話訓練" | "대화훈련" | "ฝึกสนทนา"
330        | "dialog_load" | "对话载入" | "対話読込" | "대화불러오기" | "โหลดชุดสนทนา" => {
331            Value::Number(-1.0)
332        },
333        "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" => {
334            Value::Str(String::new())
335        },
336        "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" => {
337            Value::Bool(false)
338        },
339        "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" => Value::Unit,
340
341        // ── networking ──────────────────────────────────────────────────────
342        "net_connect" | "联网" | "ネット接続" | "네트연결" | "เชื่อมเน็ต"
343        | "net_listen" | "监听" | "待機" | "리슨" | "รอรับ"
344        | "net_send" | "发送" | "送信" | "전송" | "ส่ง" => Value::Number(-1.0),
345        "net_recv" | "接收" | "受信" | "수신" | "รับ"
346        | "net_status" | "连接状态" | "接続状態" | "연결상태" | "สถานะการเชื่อม" => {
347            Value::Str(String::new())
348        },
349        "net_discover" | "发现" | "探索" | "검색" | "ค้นหาเครือข่าย" => {
350            Value::List(Vec::new())
351        },
352        "net_close" | "断开" | "切断" | "연결종료" | "ตัดเชื่อม"
353        | "net_test" | "测连接" | "接続テスト" | "연결테스트" | "ทดสอบเน็ต" => {
354            Value::Number(0.0)
355        },
356
357        // catch-all: any other native-only builtin silently no-ops on wasm32
358        _ => Value::Unit,
359    })
360}
361
362// ─── Control flow ────────────────────────────────────────────────────────────
363
364#[derive(Debug)]
365pub(crate) enum EvalErr {
366    Runtime(String),
367    Return(Value),
368    #[allow(dead_code)] // reserved for future `break` statement support
369    Break,
370}
371
372impl From<String> for EvalErr {
373    fn from(s: String) -> Self {
374        EvalErr::Runtime(s)
375    }
376}
377
378type EvalResult = Result<Value, EvalErr>;
379
380// GfxState is now defined in crate::gfx — see src/gfx/mod.rs.
381
382// ─── SVG writer ───────────────────────────────────────────────────────────────
383
384struct SvgWriter {
385    path: String,
386    width: f64,
387    height: f64,
388    elements: Vec<String>,
389}
390
391impl SvgWriter {
392    fn new(path: String, width: f64, height: f64) -> Self {
393        let bg = format!("<rect width=\"{width}\" height=\"{height}\" fill=\"#0a0a0a\"/>");
394        Self { path, width, height, elements: vec![bg] }
395    }
396
397    fn save(&self) -> std::io::Result<()> {
398        let w = self.width;
399        let h = self.height;
400        let mut out = format!(
401            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
402             <svg xmlns=\"http://www.w3.org/2000/svg\" \
403             width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\">\n"
404        );
405        for elem in &self.elements {
406            out.push_str("  ");
407            out.push_str(elem);
408            out.push('\n');
409        }
410        out.push_str("</svg>\n");
411        // Create parent directory if it doesn't exist.
412        if let Some(parent) = std::path::Path::new(&self.path).parent() {
413            if !parent.as_os_str().is_empty() {
414                let _ = std::fs::create_dir_all(parent);
415            }
416        }
417        std::fs::write(&self.path, out.as_bytes())
418    }
419}
420
421fn hsl_to_hex(h: f64, s: f64, l: f64) -> String {
422    let s = s / 100.0;
423    let l = l / 100.0;
424    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
425    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
426    let m = l - c / 2.0;
427    let (r1, g1, b1) = if h < 60.0 {
428        (c, x, 0.0)
429    } else if h < 120.0 {
430        (x, c, 0.0)
431    } else if h < 180.0 {
432        (0.0, c, x)
433    } else if h < 240.0 {
434        (0.0, x, c)
435    } else if h < 300.0 {
436        (x, 0.0, c)
437    } else {
438        (c, 0.0, x)
439    };
440    let r = ((r1 + m) * 255.0).round() as u8;
441    let g = ((g1 + m) * 255.0).round() as u8;
442    let b = ((b1 + m) * 255.0).round() as u8;
443    format!("#{r:02x}{g:02x}{b:02x}")
444}
445
446// ─── Procedural texture helpers ───────────────────────────────────────────────
447
448fn tex_hash(x: i32, y: i32, seed: u32) -> f32 {
449    let mut h = (x as u32)
450        .wrapping_add((y as u32).wrapping_mul(2654435769))
451        .wrapping_add(seed.wrapping_mul(1234567891));
452    h ^= h >> 16;
453    h = h.wrapping_mul(0x45d9f3b);
454    h ^= h >> 16;
455    h as f32 / u32::MAX as f32
456}
457
458fn tex_vnoise(x: f32, y: f32, seed: u32) -> f32 {
459    let xi = x.floor() as i32;
460    let yi = y.floor() as i32;
461    let sm = |t: f32| t * t * (3.0 - 2.0 * t);
462    let xf = sm(x - xi as f32);
463    let yf = sm(y - yi as f32);
464    let a = tex_hash(xi, yi, seed);
465    let b = tex_hash(xi + 1, yi, seed);
466    let c = tex_hash(xi, yi + 1, seed);
467    let d = tex_hash(xi + 1, yi + 1, seed);
468    a + (b - a) * xf + (c - a) * yf + (a - b - c + d) * xf * yf
469}
470
471fn tex_fbm(x: f32, y: f32, octaves: u32, seed: u32) -> f32 {
472    let mut v = 0.0f32;
473    let mut amp = 0.5f32;
474    let mut f = 1.0f32;
475    for i in 0..octaves {
476        v += tex_vnoise(x * f, y * f, seed.wrapping_add(i * 7919)) * amp;
477        amp *= 0.5;
478        f *= 2.0;
479    }
480    v
481}
482
483fn tex_palette(name: &str, t: f32) -> [f32; 3] {
484    let (a, b, c, d): ([f32; 3], [f32; 3], [f32; 3], [f32; 3]) = match name {
485        "fire" => (
486            [0.8, 0.4, 0.1],
487            [0.7, 0.3, 0.1],
488            [1.0, 0.5, 0.3],
489            [0.0, 0.5, 0.8],
490        ),
491        "ocean" => (
492            [0.1, 0.4, 0.7],
493            [0.3, 0.3, 0.4],
494            [0.8, 1.0, 0.5],
495            [0.3, 0.0, 0.6],
496        ),
497        "psychedelic" => (
498            [0.5, 0.5, 0.5],
499            [0.8, 0.8, 0.8],
500            [1.0, 1.3, 0.7],
501            [0.0, 0.15, 0.3],
502        ),
503        "neon" => (
504            [0.5, 0.5, 0.5],
505            [0.5, 0.5, 0.5],
506            [2.0, 1.0, 0.0],
507            [0.5, 0.2, 0.25],
508        ),
509        "forest" => (
510            [0.3, 0.5, 0.2],
511            [0.2, 0.3, 0.1],
512            [1.0, 0.5, 0.8],
513            [0.1, 0.3, 0.6],
514        ),
515        _ => (
516            [0.5, 0.5, 0.5],
517            [0.5, 0.5, 0.5],
518            [1.0, 1.0, 1.0],
519            [0.0, 0.333, 0.667],
520        ),
521    };
522    [0, 1, 2]
523        .map(|i| (a[i] + b[i] * (std::f32::consts::TAU * (c[i] * t + d[i])).cos()).clamp(0.0, 1.0))
524}
525
526/// Map a physical key to a typed character for ling-ui text input (lowercase).
527#[cfg(not(target_arch = "wasm32"))]
528fn key_char(k: minifb::Key) -> Option<char> {
529    use minifb::Key::*;
530    Some(match k {
531        A => 'a',
532        B => 'b',
533        C => 'c',
534        D => 'd',
535        E => 'e',
536        F => 'f',
537        G => 'g',
538        H => 'h',
539        I => 'i',
540        J => 'j',
541        K => 'k',
542        L => 'l',
543        M => 'm',
544        N => 'n',
545        O => 'o',
546        P => 'p',
547        Q => 'q',
548        R => 'r',
549        S => 's',
550        T => 't',
551        U => 'u',
552        V => 'v',
553        W => 'w',
554        X => 'x',
555        Y => 'y',
556        Z => 'z',
557        Key0 => '0',
558        Key1 => '1',
559        Key2 => '2',
560        Key3 => '3',
561        Key4 => '4',
562        Key5 => '5',
563        Key6 => '6',
564        Key7 => '7',
565        Key8 => '8',
566        Key9 => '9',
567        Space => ' ',
568        Minus => '-',
569        Period => '.',
570        _ => return None,
571    })
572}
573
574/// Lowercase-hex encode bytes (the wire format for crypto values in Ling).
575fn hex_encode(bytes: &[u8]) -> String {
576    let mut s = String::with_capacity(bytes.len() * 2);
577    for b in bytes {
578        s.push_str(&format!("{b:02x}"));
579    }
580    s
581}
582
583/// Decode a `ling convert` blob: base64 → zlib-inflate → raw little-endian bytes.
584#[cfg(not(target_arch = "wasm32"))]
585fn decode_blob(s: &str) -> Result<Vec<u8>, String> {
586    use base64::Engine as _;
587    use std::io::Read as _;
588    let comp = base64::engine::general_purpose::STANDARD
589        .decode(s.trim())
590        .map_err(|e| format!("base64: {e}"))?;
591    let mut out = Vec::new();
592    flate2::read::ZlibDecoder::new(&comp[..])
593        .read_to_end(&mut out)
594        .map_err(|e| format!("inflate: {e}"))?;
595    Ok(out)
596}
597
598/// Decode a lowercase/uppercase hex string to bytes (ignores malformed tail).
599#[cfg(not(target_arch = "wasm32"))]
600fn hex_decode(s: &str) -> Vec<u8> {
601    let s = s.trim();
602    (0..s.len() / 2)
603        .filter_map(|i| u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok())
604        .collect()
605}
606
607/// Decode a hex string into a fixed 32-byte key (zero-padded / truncated).
608#[cfg(not(target_arch = "wasm32"))]
609fn hex_to_32(s: &str) -> [u8; 32] {
610    let v = hex_decode(s);
611    let mut out = [0u8; 32];
612    let n = v.len().min(32);
613    out[..n].copy_from_slice(&v[..n]);
614    out
615}
616
617fn tex_rgb(r: f32, g: f32, b: f32) -> u32 {
618    ((r * 255.0) as u32) << 16 | ((g * 255.0) as u32) << 8 | (b * 255.0) as u32
619}
620
621// ─── 3D Perlin Noise (Improved Perlin 2002) ───────────────────────────────────
622
623const PERM: [u8; 512] = [
624    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
625    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
626    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
627    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
628    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
629    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
630    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
631    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
632    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
633    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
634    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180,
635    198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185,
636    134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223,
637    140, 161, 137, 13, 191, 230, 66, 104, 153, 199, 167, 147, 99, 179, 92,
638    // Duplicate for wrap-around indexing
639    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
640    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
641    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
642    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
643    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
644    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
645    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
646    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
647    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
648    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
649    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174,
650];
651
652fn fade(t: f32) -> f32 {
653    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
654}
655
656fn grad(hash: u8, x: f32, y: f32, z: f32) -> f32 {
657    let h = hash & 15;
658    let u = if h < 8 { x } else { y };
659    let v = if h < 8 { y } else { z };
660    (if (h & 1) == 0 { u } else { -u }) + (if (h & 2) == 0 { v } else { -v })
661}
662
663fn perlin3(x: f32, y: f32, z: f32) -> f32 {
664    let xi = (x.floor() as i32) & 255;
665    let yi = (y.floor() as i32) & 255;
666    let zi = (z.floor() as i32) & 255;
667
668    let xf = x - x.floor();
669    let yf = y - y.floor();
670    let zf = z - z.floor();
671
672    let u = fade(xf);
673    let v = fade(yf);
674    let w = fade(zf);
675
676    let p0 = PERM[xi as usize] as usize;
677    let p1 = PERM[((xi + 1) & 255) as usize] as usize;
678    let pa = PERM[(p0 + yi as usize) & 255] as usize;
679    let pb = PERM[(p0 + ((yi + 1) & 255) as usize) & 255] as usize;
680    let pc = PERM[(p1 + yi as usize) & 255] as usize;
681    let pd = PERM[(p1 + ((yi + 1) & 255) as usize) & 255] as usize;
682
683    let g000 = grad(PERM[(pa + zi as usize) & 255], xf, yf, zf);
684    let g001 = grad(
685        PERM[(pa + ((zi + 1) & 255) as usize) & 255],
686        xf,
687        yf,
688        zf - 1.0,
689    );
690    let g010 = grad(PERM[(pb + zi as usize) & 255], xf, yf - 1.0, zf);
691    let g011 = grad(
692        PERM[(pb + ((zi + 1) & 255) as usize) & 255],
693        xf,
694        yf - 1.0,
695        zf - 1.0,
696    );
697    let g100 = grad(PERM[(pc + zi as usize) & 255], xf - 1.0, yf, zf);
698    let g101 = grad(
699        PERM[(pc + ((zi + 1) & 255) as usize) & 255],
700        xf - 1.0,
701        yf,
702        zf - 1.0,
703    );
704    let g110 = grad(PERM[(pd + zi as usize) & 255], xf - 1.0, yf - 1.0, zf);
705    let g111 = grad(
706        PERM[(pd + ((zi + 1) & 255) as usize) & 255],
707        xf - 1.0,
708        yf - 1.0,
709        zf - 1.0,
710    );
711
712    let l00 = g000 + u * (g100 - g000);
713    let l01 = g001 + u * (g101 - g001);
714    let l10 = g010 + u * (g110 - g010);
715    let l11 = g011 + u * (g111 - g011);
716
717    let l0 = l00 + v * (l10 - l00);
718    let l1 = l01 + v * (l11 - l01);
719
720    l0 + w * (l1 - l0)
721}
722
723fn fast_rand_f64(state: &mut u64) -> f64 {
724    *state = state
725        .wrapping_mul(6364136223846793005)
726        .wrapping_add(1442695040888963407);
727    ((*state >> 32) as u32) as f64 / 4294967296.0
728}
729
730// ─── Circle Drawing Primitives ────────────────────────────────────────────────
731
732/// Write one pixel into the framebuffer (normal or additive blend).
733#[inline]
734fn put_px(buf: &mut [u32], idx: usize, color: u32, blend: u8) {
735    if idx >= buf.len() {
736        return;
737    }
738    if blend == 0 {
739        buf[idx] = color;
740    } else {
741        let old = buf[idx];
742        let r = (((old >> 16) & 255) + ((color >> 16) & 255)).min(255);
743        let g = (((old >> 8) & 255) + ((color >> 8) & 255)).min(255);
744        let b = ((old & 255) + (color & 255)).min(255);
745        buf[idx] = (r << 16) | (g << 8) | b;
746    }
747}
748
749/// Pack three float colour channels (0..255) into a 0x00RRGGBB word, clamping.
750#[inline]
751fn rgb(r: f64, g: f64, b: f64) -> u32 {
752    let r = (r as i64).clamp(0, 255) as u32;
753    let g = (g as i64).clamp(0, 255) as u32;
754    let b = (b as i64).clamp(0, 255) as u32;
755    (r << 16) | (g << 8) | b
756}
757
758fn draw_circle_outline(
759    buf: &mut [u32],
760    w: i32,
761    h: i32,
762    cx: i32,
763    cy: i32,
764    r: i32,
765    color: u32,
766    blend: u8,
767) {
768    let r = r.clamp(0, 20000); // guard against overflow / runaway from tiny depths
769    if r == 0 {
770        return;
771    }
772    let mut x = 0;
773    let mut y = r;
774    let mut d = 3 - 2 * r;
775    while x <= y {
776        plot_circle_points(buf, w, h, cx, cy, x, y, color, blend);
777        if d < 0 {
778            d += 4 * x + 6;
779        } else {
780            d += 4 * (x - y) + 10;
781            y -= 1;
782        }
783        x += 1;
784    }
785}
786
787fn plot_circle_points(
788    buf: &mut [u32],
789    w: i32,
790    h: i32,
791    cx: i32,
792    cy: i32,
793    x: i32,
794    y: i32,
795    color: u32,
796    blend: u8,
797) {
798    let points = [
799        (cx + x, cy + y),
800        (cx - x, cy + y),
801        (cx + x, cy - y),
802        (cx - x, cy - y),
803        (cx + y, cy + x),
804        (cx - y, cy + x),
805        (cx + y, cy - x),
806        (cx - y, cy - x),
807    ];
808    for &(px, py) in &points {
809        if px >= 0 && px < w && py >= 0 && py < h {
810            put_px(buf, (py * w + px) as usize, color, blend);
811        }
812    }
813}
814
815fn draw_circle_filled(
816    buf: &mut [u32],
817    w: i32,
818    h: i32,
819    cx: i32,
820    cy: i32,
821    r: i32,
822    color: u32,
823    blend: u8,
824) {
825    if r <= 0 {
826        return;
827    }
828    for dy in -r..=r {
829        let dx_max = ((r * r - dy * dy) as f64).sqrt() as i32;
830        let py = cy + dy;
831        if py < 0 || py >= h {
832            continue;
833        }
834        for dx in -dx_max..=dx_max {
835            let px = cx + dx;
836            if px >= 0 && px < w {
837                put_px(buf, (py * w + px) as usize, color, blend);
838            }
839        }
840    }
841}
842
843#[cfg(test)]
844mod draw_tests {
845    use super::*;
846
847    #[test]
848    fn filled_circle_actually_writes_pixels() {
849        let mut buf = vec![0u32; 100 * 100];
850        draw_circle_filled(&mut buf, 100, 100, 50, 50, 10, 0xFF00FF, 0);
851        assert_eq!(buf[50 * 100 + 50], 0xFF00FF, "centre pixel must be filled");
852        assert_eq!(buf[0], 0, "far corner must stay clear");
853        let n = buf.iter().filter(|&&p| p != 0).count();
854        assert!(n > 200 && n < 500, "r=10 disc area ≈ 314, got {n}");
855    }
856
857    #[test]
858    fn circle_outline_writes_a_ring() {
859        let mut buf = vec![0u32; 100 * 100];
860        draw_circle_outline(&mut buf, 100, 100, 50, 50, 20, 0x00FF00, 0);
861        assert_eq!(buf[50 * 100 + 50], 0, "outline must NOT fill the centre");
862        assert!(
863            buf.iter().any(|&p| p == 0x00FF00),
864            "outline must draw a ring"
865        );
866    }
867
868    #[test]
869    fn additive_blend_accumulates_channels() {
870        let mut buf = vec![0x202020u32; 1];
871        put_px(&mut buf, 0, 0x404040, 1);
872        assert_eq!(buf[0], 0x606060);
873    }
874}
875
876// ─── Interpreter ─────────────────────────────────────────────────────────────
877
878/// Customizable colour palette for the vector UI toolkit (packed 0x00RRGGBB).
879/// `ui_theme(...)` sets it; every widget falls back to these and accepts a
880/// trailing `r,g,b` override.
881#[derive(Clone, Copy)]
882pub struct UiTheme {
883    pub primary: u32,
884    pub accent: u32,
885    pub track: u32,
886    pub warn: u32,
887    pub text: u32,
888    pub bg: u32,
889}
890
891impl Default for UiTheme {
892    fn default() -> Self {
893        Self {
894            primary: 0x00D2FF, // holographic cyan
895            accent: 0x28FFB4,  // mint
896            track: 0x2C3E64,   // dim slate
897            warn: 0xFF5A5A,    // alert red
898            text: 0xBEEBFF,    // pale cyan
899            bg: 0x0A1018,      // near-black panel
900        }
901    }
902}
903
904#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
905pub struct Interpreter {
906    globals: HashMap<String, Expr>,
907    /// Globals evaluated ONCE at program start (immutable after load).
908    /// call_named clones this instead of re-evaluating every global per call.
909    global_seed: Env,
910    functions: FxHashMap<String, Rc<FnDef>>,
911    /// `form` definitions: struct name → ordered field names.
912    pub(crate) structs: HashMap<String, Vec<String>>,
913    /// `choose` variants: variant name (bare and `Enum::Variant`) → (enum name, arity).
914    enum_variants: HashMap<String, (String, usize)>,
915    _modules: HashMap<String, Vec<FnDef>>,
916    gfx: RefCell<GfxState>,
917    svg: RefCell<Option<SvgWriter>>,
918    /// Directory of the primary source file, for relative `use` resolution.
919    pub source_dir: Option<std::path::PathBuf>,
920    /// Files already loaded — prevents circular imports.
921    loaded_files: std::collections::HashSet<String>,
922    /// Optional audio engine — `None` if no audio device is available.
923    #[cfg(not(target_arch = "wasm32"))]
924    audio: Option<AudioEngine>,
925    #[cfg(not(target_arch = "wasm32"))]
926    fft: RefCell<FftAnalyzer>,
927    fft_bands_cache: RefCell<Vec<f32>>,
928    /// Real-time clock — seconds since Unix epoch at startup (f64 works on both
929    /// native and wasm32; Instant is not available on wasm32).
930    start_time_secs: f64,
931    /// Frame counter — incremented at each present()
932    frame_num: u64,
933    /// Target framerate used to pace `present()` on wasm32.
934    #[cfg(target_arch = "wasm32")]
935    wasm_target_fps: f64,
936    /// Next frame deadline (ms since epoch) for wasm frame pacing.
937    #[cfg(target_arch = "wasm32")]
938    wasm_next_present_ms: f64,
939    /// Random state for rand() builtin (xorshift)
940    rand_state: u64,
941    /// Microphone input (Phase 1 audio reactivity)
942    #[cfg(not(target_arch = "wasm32"))]
943    mic: Option<ling_mic::MicInput>,
944    /// Persistent KEM keypairs (knot / hybrid identities), referenced by handle.
945    #[cfg(not(target_arch = "wasm32"))]
946    crypto_ids: Vec<ling_crypto::KnotIdentity>,
947    /// Editable text-input buffer (ling-ui text fields).
948    text_buffer: String,
949    /// Frame counter for record_frame().
950    record_n: u32,
951    /// Accumulated microphone samples (for turning sound into crypto donuts).
952    #[cfg(not(target_arch = "wasm32"))]
953    mic_buffer: Vec<f32>,
954    /// Loaded vector UI fonts, referenced by handle (index) from `font_load`.
955    #[cfg(not(target_arch = "wasm32"))]
956    fonts: Vec<ling_graphics::VectorFont>,
957    /// Customizable UI colour palette (set via `ui_theme`).
958    ui_theme: UiTheme,
959    /// Left-mouse state on the previous frame — for widget click-edge detection.
960    mouse_was_down: bool,
961    /// Live music engine (decode playback + GM synth) — lazily initialised.
962    #[cfg(not(target_arch = "wasm32"))]
963    music: Option<ling_music::MusicEngine>,
964    #[cfg(not(target_arch = "wasm32"))]
965    music_init: bool,
966    /// Decoded tracks (for analysis + playback), by `music_load` handle.
967    tracks: Vec<ling_music::DecodedAudio>,
968    /// Parsed `.lrc` lyrics, by `music_lrc` handle.
969    lyrics: Vec<ling_music::Lyrics>,
970    /// Parsed MIDI songs, by `music_midi_load` handle.
971    midis: Vec<ling_music::MidiSong>,
972    /// Soft bodies (deformable balls), by `soft_ball` handle.
973    soft_bodies: Vec<ling_physics::soft::SoftBody>,
974    /// Rigid-body world (angular dynamics), shared by `rb_*`.
975    rigid_world: ling_physics::rigid::PhysicsWorld,
976    /// Liquid grids (water/oil), by `liquid_new` handle.
977    liquids: Vec<ling_physics::liquid::LiquidGrid>,
978    meshes: Vec<crate::gfx::shapes::ColorMesh>,
979    /// Active cinematic dialog box (Ocarina/Majora-style), if any.
980    dialog: Option<ling_game::dialog::Dialog>,
981    /// Dialog highlight colours by role: text, name, place, item (0x00RRGGBB).
982    dialog_colors: [u32; 4],
983    /// Active user-function call frames (names), for error tracebacks.
984    frames: Vec<String>,
985    /// Snapshot of `frames` captured the moment a runtime error first arose
986    /// (the deepest call). Consumed by `take_error_trace`.
987    error_trace: Option<Vec<String>>,
988    /// Unified input (gamepads/joysticks/VR/touch via the ling-input
989    /// "Sensorium"). Lazily initialised on the first `pad_*` builtin call;
990    /// `None` if no native input backend is available.
991    #[cfg(not(target_arch = "wasm32"))]
992    input: RefCell<Option<InputState>>,
993}
994
995/// Live gamepad input state: a ling-input hub fed by the native `gilrs` backend.
996#[cfg(not(target_arch = "wasm32"))]
997struct InputState {
998    sensorium: ling_input::Sensorium,
999    backend: ling_input::backend::GilrsBackend,
1000}
1001
1002impl Interpreter {
1003    pub fn new() -> Self {
1004        #[cfg(not(target_arch = "wasm32"))]
1005        let audio = AudioEngine::new()
1006            .map_err(|e| eprintln!("audio init failed (no sound): {e}"))
1007            .ok();
1008        Self {
1009            globals: HashMap::new(),
1010            global_seed: new_env(),
1011            functions: FxHashMap::default(),
1012            structs: HashMap::new(),
1013            enum_variants: HashMap::new(),
1014            _modules: HashMap::new(),
1015            gfx: RefCell::new(GfxState::new()),
1016            svg: RefCell::new(None),
1017            source_dir: None,
1018            loaded_files: std::collections::HashSet::new(),
1019            #[cfg(not(target_arch = "wasm32"))]
1020            audio,
1021            #[cfg(not(target_arch = "wasm32"))]
1022            fft: RefCell::new(FftAnalyzer::new(2048, 44100)),
1023            fft_bands_cache: RefCell::new(vec![]),
1024            start_time_secs: crate::runtime::now_secs(),
1025            frame_num: 0,
1026            #[cfg(target_arch = "wasm32")]
1027            wasm_target_fps: 60.0,
1028            #[cfg(target_arch = "wasm32")]
1029            wasm_next_present_ms: 0.0,
1030            rand_state: 0x123456789ABCDEF,
1031            #[cfg(not(target_arch = "wasm32"))]
1032            mic: None,
1033            #[cfg(not(target_arch = "wasm32"))]
1034            crypto_ids: Vec::new(),
1035            text_buffer: String::new(),
1036            record_n: 0,
1037            #[cfg(not(target_arch = "wasm32"))]
1038            mic_buffer: Vec::new(),
1039            #[cfg(not(target_arch = "wasm32"))]
1040            fonts: Vec::new(),
1041            ui_theme: UiTheme::default(),
1042            mouse_was_down: false,
1043            #[cfg(not(target_arch = "wasm32"))]
1044            music: None,
1045            #[cfg(not(target_arch = "wasm32"))]
1046            music_init: false,
1047            tracks: Vec::new(),
1048            lyrics: Vec::new(),
1049            midis: Vec::new(),
1050            soft_bodies: Vec::new(),
1051            rigid_world: ling_physics::rigid::PhysicsWorld::new(),
1052            liquids: Vec::new(),
1053            meshes: Vec::new(),
1054            dialog: None,
1055            dialog_colors: [0xE6F2FF, 0xFFD24A, 0x4AD2FF, 0x6CFF8C], // text · name · place · item
1056            frames: Vec::new(),
1057            error_trace: None,
1058            #[cfg(not(target_arch = "wasm32"))]
1059            input: RefCell::new(None),
1060        }
1061    }
1062
1063    /// Lazily initialise the input system and advance it one frame; returns the
1064    /// number of connected gamepads. Call once per game-loop iteration (like a
1065    /// window update) before reading `pad_*` state.
1066    #[cfg(not(target_arch = "wasm32"))]
1067    fn pad_poll(&self) -> usize {
1068        let mut slot = self.input.borrow_mut();
1069        if slot.is_none() {
1070            match ling_input::backend::GilrsBackend::new() {
1071                Ok(backend) => {
1072                    *slot = Some(InputState { sensorium: ling_input::Sensorium::new(4), backend });
1073                },
1074                Err(_) => return 0,
1075            }
1076        }
1077        let st = slot.as_mut().unwrap();
1078        st.sensorium.begin_frame();
1079        st.sensorium.pump(&mut st.backend);
1080        st.sensorium.update(1.0 / 60.0);
1081        st.sensorium.devices.count()
1082    }
1083
1084    /// Read player `slot`'s gamepad with `f`, or return `default` if there is no
1085    /// input system / no such pad.
1086    #[cfg(not(target_arch = "wasm32"))]
1087    fn with_pad<T>(&self, slot: usize, default: T, f: impl FnOnce(&ling_input::Gamepad) -> T) -> T {
1088        let inp = self.input.borrow();
1089        match inp.as_ref().and_then(|s| s.sensorium.player(slot)) {
1090            Some(p) => f(p),
1091            None => default,
1092        }
1093    }
1094
1095    /// Take the call-stack snapshot captured at the deepest runtime error, if any.
1096    /// Frames are ordered outermost-first (entry point first, failing call last).
1097    pub fn take_error_trace(&mut self) -> Vec<String> {
1098        self.error_trace.take().unwrap_or_default()
1099    }
1100
1101    #[cfg(target_arch = "wasm32")]
1102    fn wasm_pace_frame(&mut self) {
1103        let fps = self.wasm_target_fps.max(1.0);
1104        let frame_ms = 1000.0 / fps;
1105        let now = js_sys::Date::now();
1106        if self.wasm_next_present_ms <= 0.0 {
1107            self.wasm_next_present_ms = now + frame_ms;
1108            return;
1109        }
1110
1111        let wait_ms = (self.wasm_next_present_ms - now).floor() as i32;
1112        if wait_ms > 0 {
1113            wasm_sleep_ms(wait_ms);
1114        }
1115
1116        let after = js_sys::Date::now();
1117        if after > self.wasm_next_present_ms + frame_ms * 3.0 {
1118            self.wasm_next_present_ms = after + frame_ms;
1119        } else {
1120            self.wasm_next_present_ms += frame_ms;
1121        }
1122    }
1123
1124    /// Run `body`, recording `name` as a call frame and snapshotting the stack on
1125    /// the first runtime error so a traceback can be reported.
1126    fn framed<T, F>(&mut self, name: &str, body: F) -> Result<T, EvalErr>
1127    where
1128        F: FnOnce(&mut Self) -> Result<T, EvalErr>,
1129    {
1130        self.frames.push(name.to_string());
1131        let result = body(self);
1132        if matches!(result, Err(EvalErr::Runtime(_))) && self.error_trace.is_none() {
1133            self.error_trace = Some(self.frames.clone());
1134        }
1135        self.frames.pop();
1136        result
1137    }
1138
1139    /// Render the active dialog box: beveled frame + dark fill, then the visible
1140    /// (typewriter-revealed) text word-wrapped with colour-coded runs, plus a
1141    /// blinking advance arrow once the page is fully typed.
1142    #[cfg(not(target_arch = "wasm32"))]
1143    fn render_dialog(&mut self, x: f32, y: f32, w: f32, h: f32, font: i64, t: f32) {
1144        let (runs, typing) = match &self.dialog {
1145            Some(d) if !d.is_closed() => {
1146                let runs: Vec<(String, usize, bool)> = d
1147                    .visible_runs()
1148                    .into_iter()
1149                    .map(|r| (r.text, r.role.index(), r.newline_before))
1150                    .collect();
1151                (runs, d.is_typing())
1152            },
1153            _ => return,
1154        };
1155        let colors = self.dialog_colors;
1156        // ── frame + fill ──
1157        let b = 12.0;
1158        let corners: Vec<[f32; 2]> = vec![
1159            [x + b, y],
1160            [x + w - b, y],
1161            [x + w, y + b],
1162            [x + w, y + h - b],
1163            [x + w - b, y + h],
1164            [x + b, y + h],
1165            [x, y + h - b],
1166            [x, y + b],
1167            [x + b, y],
1168        ];
1169        {
1170            let mut gfx = self.gfx.borrow_mut();
1171            let (bw, bh) = (gfx.width, gfx.height);
1172            crate::gfx::raster::fill_contours_aa(
1173                &mut gfx.buffer,
1174                bw,
1175                bh,
1176                0x0A1018,
1177                false,
1178                std::slice::from_ref(&corners),
1179            );
1180            for seg in corners.windows(2) {
1181                crate::gfx::raster::draw_line_aa(
1182                    &mut gfx.buffer,
1183                    bw,
1184                    bh,
1185                    0x00D2FF,
1186                    false,
1187                    seg[0][0],
1188                    seg[0][1],
1189                    seg[1][0],
1190                    seg[1][1],
1191                );
1192            }
1193        }
1194        // ── word-wrapped, colour-coded text ──
1195        let px = 22.0f32;
1196        let pad = 20.0f32;
1197        let line_h = px * 1.45;
1198        let mut cx = x + pad;
1199        let mut cy = y + pad;
1200        let use_font = font >= 0 && (font as usize) < self.fonts.len();
1201        for (text, role, nl) in &runs {
1202            if *nl {
1203                cx = x + pad;
1204                cy += line_h;
1205            }
1206            for word in text.split_inclusive(' ') {
1207                let wpx = if use_font {
1208                    self.fonts[font as usize].measure(word, px)
1209                } else {
1210                    ling_ui::holo::text_width(word, px * 0.6, px * 0.24)
1211                };
1212                if cx + wpx > x + w - pad && cx > x + pad + 1.0 {
1213                    cx = x + pad;
1214                    cy += line_h;
1215                }
1216                if cy + line_h > y + h {
1217                    break;
1218                }
1219                let col = colors[(*role).min(3)];
1220                if use_font {
1221                    let glyphs = self.font_layout_2d_glyphs(font as usize, cx, cy, px, word);
1222                    let mut gfx = self.gfx.borrow_mut();
1223                    let (bw, bh, add) = (gfx.width, gfx.height, gfx.blend == 1);
1224                    for contours in &glyphs {
1225                        crate::gfx::raster::fill_contours_aa(
1226                            &mut gfx.buffer,
1227                            bw,
1228                            bh,
1229                            col,
1230                            add,
1231                            contours,
1232                        );
1233                    }
1234                } else {
1235                    let segs = ling_ui::holo::text_lines(word, cx, cy, px * 0.6, px, px * 0.24);
1236                    let mut gfx = self.gfx.borrow_mut();
1237                    let (bw, bh) = (gfx.width, gfx.height);
1238                    for s in segs {
1239                        draw_line(&mut gfx.buffer, bw, bh, col, s[0], s[1], s[2], s[3]);
1240                    }
1241                }
1242                cx += wpx;
1243            }
1244        }
1245        // ── blinking advance arrow when fully typed ──
1246        if !typing && (t * 3.0).sin() > 0.0 {
1247            let ax = x + w - 26.0;
1248            let ay = y + h - 22.0;
1249            let mut gfx = self.gfx.borrow_mut();
1250            let (bw, bh) = (gfx.width, gfx.height);
1251            crate::gfx::raster::fill_contours_aa(
1252                &mut gfx.buffer,
1253                bw,
1254                bh,
1255                0x00D2FF,
1256                false,
1257                std::slice::from_ref(&vec![
1258                    [ax - 7.0, ay],
1259                    [ax + 7.0, ay],
1260                    [ax, ay + 9.0],
1261                    [ax - 7.0, ay],
1262                ]),
1263            );
1264        }
1265    }
1266
1267    /// Lazily start the music engine on first use (playback/synth need a device;
1268    /// analysis/decoding do not). Returns `false` if no audio device is available.
1269    #[cfg(not(target_arch = "wasm32"))]
1270    fn ensure_music(&mut self) -> bool {
1271        if self.music.is_some() {
1272            return true;
1273        }
1274        if self.music_init {
1275            return false;
1276        }
1277        self.music_init = true;
1278        match ling_music::MusicEngine::new() {
1279            Ok(m) => {
1280                self.music = Some(m);
1281                true
1282            },
1283            Err(e) => {
1284                eprintln!("music engine init failed (no music playback): {e}");
1285                false
1286            },
1287        }
1288    }
1289
1290    #[cfg(target_arch = "wasm32")]
1291    fn wasm_resolve_source_path(&self, path: &str) -> String {
1292        let p = path.trim();
1293        if p.is_empty() {
1294            return String::new();
1295        }
1296        if p.contains("://") || p.starts_with('/') || p.starts_with("./") || p.starts_with("../") {
1297            return p.to_string();
1298        }
1299        if let Some(d) = &self.source_dir {
1300            let base = d.to_string_lossy().replace('\\', "/");
1301            if !base.is_empty() {
1302                return format!(
1303                    "{}/{}",
1304                    base.trim_end_matches('/'),
1305                    p.trim_start_matches("./")
1306                );
1307            }
1308        }
1309        p.to_string()
1310    }
1311
1312    #[cfg(target_arch = "wasm32")]
1313    fn wasm_music_builtin(&mut self, name: &str, args: &[Value]) -> Result<Option<Value>, EvalErr> {
1314        match name {
1315            // music_load(path) -> track handle (decode from fetched bytes)
1316            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" => {
1317                let path = self.arg_str(args, 0, "");
1318                let resolved = self.wasm_resolve_source_path(&path);
1319                match wasm_fetch_bytes(&resolved)
1320                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
1321                {
1322                    Ok(t) => {
1323                        let id = self.tracks.len();
1324                        self.tracks.push(t);
1325                        return Ok(Some(Value::Number(id as f64)));
1326                    },
1327                    Err(e) => {
1328                        eprintln!("music_load failed ({path}): {e}");
1329                        return Ok(Some(Value::Number(-1.0)));
1330                    },
1331                }
1332            },
1333            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" => {
1334                let id = self.arg_num(args, 0, 0.0)? as i64;
1335                let d = self
1336                    .tracks
1337                    .get(id as usize)
1338                    .map(|t| t.duration)
1339                    .unwrap_or(0.0);
1340                return Ok(Some(Value::Number(d as f64)));
1341            },
1342            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" => {
1343                let id = self.arg_num(args, 0, 0.0)? as i64;
1344                let b = self
1345                    .tracks
1346                    .get(id as usize)
1347                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
1348                    .unwrap_or(0.0);
1349                return Ok(Some(Value::Number(b as f64)));
1350            },
1351            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" => {
1352                let id = self.arg_num(args, 0, 0.0)? as i64;
1353                let k = self
1354                    .tracks
1355                    .get(id as usize)
1356                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
1357                    .unwrap_or_default();
1358                return Ok(Some(Value::Str(k)));
1359            },
1360            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" => {
1361                let id = self.arg_num(args, 0, 0.0)? as i64;
1362                let v = self
1363                    .tracks
1364                    .get(id as usize)
1365                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
1366                    .unwrap_or_default();
1367                return Ok(Some(Value::List(
1368                    v.into_iter().map(|x| Value::Number(x as f64)).collect(),
1369                )));
1370            },
1371            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" => {
1372                let id = self.arg_num(args, 0, 0.0)? as i64;
1373                let beats = self
1374                    .tracks
1375                    .get(id as usize)
1376                    .map(|t| {
1377                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
1378                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
1379                    })
1380                    .unwrap_or_default();
1381                return Ok(Some(Value::List(
1382                    beats.into_iter().map(|x| Value::Number(x as f64)).collect(),
1383                )));
1384            },
1385            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" => {
1386                let path = self.arg_str(args, 0, "");
1387                let resolved = self.wasm_resolve_source_path(&path);
1388                match wasm_fetch_text(&resolved) {
1389                    Ok(text) => {
1390                        let id = self.lyrics.len();
1391                        self.lyrics.push(ling_music::Lyrics::parse(&text));
1392                        return Ok(Some(Value::Number(id as f64)));
1393                    },
1394                    Err(e) => {
1395                        eprintln!("music_lrc failed ({path}): {e}");
1396                        return Ok(Some(Value::Number(-1.0)));
1397                    },
1398                }
1399            },
1400            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" => {
1401                let id = self.arg_num(args, 0, 0.0)? as i64;
1402                let t = self.arg_num(args, 1, 0.0)? as f32;
1403                let line = self
1404                    .lyrics
1405                    .get(id as usize)
1406                    .map(|l| l.line_at(t).to_string())
1407                    .unwrap_or_default();
1408                return Ok(Some(Value::Str(line)));
1409            },
1410            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" => {
1411                let path = self.arg_str(args, 0, "");
1412                let resolved = self.wasm_resolve_source_path(&path);
1413                match wasm_fetch_bytes(&resolved)
1414                    .and_then(|bytes| ling_music::midi::from_bytes(&bytes).map_err(|e| e.to_string()))
1415                {
1416                    Ok(m) => {
1417                        let id = self.midis.len();
1418                        self.midis.push(m);
1419                        return Ok(Some(Value::Number(id as f64)));
1420                    },
1421                    Err(e) => {
1422                        eprintln!("music_midi_load failed ({path}): {e}");
1423                        return Ok(Some(Value::Number(-1.0)));
1424                    },
1425                }
1426            },
1427            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" => {
1428                let id = self.arg_num(args, 0, 0.0)? as i64;
1429                let n = self
1430                    .midis
1431                    .get(id as usize)
1432                    .map(|m| m.notes.len())
1433                    .unwrap_or(0);
1434                return Ok(Some(Value::Number(n as f64)));
1435            },
1436            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" => {
1437                let id = self.arg_num(args, 0, 0.0)? as i64;
1438                let mut out = Vec::new();
1439                if let Some(m) = self.midis.get(id as usize) {
1440                    for n in &m.notes {
1441                        out.push(Value::Number(n.time as f64));
1442                        out.push(Value::Number(n.midi as f64));
1443                    }
1444                }
1445                return Ok(Some(Value::List(out)));
1446            },
1447            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" => {
1448                let id = self.arg_num(args, 0, 0.0)? as i64;
1449                let mut out = Vec::new();
1450                if let Some(m) = self.midis.get(id as usize) {
1451                    for n in &m.notes {
1452                        out.push(Value::Number(n.time as f64));
1453                        out.push(Value::Number(n.midi as f64));
1454                        out.push(Value::Number(n.dur as f64));
1455                    }
1456                }
1457                return Ok(Some(Value::List(out)));
1458            },
1459            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" => {
1460                let delta_ms = self.arg_num(args, 0, 9999.0)? as f32;
1461                return Ok(Some(Value::Number(
1462                    ling_music::Grade::judge(delta_ms).index() as f64,
1463                )));
1464            },
1465            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" => {
1466                let idx = self.arg_num(args, 0, 4.0)? as i32;
1467                return Ok(Some(Value::Str(
1468                    ling_music::Grade::from_index(idx).name().to_string(),
1469                )));
1470            },
1471            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" => {
1472                let hz = self.arg_num(args, 0, 0.0)? as f32;
1473                return Ok(Some(Value::Str(ling_music::note::hz_to_name(hz))));
1474            },
1475            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" => {
1476                let midi = match args.get(0) {
1477                    Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(69),
1478                    Some(Value::Number(n)) => *n as i32,
1479                    _ => 69,
1480                };
1481                return Ok(Some(Value::Number(
1482                    ling_music::note::midi_to_hz(midi as f32) as f64,
1483                )));
1484            },
1485            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" => {
1486                let hz = self.arg_num(args, 0, 0.0)? as f32;
1487                let target = self.arg_num(args, 1, 0.0)? as f32;
1488                return Ok(Some(Value::Number(
1489                    ling_music::karaoke::pitch_score(hz, target) as f64,
1490                )));
1491            },
1492
1493            // ── Playback ──────────────────────────────────────────────────────
1494            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" => {
1495                let id = self.arg_num(args, 0, 0.0)? as usize;
1496                if let Some(t) = self.tracks.get(id) {
1497                    crate::gfx::audio_web::play_music(id, &t.stereo, t.channels, t.rate, 1.0);
1498                }
1499                return Ok(Some(Value::Unit));
1500            },
1501            "music_pause" | "暂停音乐" | "音楽一時停止" | "음악일시정지" | "หยุดเพลงชั่วคราว"
1502            | "music_stop" | "停止音乐" | "音楽停止" | "음악정지" | "หยุดเพลง" => {
1503                let id = self.arg_num(args, 0, 0.0)? as usize;
1504                crate::gfx::audio_web::stop_music(id);
1505                return Ok(Some(Value::Unit));
1506            },
1507            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" => {
1508                // Seek is not straightforward on AudioBufferSourceNode; no-op for now.
1509                return Ok(Some(Value::Unit));
1510            },
1511            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" => {
1512                return Ok(Some(Value::Number(
1513                    crate::gfx::audio_web::current_music_position(),
1514                )));
1515            },
1516            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" => {
1517                let vol = self.arg_num(args, 0, 0.8)? as f32;
1518                // Apply to the most-recently started slot (slot 0 is typical).
1519                crate::gfx::audio_web::set_music_volume(0, vol);
1520                return Ok(Some(Value::Unit));
1521            },
1522
1523            // ── FFT bands at current playback position ─────────────────────
1524            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" => {
1525                let id = self.arg_num(args, 0, 0.0)? as usize;
1526                let nbands = self.arg_num(args, 1, 16.0)? as usize;
1527                let pos = crate::gfx::audio_web::current_music_position() as f32;
1528                let bands = if let Some(t) = self.tracks.get(id) {
1529                    ling_music::analysis::fft_bands_at_pos(&t.mono, t.rate, pos, nbands)
1530                } else {
1531                    vec![0.0f32; nbands]
1532                };
1533                return Ok(Some(Value::List(
1534                    bands.into_iter().map(|x| Value::Number(x as f64)).collect(),
1535                )));
1536            },
1537
1538            _ => {},
1539        }
1540        Ok(None)
1541    }
1542
1543    /// Lay out `text` for font `id` at size `px`, returning every glyph contour as
1544    /// a screen-space polyline (x→right, y→down). `(x, y)` is the text box top-left;
1545    /// the baseline is placed `ascent*px` below it. Curves are flattened to 0.3 px.
1546    #[cfg(not(target_arch = "wasm32"))]
1547    fn font_layout_2d(
1548        &mut self,
1549        id: usize,
1550        x: f32,
1551        y: f32,
1552        px: f32,
1553        text: &str,
1554    ) -> Vec<Vec<[f32; 2]>> {
1555        let mut out = Vec::new();
1556        for g in self.font_layout_2d_glyphs(id, x, y, px, text) {
1557            out.extend(g);
1558        }
1559        out
1560    }
1561
1562    /// Same as [`font_layout_2d`] but grouped per glyph (so a fill can apply the
1563    /// non-zero winding rule within each glyph, preserving interior holes).
1564    #[cfg(not(target_arch = "wasm32"))]
1565    fn font_layout_2d_glyphs(
1566        &mut self,
1567        id: usize,
1568        x: f32,
1569        y: f32,
1570        px: f32,
1571        text: &str,
1572    ) -> Vec<Vec<Vec<[f32; 2]>>> {
1573        let font = &mut self.fonts[id];
1574        let asc = font.ascent();
1575        let tol = 0.3 / px;
1576        let mut pen = 0.0f32;
1577        let mut glyphs = Vec::new();
1578        for ch in text.chars() {
1579            let go = font.glyph_outline(ch, tol);
1580            let mut contours = Vec::with_capacity(go.polylines.len());
1581            for pl in &go.polylines {
1582                let mapped: Vec<[f32; 2]> = pl
1583                    .iter()
1584                    .map(|p| [x + (pen + p[0]) * px, y + (asc - p[1]) * px])
1585                    .collect();
1586                contours.push(mapped);
1587            }
1588            glyphs.push(contours);
1589            pen += go.advance;
1590        }
1591        glyphs
1592    }
1593
1594    /// Register every item (functions, structs, globals) and evaluate the
1595    /// non-`do` globals into `global_seed`, WITHOUT running the entry. Used to
1596    /// prime the JIT's fallback interpreter so cranelift-skipped (oversized)
1597    /// functions can still be interpreted with full access to globals + peers.
1598    pub fn register_program(&mut self, program: &Program) -> Result<(), String> {
1599        for item in &program.items {
1600            self.register_item("", item)?;
1601        }
1602        let mut env = new_env();
1603        let non_do: Vec<_> = self
1604            .globals
1605            .iter()
1606            .filter(|(_, e)| !matches!(e, Expr::Do(_)))
1607            .map(|(k, e)| (k.clone(), e.clone()))
1608            .collect();
1609        let mut pending: Vec<(String, Expr)> = Vec::new();
1610        for (k, expr) in &non_do {
1611            let mut tmp = new_env();
1612            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
1613                env.insert(k.clone(), v);
1614            } else {
1615                pending.push((k.clone(), expr.clone()));
1616            }
1617        }
1618        for (k, expr) in &pending {
1619            let mut tmp = env.clone();
1620            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
1621                env.insert(k.clone(), v);
1622            }
1623        }
1624        self.global_seed = env;
1625        Ok(())
1626    }
1627
1628    pub fn run_program(&mut self, program: &Program) -> Result<(), String> {
1629        self.register_program(program)?;
1630        let entry = self
1631            .find_entry()
1632            .ok_or("no entry point — need `bind start = do {...}` or `ผูก เริ่ม = ทำ {...}`")?;
1633        let mut env = self.global_seed.clone();
1634        self.framed("start", |me| me.eval_expr(&entry, &mut env))
1635            .map(|_| ())
1636            .map_err(|e| match e {
1637                EvalErr::Runtime(s) => s,
1638                EvalErr::Return(_) => "unexpected top-level return".to_string(),
1639                EvalErr::Break => "unexpected break at top level".to_string(),
1640            })
1641    }
1642
1643    fn register_item(&mut self, ns: &str, item: &Item) -> Result<(), String> {
1644        match item {
1645            Item::Bind(name, expr) => {
1646                let key = if ns.is_empty() {
1647                    name.clone()
1648                } else {
1649                    format!("{ns}::{name}")
1650                };
1651                self.globals.insert(key, expr.clone());
1652            },
1653            Item::Fn(def) => {
1654                let key = if ns.is_empty() {
1655                    def.name.clone()
1656                } else {
1657                    format!("{ns}::{}", def.name)
1658                };
1659                self.functions.insert(key, Rc::new(def.clone()));
1660            },
1661            Item::Mod(name, body) => {
1662                let child_ns = if ns.is_empty() {
1663                    name.clone()
1664                } else {
1665                    format!("{ns}::{name}")
1666                };
1667                for child in body {
1668                    self.register_item(&child_ns, child)?;
1669                }
1670            },
1671            Item::TypeAlias(_, _) => {},
1672            Item::Struct(name, fields) => {
1673                self.structs.insert(name.clone(), fields.clone());
1674                if !ns.is_empty() {
1675                    self.structs.insert(format!("{ns}::{name}"), fields.clone());
1676                }
1677            },
1678            Item::Enum(name, variants) => {
1679                for v in variants {
1680                    self.enum_variants
1681                        .insert(v.name.clone(), (name.clone(), v.arity));
1682                    self.enum_variants
1683                        .insert(format!("{name}::{}", v.name), (name.clone(), v.arity));
1684                    if !ns.is_empty() {
1685                        self.enum_variants
1686                            .insert(format!("{ns}::{name}::{}", v.name), (name.clone(), v.arity));
1687                    }
1688                }
1689            },
1690            Item::Use { path, alias } => {
1691                self.load_module(path, alias.as_deref(), ns)?;
1692            },
1693        }
1694        Ok(())
1695    }
1696
1697    /// Resolve `path` relative to `source_dir`, load and parse it, then
1698    /// register all its definitions.  If `alias` is given, every name is
1699    /// prefixed with `<parent_ns>::<alias>`.  Circular imports are silently
1700    /// skipped.
1701    fn load_module(
1702        &mut self,
1703        path: &str,
1704        alias: Option<&str>,
1705        parent_ns: &str,
1706    ) -> Result<(), String> {
1707        // ── Wasm32: no filesystem — use the pre-registered module registry ──
1708        #[cfg(target_arch = "wasm32")]
1709        let (source, sub_dir) = {
1710            // Skip if already loaded (circular import guard)
1711            if self.loaded_files.contains(path) {
1712                return Ok(());
1713            }
1714            self.loaded_files.insert(path.to_string());
1715
1716            let src = crate::runtime::get_wasm_module(path)
1717                .or_else(|| crate::runtime::get_wasm_module(&format!("{}.ling", path)))
1718                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
1719            (src, None::<std::path::PathBuf>)
1720        };
1721
1722        // ── Native: resolve against filesystem ──
1723        #[cfg(not(target_arch = "wasm32"))]
1724        let (source, sub_dir) = {
1725            let base_dir = self
1726                .source_dir
1727                .clone()
1728                .unwrap_or_else(|| std::path::PathBuf::from("."));
1729            let raw = std::path::Path::new(path);
1730            let candidates: Vec<std::path::PathBuf> = vec![
1731                base_dir.join(format!("{}.ling", path)),
1732                base_dir.join(format!("{}.灵", path)),
1733                base_dir.join(format!("{}.령", path)),
1734                base_dir.join(format!("{}.霊", path)),
1735                base_dir.join(format!("{}.ลิง", path)),
1736                base_dir.join(raw),
1737                std::path::PathBuf::from(format!("{}.ling", path)),
1738                std::path::PathBuf::from(path),
1739            ];
1740
1741            let resolved = candidates
1742                .into_iter()
1743                .find(|p| p.exists())
1744                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
1745
1746            let canonical = resolved
1747                .canonicalize()
1748                .unwrap_or_else(|_| resolved.clone())
1749                .to_string_lossy()
1750                .to_string();
1751
1752            // Skip if already loaded (circular import guard)
1753            if self.loaded_files.contains(&canonical) {
1754                return Ok(());
1755            }
1756            self.loaded_files.insert(canonical.clone());
1757
1758            let src = std::fs::read_to_string(&resolved)
1759                .map_err(|e| format!("use: failed to read '{path}': {e}"))?;
1760            let dir = resolved.parent().map(|p| p.to_path_buf());
1761            (src, dir)
1762        };
1763
1764        let program = crate::parser::parse(&source)
1765            .map_err(|e| format!("use: parse error in '{path}': {e}"))?;
1766
1767        // Compute target namespace: parent_ns :: alias (or just alias, or just parent_ns)
1768        let target_ns = match (parent_ns.is_empty(), alias) {
1769            (_, Some(a)) if !parent_ns.is_empty() => format!("{parent_ns}::{a}"),
1770            (_, Some(a)) => a.to_string(),
1771            (false, None) => parent_ns.to_string(),
1772            (true, None) => String::new(),
1773        };
1774
1775        // Save/restore source_dir for nested relative imports
1776        let prev_dir = self.source_dir.clone();
1777        self.source_dir = sub_dir;
1778
1779        for item in &program.items {
1780            self.register_item(&target_ns, item)?;
1781        }
1782
1783        self.source_dir = prev_dir;
1784        Ok(())
1785    }
1786
1787    fn find_entry(&self) -> Option<Expr> {
1788        // Known entry-point names across supported human languages.
1789        for key in crate::entry::ENTRY_NAMES {
1790            if let Some(e) = self.globals.get(*key) {
1791                return Some(e.clone());
1792            }
1793        }
1794        self.globals
1795            .values()
1796            .find(|e| matches!(e, Expr::Do(_)))
1797            .cloned()
1798    }
1799
1800    // ─── Expression evaluation ────────────────────────────────────────────────
1801
1802    fn eval_expr(&mut self, expr: &Expr, env: &mut Env) -> EvalResult {
1803        match expr {
1804            Expr::Str(s) => Ok(Value::Str(s.clone())),
1805            Expr::Number(n) => Ok(Value::Number(*n)),
1806            Expr::Bool(b) => Ok(Value::Bool(*b)),
1807            Expr::Unit => Ok(Value::Unit),
1808            Expr::Array(elems) => {
1809                let vs: Vec<_> = elems
1810                    .iter()
1811                    .map(|e| self.eval_expr(e, env))
1812                    .collect::<Result<_, _>>()?;
1813                Ok(Value::List(Rc::new(vs)))
1814            },
1815
1816            Expr::Ident(name) => self.lookup(name, env),
1817
1818            Expr::Path(segs) => {
1819                if segs.len() == 1 {
1820                    return self.lookup(&segs[0], env);
1821                }
1822                Ok(Value::Str(segs.join("::")))
1823            },
1824
1825            Expr::Ref(inner) => self.eval_expr(inner, env),
1826            Expr::Await(inner) => self.eval_expr(inner, env),
1827
1828            Expr::Do(stmts) => {
1829                let mut local = env.clone();
1830                Ok(self.exec_block(stmts, &mut local)?.unwrap_or(Value::Unit))
1831            },
1832
1833            Expr::BinOp(op, lhs, rhs) => {
1834                let l = self.eval_expr(lhs, env)?;
1835                let r = self.eval_expr(rhs, env)?;
1836                self.apply_binop(op, l, r)
1837            },
1838
1839            Expr::If { cond, then, elseifs, else_body } => {
1840                let cond_val = self.eval_expr(cond, env)?;
1841                if self.is_truthy(&cond_val) {
1842                    return Ok(self.exec_block(then, env)?.unwrap_or(Value::Unit));
1843                }
1844                for (ei_cond, ei_body) in elseifs {
1845                    let ei_cond_val = self.eval_expr(ei_cond, env)?;
1846                    if self.is_truthy(&ei_cond_val) {
1847                        return Ok(self.exec_block(ei_body, env)?.unwrap_or(Value::Unit));
1848                    }
1849                }
1850                if let Some(eb) = else_body {
1851                    return Ok(self.exec_block(eb, env)?.unwrap_or(Value::Unit));
1852                }
1853                Ok(Value::Unit)
1854            },
1855
1856            Expr::While { cond, body } => {
1857                // Run the body directly in the *outer* env so that
1858                // `bind counter = counter + 1` persists across iterations,
1859                // which is the expected behaviour in a scripting language.
1860                loop {
1861                    let cv = self.eval_expr(cond, env)?;
1862                    if !self.is_truthy(&cv) {
1863                        break;
1864                    }
1865                    match self.exec_block(body, env) {
1866                        Ok(_) => {},
1867                        Err(EvalErr::Break) => break,
1868                        Err(e) => return Err(e),
1869                    }
1870                }
1871                Ok(Value::Unit)
1872            },
1873
1874            Expr::For { var, iter, body } => {
1875                let iter_val = self.eval_expr(iter, env)?;
1876                let items = self.value_to_iter(iter_val)?;
1877                for item in items {
1878                    let mut local = env.clone();
1879                    local.insert(var.clone(), item);
1880                    match self.exec_block(body, &mut local) {
1881                        Ok(_) => {},
1882                        Err(EvalErr::Break) => break,
1883                        Err(e) => return Err(e),
1884                    }
1885                }
1886                Ok(Value::Unit)
1887            },
1888
1889            Expr::Match(subject, arms) => {
1890                let subj = self.eval_expr(subject, env)?;
1891                for arm in arms {
1892                    if let Some(bindings) = self.match_pattern(&arm.pattern, &subj) {
1893                        let mut local = env.clone();
1894                        local.extend(bindings);
1895                        return self.eval_expr(&arm.body, &mut local);
1896                    }
1897                }
1898                Ok(Value::Unit)
1899            },
1900
1901            Expr::Range(lo, hi) => {
1902                let lo_v = self.eval_expr(lo, env)?;
1903                let hi_v = self.eval_expr(hi, env)?;
1904                let lo_n = self.to_number(&lo_v)? as i64;
1905                let hi_n = self.to_number(&hi_v)? as i64;
1906                Ok(Value::List(Rc::new(
1907                    (lo_n..hi_n).map(|i| Value::Number(i as f64)).collect(),
1908                )))
1909            },
1910
1911            Expr::Index(base, idx) => {
1912                let b = self.eval_expr(base, env)?;
1913                let i = self.eval_expr(idx, env)?;
1914                let n = self.to_number(&i)? as usize;
1915                match b {
1916                    Value::List(v) => v
1917                        .get(n)
1918                        .cloned()
1919                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
1920                    Value::Str(s) => s
1921                        .chars()
1922                        .nth(n)
1923                        .map(|c| Value::Str(c.to_string()))
1924                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
1925                    other => Err(EvalErr::from(format!("cannot index {:?}", other))),
1926                }
1927            },
1928
1929            Expr::Call(callee, args) => {
1930                let arg_vals: Vec<Value> = args
1931                    .iter()
1932                    .map(|a| self.eval_expr(a, env))
1933                    .collect::<Result<_, _>>()?;
1934                match callee.as_ref() {
1935                    Expr::Ident(name) => self.call_named(name, arg_vals, env),
1936                    Expr::Path(segs) => self.call_named(&segs.join("::"), arg_vals, env),
1937                    _ => {
1938                        let v = self.eval_expr(callee, env)?;
1939                        self.call_value(v, arg_vals)
1940                    },
1941                }
1942            },
1943
1944            Expr::MethodCall { receiver, method, args } => {
1945                let recv = self.eval_expr(receiver, env)?;
1946                let arg_vals: Vec<Value> = args
1947                    .iter()
1948                    .map(|a| self.eval_expr(a, env))
1949                    .collect::<Result<_, _>>()?;
1950                self.call_method(recv, method, arg_vals)
1951            },
1952
1953            Expr::Closure(params, body) => Ok(Value::Fn(
1954                params.clone(),
1955                vec![Stmt::Expr(*body.clone())],
1956                env.clone(),
1957            )),
1958        }
1959    }
1960
1961    // ─── Block execution ─────────────────────────────────────────────────────
1962
1963    fn exec_block(&mut self, stmts: &[Stmt], env: &mut Env) -> Result<Option<Value>, EvalErr> {
1964        let mut last: Option<Value> = None;
1965        for stmt in stmts {
1966            match stmt {
1967                Stmt::Bind(name, expr) => {
1968                    match self.try_inplace_list_update(name, expr, env)? {
1969                        Some(v) => env.insert(name.clone(), v),
1970                        None => {
1971                            let v = self.eval_expr(expr, env)?;
1972                            env.insert(name.clone(), v)
1973                        },
1974                    };
1975                    last = None;
1976                },
1977                Stmt::Return(expr) => {
1978                    let v = self.eval_expr(expr, env)?;
1979                    return Err(EvalErr::Return(v));
1980                },
1981                Stmt::Expr(expr) => {
1982                    last = Some(self.eval_expr(expr, env)?);
1983                },
1984            }
1985        }
1986        Ok(last)
1987    }
1988
1989    /// Fast path for `bind v = list_push(v, x)` / `bind v = list_set(v, i, x)`:
1990    /// the binding aliases the same list being rebuilt, so the env copy keeps the
1991    /// `Rc` shared and `make_mut` copies the whole vector every call. Taking the
1992    /// value out of env first leaves the `Rc` unique (unless truly aliased
1993    /// elsewhere, where copy-on-write still applies), turning O(n) into O(1).
1994    /// Returns `None` to fall back to normal evaluation.
1995    fn try_inplace_list_update(
1996        &mut self,
1997        name: &str,
1998        expr: &Expr,
1999        env: &mut Env,
2000    ) -> Result<Option<Value>, EvalErr> {
2001        let Expr::Call(callee, args) = expr else { return Ok(None) };
2002        let Expr::Ident(fname) = callee.as_ref() else { return Ok(None) };
2003        let is_push = matches!(
2004            fname.as_str(),
2005            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가"
2006        );
2007        let is_set = matches!(
2008            fname.as_str(),
2009            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정"
2010        );
2011        if !is_push && !is_set {
2012            return Ok(None);
2013        }
2014        // First arg must be the same variable we are binding, and the builtin
2015        // must not be shadowed by a user function.
2016        match args.first() {
2017            Some(Expr::Ident(a0)) if a0 == name => {},
2018            _ => return Ok(None),
2019        }
2020        if self.functions.contains_key(fname.as_str()) {
2021            return Ok(None);
2022        }
2023        if is_push {
2024            if args.len() != 2 {
2025                return Ok(None);
2026            }
2027            let val = self.eval_expr(&args[1], env)?;
2028            match env.remove(name) {
2029                Some(Value::List(mut v)) => {
2030                    Rc::make_mut(&mut v).push(val);
2031                    Ok(Some(Value::List(v)))
2032                },
2033                other => {
2034                    if let Some(o) = other {
2035                        env.insert(name.to_string(), o);
2036                    }
2037                    Ok(None)
2038                },
2039            }
2040        } else {
2041            if args.len() != 3 {
2042                return Ok(None);
2043            }
2044            let idx_v = self.eval_expr(&args[1], env)?;
2045            let idx = self.to_number(&idx_v).unwrap_or(0.0) as usize;
2046            let val = self.eval_expr(&args[2], env)?;
2047            match env.remove(name) {
2048                Some(Value::List(mut v)) => {
2049                    if idx < v.len() {
2050                        Rc::make_mut(&mut v)[idx] = val;
2051                    }
2052                    Ok(Some(Value::List(v)))
2053                },
2054                other => {
2055                    if let Some(o) = other {
2056                        env.insert(name.to_string(), o);
2057                    }
2058                    Ok(None)
2059                },
2060            }
2061        }
2062    }
2063
2064    // ─── Dispatch helpers ─────────────────────────────────────────────────────
2065
2066    fn lookup(&self, name: &str, env: &Env) -> EvalResult {
2067        if let Some(v) = env.get(name) {
2068            return Ok(v.clone());
2069        }
2070        // Globals are an immutable load-time snapshot shared by every call frame;
2071        // a function reads them here instead of receiving a per-call clone.
2072        if let Some(v) = self.global_seed.get(name) {
2073            return Ok(v.clone());
2074        }
2075        if self.functions.contains_key(name) {
2076            let def = &self.functions[name];
2077            return Ok(Value::Fn(def.params.clone(), def.body.clone(), new_env()));
2078        }
2079        // Bare nullary enum variant used as a value (e.g. `bind p = Origin`).
2080        if let Some((enum_name, 0)) = self.enum_variants.get(name).cloned() {
2081            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
2082            return Ok(Value::Variant { enum_name, variant, payload: Vec::new() });
2083        }
2084        // Math constants usable as plain identifiers (e.g. `sin(pi)`)
2085        match name {
2086            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" => {
2087                return Ok(Value::Number(std::f64::consts::PI))
2088            },
2089            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" => {
2090                return Ok(Value::Number(std::f64::consts::TAU))
2091            },
2092            _ => {},
2093        }
2094        Err(EvalErr::from(format!("undefined: '{name}'")))
2095    }
2096
2097    /// Profiling wrapper around the real dispatch. Zero overhead unless
2098    /// `LING_PROFILE` is set (one thread-local bool check per call). When on,
2099    /// it tallies per-name call count + inclusive time and, on each frame
2100    /// boundary (`present`), prints a sorted top-down report every
2101    /// `LING_PROFILE_EVERY` frames (default 240). Both the JIT (`ling_builtin` →
2102    /// here) and the tree-walker route through this, so it sees every builtin —
2103    /// in JIT mode user fns are native, so it's a clean builtin/render/physics
2104    /// profile with no nesting double-count.
2105    pub(crate) fn call_named(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2106        if !ling_profile_enabled() {
2107            return self.call_named_inner(name, args, env);
2108        }
2109        let t0 = crate::runtime::now_secs();
2110        let r = self.call_named_inner(name, args, env);
2111        ling_profile_record(name, ((crate::runtime::now_secs() - t0) * 1_000_000_000.0) as u128);
2112        r
2113    }
2114
2115    fn call_named_inner(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2116        // A user-defined function shadows any builtin of the same name, matching
2117        // the JIT/AOT backends (which always resolve a defined function first).
2118        if let Some(def) = self.functions.get(name).cloned() {
2119            let mut call_env = FxHashMap::with_capacity_and_hasher(def.params.len(), Default::default());
2120            let _ = env; // call-site locals are intentionally NOT visible to fns
2121            for (param, arg) in def.params.iter().zip(args) {
2122                call_env.insert(param.clone(), arg);
2123            }
2124            return match self.framed(name, |me| me.exec_block(&def.body, &mut call_env)) {
2125                Ok(v) => Ok(v.unwrap_or(Value::Unit)),
2126                Err(EvalErr::Return(v)) => Ok(v),
2127                Err(e) => Err(e),
2128            };
2129        }
2130
2131        #[cfg(target_arch = "wasm32")]
2132        if let Some(v) = self.wasm_music_builtin(name, &args)? {
2133            return Ok(v);
2134        }
2135
2136        match name {
2137            // Module global read emitted by the MIR backend: resolve against the
2138            // evaluated global snapshot (functions see globals read-only).
2139            "__ling_global" => {
2140                if let Some(Value::Str(g)) = args.first() {
2141                    if let Some(v) = self.global_seed.get(g.as_str()) {
2142                        return Ok(v.clone());
2143                    }
2144                }
2145                return Ok(Value::Unit);
2146            },
2147            // ── Print ──
2148            "print" | "println" | "印" | "打印" | "印刷" | "พิมพ์" | "출력" | "вывести"
2149            | "imprimir" | "afficher" => {
2150                let s = args
2151                    .iter()
2152                    .map(|v| v.to_string())
2153                    .collect::<Vec<_>>()
2154                    .join("");
2155                println!("{s}");
2156                return Ok(Value::Unit);
2157            },
2158            // print_color(colorIdx, text...) — ANSI-coloured console line.
2159            //   colorIdx 0..7 → bright fg (90+idx): 1=red 2=green 3=yellow 4=blue 6=cyan 7=white.
2160            "print_color" | "พิมพ์สี" => {
2161                #[cfg(windows)]
2162                {
2163                    use std::sync::Once;
2164                    static VT: Once = Once::new();
2165                    VT.call_once(|| {
2166                        extern "system" {
2167                            fn GetStdHandle(n: u32) -> *mut std::ffi::c_void;
2168                            fn GetConsoleMode(h: *mut std::ffi::c_void, m: *mut u32) -> i32;
2169                            fn SetConsoleMode(h: *mut std::ffi::c_void, m: u32) -> i32;
2170                        }
2171                        unsafe {
2172                            let h = GetStdHandle(0xFFFF_FFF5u32); // STD_OUTPUT_HANDLE (-11)
2173                            let mut mode = 0u32;
2174                            if GetConsoleMode(h, &mut mode) != 0 {
2175                                SetConsoleMode(h, mode | 0x0004); // ENABLE_VIRTUAL_TERMINAL_PROCESSING
2176                            }
2177                        }
2178                    });
2179                }
2180                let col = self.arg_num(&args, 0, 7.0)? as i64;
2181                let s = args
2182                    .iter()
2183                    .skip(1)
2184                    .map(|v| v.to_string())
2185                    .collect::<Vec<_>>()
2186                    .join("");
2187                let code = 90 + col.clamp(0, 7);
2188                println!("\x1b[1;{code}m{s}\x1b[0m");
2189                return Ok(Value::Unit);
2190            },
2191            // ── Format ──
2192            "format"
2193            | "格式"
2194            | "フォーマット"
2195            | "서식"
2196            | "รูปแบบ"
2197            | "форматировать"
2198            | "formatear"
2199            | "formater" => {
2200                return Ok(Value::Str(self.builtin_format(&args)?));
2201            },
2202            // ── String join / concatenation ──
2203            "格式::拼接" | "format::join" => match args.first() {
2204                Some(Value::List(items)) => {
2205                    return Ok(Value::Str(items.iter().map(|v| v.to_string()).collect()));
2206                },
2207                _ => return Ok(Value::Str(self.builtin_format(&args)?)),
2208            },
2209            // ── Result constructors ──
2210            "ok" | "好" | "良し" | "좋아" | "โอเค" => {
2211                let val = args.into_iter().next().unwrap_or(Value::Unit);
2212                return Ok(Value::Ok(Box::new(val)));
2213            },
2214            "bad" | "坏" | "err" | "悪い" | "나쁨" | "ผิด" => {
2215                let val = args.into_iter().next().unwrap_or(Value::Unit);
2216                return Ok(Value::Err(Box::new(val)));
2217            },
2218            // ── Vec constructors ──
2219            "向量::从" | "Vec::from" => {
2220                if let Some(Value::List(v)) = args.first() {
2221                    return Ok(Value::List(v.clone()));
2222                }
2223                return Ok(Value::List(Rc::new(args)));
2224            },
2225            "向量::有容量" | "Vec::with_capacity" => return Ok(Value::List(Rc::new(Vec::new()))),
2226            // ── Timer stubs ──
2227            "计时::获取当前小时" | "Timer::hour" => return Ok(Value::Number(14.0)),
2228            "计时::现在" | "Timer::now" => return Ok(Value::Number(1000.0)),
2229            // ── Sleep ──
2230            "sleep" | "หยุด" | "นอน" | "sleep_ms" | "睡眠" | "眠る" | "スリープ" | "잠자기"
2231            | "잠" | "流水::睡眠" | "Flow::sleep" => {
2232                if let Some(ms_val) = args.first() {
2233                    if let Ok(ms) = self.to_number(ms_val) {
2234                        #[cfg(target_arch = "wasm32")]
2235                        wasm_sleep_ms(ms.max(0.0) as i32);
2236                        #[cfg(not(target_arch = "wasm32"))]
2237                        std::thread::sleep(std::time::Duration::from_millis(ms as u64));
2238                    }
2239                }
2240                return Ok(Value::Unit);
2241            },
2242            // ── Flow::parallel stub ──
2243            "流水::并行" | "Flow::parallel" => {
2244                if let Some(Value::Fn(params, body, mut cap)) = args.first().cloned() {
2245                    let _ = params;
2246                    match self.exec_block(&body, &mut cap) {
2247                        Ok(Some(v)) => return Ok(v),
2248                        Ok(None) => return Ok(Value::Unit),
2249                        Err(EvalErr::Return(v)) => return Ok(v),
2250                        Err(e) => return Err(e),
2251                    }
2252                }
2253                return Ok(Value::Unit);
2254            },
2255
2256            // ══════════════════════════════════════════════════════════════════
2257            // MATH BUILTINS  (all args and results are f64)
2258            // Thai aliases: ไซน์ โคไซน์ แทนเจนต์ รากที่สอง ค่าสัมบูรณ์
2259            //               ปัดลง ปัดขึ้น ปัดเศษ ตัดทศนิยม ต่ำสุด สูงสุด
2260            //               จำกัด ยกกำลัง ลอการิทึม พาย
2261            // ══════════════════════════════════════════════════════════════════
2262
2263            // ── Trigonometry (input in radians) ──
2264            "sin" | "ไซน์" | "正弦" | "サイン" | "사인" => {
2265                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sin()));
2266            },
2267            "cos" | "โคไซน์" | "余弦" | "コサイン" | "코사인" => {
2268                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cos()));
2269            },
2270
2271            // ── Hyperbolic functions ──
2272            // Hyperbolic tangent
2273            "tanh" | "tanhf" | "双曲正切" | "双曲線正接" | "쌍곡탄젠트" => {
2274                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tanh()));
2275            },
2276
2277            "tan" | "แทนเจนต์" | "正切" | "タンジェント" | "탄젠트" => {
2278                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tan()));
2279            },
2280            "asin" | "arcsin" | "反正弦" | "アークサイン" | "아크사인" | "อาร์กไซน์" =>
2281            {
2282                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.asin()));
2283            },
2284            "acos" | "arccos" | "反余弦" | "アークコサイン" | "아크코사인" | "อาร์กโคไซน์" =>
2285            {
2286                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.acos()));
2287            },
2288            "atan" | "arctan" | "反正切" | "アークタンジェント" | "아크탄젠트" | "อาร์กแทนเจนต์" =>
2289            {
2290                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.atan()));
2291            },
2292            "atan2" | "arctan2" | "反正切2" | "アークタンジェント2" | "아크탄젠트2" =>
2293            {
2294                let y = self.arg_num(&args, 0, 0.0)?;
2295                let x = self.arg_num(&args, 1, 1.0)?;
2296                return Ok(Value::Number(y.atan2(x)));
2297            },
2298
2299            // ── Roots / powers ──
2300            "sqrt" | "รากที่สอง" | "平方根" | "根" | "제곱근" => {
2301                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sqrt()));
2302            },
2303            "cbrt" | "立方根" | "세제곱근" | "รากที่สาม" => {
2304                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cbrt()));
2305            },
2306            "pow" | "ยกกำลัง" | "幂" | "べき乗" | "거듭제곱" => {
2307                let base = self.arg_num(&args, 0, 0.0)?;
2308                let exp = self.arg_num(&args, 1, 1.0)?;
2309                return Ok(Value::Number(base.powf(exp)));
2310            },
2311            "exp" | "指数" | "指数関数" | "지수" => {
2312                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.exp()));
2313            },
2314            "hypot" | "斜边" | "斜辺" | "빗변" => {
2315                let x = self.arg_num(&args, 0, 0.0)?;
2316                let y = self.arg_num(&args, 1, 0.0)?;
2317                return Ok(Value::Number(x.hypot(y)));
2318            },
2319
2320            // ── Logarithms ──
2321            "ln" | "log" | "ลอการิทึม" | "对数" | "対数" | "로그" => {
2322                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.ln()));
2323            },
2324            "log2" | "对数2" | "対数2" | "로그2" => {
2325                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log2()));
2326            },
2327            "log10" | "对数10" | "対数10" | "로그10" => {
2328                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log10()));
2329            },
2330
2331            // ── Rounding / truncation ──
2332            "abs" | "ค่าสัมบูรณ์" | "绝对值" | "绝对" | "絶対値" | "절댓값" | "절대값" =>
2333            {
2334                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.abs()));
2335            },
2336            "floor" | "ปัดลง" | "向下取整" | "下整" | "床関数" | "내림" => {
2337                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.floor()));
2338            },
2339            "ceil" | "ปัดขึ้น" | "向上取整" | "上整" | "天井関数" | "올림" =>
2340            {
2341                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.ceil()));
2342            },
2343            "round" | "ปัดเศษ" | "四舍五入" | "四舍" | "四捨五入" | "반올림" =>
2344            {
2345                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.round()));
2346            },
2347            "trunc"
2348            | "int"
2349            | "ตัดทศนิยม"
2350            | "取整"
2351            | "整数化"
2352            | "整数"
2353            | "截整"
2354            | "정수화"
2355            | "정수"
2356            | "切り捨て"
2357            | "버림" => {
2358                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.trunc()));
2359            },
2360            "fract" | "小数部分" | "小数部" | "소수부" => {
2361                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.fract()));
2362            },
2363
2364            // ── min / max / clamp ──
2365            "min" | "ต่ำสุด" | "最小" | "최솟값" => {
2366                let a = self.arg_num(&args, 0, 0.0)?;
2367                let b = self.arg_num(&args, 1, 0.0)?;
2368                return Ok(Value::Number(a.min(b)));
2369            },
2370            "max" | "สูงสุด" | "最大" | "최댓값" => {
2371                let a = self.arg_num(&args, 0, 0.0)?;
2372                let b = self.arg_num(&args, 1, 0.0)?;
2373                return Ok(Value::Number(a.max(b)));
2374            },
2375            "clamp" | "จำกัด" | "截取" | "範囲制限" | "범위제한" => {
2376                let x = self.arg_num(&args, 0, 0.0)?;
2377                let lo = self.arg_num(&args, 1, 0.0)?;
2378                let hi = self.arg_num(&args, 2, 1.0)?;
2379                return Ok(Value::Number(x.clamp(lo, hi)));
2380            },
2381
2382            // ── Constants (also accessible as plain identifiers via lookup) ──
2383            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" => {
2384                return Ok(Value::Number(std::f64::consts::PI))
2385            },
2386            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" => {
2387                return Ok(Value::Number(std::f64::consts::TAU))
2388            },
2389
2390            // ══════════════════════════════════════════════════════════════════
2391            // PHASE 1: DMT TRIP CODER FEATURES
2392            // ══════════════════════════════════════════════════════════════════
2393
2394            // ── Step 1: Noise Functions ──
2395            "vnoise" | "noise2" | "นอยส์2ดี" | "柏林噪声2D" | "バリューノイズ2D" | "값노이즈2D" =>
2396            {
2397                let x = self.arg_num(&args, 0, 0.0)? as f32;
2398                let y = self.arg_num(&args, 1, 0.0)? as f32;
2399                let seed = self.arg_num(&args, 2, 0.0)? as u32;
2400                return Ok(Value::Number(tex_vnoise(x, y, seed) as f64));
2401            },
2402
2403            "fbm" | "นอยส์ออร์แกนิก" | "分形噪声" | "フラクタルノイズ" | "프랙탈노이즈" =>
2404            {
2405                let x = self.arg_num(&args, 0, 0.0)? as f32;
2406                let y = self.arg_num(&args, 1, 0.0)? as f32;
2407                let octaves = self.arg_num(&args, 2, 4.0)? as u32;
2408                let seed = self.arg_num(&args, 3, 0.0)? as u32;
2409                return Ok(Value::Number(tex_fbm(x, y, octaves, seed) as f64));
2410            },
2411
2412            "perlin"
2413            | "perlin3"
2414            | "เพอร์ลิน3ดี"
2415            | "柏林噪声3D"
2416            | "パーリンノイズ3D"
2417            | "펄린노이즈3D" => {
2418                let x = self.arg_num(&args, 0, 0.0)? as f32;
2419                let y = self.arg_num(&args, 1, 0.0)? as f32;
2420                let z = self.arg_num(&args, 2, 0.0)? as f32;
2421                return Ok(Value::Number(perlin3(x, y, z) as f64));
2422            },
2423
2424            // ── Step 2: Math Ergonomics ──
2425            "lerp" | "ค่าระหว่าง" | "线性插值" | "線形補間" | "선형보간" =>
2426            {
2427                let a = self.arg_num(&args, 0, 0.0)?;
2428                let b = self.arg_num(&args, 1, 1.0)?;
2429                let t = self.arg_num(&args, 2, 0.0)?;
2430                return Ok(Value::Number(a + (b - a) * t));
2431            },
2432
2433            "smoothstep" | "เปลี่ยนแบบนุ่ม" | "平滑步进" | "スムーズステップ" | "스무스스텝" =>
2434            {
2435                let lo = self.arg_num(&args, 0, 0.0)?;
2436                let hi = self.arg_num(&args, 1, 1.0)?;
2437                let x = self.arg_num(&args, 2, 0.5)?;
2438                let t = ((x - lo) / (hi - lo)).clamp(0.0, 1.0);
2439                return Ok(Value::Number(t * t * (3.0 - 2.0 * t)));
2440            },
2441
2442            "rand" | "สุ่ม" | "随机" | "乱数" | "난수" => {
2443                let val = fast_rand_f64(&mut self.rand_state);
2444                return Ok(Value::Number(val));
2445            },
2446
2447            "sign" | "เครื่องหมาย" | "符号" | "符号関数" | "부호" => {
2448                let x = self.arg_num(&args, 0, 0.0)?;
2449                return Ok(Value::Number(x.signum()));
2450            },
2451
2452            "hsv_to_rgb" | "เอชเอสวีเป็นRGB" | "HSV转RGB" | "HSV変換RGB" | "HSV변환RGB" =>
2453            {
2454                let h = self.arg_num(&args, 0, 0.0)?; // 0-360
2455                let s = self.arg_num(&args, 1, 1.0)?; // 0-1
2456                let v = self.arg_num(&args, 2, 1.0)?; // 0-1
2457                let c = v * s;
2458                let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
2459                let m = v - c;
2460                let (r1, g1, b1) = if h < 60.0 {
2461                    (c, x, 0.0)
2462                } else if h < 120.0 {
2463                    (x, c, 0.0)
2464                } else if h < 180.0 {
2465                    (0.0, c, x)
2466                } else if h < 240.0 {
2467                    (0.0, x, c)
2468                } else if h < 300.0 {
2469                    (x, 0.0, c)
2470                } else {
2471                    (c, 0.0, x)
2472                };
2473                let r = ((r1 + m) * 255.0).round();
2474                let g = ((g1 + m) * 255.0).round();
2475                let b = ((b1 + m) * 255.0).round();
2476                return Ok(Value::List(Rc::new(vec![
2477                    Value::Number(r),
2478                    Value::Number(g),
2479                    Value::Number(b),
2480                ])));
2481            },
2482
2483            "lerp_color" | "ไล่สี" | "颜色插值" | "色補間" | "색보간" => {
2484                let r1 = self.arg_num(&args, 0, 0.0)?;
2485                let g1 = self.arg_num(&args, 1, 0.0)?;
2486                let b1 = self.arg_num(&args, 2, 0.0)?;
2487                let r2 = self.arg_num(&args, 3, 255.0)?;
2488                let g2 = self.arg_num(&args, 4, 255.0)?;
2489                let b2 = self.arg_num(&args, 5, 255.0)?;
2490                let t = self.arg_num(&args, 6, 0.0)?;
2491                let r = r1 + (r2 - r1) * t;
2492                let g = g1 + (g2 - g1) * t;
2493                let b = b1 + (b2 - b1) * t;
2494                let c = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
2495                self.gfx.borrow_mut().color = c;
2496                return Ok(Value::Unit);
2497            },
2498
2499            // ── Step 3: Real-Time Clock ──
2500            "time_now" | "เวลาปัจจุบัน" | "当前时间" | "経過時間" | "현재시간" =>
2501            {
2502                return Ok(Value::Number(crate::runtime::now_secs() - self.start_time_secs));
2503            },
2504
2505            // Wall-clock seconds since the Unix epoch (real date/time). Lets a
2506            // program defer deterministic-yet-evolving generation to the actual
2507            // datetime — same clock → same world, advancing as real time passes.
2508            "epoch_now" | "เวลาโลก" | "datetime" | "现在时刻" | "現在時刻" | "현재시각" =>
2509            {
2510                return Ok(Value::Number(crate::runtime::now_secs()));
2511            },
2512
2513            "frame_count" | "เฟรม" | "帧数" | "フレーム数" | "프레임수" => {
2514                return Ok(Value::Number(self.frame_num as f64));
2515            },
2516
2517            // ── Step 4: Microphone Input ──
2518            "mic_open" | "เปิดไมค์" | "开麦克风" | "マイク開く" | "마이크열기" =>
2519            {
2520                #[cfg(not(target_arch = "wasm32"))]
2521                {
2522                    match ling_mic::MicInput::open(Default::default()) {
2523                        Ok(mic) => {
2524                            let _ = mic.start(|_samples: &[f32]| {}); // No-op callback
2525                            self.mic = Some(mic);
2526                            return Ok(Value::Number(1.0)); // opened
2527                        },
2528                        // No device / permission denied → graceful: don't crash the game loop.
2529                        // Returns 0.0; mic_rms/mic_peak return 0.0 while self.mic is None.
2530                        Err(_e) => {
2531                            self.mic = None;
2532                            return Ok(Value::Number(0.0));
2533                        },
2534                    }
2535                }
2536                #[cfg(target_arch = "wasm32")]
2537                return Ok(Value::Unit);
2538            },
2539
2540            "mic_rms" | "เสียงRMS" | "麦克风音量" | "マイクRMS" | "마이크RMS" =>
2541            {
2542                #[cfg(not(target_arch = "wasm32"))]
2543                {
2544                    let rms = self
2545                        .mic
2546                        .as_ref()
2547                        .map(|m: &ling_mic::MicInput| m.rms())
2548                        .unwrap_or(0.0);
2549                    return Ok(Value::Number(rms as f64));
2550                }
2551                #[cfg(target_arch = "wasm32")]
2552                return Ok(Value::Number(0.0));
2553            },
2554
2555            "mic_peak" | "เสียงพีค" | "麦克风峰值" | "マイクピーク" | "마이크피크" =>
2556            {
2557                #[cfg(not(target_arch = "wasm32"))]
2558                {
2559                    let peak = self
2560                        .mic
2561                        .as_ref()
2562                        .map(|m: &ling_mic::MicInput| m.peak())
2563                        .unwrap_or(0.0);
2564                    return Ok(Value::Number(peak as f64));
2565                }
2566                #[cfg(target_arch = "wasm32")]
2567                return Ok(Value::Number(0.0));
2568            },
2569
2570            "mic_fft" | "วิเคราะห์เสียงสด" | "实时频谱" | "リアルタイムFFT" | "실시간FFT" =>
2571            {
2572                #[cfg(not(target_arch = "wasm32"))]
2573                {
2574                    let n = self.arg_num(&args, 0, 8.0)? as usize;
2575                    if let Some(mic) = self.mic.as_ref() {
2576                        let samples = mic.latest_samples();
2577                        self.fft.borrow_mut().push_samples(&samples);
2578                    }
2579                    let bands = self.fft.borrow().freq_bands(n);
2580                    let result: Vec<Value> = bands.iter().map(|&v| Value::Number(v as f64)).collect();
2581                    return Ok(Value::List(Rc::new(result)));
2582                }
2583                #[cfg(target_arch = "wasm32")]
2584                return Ok(Value::List(vec![]));
2585            },
2586
2587            // ── Step 5: Additive Blend Mode ──
2588            "set_blend" | "โหมดผสม" | "混合模式" | "ブレンドモード" | "블렌드모드" =>
2589            {
2590                let mode = self.arg_num(&args, 0, 0.0)? as u8;
2591                let mut gfx = self.gfx.borrow_mut();
2592                gfx.blend = mode;
2593                let a = gfx.alpha;
2594                gfx.depth_queue.set_state(mode, a); // 3-D queue captures blend for subsequent pushes
2595                return Ok(Value::Unit);
2596            },
2597
2598            // ── Step 6: Circle Primitives ──
2599            "draw_circle" | "วาดวงกลม" | "画圆" | "円描画" | "원그리기" =>
2600            {
2601                let cx = self.arg_num(&args, 0, 0.0)? as i32;
2602                let cy = self.arg_num(&args, 1, 0.0)? as i32;
2603                let r = self.arg_num(&args, 2, 10.0)? as i32;
2604                let mut gfx = self.gfx.borrow_mut();
2605                let (w, h, color, blend) =
2606                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
2607                draw_circle_outline(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
2608                return Ok(Value::Unit);
2609            },
2610
2611            "draw_filled_circle"
2612            | "draw_disc"
2613            | "วาดวงกลมทึบ"
2614            | "画实心圆"
2615            | "塗りつぶし円"
2616            | "원채우기" => {
2617                let cx = self.arg_num(&args, 0, 0.0)? as i32;
2618                let cy = self.arg_num(&args, 1, 0.0)? as i32;
2619                let r = self.arg_num(&args, 2, 10.0)? as i32;
2620                let mut gfx = self.gfx.borrow_mut();
2621                let (w, h, color, blend) =
2622                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
2623                draw_circle_filled(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
2624                return Ok(Value::Unit);
2625            },
2626
2627            // ── Step 7: Transparent fills, gradient surfaces & colored shadows ──
2628            // These all write straight into the software framebuffer (gfx.buffer)
2629            // on both native and web, so no target gating is needed.
2630
2631            // set_alpha(a) — pen opacity 0..1 for the alpha-blended fills below.
2632            "set_alpha" | "ตั้งความโปร่งใส" | "设透明" | "アルファ設定" | "투명도설정" =>
2633            {
2634                let a = self.arg_num(&args, 0, 1.0)? as f32;
2635                let mut gfx = self.gfx.borrow_mut();
2636                gfx.alpha = a.clamp(0.0, 1.0);
2637                let (m, al) = (gfx.blend, gfx.alpha);
2638                gfx.depth_queue.set_state(m, al); // 3-D queue captures alpha for subsequent pushes
2639                return Ok(Value::Unit);
2640            },
2641
2642            // set_color_space(mode) — 0 = legacy sRGB compositing (default),
2643            // 1 = gamma-correct linear-light compositing (blend in linear, store
2644            // sRGB) so alpha and gradients don't darken/shift hue.
2645            "set_color_space" | "ปริภูมิสี" | "色彩空间" | "色空間" | "색공간" =>
2646            {
2647                let m = self.arg_num(&args, 0, 0.0)? as i64;
2648                self.gfx.borrow_mut().linear_blend = m != 0;
2649                return Ok(Value::Unit);
2650            },
2651
2652            // set_gradient_space(mode) — 1 = perceptual OkLab gradient interp
2653            // (default), 0 = legacy sRGB. Affects grad_triangle / grad_rect.
2654            "set_gradient_space" | "ปริภูมิไล่สี" | "渐变空间" | "グラデ空間" | "그라데이션공간" =>
2655            {
2656                let m = self.arg_num(&args, 0, 1.0)? as i64;
2657                self.gfx.borrow_mut().grad_oklab = m != 0;
2658                return Ok(Value::Unit);
2659            },
2660
2661            // mix_color(r0,g0,b0, r1,g1,b1, t) — set the pen colour to the
2662            // perceptual OkLab blend of two colours (t in 0..1). Far nicer
2663            // mid-tones than a raw RGB lerp.
2664            "mix_color" | "ผสมสี" | "混合颜色" | "色混合" | "색혼합" => {
2665                let c0 = rgb(
2666                    self.arg_num(&args, 0, 0.0)?,
2667                    self.arg_num(&args, 1, 0.0)?,
2668                    self.arg_num(&args, 2, 0.0)?,
2669                );
2670                let c1 = rgb(
2671                    self.arg_num(&args, 3, 255.0)?,
2672                    self.arg_num(&args, 4, 255.0)?,
2673                    self.arg_num(&args, 5, 255.0)?,
2674                );
2675                let t = self.arg_num(&args, 6, 0.5)? as f32;
2676                self.gfx.borrow_mut().color = crate::gfx::color::mix_oklab(c0, c1, t);
2677                return Ok(Value::Unit);
2678            },
2679
2680            // set_depth_test(on) — enable the per-pixel z-buffer for the deferred
2681            // 3-D/queued draws (correct interpenetration) instead of painter's-
2682            // only sort. 0 = off (default), non-zero = on.
2683            "set_depth_test" | "ทดสอบความลึก" | "深度测试" | "深度テスト" | "깊이테스트" =>
2684            {
2685                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
2686                self.gfx.borrow_mut().depth_test = on;
2687                return Ok(Value::Unit);
2688            },
2689
2690            // set_flat_shade(on) / ตั้งแฟลตเชด — perf test: skip all per-triangle/mesh
2691            // lighting (compute_lit_color) and draw with the raw pen colour.
2692            "set_flat_shade" | "ตั้งแฟลตเชด" | "平面着色" | "フラット着色" | "평면음영" =>
2693            {
2694                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
2695                self.gfx.borrow_mut().flat_shade = on;
2696                return Ok(Value::Unit);
2697            },
2698
2699            // clear_depth() / ล้างความลึก — force the z-buffer to clear on the next
2700            // flush. `เติม` already does this; call explicitly to start a fresh
2701            // depth pass mid-frame (e.g. a separate overlay scene).
2702            "clear_depth" | "ล้างความลึก" | "清深度" | "深度クリア" | "깊이지우기" =>
2703            {
2704                self.gfx.borrow_mut().zbuf_needs_clear = true;
2705                return Ok(Value::Unit);
2706            },
2707
2708            // depth_blur(focus, range, radius) / เบลอความลึก — depth-of-field post
2709            // pass over the framebuffer using the z-buffer: sharp at camera-space
2710            // depth `focus`, blurred up to `radius` px as depth departs by `range`.
2711            // Background (no geometry) blurs fully. Call AFTER `flush_3d` (so the
2712            // z-buffer is populated) and BEFORE `present`. Needs `set_depth_test(1)`.
2713            "depth_blur" | "เบลอความลึก" | "dof" | "depth_of_field" | "景深" =>
2714            {
2715                let focus = self.arg_num(&args, 0, 30.0)? as f32;
2716                let range = self.arg_num(&args, 1, 60.0)? as f32;
2717                let radius = self.arg_num(&args, 2, 3.0)?.max(0.0) as usize;
2718                let mut gfx = self.gfx.borrow_mut();
2719                let w = gfx.width;
2720                let h = gfx.height;
2721                if gfx.depth_buf.len() == w * h {
2722                    let g = &mut *gfx;
2723                    crate::gfx::raster::depth_of_field(
2724                        &mut g.buffer,
2725                        &g.depth_buf,
2726                        w,
2727                        h,
2728                        focus,
2729                        range,
2730                        radius,
2731                    );
2732                }
2733                return Ok(Value::Unit);
2734            },
2735
2736            // grad_triangle(x0,y0,r0,g0,b0, x1,y1,r1,g1,b1, x2,y2,r2,g2,b2)
2737            // Smooth per-vertex gradient triangle — a cheap lit surface: put the
2738            // bright colour on the vertex facing the light. Honours set_alpha.
2739            "grad_triangle" | "สามเหลี่ยมไล่สี" | "渐变三角" | "グラデ三角" | "그라데삼각" =>
2740            {
2741                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
2742                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
2743                let c0 = rgb(
2744                    self.arg_num(&args, 2, 255.0)?,
2745                    self.arg_num(&args, 3, 255.0)?,
2746                    self.arg_num(&args, 4, 255.0)?,
2747                );
2748                let x1 = self.arg_num(&args, 5, 0.0)? as f32;
2749                let y1 = self.arg_num(&args, 6, 0.0)? as f32;
2750                let c1 = rgb(
2751                    self.arg_num(&args, 7, 255.0)?,
2752                    self.arg_num(&args, 8, 255.0)?,
2753                    self.arg_num(&args, 9, 255.0)?,
2754                );
2755                let x2 = self.arg_num(&args, 10, 0.0)? as f32;
2756                let y2 = self.arg_num(&args, 11, 0.0)? as f32;
2757                let c2 = rgb(
2758                    self.arg_num(&args, 12, 255.0)?,
2759                    self.arg_num(&args, 13, 255.0)?,
2760                    self.arg_num(&args, 14, 255.0)?,
2761                );
2762                let mut gfx = self.gfx.borrow_mut();
2763                let (w, h, alpha, mode, lin, ok) = (
2764                    gfx.width,
2765                    gfx.height,
2766                    gfx.alpha,
2767                    gfx.blend,
2768                    gfx.linear_blend,
2769                    gfx.grad_oklab,
2770                );
2771                crate::gfx::raster::fill_triangle_grad(
2772                    &mut gfx.buffer,
2773                    w,
2774                    h,
2775                    alpha,
2776                    mode,
2777                    lin,
2778                    ok,
2779                    x0,
2780                    y0,
2781                    c0,
2782                    x1,
2783                    y1,
2784                    c1,
2785                    x2,
2786                    y2,
2787                    c2,
2788                );
2789                return Ok(Value::Unit);
2790            },
2791
2792            // grad_rect(x,y,w,h, r0,g0,b0, r1,g1,b1, dir) — linear-gradient rect.
2793            // dir 0 = horizontal (left→right), else vertical (top→bottom).
2794            "grad_rect" | "สี่เหลี่ยมไล่สี" | "渐变矩形" | "グラデ矩形" | "그라데사각" =>
2795            {
2796                let x = self.arg_num(&args, 0, 0.0)? as f32;
2797                let y = self.arg_num(&args, 1, 0.0)? as f32;
2798                let rw = self.arg_num(&args, 2, 0.0)? as f32;
2799                let rh = self.arg_num(&args, 3, 0.0)? as f32;
2800                let c0 = rgb(
2801                    self.arg_num(&args, 4, 255.0)?,
2802                    self.arg_num(&args, 5, 255.0)?,
2803                    self.arg_num(&args, 6, 255.0)?,
2804                );
2805                let c1 = rgb(
2806                    self.arg_num(&args, 7, 0.0)?,
2807                    self.arg_num(&args, 8, 0.0)?,
2808                    self.arg_num(&args, 9, 0.0)?,
2809                );
2810                let dir = self.arg_num(&args, 10, 1.0)? as u8;
2811                let mut gfx = self.gfx.borrow_mut();
2812                let (w, h, alpha, mode, lin, ok) = (
2813                    gfx.width,
2814                    gfx.height,
2815                    gfx.alpha,
2816                    gfx.blend,
2817                    gfx.linear_blend,
2818                    gfx.grad_oklab,
2819                );
2820                crate::gfx::raster::fill_rect_grad(
2821                    &mut gfx.buffer,
2822                    w,
2823                    h,
2824                    alpha,
2825                    mode,
2826                    lin,
2827                    ok,
2828                    x,
2829                    y,
2830                    rw,
2831                    rh,
2832                    c0,
2833                    c1,
2834                    dir,
2835                );
2836                return Ok(Value::Unit);
2837            },
2838
2839            // shadow_blob(cx,cy, rx,ry, alpha) — soft colored shadow ellipse in
2840            // the current pen colour. Dark colour = normal shadow; any hue = a
2841            // tinted/coloured shadow. Edge softness comes from shadow_params.
2842            "shadow_blob" | "เงาวงรี" | "阴影斑" | "影ブロブ" | "그림자블롭" =>
2843            {
2844                let cx = self.arg_num(&args, 0, 0.0)? as f32;
2845                let cy = self.arg_num(&args, 1, 0.0)? as f32;
2846                let rx = self.arg_num(&args, 2, 16.0)? as f32;
2847                let ry = self.arg_num(&args, 3, 8.0)? as f32;
2848                let a = self.arg_num(&args, 4, 0.5)? as f32;
2849                let mut gfx = self.gfx.borrow_mut();
2850                let (w, h, color, soft, mode, lin) = (
2851                    gfx.width,
2852                    gfx.height,
2853                    gfx.color,
2854                    gfx.shadow.soft,
2855                    gfx.blend,
2856                    gfx.linear_blend,
2857                );
2858                crate::gfx::raster::fill_disc_soft(
2859                    &mut gfx.buffer,
2860                    w,
2861                    h,
2862                    cx,
2863                    cy,
2864                    rx,
2865                    ry,
2866                    color,
2867                    a,
2868                    soft,
2869                    mode,
2870                    lin,
2871                );
2872                return Ok(Value::Unit);
2873            },
2874
2875            // cast_shadow(cx,cy, height) — height-driven contact shadow in the
2876            // current pen colour. Closer to the surface (small height) = smaller,
2877            // darker, sharper; farther (large height) = bigger, fainter, softer.
2878            // Tune the ramp with shadow_params.
2879            "cast_shadow" | "ทอดเงา" | "投射阴影" | "影を落とす" | "그림자드리우기" =>
2880            {
2881                let cx = self.arg_num(&args, 0, 0.0)? as f32;
2882                let cy = self.arg_num(&args, 1, 0.0)? as f32;
2883                let height = (self.arg_num(&args, 2, 0.0)? as f32).max(0.0);
2884                let mut gfx = self.gfx.borrow_mut();
2885                let sp = gfx.shadow;
2886                let radius = (sp.base + sp.grow * height).max(0.5);
2887                let alpha = (sp.alpha - sp.fade * height).clamp(0.04, 1.0);
2888                let soft = (sp.soft + height * 0.004).clamp(0.0, 0.95);
2889                let (w, h, color, mode, lin) = (
2890                    gfx.width,
2891                    gfx.height,
2892                    gfx.color,
2893                    gfx.blend,
2894                    gfx.linear_blend,
2895                );
2896                crate::gfx::raster::fill_disc_soft(
2897                    &mut gfx.buffer,
2898                    w,
2899                    h,
2900                    cx,
2901                    cy,
2902                    radius,
2903                    radius * 0.62,
2904                    color,
2905                    alpha,
2906                    soft,
2907                    mode,
2908                    lin,
2909                );
2910                return Ok(Value::Unit);
2911            },
2912
2913            // shadow_params(base, grow, alpha, fade, soft) — tune cast_shadow.
2914            // Each arg defaults to the current value, so you can set just one.
2915            "shadow_params" | "ตั้งค่าเงา" | "阴影参数" | "影設定" | "그림자설정" =>
2916            {
2917                let cur = self.gfx.borrow().shadow;
2918                let base = self.arg_num(&args, 0, cur.base as f64)? as f32;
2919                let grow = self.arg_num(&args, 1, cur.grow as f64)? as f32;
2920                let alpha = self.arg_num(&args, 2, cur.alpha as f64)? as f32;
2921                let fade = self.arg_num(&args, 3, cur.fade as f64)? as f32;
2922                let soft = self.arg_num(&args, 4, cur.soft as f64)? as f32;
2923                self.gfx.borrow_mut().shadow =
2924                    crate::gfx::ShadowParams { base, grow, alpha, fade, soft };
2925                return Ok(Value::Unit);
2926            },
2927
2928            // depth_triangle(x0,y0, x1,y1, x2,y2, z) — queue a depth-sorted tri in
2929            // the current colour. Drawn back-to-front (painter's algorithm) at
2930            // present(); larger z = farther away. Lets 2-D sprites/quads sort by
2931            // depth the same way 3-D faces do.
2932            "depth_triangle" | "สามเหลี่ยมเรียงลึก" | "深度三角" | "深度三角形" | "깊이삼각" =>
2933            {
2934                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
2935                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
2936                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
2937                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
2938                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
2939                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
2940                let z = self.arg_num(&args, 6, 0.0)? as f32;
2941                let mut gfx = self.gfx.borrow_mut();
2942                let color = gfx.color;
2943                gfx.depth_queue
2944                    .push_triangle(z, color, x0, y0, x1, y1, x2, y2);
2945                return Ok(Value::Unit);
2946            },
2947
2948            // depth_line(x0,y0, x1,y1, z) — queue a depth-sorted line in the
2949            // current colour (same painter's queue as depth_triangle).
2950            "depth_line" | "เส้นเรียงลึก" | "深度线" | "深度線" | "깊이선" =>
2951            {
2952                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
2953                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
2954                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
2955                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
2956                let z = self.arg_num(&args, 4, 0.0)? as f32;
2957                let mut gfx = self.gfx.borrow_mut();
2958                let color = gfx.color;
2959                gfx.depth_queue.push_line(z, color, x0, y0, x1, y1);
2960                return Ok(Value::Unit);
2961            },
2962
2963            // ══════════════════════════════════════════════════════════════════
2964            // GRAPHICS BUILTINS
2965            // Thai names first, then English aliases.
2966            // ══════════════════════════════════════════════════════════════════
2967
2968            // ── เปิดหน้าต่าง(width, height, title) — open_window ──
2969            "เปิดหน้าต่าง" | "open_window" | "gfx_window" | "开窗" | "ウィンドウ開く" | "창열기" =>
2970            {
2971                let w = self.arg_num(&args, 0, 800.0)? as usize;
2972                let h = self.arg_num(&args, 1, 600.0)? as usize;
2973                #[cfg(not(target_arch = "wasm32"))]
2974                {
2975                    let title = args
2976                        .get(2)
2977                        .map(|v| v.to_string())
2978                        .unwrap_or_else(|| "Ling".into());
2979                    let mut gfx = self.gfx.borrow_mut();
2980                    let mut win = minifb::Window::new(
2981                        &title,
2982                        w,
2983                        h,
2984                        minifb::WindowOptions {
2985                            resize: false,
2986                            scale: minifb::Scale::X1,
2987                            ..Default::default()
2988                        },
2989                    )
2990                    .map_err(|e| EvalErr::from(format!("cannot open window: {e}")))?;
2991                    #[allow(deprecated)]
2992                    win.limit_update_rate(Some(std::time::Duration::from_millis(8)));
2993                    gfx.buffer = vec![0u32; w * h];
2994                    gfx.width = w;
2995                    gfx.height = h;
2996                    gfx.window = Some(win);
2997                    gfx.sync_projection();
2998                    hide_console_window();
2999                }
3000                #[cfg(target_arch = "wasm32")]
3001                {
3002                    let mut gfx = self.gfx.borrow_mut();
3003                    gfx.width = w;
3004                    gfx.height = h;
3005                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
3006                    gfx.sync_projection();
3007                    crate::gfx::webgl::resize(w as u32, h as u32);
3008                }
3009                return Ok(Value::Unit);
3010            },
3011
3012            // ── เติม(r, g, b) — fill / clear screen with colour ──
3013            "เติม" | "fill" | "gfx_fill" | "clear" | "填" | "塗り潰し" | "채우기" | "清"
3014            | "消去" | "지우기" => {
3015                let r = self.arg_num(&args, 0, 0.0)? as u32;
3016                let g = self.arg_num(&args, 1, 0.0)? as u32;
3017                let b = self.arg_num(&args, 2, 0.0)? as u32;
3018                #[cfg(not(target_arch = "wasm32"))]
3019                {
3020                    let c = (r << 16) | (g << 8) | b;
3021                    let mut gfx = self.gfx.borrow_mut();
3022                    gfx.buffer.fill(c);
3023                    gfx.zbuf_needs_clear = true; // clear color ⇒ clear depth next flush
3024                    gfx.edge_set.clear();         // reset shared-edge dedup for new frame
3025                }
3026                #[cfg(target_arch = "wasm32")]
3027                {
3028                    let mut gfx = self.gfx.borrow_mut();
3029                    gfx.fill_r = r as f32 / 255.0;
3030                    gfx.fill_g = g as f32 / 255.0;
3031                    gfx.fill_b = b as f32 / 255.0;
3032                    let c = (r << 16) | (g << 8) | b;
3033                    gfx.buffer.fill(c);
3034                    gfx.zbuf_needs_clear = true;
3035                    gfx.edge_set.clear();
3036                }
3037                return Ok(Value::Unit);
3038            },
3039
3040            // ── set_color_hsl(h, s, l) — set drawing colour from HSL ──
3041            // h: 0–360 degrees, s: 0–100 saturation, l: 0–100 lightness
3042            "set_color_hsl" | "颜色HSL" | "色相" | "HSL色" | "HSL색설정" | "สีHSLวาด" =>
3043            {
3044                let h = self.arg_num(&args, 0, 0.0)?;
3045                let s = self.arg_num(&args, 1, 70.0)?;
3046                let l = self.arg_num(&args, 2, 50.0)?;
3047                let hex = hsl_to_hex(h, s, l);
3048                let r = u32::from_str_radix(&hex[1..3], 16).unwrap_or(255);
3049                let g = u32::from_str_radix(&hex[3..5], 16).unwrap_or(255);
3050                let b = u32::from_str_radix(&hex[5..7], 16).unwrap_or(255);
3051                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
3052                return Ok(Value::Unit);
3053            },
3054
3055            // ── สีดินสอ(r, g, b) — set drawing colour ──
3056            "สีดินสอ" | "set_color" | "gfx_color" | "color" | "设色" | "色設定" | "색설정" =>
3057            {
3058                let r = self.arg_num(&args, 0, 255.0)? as u32;
3059                let g = self.arg_num(&args, 1, 255.0)? as u32;
3060                let b = self.arg_num(&args, 2, 255.0)? as u32;
3061                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
3062                return Ok(Value::Unit);
3063            },
3064
3065            // ── วาดสามเหลี่ยม(x1,y1, x2,y2, x3,y3) — draw filled triangle ──
3066            "วาดสามเหลี่ยม"
3067            | "draw_triangle"
3068            | "gfx_triangle"
3069            | "triangle"
3070            | "画三角"
3071            | "三角形描画"
3072            | "삼각형그리기" => {
3073                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3074                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3075                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3076                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3077                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
3078                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
3079                let mut gfx = self.gfx.borrow_mut();
3080                let color = gfx.color;
3081                #[cfg(not(target_arch = "wasm32"))]
3082                {
3083                    let w = gfx.width;
3084                    let h = gfx.height;
3085                    fill_triangle(&mut gfx.buffer, w, h, color, x0, y0, x1, y1, x2, y2);
3086                }
3087                #[cfg(target_arch = "wasm32")]
3088                gfx.depth_queue
3089                    .push_triangle(0.0, color, x0, y0, x1, y1, x2, y2);
3090                return Ok(Value::Unit);
3091            },
3092
3093            // ── วาดเส้น(x1,y1, x2,y2) — draw line ──
3094            "วาดเส้น" | "draw_line" | "gfx_line" | "line" | "画线" | "線描く" | "선그리기" =>
3095            {
3096                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3097                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3098                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3099                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3100                let mut gfx = self.gfx.borrow_mut();
3101                let color = gfx.color;
3102                #[cfg(not(target_arch = "wasm32"))]
3103                {
3104                    let w = gfx.width;
3105                    let h = gfx.height;
3106                    draw_line(&mut gfx.buffer, w, h, color, x0, y0, x1, y1);
3107                }
3108                #[cfg(target_arch = "wasm32")]
3109                gfx.depth_queue.push_line(0.0, color, x0, y0, x1, y1);
3110                return Ok(Value::Unit);
3111            },
3112
3113            // ── วาดจุด(x, y) — plot a single pixel ──
3114            "วาดจุด" | "draw_pixel" | "gfx_pixel" | "pixel" | "画点" | "点描く" | "점그리기" =>
3115            {
3116                let px = self.arg_num(&args, 0, 0.0)? as i32;
3117                let py = self.arg_num(&args, 1, 0.0)? as i32;
3118                #[cfg(not(target_arch = "wasm32"))]
3119                {
3120                    let mut gfx = self.gfx.borrow_mut();
3121                    let color = gfx.color;
3122                    let w = gfx.width;
3123                    let h = gfx.height;
3124                    if px >= 0 && py >= 0 && (px as usize) < w && (py as usize) < h {
3125                        gfx.buffer[py as usize * w + px as usize] = color;
3126                    }
3127                }
3128                #[cfg(target_arch = "wasm32")]
3129                {
3130                    // Render pixel as a 1×1 square via two triangles.
3131                    let mut gfx = self.gfx.borrow_mut();
3132                    let color = gfx.color;
3133                    let x = px as f32;
3134                    let y = py as f32;
3135                    gfx.depth_queue
3136                        .push_triangle(0.0, color, x, y, x + 1.0, y, x + 1.0, y + 1.0);
3137                    gfx.depth_queue
3138                        .push_triangle(0.0, color, x, y, x + 1.0, y + 1.0, x, y + 1.0);
3139                }
3140                return Ok(Value::Unit);
3141            },
3142
3143            // ── แสดงผล() — flush depth queue, then present frame to screen ──
3144            "แสดงผล" | "present" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시" =>
3145            {
3146                #[cfg(not(target_arch = "wasm32"))]
3147                {
3148                    ling_fps_tick();
3149                    ling_phase_frame();
3150                    // Flush depth queue and present — release borrow before reading mouse.
3151                    {
3152                        let mut gfx = self.gfx.borrow_mut();
3153                        if !gfx.depth_queue.is_empty() {
3154                            let w = gfx.width;
3155                            let h = gfx.height;
3156                            let dt = gfx.depth_test;
3157                            let reset_z = gfx.zbuf_needs_clear;
3158                            let (bm, ba) = (gfx.blend, gfx.alpha);
3159                            let queue = std::mem::take(&mut gfx.depth_queue);
3160                            {
3161                                let g = &mut *gfx;
3162                                let z = if dt { Some(&mut g.depth_buf) } else { None };
3163                                queue.flush(&mut g.buffer, z, reset_z, w, h);
3164                            }
3165                            gfx.depth_queue.set_state(bm, ba);
3166                            gfx.zbuf_needs_clear = false;
3167                        }
3168                        let _t = std::time::Instant::now();
3169                        gfx.toon_post_process();
3170                        ling_phase_add(phase::TOON, _t.elapsed().as_nanos());
3171                        let w = gfx.width;
3172                        let h = gfx.height;
3173                        let g = &mut *gfx;
3174                        if let Some(win) = g.window.as_mut() {
3175                            let _b = std::time::Instant::now();
3176                            win.update_with_buffer(&g.buffer, w, h)
3177                                .map_err(|e| EvalErr::from(format!("present error: {e}")))?;
3178                            ling_phase_add(phase::BLIT, _b.elapsed().as_nanos());
3179                        }
3180                    }
3181                    // Read mouse AFTER update_with_buffer so events are processed.
3182                    let mouse_pos = {
3183                        let gfx = self.gfx.borrow();
3184                        gfx.window
3185                            .as_ref()
3186                            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
3187                    };
3188                    let mut gfx = self.gfx.borrow_mut();
3189                    if gfx.mouse_captured {
3190                        let w = gfx.width as f32;
3191                        let h = gfx.height as f32;
3192                        if let Some((mx, my)) = mouse_pos {
3193                            if gfx.last_mx.is_nan() {
3194                                gfx.mouse_dx = 0.0;
3195                                gfx.mouse_dy = 0.0;
3196                                gfx.last_mx = mx;
3197                                gfx.last_my = my;
3198                            } else {
3199                                gfx.mouse_dx = mx - gfx.last_mx;
3200                                gfx.mouse_dy = my - gfx.last_my;
3201                                // Wrap the cursor at every edge (L/R/U/D) → infinite look
3202                                // on both axes, and the cursor is NOT trapped (alt-tab works).
3203                                let margin = 6.0;
3204                                let (mut nx, mut ny, mut warp) = (mx, my, false);
3205                                if mx < margin {
3206                                    nx = w - margin - 2.0;
3207                                    warp = true;
3208                                } else if mx > w - margin {
3209                                    nx = margin + 2.0;
3210                                    warp = true;
3211                                }
3212                                if my < margin {
3213                                    ny = h - margin - 2.0;
3214                                    warp = true;
3215                                } else if my > h - margin {
3216                                    ny = margin + 2.0;
3217                                    warp = true;
3218                                }
3219                                if warp {
3220                                    #[cfg(windows)]
3221                                    unsafe {
3222                                        #[repr(C)]
3223                                        struct RECT {
3224                                            left: i32,
3225                                            top: i32,
3226                                            right: i32,
3227                                            bottom: i32,
3228                                        }
3229                                        extern "system" {
3230                                            fn GetForegroundWindow() -> isize;
3231                                            fn GetWindowRect(hwnd: isize, lpRect: *mut RECT)
3232                                                -> i32;
3233                                            fn SetCursorPos(x: i32, y: i32) -> i32;
3234                                        }
3235                                        let hwnd = GetForegroundWindow();
3236                                        let mut rect =
3237                                            RECT { left: 0, top: 0, right: 0, bottom: 0 };
3238                                        if GetWindowRect(hwnd, &mut rect) != 0 {
3239                                            SetCursorPos(
3240                                                rect.left + nx as i32,
3241                                                rect.top + ny as i32,
3242                                            );
3243                                        }
3244                                    }
3245                                    gfx.last_mx = nx;
3246                                    gfx.last_my = ny;
3247                                } else {
3248                                    gfx.last_mx = mx;
3249                                    gfx.last_my = my;
3250                                }
3251                            }
3252                        } else {
3253                            gfx.mouse_dx = 0.0;
3254                            gfx.mouse_dy = 0.0;
3255                        }
3256                    } else if let Some((mx, my)) = mouse_pos {
3257                        if gfx.last_mx.is_nan() {
3258                            gfx.mouse_dx = 0.0;
3259                            gfx.mouse_dy = 0.0;
3260                        } else {
3261                            gfx.mouse_dx = mx - gfx.last_mx;
3262                            gfx.mouse_dy = my - gfx.last_my;
3263                        }
3264                        gfx.last_mx = mx;
3265                        gfx.last_my = my;
3266                    } else {
3267                        gfx.mouse_dx = 0.0;
3268                        gfx.mouse_dy = 0.0;
3269                    }
3270                }
3271                #[cfg(target_arch = "wasm32")]
3272                {
3273                    {
3274                        // Software-render everything (3-D depth queue + 2-D vtex/ui that
3275                        // already wrote into the buffer) into the framebuffer, exactly
3276                        // like native, then upload that buffer to the canvas in one blit.
3277                        let mut gfx = self.gfx.borrow_mut();
3278                        let w = gfx.width;
3279                        let h = gfx.height;
3280                        if gfx.buffer.len() != w * h {
3281                            gfx.buffer.resize(w * h, 0);
3282                        }
3283                        if !gfx.depth_queue.is_empty() {
3284                            let dt = gfx.depth_test;
3285                            let reset_z = gfx.zbuf_needs_clear;
3286                            let queue = std::mem::take(&mut gfx.depth_queue);
3287                            {
3288                                let g = &mut *gfx;
3289                                let z = if dt { Some(&mut g.depth_buf) } else { None };
3290                                queue.flush(&mut g.buffer, z, reset_z, w, h);
3291                            }
3292                            gfx.zbuf_needs_clear = false;
3293                        }
3294                        gfx.toon_post_process();
3295                        crate::gfx::webgl::blit_rgb(&gfx.buffer, w, h);
3296                    }
3297                    self.wasm_pace_frame();
3298                }
3299                // Update the click-edge latch for interactive UI widgets.
3300                #[cfg(not(target_arch = "wasm32"))]
3301                {
3302                    let (_, _, down) = self.mouse_now();
3303                    self.mouse_was_down = down;
3304                }
3305                // Increment frame counter
3306                self.frame_num += 1;
3307                return Ok(Value::Unit);
3308            },
3309
3310            // ── เปิดหน้าต่างเต็มจอ(title) — true native-res fullscreen window ──
3311            "เปิดหน้าต่างเต็มจอ"
3312            | "open_fullscreen"
3313            | "fullscreen"
3314            | "全屏"
3315            | "全画面"
3316            | "전체화면" => {
3317                // In WASM the canvas defines the viewport; use its current size
3318                // as the default so the projection matches what's actually visible.
3319                #[cfg(target_arch = "wasm32")]
3320                let (default_w, default_h) = {
3321                    let (cw, ch) = crate::gfx::webgl::canvas_size();
3322                    (cw as f64, ch as f64)
3323                };
3324                // On native: query the actual primary monitor resolution.
3325                #[cfg(all(not(target_arch = "wasm32"), windows))]
3326                let (default_w, default_h) = unsafe {
3327                    extern "system" {
3328                        fn GetSystemMetrics(nIndex: i32) -> i32;
3329                    }
3330                    (GetSystemMetrics(0) as f64, GetSystemMetrics(1) as f64)
3331                };
3332                #[cfg(all(not(target_arch = "wasm32"), not(windows)))]
3333                let (default_w, default_h) = native_screen_size();
3334
3335                let w = args
3336                    .get(1)
3337                    .map(|v| self.to_number(v).unwrap_or(default_w) as usize)
3338                    .unwrap_or(default_w as usize);
3339                let h = args
3340                    .get(2)
3341                    .map(|v| self.to_number(v).unwrap_or(default_h) as usize)
3342                    .unwrap_or(default_h as usize);
3343                #[cfg(not(target_arch = "wasm32"))]
3344                {
3345                    let title = args
3346                        .get(0)
3347                        .map(|v| v.to_string())
3348                        .unwrap_or_else(|| "Ling".into());
3349                    let mut gfx = self.gfx.borrow_mut();
3350                    let mut win = minifb::Window::new(
3351                        &title,
3352                        w,
3353                        h,
3354                        minifb::WindowOptions {
3355                            borderless: true,
3356                            title: false,
3357                            resize: false,
3358                            topmost: true,
3359                            scale: minifb::Scale::X1,
3360                            ..Default::default()
3361                        },
3362                    )
3363                    .map_err(|e| EvalErr::from(format!("cannot open fullscreen: {e}")))?;
3364                    // Drive the loop at the monitor's real refresh rate instead of
3365                    // a hard-coded cap, so a 144 Hz panel runs at 144 fps.
3366                    // LING_FPS_CAP overrides the target frame rate (0 = uncapped);
3367                    // otherwise drive at the monitor's real refresh.
3368                    match std::env::var("LING_FPS_CAP").ok().and_then(|v| v.parse::<usize>().ok()) {
3369                        Some(0) => win.set_target_fps(100_000),
3370                        Some(cap) => win.set_target_fps(cap),
3371                        None => win.set_target_fps(monitor_info().2.max(30) as usize),
3372                    }
3373                    // Grab the native handle *before* moving the window into gfx.
3374                    #[cfg(windows)]
3375                    let hwnd = win.get_window_handle() as isize;
3376                    gfx.buffer = vec![0u32; w * h];
3377                    gfx.width = w;
3378                    gfx.height = h;
3379                    gfx.window = Some(win);
3380                    gfx.sync_projection();
3381                    // Strip all chrome and cover the full screen, above the taskbar.
3382                    #[cfg(windows)]
3383                    make_borderless_fullscreen(hwnd, w as i32, h as i32);
3384                    hide_console_window();
3385                }
3386                #[cfg(target_arch = "wasm32")]
3387                {
3388                    let mut gfx = self.gfx.borrow_mut();
3389                    gfx.width = w;
3390                    gfx.height = h;
3391                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
3392                    gfx.sync_projection();
3393                    crate::gfx::webgl::resize(w as u32, h as u32);
3394                }
3395                return Ok(Value::Unit);
3396            },
3397
3398            // ── ความกว้าง() / ความสูง() — current framebuffer size ──
3399            "get_width" | "ความกว้าง" | "宽" | "幅取得" | "너비" => {
3400                return Ok(Value::Number(self.gfx.borrow().width as f64));
3401            },
3402            "get_height" | "ความสูง" | "高" | "高取得" | "높이" => {
3403                return Ok(Value::Number(self.gfx.borrow().height as f64));
3404            },
3405
3406            // ── monitor detection: physical display, not the framebuffer ──────
3407            // monitor_width() → primary-monitor pixel width
3408            "monitor_width" | "screen_width" | "屏宽" | "画面幅" | "화면너비" | "ความกว้างจอ" =>
3409            {
3410                return Ok(Value::Number(monitor_info().0 as f64));
3411            },
3412            // monitor_height() → primary-monitor pixel height
3413            "monitor_height" | "screen_height" | "屏高" | "画面高" | "화면높이" | "ความสูงจอ" =>
3414            {
3415                return Ok(Value::Number(monitor_info().1 as f64));
3416            },
3417            // monitor_refresh() → refresh rate in Hz (a.k.a. the monitor framerate)
3418            "monitor_refresh"
3419            | "monitor_hz"
3420            | "monitor_fps"
3421            | "refresh_rate"
3422            | "刷新率"
3423            | "リフレッシュレート"
3424            | "주사율"
3425            | "อัตรารีเฟรช" => {
3426                return Ok(Value::Number(monitor_info().2 as f64));
3427            },
3428            // monitor_info() → [width, height, refresh_hz]
3429            "monitor_info" | "screen_info" | "屏幕信息" | "画面情報" | "화면정보" | "ข้อมูลจอ" =>
3430            {
3431                let (w, h, hz) = monitor_info();
3432                return Ok(Value::List(Rc::new(vec![
3433                    Value::Number(w as f64),
3434                    Value::Number(h as f64),
3435                    Value::Number(hz as f64),
3436                ])));
3437            },
3438            // set_fps(n) → cap the render loop at n frames per second
3439            "set_fps"
3440            | "set_target_fps"
3441            | "target_fps"
3442            | "设帧率"
3443            | "フレームレート設定"
3444            | "프레임설정"
3445            | "ตั้งเฟรมเรต" => {
3446                #[cfg(not(target_arch = "wasm32"))]
3447                {
3448                    let fps = self.arg_num(&args, 0, 60.0)?.max(1.0) as usize;
3449                    let mut gfx = self.gfx.borrow_mut();
3450                    if let Some(win) = gfx.window.as_mut() {
3451                        win.set_target_fps(fps);
3452                    }
3453                }
3454                #[cfg(target_arch = "wasm32")]
3455                {
3456                    self.wasm_target_fps = self.arg_num(&args, 0, 60.0)?.max(1.0);
3457                    self.wasm_next_present_ms = 0.0;
3458                }
3459                return Ok(Value::Unit);
3460            },
3461
3462            // ── หน้าต่างเปิดอยู่() → bool — is the window still open? ──
3463            "หน้าต่างเปิดอยู่"
3464            | "window_is_open"
3465            | "gfx_is_open"
3466            | "is_open"
3467            | "窗开"
3468            | "開いている"
3469            | "창열림" => {
3470                #[cfg(not(target_arch = "wasm32"))]
3471                {
3472                    let gfx = self.gfx.borrow();
3473                    let open = gfx
3474                        .window
3475                        .as_ref()
3476                        .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
3477                        .unwrap_or(false);
3478                    return Ok(Value::Bool(open));
3479                }
3480                #[cfg(target_arch = "wasm32")]
3481                return Ok(Value::Bool(true));
3482            },
3483
3484            // ── key_down(name) → bool — is a key held? ──
3485            "key_down" | "กดค้าง" | "按键" | "キー押す" | "키누름" => {
3486                #[cfg(not(target_arch = "wasm32"))]
3487                {
3488                    let name = self.arg_str(&args, 0, "");
3489                    let gfx = self.gfx.borrow();
3490                    let down = gfx
3491                        .window
3492                        .as_ref()
3493                        .and_then(|w| str_to_minifb_key(&name).map(|k| w.is_key_down(k)))
3494                        .unwrap_or(false);
3495                    return Ok(Value::Bool(down));
3496                }
3497                #[cfg(target_arch = "wasm32")]
3498                return Ok(Value::Bool(false));
3499            },
3500
3501            // ── key_pressed(name) → bool — was a key pressed this frame? ──
3502            "key_pressed" | "กดปุ่ม" | "键按" | "キー押した" | "키눌림" => {
3503                #[cfg(not(target_arch = "wasm32"))]
3504                {
3505                    let name = self.arg_str(&args, 0, "");
3506                    let pressed = {
3507                        let gfx = self.gfx.borrow();
3508                        gfx.window
3509                            .as_ref()
3510                            .and_then(|w| {
3511                                str_to_minifb_key(&name)
3512                                    .map(|k| w.is_key_pressed(k, minifb::KeyRepeat::No))
3513                            })
3514                            .unwrap_or(false)
3515                    };
3516                    // gamepad Start behaves like Enter everywhere
3517                    let pressed =
3518                        pressed || ((name == "enter" || name == "return") && gamepad::start_edge());
3519                    return Ok(Value::Bool(pressed));
3520                }
3521                #[cfg(target_arch = "wasm32")]
3522                {
3523                    let name = self.arg_str(&args, 0, "");
3524                    let pressed = crate::gfx::wasm_is_key_pressed(&name);
3525                    return Ok(Value::Bool(pressed));
3526                }
3527            },
3528
3529            // ── mouse_dx() / mouse_dy() → f64 — delta since last frame ──
3530            "mouse_dx" | "เมาส์X" | "鼠ΔX" | "マウスΔX" | "마우스ΔX" => {
3531                #[cfg(not(target_arch = "wasm32"))]
3532                return Ok(Value::Number(self.gfx.borrow().mouse_dx as f64));
3533                #[cfg(target_arch = "wasm32")]
3534                return Ok(Value::Number(0.0));
3535            },
3536            // ── mouse_scroll() → f64 — vertical scroll-wheel delta this frame ──
3537            #[cfg(not(target_arch = "wasm32"))]
3538            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" =>
3539            {
3540                let gfx = self.gfx.borrow();
3541                let s = gfx
3542                    .window
3543                    .as_ref()
3544                    .and_then(|w| w.get_scroll_wheel())
3545                    .map(|(_, y)| y as f64)
3546                    .unwrap_or(0.0);
3547                return Ok(Value::Number(s));
3548            },
3549            #[cfg(target_arch = "wasm32")]
3550            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" =>
3551            {
3552                return Ok(Value::Number(0.0));
3553            },
3554            "mouse_dy" | "เมาส์Y" | "鼠ΔY" | "マウスΔY" | "마우스ΔY" => {
3555                #[cfg(not(target_arch = "wasm32"))]
3556                return Ok(Value::Number(self.gfx.borrow().mouse_dy as f64));
3557                #[cfg(target_arch = "wasm32")]
3558                return Ok(Value::Number(0.0));
3559            },
3560
3561            // ── Gamepad / joystick input (ling-input "Sensorium" + gilrs) ──
3562            // pad_poll() → number — advance input one frame; returns # connected pads.
3563            "pad_poll" | "手柄轮询" | "パッド更新" | "패드폴링" | "อัปเดตแพด" =>
3564            {
3565                #[cfg(not(target_arch = "wasm32"))]
3566                return Ok(Value::Number(self.pad_poll() as f64));
3567                #[cfg(target_arch = "wasm32")]
3568                return Ok(Value::Number(input_web::poll() as f64));
3569            },
3570            // pad_count() → number — connected gamepads.
3571            "pad_count" | "手柄数" | "パッド数" | "패드수" | "จำนวนแพด" =>
3572            {
3573                #[cfg(not(target_arch = "wasm32"))]
3574                {
3575                    let inp = self.input.borrow();
3576                    let n = inp.as_ref().map_or(0, |s| s.sensorium.devices.count());
3577                    return Ok(Value::Number(n as f64));
3578                }
3579                #[cfg(target_arch = "wasm32")]
3580                return Ok(Value::Number(input_web::count() as f64));
3581            },
3582            // pad_connected(i) → bool.
3583            "pad_connected" | "手柄连接" | "パッド接続" | "패드연결" | "แพดเชื่อม" =>
3584            {
3585                #[cfg(not(target_arch = "wasm32"))]
3586                {
3587                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3588                    let inp = self.input.borrow();
3589                    let c = inp
3590                        .as_ref()
3591                        .is_some_and(|s| s.sensorium.devices.for_player(i as u8).is_some());
3592                    return Ok(Value::Bool(c));
3593                }
3594                #[cfg(target_arch = "wasm32")]
3595                {
3596                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3597                    return Ok(Value::Bool(input_web::is_connected(i)));
3598                }
3599            },
3600            // pad_button(i, name) → bool — is the button held?
3601            "pad_button" | "手柄按键" | "パッドボタン" | "패드버튼" | "ปุ่มแพด" =>
3602            {
3603                #[cfg(not(target_arch = "wasm32"))]
3604                {
3605                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3606                    let name = self.arg_str(&args, 1, "");
3607                    let down = parse_pad_button(&name)
3608                        .is_some_and(|b| self.with_pad(i, false, |p| p.is_down(b)));
3609                    return Ok(Value::Bool(down));
3610                }
3611                #[cfg(target_arch = "wasm32")]
3612                {
3613                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3614                    let name = self.arg_str(&args, 1, "");
3615                    return Ok(Value::Bool(input_web::button_down(i, &name)));
3616                }
3617            },
3618            // pad_pressed(i, name) → bool — pressed this frame?
3619            // On WASM we only have the current snapshot, so treat as button_down.
3620            "pad_pressed" | "手柄按下" | "パッド押下" | "패드눌림" | "แพดกด" =>
3621            {
3622                #[cfg(not(target_arch = "wasm32"))]
3623                {
3624                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3625                    let name = self.arg_str(&args, 1, "");
3626                    let p = parse_pad_button(&name)
3627                        .is_some_and(|b| self.with_pad(i, false, |g| g.just_pressed(b)));
3628                    return Ok(Value::Bool(p));
3629                }
3630                #[cfg(target_arch = "wasm32")]
3631                {
3632                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3633                    let name = self.arg_str(&args, 1, "");
3634                    return Ok(Value::Bool(input_web::button_down(i, &name)));
3635                }
3636            },
3637            // pad_lx(i)/pad_ly(i)/pad_rx(i)/pad_ry(i) → number — stick axes (−1..=1).
3638            "pad_lx" | "手柄左X" | "パッド左X" | "패드왼X" | "แพดซ้ายX" => {
3639                #[cfg(not(target_arch = "wasm32"))]
3640                {
3641                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3642                    return Ok(Value::Number(
3643                        self.with_pad(i, 0.0, |p| p.left_stick.x as f64),
3644                    ));
3645                }
3646                #[cfg(target_arch = "wasm32")]
3647                {
3648                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3649                    return Ok(Value::Number(input_web::axis_lx(i) as f64));
3650                }
3651            },
3652            "pad_ly" | "手柄左Y" | "パッド左Y" | "패드왼Y" | "แพดซ้ายY" => {
3653                #[cfg(not(target_arch = "wasm32"))]
3654                {
3655                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3656                    return Ok(Value::Number(
3657                        self.with_pad(i, 0.0, |p| p.left_stick.y as f64),
3658                    ));
3659                }
3660                #[cfg(target_arch = "wasm32")]
3661                {
3662                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3663                    return Ok(Value::Number(input_web::axis_ly(i) as f64));
3664                }
3665            },
3666            "pad_rx" | "手柄右X" | "パッド右X" | "패드오X" | "แพดขวาX" => {
3667                #[cfg(not(target_arch = "wasm32"))]
3668                {
3669                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3670                    return Ok(Value::Number(
3671                        self.with_pad(i, 0.0, |p| p.right_stick.x as f64),
3672                    ));
3673                }
3674                #[cfg(target_arch = "wasm32")]
3675                {
3676                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3677                    return Ok(Value::Number(input_web::axis_rx(i) as f64));
3678                }
3679            },
3680            "pad_ry" | "手柄右Y" | "パッド右Y" | "패드오Y" | "แพดขวาY" => {
3681                #[cfg(not(target_arch = "wasm32"))]
3682                {
3683                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3684                    return Ok(Value::Number(
3685                        self.with_pad(i, 0.0, |p| p.right_stick.y as f64),
3686                    ));
3687                }
3688                #[cfg(target_arch = "wasm32")]
3689                {
3690                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3691                    return Ok(Value::Number(input_web::axis_ry(i) as f64));
3692                }
3693            },
3694            // pad_lt(i)/pad_rt(i) → number — analog triggers (0..=1).
3695            "pad_lt" | "手柄左扳机" | "パッド左トリガー" | "패드왼트리거" | "ไกแพดซ้าย" =>
3696            {
3697                #[cfg(not(target_arch = "wasm32"))]
3698                {
3699                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3700                    return Ok(Value::Number(
3701                        self.with_pad(i, 0.0, |p| p.left_trigger as f64),
3702                    ));
3703                }
3704                #[cfg(target_arch = "wasm32")]
3705                {
3706                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3707                    return Ok(Value::Number(input_web::trigger_lt(i) as f64));
3708                }
3709            },
3710            "pad_rt" | "手柄右扳机" | "パッド右トリガー" | "패드오트리거" | "ไกแพดขวา" =>
3711            {
3712                #[cfg(not(target_arch = "wasm32"))]
3713                {
3714                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3715                    return Ok(Value::Number(
3716                        self.with_pad(i, 0.0, |p| p.right_trigger as f64),
3717                    ));
3718                }
3719                #[cfg(target_arch = "wasm32")]
3720                {
3721                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3722                    return Ok(Value::Number(input_web::trigger_rt(i) as f64));
3723                }
3724            },
3725            // pad_rumble(i, lo, hi) → unit — set rumble motor amplitudes (0..=1).
3726            "pad_rumble" | "手柄震动" | "パッド振動" | "패드진동" | "แพดสั่น" =>
3727            {
3728                #[cfg(not(target_arch = "wasm32"))]
3729                {
3730                    use ling_input::backend::InputBackend;
3731                    let i = self.arg_num(&args, 0, 0.0)? as usize;
3732                    let lo = self.arg_num(&args, 1, 0.0)? as f32;
3733                    let hi = self.arg_num(&args, 2, lo as f64)? as f32;
3734                    let mut inp = self.input.borrow_mut();
3735                    if let Some(s) = inp.as_mut() {
3736                        if let Some(dev) = s.sensorium.devices.for_player(i as u8).map(|d| d.id) {
3737                            s.backend.set_rumble(
3738                                dev,
3739                                ling_input::Rumble { low: lo, high: hi, ..Default::default() },
3740                            );
3741                        }
3742                    }
3743                    return Ok(Value::Unit);
3744                }
3745                #[cfg(target_arch = "wasm32")]
3746                return Ok(Value::Unit);
3747            },
3748
3749            // ── set_camera_pos(x, y, z) — move camera to world position ──
3750            "set_camera_pos" | "ตั้งตำแหน่งกล้อง" | "镜坐标" | "カメラ座標" | "카메라좌표" =>
3751            {
3752                let x = self.arg_num(&args, 0, 0.0)? as f32;
3753                let y = self.arg_num(&args, 1, 0.0)? as f32;
3754                let z = self.arg_num(&args, 2, 0.0)? as f32;
3755                {
3756                    let mut gfx = self.gfx.borrow_mut();
3757                    gfx.camera.tx = x;
3758                    gfx.camera.ty = y;
3759                    gfx.camera.tz = z;
3760                }
3761                #[cfg(not(target_arch = "wasm32"))]
3762                if let Some(audio) = &self.audio {
3763                    audio.set_listener_pos(x, y, z);
3764                }
3765                return Ok(Value::Unit);
3766            },
3767
3768            // ── move_camera(dx, dy, dz) — translate camera by delta ──
3769            "move_camera" => {
3770                let dx = self.arg_num(&args, 0, 0.0)? as f32;
3771                let dy = self.arg_num(&args, 1, 0.0)? as f32;
3772                let dz = self.arg_num(&args, 2, 0.0)? as f32;
3773                let mut gfx = self.gfx.borrow_mut();
3774                gfx.camera.tx += dx;
3775                gfx.camera.ty += dy;
3776                gfx.camera.tz += dz;
3777                return Ok(Value::Unit);
3778            },
3779
3780            // ── set_zdist(d) — set perspective z-offset (field-of-view taper) ──
3781            "set_zdist" | "ตั้งระยะห่าง" | "镜距" | "Z距離設定" | "Z거리설정" =>
3782            {
3783                let d = self.arg_num(&args, 0, 5.0)? as f32;
3784                self.gfx.borrow_mut().camera.zdist = d;
3785                return Ok(Value::Unit);
3786            },
3787
3788            // ── capture_mouse() — hide cursor and warp to centre each frame ──
3789            "capture_mouse" | "จับเมาส์" | "捕鼠" | "マウス捕捉" | "마우스잡기" =>
3790            {
3791                #[cfg(not(target_arch = "wasm32"))]
3792                {
3793                    let mut gfx = self.gfx.borrow_mut();
3794                    gfx.mouse_captured = true;
3795                    gfx.last_mx = f32::NAN;
3796                    if let Some(win) = gfx.window.as_mut() {
3797                        win.set_cursor_visibility(false);
3798                    }
3799                }
3800                return Ok(Value::Unit);
3801            },
3802
3803            // ── release_mouse() — restore cursor and remove clip region ──
3804            "release_mouse" => {
3805                #[cfg(not(target_arch = "wasm32"))]
3806                {
3807                    let mut gfx = self.gfx.borrow_mut();
3808                    gfx.mouse_captured = false;
3809                    gfx.last_mx = f32::NAN;
3810                    if let Some(win) = gfx.window.as_mut() {
3811                        win.set_cursor_visibility(true);
3812                    }
3813                    #[cfg(windows)]
3814                    unsafe {
3815                        // Null releases the clip; reuse the RECT-typed declaration above.
3816                        extern "system" {
3817                            fn ClipCursor(lpRect: *const std::ffi::c_void) -> i32;
3818                        }
3819                        ClipCursor(std::ptr::null());
3820                    }
3821                }
3822                return Ok(Value::Unit);
3823            },
3824
3825            // ══════════════════════════════════════════════════════════════════
3826            // 3-D / 4-D DRAWING — camera, lights, depth-sorted geometry
3827            // ══════════════════════════════════════════════════════════════════
3828
3829            // ── set_camera(cry, sry, crx, srx) — store precomputed camera trig ──
3830            // Call once per frame after computing cos/sin of your rotation angles.
3831            "set_camera" | "ตั้งกล้อง" | "设镜" | "设置摄像机" | "カメラ設定" | "카메라설정" =>
3832            {
3833                let cry = self.arg_num(&args, 0, 1.0)? as f32;
3834                let sry = self.arg_num(&args, 1, 0.0)? as f32;
3835                let crx = self.arg_num(&args, 2, 1.0)? as f32;
3836                let srx = self.arg_num(&args, 3, 0.0)? as f32;
3837                let mut gfx = self.gfx.borrow_mut();
3838                gfx.camera.cry = cry;
3839                gfx.camera.sry = sry;
3840                gfx.camera.crx = crx;
3841                gfx.camera.srx = srx;
3842                return Ok(Value::Unit);
3843            },
3844
3845            // ── set_projection(cx, cy, focal, zdist) — override projection params ──
3846            // Automatically set when the window opens; override only if needed.
3847            "set_projection" | "ตั้งโปรเจกชัน" | "投影" | "投影設定" | "투영설정" =>
3848            {
3849                let cx = self.arg_num(&args, 0, 960.0)? as f32;
3850                let cy = self.arg_num(&args, 1, 540.0)? as f32;
3851                let focal = self.arg_num(&args, 2, 1080.0)? as f32;
3852                let zdist = self.arg_num(&args, 3, 5.0)? as f32;
3853                let mut gfx = self.gfx.borrow_mut();
3854                gfx.camera.cx = cx;
3855                gfx.camera.cy = cy;
3856                gfx.camera.focal = focal;
3857                gfx.camera.zdist = zdist;
3858                return Ok(Value::Unit);
3859            },
3860
3861            // ── draw_mesh(pos, idx, ox, oy, oz, scale, mode) ──
3862            //   Native batched triangle mesh. pos = flat [x,y,z,…], idx = flat tri indices.
3863            //   mode 0 = lit with current pen colour; 1 = per-face hue cycle.
3864            //   Vertices are batch-projected via ling-gpu (CPU fallback, or CUDA when the
3865            //   `cuda` feature is on); the per-triangle loop runs natively (not in the
3866            //   interpreter) so dense meshes (imported glTF, grids) stay fast.
3867            "draw_mesh" | "วาดเมช" => {
3868                let pos = match args.first() {
3869                    Some(Value::List(v)) => v,
3870                    _ => return Ok(Value::Unit),
3871                };
3872                let idx = match args.get(1) {
3873                    Some(Value::List(v)) => v,
3874                    _ => return Ok(Value::Unit),
3875                };
3876                let ox = self.arg_num(&args, 2, 0.0)? as f32;
3877                let oy = self.arg_num(&args, 3, 0.0)? as f32;
3878                let oz = self.arg_num(&args, 4, 0.0)? as f32;
3879                let scale = self.arg_num(&args, 5, 1.0)? as f32;
3880                let mode = self.arg_num(&args, 6, 0.0)? as i64;
3881                let nv = pos.len() / 3;
3882                if nv == 0 {
3883                    return Ok(Value::Unit);
3884                }
3885                let mut world = vec![0.0f32; nv * 3];
3886                for i in 0..nv {
3887                    world[i * 3] = ox + self.to_number(&pos[i * 3]).unwrap_or(0.0) as f32 * scale;
3888                    world[i * 3 + 1] =
3889                        oy + self.to_number(&pos[i * 3 + 1]).unwrap_or(0.0) as f32 * scale;
3890                    world[i * 3 + 2] =
3891                        oz + self.to_number(&pos[i * 3 + 2]).unwrap_or(0.0) as f32 * scale;
3892                }
3893                let mut gfx = self.gfx.borrow_mut();
3894                let cp = {
3895                    let c = &gfx.camera;
3896                    ling_gpu::CameraParams {
3897                        cry: c.cry,
3898                        sry: c.sry,
3899                        crx: c.crx,
3900                        srx: c.srx,
3901                        cx: c.cx,
3902                        cy: c.cy,
3903                        focal: c.focal,
3904                        zdist: c.zdist,
3905                        tx: c.tx,
3906                        ty: c.ty,
3907                        tz: c.tz,
3908                    }
3909                };
3910                let near = -gfx.camera.zdist + 0.02;
3911                let base = gfx.color;
3912                let ambient = gfx.ambient;
3913                let mut proj = vec![0.0f32; nv * 3]; // (sx, sy, depth) per vertex
3914                ling_gpu::backend().project_points(&world, &cp, &mut proj);
3915                let nt = idx.len() / 3;
3916                for t in 0..nt {
3917                    let ia = self.to_number(&idx[t * 3]).unwrap_or(0.0) as usize;
3918                    let ib = self.to_number(&idx[t * 3 + 1]).unwrap_or(0.0) as usize;
3919                    let ic = self.to_number(&idx[t * 3 + 2]).unwrap_or(0.0) as usize;
3920                    if ia >= nv || ib >= nv || ic >= nv {
3921                        continue;
3922                    }
3923                    let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
3924                    if (da + db + dc) / 3.0 <= near {
3925                        continue;
3926                    } // near-plane cull (centroid)
3927                    let col = if mode == 1 {
3928                        let h = t as f32 * 0.6;
3929                        let r = ((h.sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
3930                        let g = (((h + 2.094).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
3931                        let b = (((h + 4.189).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
3932                        (r << 16) | (g << 8) | b
3933                    } else {
3934                        let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
3935                        let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
3936                        let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
3937                        let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
3938                        let (vx, vy, vz) = (px - ax, py - ay, pz - az);
3939                        let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
3940                        let centroid = [
3941                            (ax + bx + px) / 3.0,
3942                            (ay + by + py) / 3.0,
3943                            (az + bz + pz) / 3.0,
3944                        ];
3945                        if gfx.flat_shade {
3946                            base
3947                        } else {
3948                            crate::gfx::light::compute_lit_color(
3949                                base,
3950                                normal,
3951                                centroid,
3952                                &gfx.lights,
3953                                ambient,
3954                            )
3955                        }
3956                    };
3957                    let depth = (da + db + dc) / 3.0;
3958                    let col = gfx.fog_apply(col, depth);
3959                    // True per-vertex depth so the z-buffer resolves mesh
3960                    // self-occlusion (when depth_test is off, the flush ignores
3961                    // z and uses the screen x/y exactly as before).
3962                    gfx.depth_queue.push_triangle_zv(
3963                        col,
3964                        proj[ia * 3],
3965                        proj[ia * 3 + 1],
3966                        da,
3967                        proj[ib * 3],
3968                        proj[ib * 3 + 1],
3969                        db,
3970                        proj[ic * 3],
3971                        proj[ic * 3 + 1],
3972                        dc,
3973                    );
3974                }
3975                return Ok(Value::Unit);
3976            },
3977
3978            // ── add_light(x, y, z, r, g, b, intensity, radius) ──
3979            // Adds a point light in world space.  r/g/b in [0..1].
3980            // radius == 0 → no distance falloff.
3981            "add_light" | "เพิ่มแสง" | "加灯" | "ライト追加" | "조명추가" =>
3982            {
3983                let x = self.arg_num(&args, 0, 0.0)? as f32;
3984                let y = self.arg_num(&args, 1, -3.0)? as f32;
3985                let z = self.arg_num(&args, 2, 3.0)? as f32;
3986                let mut r = self.arg_num(&args, 3, 1.0)? as f32;
3987                let mut g = self.arg_num(&args, 4, 1.0)? as f32;
3988                let mut b = self.arg_num(&args, 5, 1.0)? as f32;
3989                // Forgive 0-255 colour values: if any channel is clearly > 1,
3990                // treat the triple as 0-255 and normalise. Keeps 0-1 callers exact.
3991                if r > 1.5 || g > 1.5 || b > 1.5 {
3992                    r /= 255.0;
3993                    g /= 255.0;
3994                    b /= 255.0;
3995                }
3996                let intensity = self.arg_num(&args, 6, 1.0)? as f32;
3997                let radius = self.arg_num(&args, 7, 0.0)? as f32;
3998                self.gfx
3999                    .borrow_mut()
4000                    .lights
4001                    .push(Light { x, y, z, r, g, b, intensity, radius });
4002                return Ok(Value::Unit);
4003            },
4004
4005            // ── clear_lights() — remove all lights ──
4006            "clear_lights" | "ล้างแสง" | "清灯" | "ライト消去" | "조명초기화" =>
4007            {
4008                self.gfx.borrow_mut().lights.clear();
4009                return Ok(Value::Unit);
4010            },
4011
4012            // ── set_material(key, value) — configure LingMaterial field ──
4013            // Activates the material BSDF for subsequent polygon/triangle draws.
4014            // Keys (string): "albedo" "roughness" "metallic" "emission"
4015            //   "emission_strength" "specular" "specular_tint" "subsurface"
4016            //   "subsurface_color" "clearcoat" "clearcoat_roughness"
4017            //   "transmission" "ior" "iridescence" "sheen" "anisotropy"
4018            //   "anisotropy_angle" "toon_bands" "shadow_softness"
4019            //   "outline_px" "outline_color" "highlight_color"
4020            // Value: number (or packed 0xRRGGBB for colour fields)
4021            "set_material" | "ตั้งวัสดุ" | "设置材质" | "マテリアル設定" | "재질설정" =>
4022            {
4023                let key = self.arg_str(&args, 0, "");
4024                let val = self.arg_num(&args, 1, 0.0)?;
4025                let mut gfx = self.gfx.borrow_mut();
4026                let mat = gfx.material.get_or_insert_with(crate::gfx::LingMaterial::default);
4027                match key.as_str() {
4028                    "albedo"             => mat.albedo             = val as u32,
4029                    "roughness"          => mat.roughness          = val as f32,
4030                    "metallic"           => mat.metallic           = val as f32,
4031                    "emission"           => mat.emission           = val as u32,
4032                    "emission_strength"  => mat.emission_strength  = val as f32,
4033                    "specular"           => mat.specular           = val as f32,
4034                    "specular_tint"      => mat.specular_tint      = val as f32,
4035                    "subsurface"         => mat.subsurface         = val as f32,
4036                    "subsurface_color"   => mat.subsurface_color   = val as u32,
4037                    "clearcoat"          => mat.clearcoat          = val as f32,
4038                    "clearcoat_roughness"=> mat.clearcoat_roughness= val as f32,
4039                    "transmission"       => mat.transmission       = val as f32,
4040                    "ior"                => mat.ior                = val as f32,
4041                    "iridescence"        => mat.iridescence        = val as f32,
4042                    "sheen"              => mat.sheen              = val as f32,
4043                    "anisotropy"         => mat.anisotropy         = val as f32,
4044                    "anisotropy_angle"   => mat.anisotropy_angle   = val as f32,
4045                    "toon_bands"         => mat.toon_bands         = val as u32,
4046                    "shadow_softness"    => mat.shadow_softness    = val as f32,
4047                    "outline_px"         => mat.outline_px         = val as f32,
4048                    "outline_color"      => mat.outline_color      = val as u32,
4049                    "highlight_color"    => mat.highlight_color    = val as u32,
4050                    _ => {}
4051                }
4052                return Ok(Value::Unit);
4053            },
4054
4055            // ── reset_material() — disable material override ──
4056            // After this call, draws use the legacy compute_lit_color_linear path.
4057            "reset_material" | "รีเซ็ตวัสดุ" | "重置材质" | "マテリアルリセット" | "재질초기화" =>
4058            {
4059                self.gfx.borrow_mut().material = None;
4060                return Ok(Value::Unit);
4061            },
4062
4063            // ── toon_outlines(thickness, color, threshold) ──
4064            // Enable vector-smooth ink outlines on depth discontinuities.
4065            //   thickness  — ink-line half-width in pixels (0 = off, 1.5 = anime default)
4066            //   color      — 0xRRGGBB ink colour (default black = 0)
4067            //   threshold  — depth delta that triggers an edge (0.05 recommended)
4068            "toon_outlines" | "ตั้งเส้นขอบการ์ตูน" | "卡通轮廓" | "トゥーンアウトライン" | "툰아웃라인" =>
4069            {
4070                let px    = self.arg_num(&args, 0, 0.0)? as f32;
4071                let color = self.arg_num(&args, 1, 0.0)? as u32;
4072                let thresh= self.arg_num(&args, 2, 0.05)? as f32;
4073                let mut gfx = self.gfx.borrow_mut();
4074                gfx.toon.outline_px     = px;
4075                gfx.toon.outline_color  = color;
4076                gfx.toon.outline_thresh = thresh;
4077                return Ok(Value::Unit);
4078            },
4079
4080            // ── tone_stop(t, value) ──
4081            // Add a stop to the tone ramp.
4082            //   t      — input luminance position [0..1]
4083            //   value  — output brightness [0..1]
4084            // Stops are automatically sorted; call tone_ramp_reset() first to clear.
4085            "tone_stop" | "ตั้งจุดโทน" | "色调停止" | "トーンストップ" | "톤스톱" =>
4086            {
4087                let t   = self.arg_num(&args, 0, 0.0)? as f32;
4088                let val = self.arg_num(&args, 1, 1.0)? as f32;
4089                let mut gfx = self.gfx.borrow_mut();
4090                gfx.toon.ramp.stops.push(crate::gfx::toon::ToneStop {
4091                    t:     t.clamp(0.0, 1.0),
4092                    value: val.clamp(0.0, 1.0),
4093                });
4094                gfx.toon.ramp.stops.sort_by(|a, b|
4095                    a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
4096                return Ok(Value::Unit);
4097            },
4098
4099            // ── tone_smooth(enabled) ──
4100            // 0 = hard cel snap between stops (default); 1 = smooth gradient lerp.
4101            "tone_smooth" | "ตั้งโทนนุ่ม" | "色调平滑" | "トーンスムーズ" | "톤스무스" =>
4102            {
4103                let v = self.arg_num(&args, 0, 0.0)? as f32;
4104                self.gfx.borrow_mut().toon.ramp.smooth = v > 0.5;
4105                return Ok(Value::Unit);
4106            },
4107
4108            // ── tone_bezier(y1, y2) ──
4109            // Apply a cubic Bézier remap to the input luminance before stop lookup.
4110            //   y1, y2 — control-point y-values (identity: y1=0.333 y2=0.667)
4111            //   0 args or tone_bezier(0, 0)  → ease-in (shadow-heavy)
4112            //   tone_bezier(1, 1)            → ease-out (highlight-heavy)
4113            //   tone_bezier(0.1, 0.9)        → S-curve (smooth both ends)
4114            //   tone_bezier_off()            → disable (back to linear)
4115            "tone_bezier" | "ตั้งโทนเบซิเยร์" | "色调贝塞尔" | "トーンベジェ" | "톤베지어" =>
4116            {
4117                let y1 = self.arg_num(&args, 0, 1.0/3.0)? as f32;
4118                let y2 = self.arg_num(&args, 1, 2.0/3.0)? as f32;
4119                self.gfx.borrow_mut().toon.ramp.bezier = Some([y1, y2]);
4120                return Ok(Value::Unit);
4121            },
4122
4123            // ── tone_bezier_off() — disable Bézier remap ──
4124            "tone_bezier_off" | "ปิดโทนเบซิเยร์" | "关闭色调贝塞尔" | "トーンベジェオフ" | "톤베지어끄기" =>
4125            {
4126                self.gfx.borrow_mut().toon.ramp.bezier = None;
4127                return Ok(Value::Unit);
4128            },
4129
4130            // ── tone_ramp_reset() — restore default 3-band cel ramp ──
4131            "tone_ramp_reset" | "รีเซ็ตการไล่โทน" | "重置色调渐变" | "トーンランプリセット" | "톤램프리셋" =>
4132            {
4133                self.gfx.borrow_mut().toon.ramp = crate::gfx::toon::ToneRamp::default();
4134                return Ok(Value::Unit);
4135            },
4136
4137            // ── tone_ramp_clear() — clear all stops (build your own ramp) ──
4138            "tone_ramp_clear" | "ล้างการไล่โทน" | "清除色调渐变" | "トーンランプクリア" | "톤램프클리어" =>
4139            {
4140                self.gfx.borrow_mut().toon.ramp.stops.clear();
4141                return Ok(Value::Unit);
4142            },
4143
4144            // ── shadow_smooth(softness) [compat] → tone_smooth + tone_bezier ──
4145            // Deprecated: use tone_smooth + tone_bezier instead.
4146            "shadow_smooth" | "ตั้งเงานุ่ม" | "柔化阴影" | "影ソフト" | "그림자부드럽게" =>
4147            {
4148                let s = self.arg_num(&args, 0, 0.0)? as f32;
4149                let mut gfx = self.gfx.borrow_mut();
4150                gfx.toon.ramp.smooth = s > 0.05;
4151                if s > 0.05 {
4152                    let y1 = (0.333 + s * 0.2).clamp(0.0, 1.0);
4153                    let y2 = (0.667 - s * 0.2).clamp(0.0, 1.0);
4154                    gfx.toon.ramp.bezier = Some([y1, y2]);
4155                } else {
4156                    gfx.toon.ramp.bezier = None;
4157                }
4158                return Ok(Value::Unit);
4159            },
4160
4161            // ── toon_highlight [compat] — no-op, use tone_stop instead ──
4162            "toon_highlight" | "ตั้งไฮไลท์การ์ตูน" | "卡通高光" | "トゥーンハイライト" | "툰하이라이트" =>
4163            {
4164                // Remap as a lit-band brightness boost: adds a stop near the highlight threshold.
4165                let _strength = self.arg_num(&args, 0, 0.0)? as f32;
4166                let _thresh   = self.arg_num(&args, 2, 0.78)? as f32;
4167                // No-op: configure via tone_stop() for precise control.
4168                return Ok(Value::Unit);
4169            },
4170
4171            // ── set_ambient(v) — ambient light level [0..1] ──
4172            "set_ambient" | "ตั้งแสงรอบข้าง" | "环境光" | "環境光設定" | "환경광설정" =>
4173            {
4174                let v = self.arg_num(&args, 0, 0.15)? as f32;
4175                self.gfx.borrow_mut().ambient = v;
4176                return Ok(Value::Unit);
4177            },
4178
4179            // ── set_fog(r,g,b, start, end) — distance fog toward (r,g,b).
4180            //    triangles/lines fade from `start`..`end` camera depth. end<=0 = off.
4181            "set_fog" | "ตั้งหมอก" | "雾" | "霧設定" | "안개설정" => {
4182                let r = self.arg_num(&args, 0, 0.0)?.clamp(0.0, 255.0) as u32;
4183                let g = self.arg_num(&args, 1, 0.0)?.clamp(0.0, 255.0) as u32;
4184                let b = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 255.0) as u32;
4185                let start = self.arg_num(&args, 3, 0.0)? as f32;
4186                let end = self.arg_num(&args, 4, 0.0)? as f32;
4187                let mut gfx = self.gfx.borrow_mut();
4188                gfx.fog_color = (r << 16) | (g << 8) | b;
4189                gfx.fog_start = start;
4190                gfx.fog_end = end;
4191                return Ok(Value::Unit);
4192            },
4193
4194            // ── วาดสามเหลี่ยม3มิติ(ax,ay,az, bx,by,bz, cx,cy,cz) ──
4195            // Computes lighting from world-space normal + active lights (cel shading),
4196            // projects via the stored camera, and pushes to the depth queue.
4197            "วาดสามเหลี่ยม3มิติ" | "draw_triangle_3d" | "triangle3d" =>
4198            {
4199                let ax = self.arg_num(&args, 0, 0.0)? as f32;
4200                let ay = self.arg_num(&args, 1, 0.0)? as f32;
4201                let az = self.arg_num(&args, 2, 0.0)? as f32;
4202                let bx = self.arg_num(&args, 3, 0.0)? as f32;
4203                let by = self.arg_num(&args, 4, 0.0)? as f32;
4204                let bz = self.arg_num(&args, 5, 0.0)? as f32;
4205                let cx = self.arg_num(&args, 6, 0.0)? as f32;
4206                let cy = self.arg_num(&args, 7, 0.0)? as f32;
4207                let cz = self.arg_num(&args, 8, 0.0)? as f32;
4208
4209                let mut gfx = self.gfx.borrow_mut();
4210
4211                // Mesh capture: record raw local coords + pen colour, skip submit.
4212                if gfx.mesh_capture.is_some() {
4213                    let col = gfx.color;
4214                    gfx.mesh_capture
4215                        .as_mut()
4216                        .unwrap()
4217                        .push(([ax, ay, az, bx, by, bz, cx, cy, cz], col));
4218                    return Ok(Value::Unit);
4219                }
4220
4221                gfx.submit_triangle(ax, ay, az, bx, by, bz, cx, cy, cz);
4222                return Ok(Value::Unit);
4223            },
4224
4225            // ── เริ่มอบเมช() — begin capturing 3-D triangles into a display list ──
4226            "เริ่มอบเมช" | "mesh_bake_begin" =>
4227            {
4228                self.gfx.borrow_mut().mesh_capture = Some(Vec::new());
4229                return Ok(Value::Unit);
4230            },
4231
4232            // ── เมชแคชรับ(key) — keyed display-list cache lookup (-1 = miss) ──
4233            "เมชแคชรับ" | "mesh_cache_get" =>
4234            {
4235                let key = self.arg_num(&args, 0, 0.0)? as i64;
4236                let h = self.gfx.borrow().mesh_cache.get(&key).copied();
4237                return Ok(Value::Number(h.map(|x| x as f64).unwrap_or(-1.0)));
4238            },
4239
4240            // ── เมชแคชตั้ง(key, handle) — store a baked mesh under key (bounded) ──
4241            "เมชแคชตั้ง" | "mesh_cache_put" =>
4242            {
4243                let key = self.arg_num(&args, 0, 0.0)? as i64;
4244                let h = self.arg_num(&args, 1, 0.0)? as usize;
4245                let mut gfx = self.gfx.borrow_mut();
4246                const CAP: usize = 256;
4247                if gfx.mesh_cache.len() >= CAP {
4248                    let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
4249                    gfx.mesh_cache.clear();
4250                    for id in evict {
4251                        if id < gfx.meshes.len() {
4252                            gfx.meshes[id].clear();
4253                            gfx.mesh_free.push(id);
4254                        }
4255                    }
4256                }
4257                gfx.mesh_cache.insert(key, h);
4258                return Ok(Value::Unit);
4259            },
4260
4261            // ── เมชแคชล้าง() — drop the keyed cache (e.g. on level change) ──
4262            "เมชแคชล้าง" | "mesh_cache_clear" =>
4263            {
4264                let mut gfx = self.gfx.borrow_mut();
4265                let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
4266                gfx.mesh_cache.clear();
4267                for id in evict {
4268                    if id < gfx.meshes.len() {
4269                        gfx.meshes[id].clear();
4270                        gfx.mesh_free.push(id);
4271                    }
4272                }
4273                return Ok(Value::Unit);
4274            },
4275
4276            // ── จบอบเมช() — bake captured triangles, return mesh handle ──
4277            "จบอบเมช" | "mesh_bake_end" =>
4278            {
4279                let mut gfx = self.gfx.borrow_mut();
4280                let tris = gfx.mesh_capture.take().unwrap_or_default();
4281                let id = gfx.mesh_register(tris);
4282                return Ok(Value::Number(id as f64));
4283            },
4284
4285            // ── วาดอบเมช[สี](id, ox,oy,oz, rx,ry,rz, ux,uy,uz, s) — draw a baked mesh ──
4286            //   วาดอบเมช: current pen colour (tinted glyphs)
4287            //   วาดอบเมชสี: per-triangle baked colour (multi-colour models)
4288            "วาดอบเมช" | "mesh_bake_draw" | "วาดอบเมชสี" | "mesh_bake_draw_col" =>
4289            {
4290                let baked_col = matches!(name, "วาดอบเมชสี" | "mesh_bake_draw_col");
4291                let id = self.arg_num(&args, 0, 0.0)? as usize;
4292                let ox = self.arg_num(&args, 1, 0.0)? as f32;
4293                let oy = self.arg_num(&args, 2, 0.0)? as f32;
4294                let oz = self.arg_num(&args, 3, 0.0)? as f32;
4295                let rx = self.arg_num(&args, 4, 1.0)? as f32;
4296                let ry = self.arg_num(&args, 5, 0.0)? as f32;
4297                let rz = self.arg_num(&args, 6, 0.0)? as f32;
4298                let ux = self.arg_num(&args, 7, 0.0)? as f32;
4299                let uy = self.arg_num(&args, 8, 1.0)? as f32;
4300                let uz = self.arg_num(&args, 9, 0.0)? as f32;
4301                let s = self.arg_num(&args, 10, 1.0)? as f32;
4302                self.gfx.borrow_mut().mesh_draw(id, ox, oy, oz, rx, ry, rz, ux, uy, uz, s, baked_col);
4303                return Ok(Value::Unit);
4304            },
4305
4306            // ── draw_quad_3d / draw_pent_3d / draw_hex_3d / draw_polygon_3d ──
4307            // Fan-triangulate convex n-gons.  Lighting, near-plane clip, fog, and
4308            // Gouraud shading all mirror draw_triangle_3d exactly.
4309            "draw_quad_3d" | "quad3d" | "วาดสี่เหลี่ยม3มิติ"
4310            | "draw_pent_3d" | "pent3d" | "วาดห้าเหลี่ยม3มิติ"
4311            | "draw_hex_3d"  | "hex3d"  | "วาดหกเหลี่ยม3มิติ"
4312            | "draw_polygon_3d" | "polygon3d" | "วาดรูปหลายเหลี่ยม3มิติ" =>
4313            {
4314                // Collect (wx, wy, wz) triples from args or list
4315                let mut wxs: [f32; 8] = [0.0; 8];
4316                let mut wys: [f32; 8] = [0.0; 8];
4317                let mut wzs: [f32; 8] = [0.0; 8];
4318                let n_verts;
4319
4320                if args.len() == 1 {
4321                    // draw_polygon_3d([x0,y0,z0, x1,y1,z1, ...])
4322                    let list = match &args[0] {
4323                        Value::List(l) => l.clone(),
4324                        _ => return Err(EvalErr::from("draw_polygon_3d: expected list".to_string())),
4325                    };
4326                    let coords: Vec<f32> = list.iter()
4327                        .map(|v| match v { Value::Number(n) => *n as f32, _ => 0.0 })
4328                        .collect();
4329                    n_verts = (coords.len() / 3).min(8);
4330                    for i in 0..n_verts {
4331                        wxs[i] = coords[i * 3];
4332                        wys[i] = coords[i * 3 + 1];
4333                        wzs[i] = coords[i * 3 + 2];
4334                    }
4335                } else {
4336                    // draw_quad/pent/hex_3d(x0,y0,z0, x1,y1,z1, ...)
4337                    n_verts = (args.len() / 3).min(8);
4338                    for i in 0..n_verts {
4339                        wxs[i] = self.arg_num(&args, i * 3,     0.0)? as f32;
4340                        wys[i] = self.arg_num(&args, i * 3 + 1, 0.0)? as f32;
4341                        wzs[i] = self.arg_num(&args, i * 3 + 2, 0.0)? as f32;
4342                    }
4343                }
4344                if n_verts < 3 { return Ok(Value::Unit); }
4345
4346                let mut gfx = self.gfx.borrow_mut();
4347
4348                // Face normal from first triangle of the fan
4349                let normal = crate::gfx::poly::face_normal(
4350                    wxs[0], wys[0], wzs[0],
4351                    wxs[1], wys[1], wzs[1],
4352                    wxs[2], wys[2], wzs[2],
4353                );
4354
4355                // Per-vertex lit colours
4356                let mut wcs: [u32; 8] = [0; 8];
4357                if gfx.flat_shade {
4358                    let c = gfx.color;
4359                    for i in 0..n_verts { wcs[i] = c; }
4360                } else if let Some(ref mat) = gfx.material.clone() {
4361                    let cam = [gfx.camera.cx, gfx.camera.cy, gfx.camera.zdist];
4362                    let lights: Vec<_> = gfx.lights.clone();
4363                    let ambient = gfx.ambient;
4364                    for i in 0..n_verts {
4365                        let v = [wxs[i], wys[i], wzs[i]];
4366                        let vd = [cam[0]-v[0], cam[1]-v[1], cam[2]-v[2]];
4367                        wcs[i] = crate::gfx::material::shade(mat, normal, vd, v, &lights, ambient);
4368                    }
4369                } else {
4370                    let base   = gfx.color;
4371                    let lights: Vec<_> = gfx.lights.clone();
4372                    let ambient = gfx.ambient;
4373                    for i in 0..n_verts {
4374                        wcs[i] = crate::gfx::light::compute_lit_color_linear(
4375                            base, normal, [wxs[i], wys[i], wzs[i]], &lights, ambient,
4376                        );
4377                    }
4378                }
4379
4380                // Near-plane clip (Sutherland-Hodgman per vertex)
4381                let near = -gfx.camera.zdist + 0.05;
4382                let mut clip_in:  [(f32,f32,f32,f32,u32); crate::gfx::poly::MAX_CLIP_VERTS]
4383                    = [(0.0,0.0,0.0,0.0,0); crate::gfx::poly::MAX_CLIP_VERTS];
4384                for i in 0..n_verts {
4385                    let d = gfx.camera.depth(wxs[i], wys[i], wzs[i]);
4386                    clip_in[i] = (wxs[i], wys[i], wzs[i], d, wcs[i]);
4387                }
4388                let mut clip_out: [(f32,f32,f32,f32,u32); crate::gfx::poly::MAX_CLIP_VERTS]
4389                    = [(0.0,0.0,0.0,0.0,0); crate::gfx::poly::MAX_CLIP_VERTS];
4390                let pn = crate::gfx::poly::clip_near(&clip_in, n_verts, near, &mut clip_out);
4391                if pn < 3 { return Ok(Value::Unit); }
4392
4393                // Project + fog
4394                let mut proj: [(f32,f32,f32,u32); crate::gfx::poly::MAX_CLIP_VERTS]
4395                    = [(0.0,0.0,0.0,0); crate::gfx::poly::MAX_CLIP_VERTS];
4396                for i in 0..pn {
4397                    let (sx, sy, sz) = gfx.camera.project(clip_out[i].0, clip_out[i].1, clip_out[i].2);
4398                    let fc = gfx.fog_apply(clip_out[i].4, sz);
4399                    proj[i] = (sx, sy, sz, fc);
4400                }
4401
4402                // Fan-triangulate and push
4403                crate::gfx::poly::fan_emit_proj(&proj, pn, |x0,y0,z0,c0, x1,y1,z1,c1, x2,y2,z2,c2| {
4404                    gfx.depth_queue.push_triangle_g_zv(
4405                        x0,y0,z0,c0, x1,y1,z1,c1, x2,y2,z2,c2, 3,
4406                    );
4407                });
4408                return Ok(Value::Unit);
4409            },
4410
4411            // ── วาดเส้น3มิติ(ax,ay,az, bx,by,bz) ──
4412            // Projects two world-space points via the stored camera and pushes
4413            // a line to the depth queue.
4414            "วาดเส้น3มิติ" | "draw_line_3d" | "line3d" | "画3D线" | "3D線描く" | "3D선그리기" =>
4415            {
4416                let ax = self.arg_num(&args, 0, 0.0)? as f32;
4417                let ay = self.arg_num(&args, 1, 0.0)? as f32;
4418                let az = self.arg_num(&args, 2, 0.0)? as f32;
4419                let bx = self.arg_num(&args, 3, 0.0)? as f32;
4420                let by = self.arg_num(&args, 4, 0.0)? as f32;
4421                let bz = self.arg_num(&args, 5, 0.0)? as f32;
4422
4423                let mut gfx = self.gfx.borrow_mut();
4424                let color = gfx.color;
4425                // Near-plane clip in 3-D before perspective divide
4426                let near = -gfx.camera.zdist + 0.05;
4427                let mut lax = ax;
4428                let mut lay = ay;
4429                let mut laz = az;
4430                let mut lbx = bx;
4431                let mut lby = by;
4432                let mut lbz = bz;
4433                let da_raw = gfx.camera.depth(lax, lay, laz);
4434                let db_raw = gfx.camera.depth(lbx, lby, lbz);
4435                if da_raw <= near && db_raw <= near {
4436                    return Ok(Value::Unit);
4437                }
4438                if da_raw <= near {
4439                    let t = (near - da_raw) / (db_raw - da_raw);
4440                    lax += t * (lbx - lax);
4441                    lay += t * (lby - lay);
4442                    laz += t * (lbz - laz);
4443                } else if db_raw <= near {
4444                    let t = (near - da_raw) / (db_raw - da_raw);
4445                    lbx = lax + t * (lbx - lax);
4446                    lby = lay + t * (lby - lay);
4447                    lbz = laz + t * (lbz - laz);
4448                }
4449                // Shared-edge dedup: skip if this world-space edge was already queued.
4450                if !gfx.edge_set.try_insert(lax, lay, laz, lbx, lby, lbz) {
4451                    return Ok(Value::Unit);
4452                }
4453                let (sax, say, da) = gfx.camera.project(lax, lay, laz);
4454                let (sbx, sby, db) = gfx.camera.project(lbx, lby, lbz);
4455                let depth = (da + db) / 2.0;
4456                let color = gfx.fog_apply(color, depth);
4457                gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
4458                return Ok(Value::Unit);
4459            },
4460
4461            // orb_shell(cx,cy,cz, radius, rot_y, rot_x, density, r,g,b)
4462            //   A single trippy, grayscale, depth-faded vector pattern wound around
4463            //   a sphere — two families of interleaved spherical spirals (a guilloché
4464            //   weave), NOT a lat/long cage. Each segment's brightness follows its
4465            //   facing (front bright, back dim), so it reads as a translucent
4466            //   grayscale "texture" with alpha rather than a hard wireframe; the
4467            //   inner marble shows through. `rot_y`/`rot_x` roll the texture around
4468            //   the orb; `density` = spirals per winding direction. r,g,b tint it
4469            //   (pass a gray like 230,230,230 for pure grayscale).
4470            #[cfg(not(target_arch = "wasm32"))]
4471            "orb_shell" | "球壳" | "オーブ殻" | "오브껍질" | "เปลือกทรงกลม" =>
4472            {
4473                let cx = self.arg_num(&args, 0, 0.)? as f32;
4474                let cy = self.arg_num(&args, 1, 0.)? as f32;
4475                let cz = self.arg_num(&args, 2, 0.)? as f32;
4476                let radius = self.arg_num(&args, 3, 1.0)? as f32;
4477                let ry = self.arg_num(&args, 4, 0.)? as f32;
4478                let rx = self.arg_num(&args, 5, 0.)? as f32;
4479                let density = (self.arg_num(&args, 6, 10.)? as i32).clamp(1, 48);
4480                let tr = (self.arg_num(&args, 7, 230.)? as f32).clamp(0., 255.);
4481                let tg = (self.arg_num(&args, 8, 230.)? as f32).clamp(0., 255.);
4482                let tb = (self.arg_num(&args, 9, 235.)? as f32).clamp(0., 255.);
4483                let (cyr, syr) = (ry.cos(), ry.sin());
4484                let (cxr, sxr) = (rx.cos(), rx.sin());
4485                let tau = std::f32::consts::TAU;
4486                let pi = std::f32::consts::PI;
4487                let turns = 6.0_f32; // how many times each spiral wraps pole→pole
4488                let nseg = 96; // segments per spiral (smoothness)
4489                let inv_r = if radius.abs() > 1e-5 {
4490                    1.0 / radius
4491                } else {
4492                    0.0
4493                };
4494                // a point along a spiral (param u 0..1, start angle theta0, winding dir),
4495                // spun by ry/rx — returns (world point, facing 0..1 where 1 = toward camera)
4496                let pt = |u: f32, theta0: f32, dir: f32| -> ([f32; 3], f32) {
4497                    let phi = pi * u; // 0..pi  (north → south)
4498                    let th = dir * turns * tau * u + theta0;
4499                    let (mut x, y, mut z) = (
4500                        phi.sin() * th.cos() * radius,
4501                        phi.cos() * radius,
4502                        phi.sin() * th.sin() * radius,
4503                    );
4504                    let x1 = x * cyr + z * syr; // yaw about Y
4505                    let z1 = -x * syr + z * cyr;
4506                    x = x1;
4507                    z = z1;
4508                    let y2 = y * cxr - z * sxr; // pitch about X
4509                    let z2 = y * sxr + z * cxr;
4510                    // facing: camera sits at -zdist looking +z, so smaller z2 = nearer = brighter
4511                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.0, 1.0);
4512                    ([cx + x, cy + y2, cz + z2], facing)
4513                };
4514                let mut gfx = self.gfx.borrow_mut();
4515                let near = -gfx.camera.zdist + 0.05;
4516                // draw one segment (near-clipped) in a grayscale tint scaled by `lum`
4517                let seg = |gfx: &mut crate::gfx::GfxState, a: [f32; 3], b: [f32; 3], lum: f32| {
4518                    let (mut lax, mut lay, mut laz) = (a[0], a[1], a[2]);
4519                    let (mut lbx, mut lby, mut lbz) = (b[0], b[1], b[2]);
4520                    let da = gfx.camera.depth(lax, lay, laz);
4521                    let db = gfx.camera.depth(lbx, lby, lbz);
4522                    if da <= near && db <= near {
4523                        return;
4524                    }
4525                    if da <= near {
4526                        let t = (near - da) / (db - da);
4527                        lax += t * (lbx - lax);
4528                        lay += t * (lby - lay);
4529                        laz += t * (lbz - laz);
4530                    } else if db <= near {
4531                        let t = (near - da) / (db - da);
4532                        lbx = lax + t * (lbx - lax);
4533                        lby = lay + t * (lby - lay);
4534                        lbz = laz + t * (lbz - laz);
4535                    }
4536                    let (sax, say, da2) = gfx.camera.project(lax, lay, laz);
4537                    let (sbx, sby, db2) = gfx.camera.project(lbx, lby, lbz);
4538                    // grayscale-alpha: front-facing bright, back faded toward black
4539                    let l = (0.12 + 0.88 * lum).clamp(0.0, 1.0);
4540                    let cr = (tr * l) as u32;
4541                    let cg = (tg * l) as u32;
4542                    let cb = (tb * l) as u32;
4543                    let color = (cr << 16) | (cg << 8) | cb;
4544                    gfx.depth_queue
4545                        .push_line((da2 + db2) * 0.5, color, sax, say, sbx, sby);
4546                };
4547                // two opposite winding directions → a soft guilloché weave (not a cage)
4548                for &dir in &[1.0_f32, -1.0_f32] {
4549                    for s in 0..density {
4550                        let theta0 = s as f32 * tau / density as f32;
4551                        let mut prev = pt(0.0, theta0, dir);
4552                        for k in 1..=nseg {
4553                            let cur = pt(k as f32 / nseg as f32, theta0, dir);
4554                            seg(&mut gfx, prev.0, cur.0, (prev.1 + cur.1) * 0.5);
4555                            prev = cur;
4556                        }
4557                    }
4558                }
4559                return Ok(Value::Unit);
4560            },
4561
4562            // orb_particles(cx,cy,cz, radius, count, t, r,g,b)
4563            //   Fills the VOLUME of a sphere with `count` swirling vector points —
4564            //   like motes suspended inside a snow-globe orb. Points are distributed
4565            //   uniformly through the ball, slowly tumble as a cloud + wobble
4566            //   individually over time `t`, and are depth-shaded (near = bright,
4567            //   far = dim) so the cloud has real volume. Additive, so it layers under
4568            //   a shell / over a liquid marble.
4569            #[cfg(not(target_arch = "wasm32"))]
4570            "orb_particles" | "球内粒子" | "オーブ粒子" | "오브입자" | "อนุภาคทรงกลม" =>
4571            {
4572                let cx = self.arg_num(&args, 0, 0.)? as f32;
4573                let cy = self.arg_num(&args, 1, 0.)? as f32;
4574                let cz = self.arg_num(&args, 2, 0.)? as f32;
4575                let radius = self.arg_num(&args, 3, 1.0)? as f32;
4576                let count = (self.arg_num(&args, 4, 160.)? as i32).clamp(1, 4000);
4577                let t = self.arg_num(&args, 5, 0.)? as f32;
4578                let tr = (self.arg_num(&args, 6, 255.)? as f32).clamp(0., 255.);
4579                let tg = (self.arg_num(&args, 7, 255.)? as f32).clamp(0., 255.);
4580                let tb = (self.arg_num(&args, 8, 255.)? as f32).clamp(0., 255.);
4581                let inv_r = if radius.abs() > 1e-5 {
4582                    1.0 / radius
4583                } else {
4584                    0.0
4585                };
4586                // cheap deterministic hash → [0,1)
4587                let h = |mut x: u32| -> f32 {
4588                    x = x.wrapping_mul(747796405).wrapping_add(2891336453);
4589                    x = ((x >> ((x >> 28).wrapping_add(4))) ^ x).wrapping_mul(277803737);
4590                    (((x >> 22) ^ x) & 0xFFFFFF) as f32 / 16_777_216.0
4591                };
4592                let tau = std::f32::consts::TAU;
4593                // slow tumble of the whole cloud
4594                let (cyr, syr) = ((t * 0.5).cos(), (t * 0.5).sin());
4595                let (cxr, sxr) = ((t * 0.23).cos(), (t * 0.23).sin());
4596                let mut gfx = self.gfx.borrow_mut();
4597                let near = -gfx.camera.zdist + 0.05;
4598                let (sw, sh) = (gfx.width as i32, gfx.height as i32);
4599                for i in 0..count {
4600                    let i = i as u32;
4601                    // uniform-in-volume: r = cbrt(u) * radius; direction from two hashes
4602                    let u = h(i.wrapping_mul(3) + 1);
4603                    let rr = u.cbrt() * radius * (0.85 + 0.15 * (t * 1.3 + i as f32).sin()); // gentle pulse
4604                    let th = h(i.wrapping_mul(3) + 2) * tau + t * (0.3 + 0.5 * h(i * 7 + 5)); // per-mote orbit
4605                    let ph = (h(i.wrapping_mul(3) + 3) * 2.0 - 1.0).acos(); // uniform cos(phi)
4606                    let (mut x, y, mut z) = (
4607                        rr * ph.sin() * th.cos(),
4608                        rr * ph.cos(),
4609                        rr * ph.sin() * th.sin(),
4610                    );
4611                    // tumble the cloud (yaw then pitch)
4612                    let x1 = x * cyr + z * syr;
4613                    let z1 = -x * syr + z * cyr;
4614                    x = x1;
4615                    z = z1;
4616                    let y2 = y * cxr - z * sxr;
4617                    let z2 = y * sxr + z * cxr;
4618                    let (wx, wy, wz) = (cx + x, cy + y2, cz + z2);
4619                    if gfx.camera.depth(wx, wy, wz) <= near {
4620                        continue;
4621                    }
4622                    let (sx, sy, dep) = gfx.camera.project(wx, wy, wz);
4623                    let sxi = sx as i32;
4624                    let syi = sy as i32;
4625                    if sxi < 0 || syi < 0 || sxi >= sw || syi >= sh {
4626                        continue;
4627                    }
4628                    // depth-shade: nearer (smaller z2) = brighter
4629                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.15, 1.0);
4630                    let l = facing;
4631                    let cr = (tr * l) as u32;
4632                    let cg = (tg * l) as u32;
4633                    let cb = (tb * l) as u32;
4634                    let color = (cr << 16) | (cg << 8) | cb;
4635                    // a 1–2px dot (bigger when near) as a short segment in the depth queue
4636                    let len = if facing > 0.7 { 1.0 } else { 0.0 };
4637                    gfx.depth_queue.push_line(dep, color, sx, sy, sx + len, sy);
4638                }
4639                return Ok(Value::Unit);
4640            },
4641
4642            // project_3d(x,y,z) -> [screen_x, screen_y, depth]; behind the camera
4643            // returns a sentinel ([-99999,-99999, depth]) so scripts can skip it.
4644            // Lets scripts place 2-D overlays (e.g. filled teardrop flames) onto 3-D points.
4645            "project_3d" | "投影3D" | "3D投影" | "3D투영" | "ฉาย3มิติ" => {
4646                let x = self.arg_num(&args, 0, 0.0)? as f32;
4647                let y = self.arg_num(&args, 1, 0.0)? as f32;
4648                let z = self.arg_num(&args, 2, 0.0)? as f32;
4649                let gfx = self.gfx.borrow();
4650                let near = -gfx.camera.zdist + 0.05;
4651                let d = gfx.camera.depth(x, y, z);
4652                if d <= near {
4653                    return Ok(Value::List(Rc::new(vec![
4654                        Value::Number(-99999.0),
4655                        Value::Number(-99999.0),
4656                        Value::Number(d as f64),
4657                    ])));
4658                }
4659                let (sx, sy, depth) = gfx.camera.project(x, y, z);
4660                return Ok(Value::List(Rc::new(vec![
4661                    Value::Number(sx as f64),
4662                    Value::Number(sy as f64),
4663                    Value::Number(depth as f64),
4664                ])));
4665            },
4666            // draw_poly([x0,y0,x1,y1,…]) — filled 2-D polygon in the current colour,
4667            // honouring the blend mode (additive → translucent glow). Auto-closes.
4668            #[cfg(not(target_arch = "wasm32"))]
4669            "draw_poly" | "填充多边形" | "ポリゴン塗り" | "다각형채우기" | "เติมรูปหลายเหลี่ยม" =>
4670            {
4671                let mut pts: Vec<[f32; 2]> = Vec::new();
4672                if let Some(Value::List(v)) = args.first() {
4673                    let mut i = 0;
4674                    while i + 1 < v.len() {
4675                        let x = self.to_number(&v[i]).unwrap_or(0.0) as f32;
4676                        let y = self.to_number(&v[i + 1]).unwrap_or(0.0) as f32;
4677                        pts.push([x, y]);
4678                        i += 2;
4679                    }
4680                }
4681                if pts.len() >= 3 {
4682                    if pts[0] != pts[pts.len() - 1] {
4683                        let p0 = pts[0];
4684                        pts.push(p0);
4685                    } // close
4686                    let mut gfx = self.gfx.borrow_mut();
4687                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
4688                    crate::gfx::raster::fill_contours_aa(
4689                        &mut gfx.buffer,
4690                        w,
4691                        h,
4692                        color,
4693                        add,
4694                        std::slice::from_ref(&pts),
4695                    );
4696                }
4697                return Ok(Value::Unit);
4698            },
4699
4700            // ══════════════════════════════════════════════════════════════════
4701            // VECTOR TEXTURE BUILTINS  (src/gfx/vtex.rs)
4702            // All patterns are depth-biased so they appear on top of surfaces.
4703            // Plane defined by: centre (cx,cy,cz) + U tangent + V tangent.
4704            // Last two args always: fr (frame f32), hue (phase offset f32).
4705            // ══════════════════════════════════════════════════════════════════
4706
4707            // vtex_grid(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cw,ch, fr,hue)
4708            "vtex_grid" | "ลายตาราง" | "纹格" | "格子模様" | "격자무늬" =>
4709            {
4710                let cx = self.arg_num(&args, 0, 0.)? as f32;
4711                let cy = self.arg_num(&args, 1, 0.)? as f32;
4712                let cz = self.arg_num(&args, 2, 0.)? as f32;
4713                let ux = self.arg_num(&args, 3, 1.)? as f32;
4714                let uy = self.arg_num(&args, 4, 0.)? as f32;
4715                let uz = self.arg_num(&args, 5, 0.)? as f32;
4716                let vx = self.arg_num(&args, 6, 0.)? as f32;
4717                let vy = self.arg_num(&args, 7, 0.)? as f32;
4718                let vz = self.arg_num(&args, 8, 1.)? as f32;
4719                let cols = self.arg_num(&args, 9, 10.)? as usize;
4720                let rows = self.arg_num(&args, 10, 10.)? as usize;
4721                let cw = self.arg_num(&args, 11, 1.)? as f32;
4722                let ch = self.arg_num(&args, 12, 1.)? as f32;
4723                let fr = self.arg_num(&args, 13, 0.)? as f32;
4724                let hue = self.arg_num(&args, 14, 0.)? as f32;
4725                let mut gfx = self.gfx.borrow_mut();
4726                let cam = gfx.camera.clone();
4727                crate::gfx::vtex::draw_grid(
4728                    &mut gfx.depth_queue,
4729                    &cam,
4730                    cx,
4731                    cy,
4732                    cz,
4733                    ux,
4734                    uy,
4735                    uz,
4736                    vx,
4737                    vy,
4738                    vz,
4739                    cols,
4740                    rows,
4741                    cw,
4742                    ch,
4743                    fr,
4744                    hue,
4745                );
4746                return Ok(Value::Unit);
4747            },
4748
4749            // vtex_rings(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_rings,n_sides, max_r,twist, fr,hue)
4750            "vtex_rings" | "ลายวงซ้อน" | "纹环" | "同心円" | "동심원" => {
4751                let cx = self.arg_num(&args, 0, 0.)? as f32;
4752                let cy = self.arg_num(&args, 1, 0.)? as f32;
4753                let cz = self.arg_num(&args, 2, 0.)? as f32;
4754                let ux = self.arg_num(&args, 3, 1.)? as f32;
4755                let uy = self.arg_num(&args, 4, 0.)? as f32;
4756                let uz = self.arg_num(&args, 5, 0.)? as f32;
4757                let vx = self.arg_num(&args, 6, 0.)? as f32;
4758                let vy = self.arg_num(&args, 7, 0.)? as f32;
4759                let vz = self.arg_num(&args, 8, 1.)? as f32;
4760                let nr = self.arg_num(&args, 9, 6.)? as usize;
4761                let ns = self.arg_num(&args, 10, 6.)? as usize;
4762                let mr = self.arg_num(&args, 11, 3.)? as f32;
4763                let tw = self.arg_num(&args, 12, 0.)? as f32;
4764                let fr = self.arg_num(&args, 13, 0.)? as f32;
4765                let hue = self.arg_num(&args, 14, 0.)? as f32;
4766                let mut gfx = self.gfx.borrow_mut();
4767                let cam = gfx.camera.clone();
4768                crate::gfx::vtex::draw_rings(
4769                    &mut gfx.depth_queue,
4770                    &cam,
4771                    cx,
4772                    cy,
4773                    cz,
4774                    ux,
4775                    uy,
4776                    uz,
4777                    vx,
4778                    vy,
4779                    vz,
4780                    nr,
4781                    ns,
4782                    mr,
4783                    tw,
4784                    fr,
4785                    hue,
4786                );
4787                return Ok(Value::Unit);
4788            },
4789
4790            // vtex_star(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_pts,r_out,r_in, rot_speed, fr,hue)
4791            "vtex_star" | "ลายดาว" | "纹星" | "星模様" | "별무늬" => {
4792                let cx = self.arg_num(&args, 0, 0.)? as f32;
4793                let cy = self.arg_num(&args, 1, 0.)? as f32;
4794                let cz = self.arg_num(&args, 2, 0.)? as f32;
4795                let ux = self.arg_num(&args, 3, 1.)? as f32;
4796                let uy = self.arg_num(&args, 4, 0.)? as f32;
4797                let uz = self.arg_num(&args, 5, 0.)? as f32;
4798                let vx = self.arg_num(&args, 6, 0.)? as f32;
4799                let vy = self.arg_num(&args, 7, 0.)? as f32;
4800                let vz = self.arg_num(&args, 8, 1.)? as f32;
4801                let np = self.arg_num(&args, 9, 6.)? as usize;
4802                let ro = self.arg_num(&args, 10, 2.)? as f32;
4803                let ri = self.arg_num(&args, 11, 1.)? as f32;
4804                let rs = self.arg_num(&args, 12, 0.01)? as f32;
4805                let fr = self.arg_num(&args, 13, 0.)? as f32;
4806                let hue = self.arg_num(&args, 14, 0.)? as f32;
4807                let mut gfx = self.gfx.borrow_mut();
4808                let cam = gfx.camera.clone();
4809                crate::gfx::vtex::draw_star(
4810                    &mut gfx.depth_queue,
4811                    &cam,
4812                    cx,
4813                    cy,
4814                    cz,
4815                    ux,
4816                    uy,
4817                    uz,
4818                    vx,
4819                    vy,
4820                    vz,
4821                    np,
4822                    ro,
4823                    ri,
4824                    rs,
4825                    fr,
4826                    hue,
4827                );
4828                return Ok(Value::Unit);
4829            },
4830
4831            // vtex_spiral(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_turns,max_r,steps, fr,hue)
4832            "vtex_spiral" | "ลายเกลียว" | "纹螺" | "螺旋" | "나선" => {
4833                let cx = self.arg_num(&args, 0, 0.)? as f32;
4834                let cy = self.arg_num(&args, 1, 0.)? as f32;
4835                let cz = self.arg_num(&args, 2, 0.)? as f32;
4836                let ux = self.arg_num(&args, 3, 1.)? as f32;
4837                let uy = self.arg_num(&args, 4, 0.)? as f32;
4838                let uz = self.arg_num(&args, 5, 0.)? as f32;
4839                let vx = self.arg_num(&args, 6, 0.)? as f32;
4840                let vy = self.arg_num(&args, 7, 0.)? as f32;
4841                let vz = self.arg_num(&args, 8, 1.)? as f32;
4842                let nt = self.arg_num(&args, 9, 3.)? as f32;
4843                let mr = self.arg_num(&args, 10, 3.)? as f32;
4844                let st = self.arg_num(&args, 11, 120.)? as usize;
4845                let fr = self.arg_num(&args, 12, 0.)? as f32;
4846                let hue = self.arg_num(&args, 13, 0.)? as f32;
4847                let mut gfx = self.gfx.borrow_mut();
4848                let cam = gfx.camera.clone();
4849                crate::gfx::vtex::draw_spiral(
4850                    &mut gfx.depth_queue,
4851                    &cam,
4852                    cx,
4853                    cy,
4854                    cz,
4855                    ux,
4856                    uy,
4857                    uz,
4858                    vx,
4859                    vy,
4860                    vz,
4861                    nt,
4862                    mr,
4863                    st,
4864                    fr,
4865                    hue,
4866                );
4867                return Ok(Value::Unit);
4868            },
4869
4870            // vtex_flower(cx,cy,cz, ux,uy,uz, vx,vy,vz, radius,n_sides, fr,hue)
4871            "vtex_flower" | "ลายดอก" | "纹花" | "花模様" | "꽃무늬" => {
4872                let cx = self.arg_num(&args, 0, 0.)? as f32;
4873                let cy = self.arg_num(&args, 1, 0.)? as f32;
4874                let cz = self.arg_num(&args, 2, 0.)? as f32;
4875                let ux = self.arg_num(&args, 3, 1.)? as f32;
4876                let uy = self.arg_num(&args, 4, 0.)? as f32;
4877                let uz = self.arg_num(&args, 5, 0.)? as f32;
4878                let vx = self.arg_num(&args, 6, 0.)? as f32;
4879                let vy = self.arg_num(&args, 7, 0.)? as f32;
4880                let vz = self.arg_num(&args, 8, 1.)? as f32;
4881                let r = self.arg_num(&args, 9, 1.)? as f32;
4882                let ns = self.arg_num(&args, 10, 24.)? as usize;
4883                let fr = self.arg_num(&args, 11, 0.)? as f32;
4884                let hue = self.arg_num(&args, 12, 0.)? as f32;
4885                let mut gfx = self.gfx.borrow_mut();
4886                let cam = gfx.camera.clone();
4887                crate::gfx::vtex::draw_flower(
4888                    &mut gfx.depth_queue,
4889                    &cam,
4890                    cx,
4891                    cy,
4892                    cz,
4893                    ux,
4894                    uy,
4895                    uz,
4896                    vx,
4897                    vy,
4898                    vz,
4899                    r,
4900                    ns,
4901                    fr,
4902                    hue,
4903                );
4904                return Ok(Value::Unit);
4905            },
4906
4907            // vtex_letter_rain(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_cols,n_vis, col_w,row_h, speed, fr,hue)
4908            "vtex_letter_rain" | "ลายอักษรไหล" | "纹字雨" | "文字雨" | "글자비" =>
4909            {
4910                let cx = self.arg_num(&args, 0, 0.)? as f32;
4911                let cy = self.arg_num(&args, 1, 0.)? as f32;
4912                let cz = self.arg_num(&args, 2, 0.)? as f32;
4913                let ux = self.arg_num(&args, 3, 1.)? as f32;
4914                let uy = self.arg_num(&args, 4, 0.)? as f32;
4915                let uz = self.arg_num(&args, 5, 0.)? as f32;
4916                let vx = self.arg_num(&args, 6, 0.)? as f32;
4917                let vy = self.arg_num(&args, 7, 0.)? as f32;
4918                let vz = self.arg_num(&args, 8, 1.)? as f32;
4919                let nc = self.arg_num(&args, 9, 16.)? as usize;
4920                let nv = self.arg_num(&args, 10, 14.)? as usize;
4921                let cw = self.arg_num(&args, 11, 0.65)? as f32;
4922                let rh = self.arg_num(&args, 12, 0.60)? as f32;
4923                let sp = self.arg_num(&args, 13, 0.025)? as f32;
4924                let fr = self.arg_num(&args, 14, 0.)? as f32;
4925                let hue = self.arg_num(&args, 15, 0.)? as f32;
4926                let mut gfx = self.gfx.borrow_mut();
4927                let cam = gfx.camera.clone();
4928                crate::gfx::vtex::draw_letter_rain(
4929                    &mut gfx.depth_queue,
4930                    &cam,
4931                    cx,
4932                    cy,
4933                    cz,
4934                    ux,
4935                    uy,
4936                    uz,
4937                    vx,
4938                    vy,
4939                    vz,
4940                    nc,
4941                    nv,
4942                    cw,
4943                    rh,
4944                    sp,
4945                    fr,
4946                    hue,
4947                );
4948                return Ok(Value::Unit);
4949            },
4950
4951            // vtex_hyperbolic_uv(cx,cy,cz, ux,uy,uz, vx,vy,vz, max_r,n_circles,n_rays, fr,hue)
4952            "vtex_hyperbolic_uv" | "ลายไฮเพอร์โบลิก" | "纹曲面" | "双曲線" | "쌍곡선" =>
4953            {
4954                let cx = self.arg_num(&args, 0, 0.)? as f32;
4955                let cy = self.arg_num(&args, 1, 0.)? as f32;
4956                let cz = self.arg_num(&args, 2, 0.)? as f32;
4957                let ux = self.arg_num(&args, 3, 1.)? as f32;
4958                let uy = self.arg_num(&args, 4, 0.)? as f32;
4959                let uz = self.arg_num(&args, 5, 0.)? as f32;
4960                let vx = self.arg_num(&args, 6, 0.)? as f32;
4961                let vy = self.arg_num(&args, 7, 0.)? as f32;
4962                let vz = self.arg_num(&args, 8, 1.)? as f32;
4963                let mr = self.arg_num(&args, 9, 5.)? as f32;
4964                let nc = self.arg_num(&args, 10, 12.)? as usize;
4965                let nr = self.arg_num(&args, 11, 18.)? as usize;
4966                let fr = self.arg_num(&args, 12, 0.)? as f32;
4967                let hue = self.arg_num(&args, 13, 0.)? as f32;
4968                let mut gfx = self.gfx.borrow_mut();
4969                let cam = gfx.camera.clone();
4970                crate::gfx::vtex::draw_hyperbolic_uv(
4971                    &mut gfx.depth_queue,
4972                    &cam,
4973                    cx,
4974                    cy,
4975                    cz,
4976                    ux,
4977                    uy,
4978                    uz,
4979                    vx,
4980                    vy,
4981                    vz,
4982                    mr,
4983                    nc,
4984                    nr,
4985                    fr,
4986                    hue,
4987                );
4988                return Ok(Value::Unit);
4989            },
4990
4991            // vtex_halftone(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell_w,cell_h, density, fr,hue)
4992            "vtex_halftone" | "ลายจุด" | "纹半调" | "網点模様" | "망점" => {
4993                let cx = self.arg_num(&args, 0, 0.)? as f32;
4994                let cy = self.arg_num(&args, 1, 0.)? as f32;
4995                let cz = self.arg_num(&args, 2, 0.)? as f32;
4996                let ux = self.arg_num(&args, 3, 1.)? as f32;
4997                let uy = self.arg_num(&args, 4, 0.)? as f32;
4998                let uz = self.arg_num(&args, 5, 0.)? as f32;
4999                let vx = self.arg_num(&args, 6, 0.)? as f32;
5000                let vy = self.arg_num(&args, 7, 0.)? as f32;
5001                let vz = self.arg_num(&args, 8, 1.)? as f32;
5002                let cols = self.arg_num(&args, 9, 16.)? as usize;
5003                let rows = self.arg_num(&args, 10, 12.)? as usize;
5004                let cw = self.arg_num(&args, 11, 0.5)? as f32;
5005                let ch = self.arg_num(&args, 12, 0.5)? as f32;
5006                let dens = self.arg_num(&args, 13, 0.4)? as f32;
5007                let fr = self.arg_num(&args, 14, 0.)? as f32;
5008                let hue = self.arg_num(&args, 15, 0.)? as f32;
5009                let mut gfx = self.gfx.borrow_mut();
5010                let cam = gfx.camera.clone();
5011                crate::gfx::vtex::draw_halftone(
5012                    &mut gfx.depth_queue,
5013                    &cam,
5014                    cx,
5015                    cy,
5016                    cz,
5017                    ux,
5018                    uy,
5019                    uz,
5020                    vx,
5021                    vy,
5022                    vz,
5023                    cols,
5024                    rows,
5025                    cw,
5026                    ch,
5027                    dens,
5028                    fr,
5029                    hue,
5030                );
5031                return Ok(Value::Unit);
5032            },
5033
5034            // vtex_tessellated(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell, amplitude,freq, fr,hue)
5035            "vtex_tessellated" | "ลายตาข่าย" | "纹镶嵌" | "網目模様" | "격자망" =>
5036            {
5037                let cx = self.arg_num(&args, 0, 0.)? as f32;
5038                let cy = self.arg_num(&args, 1, 0.)? as f32;
5039                let cz = self.arg_num(&args, 2, 0.)? as f32;
5040                let ux = self.arg_num(&args, 3, 1.)? as f32;
5041                let uy = self.arg_num(&args, 4, 0.)? as f32;
5042                let uz = self.arg_num(&args, 5, 0.)? as f32;
5043                let vx = self.arg_num(&args, 6, 0.)? as f32;
5044                let vy = self.arg_num(&args, 7, 0.)? as f32;
5045                let vz = self.arg_num(&args, 8, 1.)? as f32;
5046                let cols = self.arg_num(&args, 9, 14.)? as usize;
5047                let rows = self.arg_num(&args, 10, 10.)? as usize;
5048                let cell = self.arg_num(&args, 11, 0.6)? as f32;
5049                let amp = self.arg_num(&args, 12, 0.25)? as f32;
5050                let freq = self.arg_num(&args, 13, 4.)? as f32;
5051                let fr = self.arg_num(&args, 14, 0.)? as f32;
5052                let hue = self.arg_num(&args, 15, 0.)? as f32;
5053                let mut gfx = self.gfx.borrow_mut();
5054                let cam = gfx.camera.clone();
5055                crate::gfx::vtex::draw_tessellated(
5056                    &mut gfx.depth_queue,
5057                    &cam,
5058                    cx,
5059                    cy,
5060                    cz,
5061                    ux,
5062                    uy,
5063                    uz,
5064                    vx,
5065                    vy,
5066                    vz,
5067                    cols,
5068                    rows,
5069                    cell,
5070                    amp,
5071                    freq,
5072                    fr,
5073                    hue,
5074                );
5075                return Ok(Value::Unit);
5076            },
5077
5078            // vtex_lotus(cx,cy,cz, ux,uy,uz, vx,vy,vz, r_inner,r_outer,n_petals, fr,hue)
5079            "vtex_lotus" | "ลายดอกบัว" | "纹莲" | "蓮模様" | "연꽃무늬" =>
5080            {
5081                let cx = self.arg_num(&args, 0, 0.)? as f32;
5082                let cy = self.arg_num(&args, 1, 0.)? as f32;
5083                let cz = self.arg_num(&args, 2, 0.)? as f32;
5084                let ux = self.arg_num(&args, 3, 1.)? as f32;
5085                let uy = self.arg_num(&args, 4, 0.)? as f32;
5086                let uz = self.arg_num(&args, 5, 0.)? as f32;
5087                let vx = self.arg_num(&args, 6, 0.)? as f32;
5088                let vy = self.arg_num(&args, 7, 0.)? as f32;
5089                let vz = self.arg_num(&args, 8, 1.)? as f32;
5090                let ri = self.arg_num(&args, 9, 1.)? as f32;
5091                let ro = self.arg_num(&args, 10, 2.)? as f32;
5092                let np = self.arg_num(&args, 11, 12.)? as usize;
5093                let fr = self.arg_num(&args, 12, 0.)? as f32;
5094                let hue = self.arg_num(&args, 13, 0.)? as f32;
5095                let mut gfx = self.gfx.borrow_mut();
5096                let cam = gfx.camera.clone();
5097                crate::gfx::vtex::draw_lotus(
5098                    &mut gfx.depth_queue,
5099                    &cam,
5100                    cx,
5101                    cy,
5102                    cz,
5103                    ux,
5104                    uy,
5105                    uz,
5106                    vx,
5107                    vy,
5108                    vz,
5109                    ri,
5110                    ro,
5111                    np,
5112                    fr,
5113                    hue,
5114                );
5115                return Ok(Value::Unit);
5116            },
5117
5118            // vtex_chakra(cx,cy,cz, ux,uy,uz, vx,vy,vz, r,n_spokes, fr,hue)
5119            "vtex_chakra" | "ลายจักร" | "纹轮" | "輪模様" | "바퀴무늬" => {
5120                let cx = self.arg_num(&args, 0, 0.)? as f32;
5121                let cy = self.arg_num(&args, 1, 0.)? as f32;
5122                let cz = self.arg_num(&args, 2, 0.)? as f32;
5123                let ux = self.arg_num(&args, 3, 1.)? as f32;
5124                let uy = self.arg_num(&args, 4, 0.)? as f32;
5125                let uz = self.arg_num(&args, 5, 0.)? as f32;
5126                let vx = self.arg_num(&args, 6, 0.)? as f32;
5127                let vy = self.arg_num(&args, 7, 0.)? as f32;
5128                let vz = self.arg_num(&args, 8, 1.)? as f32;
5129                let r = self.arg_num(&args, 9, 2.)? as f32;
5130                let ns = self.arg_num(&args, 10, 8.)? as usize;
5131                let fr = self.arg_num(&args, 11, 0.)? as f32;
5132                let hue = self.arg_num(&args, 12, 0.)? as f32;
5133                let mut gfx = self.gfx.borrow_mut();
5134                let cam = gfx.camera.clone();
5135                crate::gfx::vtex::draw_chakra(
5136                    &mut gfx.depth_queue,
5137                    &cam,
5138                    cx,
5139                    cy,
5140                    cz,
5141                    ux,
5142                    uy,
5143                    uz,
5144                    vx,
5145                    vy,
5146                    vz,
5147                    r,
5148                    ns,
5149                    fr,
5150                    hue,
5151                );
5152                return Ok(Value::Unit);
5153            },
5154
5155            // vtex_yantra(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_layers,max_r, fr,hue)
5156            "vtex_yantra" | "ลายยันต์" | "纹咒" | "護符模様" | "부적무늬" =>
5157            {
5158                let cx = self.arg_num(&args, 0, 0.)? as f32;
5159                let cy = self.arg_num(&args, 1, 0.)? as f32;
5160                let cz = self.arg_num(&args, 2, 0.)? as f32;
5161                let ux = self.arg_num(&args, 3, 1.)? as f32;
5162                let uy = self.arg_num(&args, 4, 0.)? as f32;
5163                let uz = self.arg_num(&args, 5, 0.)? as f32;
5164                let vx = self.arg_num(&args, 6, 0.)? as f32;
5165                let vy = self.arg_num(&args, 7, 0.)? as f32;
5166                let vz = self.arg_num(&args, 8, 1.)? as f32;
5167                let nl = self.arg_num(&args, 9, 4.)? as usize;
5168                let mr = self.arg_num(&args, 10, 3.)? as f32;
5169                let fr = self.arg_num(&args, 11, 0.)? as f32;
5170                let hue = self.arg_num(&args, 12, 0.)? as f32;
5171                let mut gfx = self.gfx.borrow_mut();
5172                let cam = gfx.camera.clone();
5173                crate::gfx::vtex::draw_yantra(
5174                    &mut gfx.depth_queue,
5175                    &cam,
5176                    cx,
5177                    cy,
5178                    cz,
5179                    ux,
5180                    uy,
5181                    uz,
5182                    vx,
5183                    vy,
5184                    vz,
5185                    nl,
5186                    mr,
5187                    fr,
5188                    hue,
5189                );
5190                return Ok(Value::Unit);
5191            },
5192
5193            // vtex_spiked_cog(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_teeth,r_body,r_spike,r_hub,n_spokes, fr,hue)
5194            "vtex_spiked_cog" | "ฟันเฟืองหนาม" | "纹棘轮" | "歯車模様" | "톱니바퀴" =>
5195            {
5196                let cx = self.arg_num(&args, 0, 0.)? as f32;
5197                let cy = self.arg_num(&args, 1, 0.)? as f32;
5198                let cz = self.arg_num(&args, 2, 0.)? as f32;
5199                let ux = self.arg_num(&args, 3, 1.)? as f32;
5200                let uy = self.arg_num(&args, 4, 0.)? as f32;
5201                let uz = self.arg_num(&args, 5, 0.)? as f32;
5202                let vx = self.arg_num(&args, 6, 0.)? as f32;
5203                let vy = self.arg_num(&args, 7, 0.)? as f32;
5204                let vz = self.arg_num(&args, 8, 1.)? as f32;
5205                let nt = self.arg_num(&args, 9, 12.)? as usize;
5206                let rb = self.arg_num(&args, 10, 1.)? as f32;
5207                let rs = self.arg_num(&args, 11, 1.3)? as f32;
5208                let rh = self.arg_num(&args, 12, 0.2)? as f32;
5209                let ns = self.arg_num(&args, 13, 6.)? as usize;
5210                let fr = self.arg_num(&args, 14, 0.)? as f32;
5211                let hue = self.arg_num(&args, 15, 0.)? as f32;
5212                let mut gfx = self.gfx.borrow_mut();
5213                let cam = gfx.camera.clone();
5214                crate::gfx::vtex::draw_spiked_cog(
5215                    &mut gfx.depth_queue,
5216                    &cam,
5217                    cx,
5218                    cy,
5219                    cz,
5220                    ux,
5221                    uy,
5222                    uz,
5223                    vx,
5224                    vy,
5225                    vz,
5226                    nt,
5227                    rb,
5228                    rs,
5229                    rh,
5230                    ns,
5231                    fr,
5232                    hue,
5233                );
5234                return Ok(Value::Unit);
5235            },
5236
5237            // vtex_torii(cx,cy,cz, ux,uy,uz, vx,vy,vz, width,height, fr,hue)
5238            "vtex_torii" | "ประตูโทริอิ" | "纹鸟居" | "鳥居" | "도리이" =>
5239            {
5240                let cx = self.arg_num(&args, 0, 0.)? as f32;
5241                let cy = self.arg_num(&args, 1, 0.)? as f32;
5242                let cz = self.arg_num(&args, 2, 0.)? as f32;
5243                let ux = self.arg_num(&args, 3, 1.)? as f32;
5244                let uy = self.arg_num(&args, 4, 0.)? as f32;
5245                let uz = self.arg_num(&args, 5, 0.)? as f32;
5246                let vx = self.arg_num(&args, 6, 0.)? as f32;
5247                let vy = self.arg_num(&args, 7, 0.)? as f32;
5248                let vz = self.arg_num(&args, 8, 1.)? as f32;
5249                let w = self.arg_num(&args, 9, 4.)? as f32;
5250                let h = self.arg_num(&args, 10, 5.)? as f32;
5251                let fr = self.arg_num(&args, 11, 0.)? as f32;
5252                let hue = self.arg_num(&args, 12, 0.)? as f32;
5253                let mut gfx = self.gfx.borrow_mut();
5254                let cam = gfx.camera.clone();
5255                crate::gfx::vtex::draw_torii(
5256                    &mut gfx.depth_queue,
5257                    &cam,
5258                    cx,
5259                    cy,
5260                    cz,
5261                    ux,
5262                    uy,
5263                    uz,
5264                    vx,
5265                    vy,
5266                    vz,
5267                    w,
5268                    h,
5269                    fr,
5270                    hue,
5271                );
5272                return Ok(Value::Unit);
5273            },
5274
5275            // vtex_pagoda(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_tiers,base_w,tier_h,taper,eave_out, fr,hue)
5276            "vtex_pagoda" | "เจดีย์" | "纹塔" | "塔" | "탑" => {
5277                let cx = self.arg_num(&args, 0, 0.)? as f32;
5278                let cy = self.arg_num(&args, 1, 0.)? as f32;
5279                let cz = self.arg_num(&args, 2, 0.)? as f32;
5280                let ux = self.arg_num(&args, 3, 1.)? as f32;
5281                let uy = self.arg_num(&args, 4, 0.)? as f32;
5282                let uz = self.arg_num(&args, 5, 0.)? as f32;
5283                let vx = self.arg_num(&args, 6, 0.)? as f32;
5284                let vy = self.arg_num(&args, 7, 0.)? as f32;
5285                let vz = self.arg_num(&args, 8, 1.)? as f32;
5286                let nt = self.arg_num(&args, 9, 5.)? as usize;
5287                let bw = self.arg_num(&args, 10, 2.)? as f32;
5288                let th = self.arg_num(&args, 11, 1.)? as f32;
5289                let tp = self.arg_num(&args, 12, 0.72)? as f32;
5290                let eo = self.arg_num(&args, 13, 0.28)? as f32;
5291                let fr = self.arg_num(&args, 14, 0.)? as f32;
5292                let hue = self.arg_num(&args, 15, 0.)? as f32;
5293                let mut gfx = self.gfx.borrow_mut();
5294                let cam = gfx.camera.clone();
5295                crate::gfx::vtex::draw_pagoda(
5296                    &mut gfx.depth_queue,
5297                    &cam,
5298                    cx,
5299                    cy,
5300                    cz,
5301                    ux,
5302                    uy,
5303                    uz,
5304                    vx,
5305                    vy,
5306                    vz,
5307                    nt,
5308                    bw,
5309                    th,
5310                    tp,
5311                    eo,
5312                    fr,
5313                    hue,
5314                );
5315                return Ok(Value::Unit);
5316            },
5317
5318            // ══════════════════════════════════════════════════════════════════
5319            // AUDIO BUILTINS
5320            // ══════════════════════════════════════════════════════════════════
5321
5322            // audio_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth)
5323            #[cfg(not(target_arch = "wasm32"))]
5324            "audio_tone"
5325            | "เสียงโทน"
5326            | "音调"
5327            | "音調"
5328            | "음조"
5329            | "空间音"
5330            | "空間音"
5331            | "공간음" => {
5332                let idx = self.arg_num(&args, 0, 0.0)? as usize;
5333                let x = self.arg_num(&args, 1, 0.0)? as f32;
5334                let y = self.arg_num(&args, 2, 0.0)? as f32;
5335                let z = self.arg_num(&args, 3, 0.0)? as f32;
5336                let w = self.arg_num(&args, 4, 1.0)? as f32;
5337                let freq = self.arg_num(&args, 5, 220.0)? as f32;
5338                let amp = self.arg_num(&args, 6, 0.15)? as f32;
5339                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
5340                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
5341                if let Some(audio) = &self.audio {
5342                    audio.set_tone(
5343                        idx,
5344                        ToneParams { x, y, z, w, freq, amp, lfo_rate, lfo_depth },
5345                    );
5346                }
5347                return Ok(Value::Unit);
5348            },
5349
5350            #[cfg(not(target_arch = "wasm32"))]
5351            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" =>
5352            {
5353                let cry = self.arg_num(&args, 0, 1.0)? as f32;
5354                let sry = self.arg_num(&args, 1, 0.0)? as f32;
5355                let crx = self.arg_num(&args, 2, 1.0)? as f32;
5356                let srx = self.arg_num(&args, 3, 0.0)? as f32;
5357                if let Some(audio) = &self.audio {
5358                    audio.set_listener(cry, sry, crx, srx);
5359                }
5360                return Ok(Value::Unit);
5361            },
5362
5363            #[cfg(not(target_arch = "wasm32"))]
5364            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" =>
5365            {
5366                let path = match args.first() {
5367                    Some(Value::Str(s)) => s.clone(),
5368                    _ => return Ok(Value::Unit),
5369                };
5370                let vol = self.arg_num(&args, 1, 0.5)? as f32;
5371                if let Some(audio) = &self.audio {
5372                    audio.load_bgm(&path, vol);
5373                }
5374                return Ok(Value::Unit);
5375            },
5376
5377            #[cfg(not(target_arch = "wasm32"))]
5378            "audio_bgm_volume"
5379            | "ระดับเสียงพื้นหลัง"
5380            | "ระดับเพลงประกอบ"
5381            | "背景乐音量"
5382            | "BGM音量"
5383            | "배경음악음량" => {
5384                let vol = self.arg_num(&args, 0, 0.5)? as f32;
5385                if let Some(audio) = &self.audio {
5386                    audio.set_bgm_volume(vol);
5387                }
5388                return Ok(Value::Unit);
5389            },
5390
5391            #[cfg(not(target_arch = "wasm32"))]
5392            "audio_volume" | "ระดับเสียง" | "音量" | "음량" => {
5393                let vol = self.arg_num(&args, 0, 0.7)? as f32;
5394                if let Some(audio) = &self.audio {
5395                    audio.set_master_volume(vol);
5396                }
5397                return Ok(Value::Unit);
5398            },
5399
5400            // WASM audio builtins — delegate to Web Audio API
5401            #[cfg(target_arch = "wasm32")]
5402            "audio_tone"
5403            | "เสียงโทน"
5404            | "音调"
5405            | "音調"
5406            | "음조"
5407            | "空间音"
5408            | "空間音"
5409            | "공간음" => {
5410                let idx = self.arg_num(&args, 0, 0.0)? as usize;
5411                let x = self.arg_num(&args, 1, 0.0)? as f32;
5412                let y = self.arg_num(&args, 2, 0.0)? as f32;
5413                let z = self.arg_num(&args, 3, 0.0)? as f32;
5414                let w = self.arg_num(&args, 4, 1.0)? as f32;
5415                let freq = self.arg_num(&args, 5, 220.0)? as f32;
5416                let amp = self.arg_num(&args, 6, 0.15)? as f32;
5417                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
5418                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
5419                crate::gfx::audio_web::set_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth);
5420                return Ok(Value::Unit);
5421            },
5422
5423            #[cfg(target_arch = "wasm32")]
5424            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" =>
5425            {
5426                let cry = self.arg_num(&args, 0, 1.0)? as f32;
5427                let sry = self.arg_num(&args, 1, 0.0)? as f32;
5428                let crx = self.arg_num(&args, 2, 1.0)? as f32;
5429                let srx = self.arg_num(&args, 3, 0.0)? as f32;
5430                crate::gfx::audio_web::set_listener(cry, sry, crx, srx);
5431                return Ok(Value::Unit);
5432            },
5433
5434            #[cfg(target_arch = "wasm32")]
5435            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" =>
5436            {
5437                let path = self.arg_str(&args, 0, "");
5438                let vol = self.arg_num(&args, 1, 0.5)? as f32;
5439                crate::gfx::audio_web::load_bgm(&path, vol);
5440                return Ok(Value::Unit);
5441            },
5442
5443            #[cfg(target_arch = "wasm32")]
5444            "audio_bgm_volume"
5445            | "ระดับเสียงพื้นหลัง"
5446            | "ระดับเพลงประกอบ"
5447            | "背景乐音量"
5448            | "BGM音量"
5449            | "배경음악음량" => {
5450                let vol = self.arg_num(&args, 0, 0.5)? as f32;
5451                crate::gfx::audio_web::set_bgm_volume(vol);
5452                return Ok(Value::Unit);
5453            },
5454
5455            #[cfg(target_arch = "wasm32")]
5456            "audio_volume" | "ระดับเสียง" | "音量" | "음량" => {
5457                let vol = self.arg_num(&args, 0, 0.7)? as f32;
5458                crate::gfx::audio_web::set_master_volume(vol);
5459                return Ok(Value::Unit);
5460            },
5461
5462            // ── WASM sample load / play / stop / FX (Web Audio pool) ─────────
5463            #[cfg(target_arch = "wasm32")]
5464            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" =>
5465            {
5466                let path = self.arg_str(&args, 0, "");
5467                let resolved = self.wasm_resolve_source_path(&path);
5468                match wasm_fetch_bytes(&resolved)
5469                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
5470                {
5471                    Ok(t) => {
5472                        let id = crate::gfx::audio_web::add_sample(&t.stereo, t.channels, t.rate);
5473                        return Ok(Value::Number(id as f64));
5474                    },
5475                    Err(e) => {
5476                        eprintln!("audio_sample_load failed ({path}): {e}");
5477                        return Ok(Value::Number(-1.0));
5478                    },
5479                }
5480            },
5481            #[cfg(target_arch = "wasm32")]
5482            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" =>
5483            {
5484                let id = self.arg_num(&args, 0, 0.0)? as usize;
5485                let x = self.arg_num(&args, 1, 0.0)? as f32;
5486                let y = self.arg_num(&args, 2, 0.0)? as f32;
5487                let z = self.arg_num(&args, 3, 0.0)? as f32;
5488                // arg 4 is w (4th spatial dim) — ignored for 3-D panner
5489                let vol = self.arg_num(&args, 5, 1.0)? as f32;
5490                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
5491                crate::gfx::audio_web::play_sample(id, x, y, z, vol, looping);
5492                return Ok(Value::Number(0.0));
5493            },
5494            #[cfg(target_arch = "wasm32")]
5495            "audio_sample_stop" | "停止采样" | "サンプル停止" | "샘플정지" | "หยุดตัวอย่างเสียง"
5496            | "audio_fx_reverb" | "混响" | "リバーブ" | "리버브" | "เสียงก้อง"
5497            | "audio_fx_delay" | "回声" | "ディレイ効果" | "딜레이" | "เสียงสะท้อน"
5498            | "audio_fx_lowpass" | "低通滤波" | "ローパス" | "저역통과" | "กรองความถี่ต่ำ" => {
5499                return Ok(Value::Unit);
5500            },
5501
5502            // ── รอหน้าต่าง() — block until window closed / Escape ──
5503            "รอหน้าต่าง" | "wait_window" | "gfx_wait" => {
5504                #[cfg(not(target_arch = "wasm32"))]
5505                loop {
5506                    let still_open = {
5507                        let gfx = self.gfx.borrow();
5508                        gfx.window
5509                            .as_ref()
5510                            .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
5511                            .unwrap_or(false)
5512                    };
5513                    if !still_open {
5514                        break;
5515                    }
5516                    let (buf, w, h) = {
5517                        let gfx = self.gfx.borrow();
5518                        (gfx.buffer.clone(), gfx.width, gfx.height)
5519                    };
5520                    let mut gfx = self.gfx.borrow_mut();
5521                    if let Some(win) = gfx.window.as_mut() {
5522                        if win.update_with_buffer(&buf, w, h).is_err() {
5523                            break;
5524                        }
5525                    }
5526                }
5527                return Ok(Value::Unit);
5528            },
5529
5530            // ── File I/O ──────────────────────────────────────────────────────
5531            "read_file" | "อ่านไฟล์" => {
5532                #[cfg(target_arch = "wasm32")]
5533                return Ok(Value::Str(String::new()));
5534                #[cfg(not(target_arch = "wasm32"))]
5535                {
5536                    let path = self.arg_str(&args, 0, "");
5537                    return std::fs::read_to_string(&path)
5538                        .map(Value::Str)
5539                        .map_err(|e| EvalErr::from(format!("read_file '{path}': {e}")));
5540                }
5541            },
5542            // ── networking (TCP, 2-peer co-op) ───────────────────────────────
5543            #[cfg(not(target_arch = "wasm32"))]
5544            "net_host" | "เน็ตโฮสต์" => {
5545                let port = self.arg_num(&args, 0, 7777.0)? as u16;
5546                net::host(port);
5547                return Ok(Value::Unit);
5548            },
5549            #[cfg(not(target_arch = "wasm32"))]
5550            "net_join" | "เน็ตจอย" => {
5551                let ip = self.arg_str(&args, 0, "127.0.0.1");
5552                let port = self.arg_num(&args, 1, 7777.0)? as u16;
5553                net::join(&ip, port);
5554                return Ok(Value::Unit);
5555            },
5556            #[cfg(not(target_arch = "wasm32"))]
5557            "net_send" | "เน็ตส่ง" => {
5558                let s = self.arg_str(&args, 0, "");
5559                net::send(&s);
5560                return Ok(Value::Unit);
5561            },
5562            #[cfg(not(target_arch = "wasm32"))]
5563            "net_recv" | "เน็ตรับ" => {
5564                return Ok(Value::Str(net::recv()));
5565            },
5566            #[cfg(not(target_arch = "wasm32"))]
5567            "net_status" | "เน็ตสถานะ" => {
5568                return Ok(Value::Number(net::status() as f64));
5569            },
5570            // ── LAN lobby discovery (UDP broadcast) ──
5571            #[cfg(not(target_arch = "wasm32"))]
5572            "net_announce" | "เน็ตประกาศ" => {
5573                let port = self.arg_num(&args, 0, 7778.0)? as u16;
5574                let info = self.arg_str(&args, 1, "");
5575                net::announce(port, &info);
5576                return Ok(Value::Unit);
5577            },
5578            #[cfg(not(target_arch = "wasm32"))]
5579            "net_announce_stop" | "เน็ตหยุดประกาศ" => {
5580                net::announce_stop();
5581                return Ok(Value::Unit);
5582            },
5583            #[cfg(not(target_arch = "wasm32"))]
5584            "net_discover" | "เน็ตค้นหา" => {
5585                let port = self.arg_num(&args, 0, 7778.0)? as u16;
5586                return Ok(Value::Str(net::discover(port)));
5587            },
5588            #[cfg(not(target_arch = "wasm32"))]
5589            "net_test" | "เน็ตทดสอบ" => {
5590                let port = self.arg_num(&args, 0, 7777.0)? as u16;
5591                return Ok(Value::Str(net::test_bind(port)));
5592            },
5593            // ── gamepad (gilrs) ──
5594            #[cfg(not(target_arch = "wasm32"))]
5595            "gamepad_poll" | "จอยโพล" => {
5596                gamepad::poll();
5597                return Ok(Value::Unit);
5598            },
5599            #[cfg(not(target_arch = "wasm32"))]
5600            "gamepad_button" | "จอยปุ่ม" => {
5601                let name = self.arg_str(&args, 0, "");
5602                return Ok(Value::Number(if gamepad::button(&name) {
5603                    1.0
5604                } else {
5605                    0.0
5606                }));
5607            },
5608            #[cfg(not(target_arch = "wasm32"))]
5609            "gamepad_axis" | "จอยแกน" => {
5610                let name = self.arg_str(&args, 0, "");
5611                return Ok(Value::Number(gamepad::axis(&name) as f64));
5612            },
5613            #[cfg(not(target_arch = "wasm32"))]
5614            "gamepad_rumble" | "จอยสั่น" => {
5615                let low = self.arg_num(&args, 0, 0.0)? as f32;
5616                let high = self.arg_num(&args, 1, 0.0)? as f32;
5617                let ms = self.arg_num(&args, 2, 200.0)? as u32;
5618                gamepad::rumble(low, high, ms);
5619                return Ok(Value::Unit);
5620            },
5621            #[cfg(not(target_arch = "wasm32"))]
5622            "gamepad_list" | "จอยรายการ" => {
5623                return Ok(Value::Str(gamepad::list()));
5624            },
5625            #[cfg(not(target_arch = "wasm32"))]
5626            "gamepad_any" | "จอยใดๆ" => {
5627                return Ok(Value::Number(if gamepad::any_button() { 1.0 } else { 0.0 }));
5628            },
5629            // wasm32: gamepad not available — return safe no-op values
5630            #[cfg(target_arch = "wasm32")]
5631            "gamepad_poll" | "จอยโพล"
5632            | "gamepad_rumble" | "จอยสั่น" => {
5633                return Ok(Value::Unit);
5634            },
5635            #[cfg(target_arch = "wasm32")]
5636            "gamepad_button" | "จอยปุ่ม"
5637            | "gamepad_axis" | "จอยแกน"
5638            | "gamepad_any" | "จอยใดๆ" => {
5639                return Ok(Value::Number(0.0));
5640            },
5641            #[cfg(target_arch = "wasm32")]
5642            "gamepad_list" | "จอยรายการ" => {
5643                return Ok(Value::Str(String::new()));
5644            },
5645
5646            // ── game AI: neural networks ─────────────────────────────────────
5647            // nn_new(inputs[, seed]) → handle
5648            #[cfg(not(target_arch = "wasm32"))]
5649            "nn_new" | "建神经网" | "ニューラル作成" | "신경망생성" | "สร้างโครงข่าย" =>
5650            {
5651                let n_in = self.arg_num(&args, 0, 1.0)?.max(0.0) as usize;
5652                let seed = self.arg_num(&args, 1, 1.0)? as u64;
5653                return Ok(Value::Number(ai::nn_new(n_in, seed) as f64));
5654            },
5655            // nn_dense(handle, units[, activation]) — append a layer
5656            #[cfg(not(target_arch = "wasm32"))]
5657            "nn_dense" | "密集层" | "密層追加" | "밀집층" | "ชั้นหนาแน่น" =>
5658            {
5659                let id = self.arg_num(&args, 0, -1.0)? as i64;
5660                let units = self.arg_num(&args, 1, 1.0)?.max(1.0) as usize;
5661                let act = self.arg_str(&args, 2, "relu");
5662                ai::nn_dense(id, units, &act);
5663                return Ok(Value::Unit);
5664            },
5665            // nn_forward(handle, [inputs]) → [outputs]
5666            #[cfg(not(target_arch = "wasm32"))]
5667            "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" =>
5668            {
5669                let id = self.arg_num(&args, 0, -1.0)? as i64;
5670                let input = self.arg_list_f32(&args, 1);
5671                let out = ai::nn_forward(id, &input);
5672                return Ok(Value::List(Rc::new(
5673                    out.into_iter().map(|v| Value::Number(v as f64)).collect(),
5674                )));
5675            },
5676            // nn_train(handle, [inputs], [targets][, lr]) → loss
5677            #[cfg(not(target_arch = "wasm32"))]
5678            "nn_train" | "训练网" | "ニューラル学習" | "신경망학습" | "ฝึกโครงข่าย" =>
5679            {
5680                let id = self.arg_num(&args, 0, -1.0)? as i64;
5681                let input = self.arg_list_f32(&args, 1);
5682                let target = self.arg_list_f32(&args, 2);
5683                let lr = self.arg_num(&args, 3, 0.01)? as f32;
5684                return Ok(Value::Number(ai::nn_train(id, &input, &target, lr) as f64));
5685            },
5686            // nn_save(handle, path) → bool
5687            #[cfg(not(target_arch = "wasm32"))]
5688            "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" =>
5689            {
5690                let id = self.arg_num(&args, 0, -1.0)? as i64;
5691                let path = self.arg_str(&args, 1, "model.lnn");
5692                return Ok(Value::Bool(ai::nn_save(id, &path)));
5693            },
5694            // nn_load(path) → handle (-1 on failure)
5695            #[cfg(not(target_arch = "wasm32"))]
5696            "nn_load" | "载入网" | "網読込" | "신경망불러오기" | "โหลดโครงข่าย" =>
5697            {
5698                let path = self.arg_str(&args, 0, "model.lnn");
5699                return Ok(Value::Number(ai::nn_load(&path) as f64));
5700            },
5701
5702            // ── game AI: behavior trees ──────────────────────────────────────
5703            // bt_build(dsl_string) → handle
5704            #[cfg(not(target_arch = "wasm32"))]
5705            "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" =>
5706            {
5707                let spec = self.arg_str(&args, 0, "");
5708                return Ok(Value::Number(ai::bt_build(&spec) as f64));
5709            },
5710            // bt_set(handle, key, value) — set a blackboard fact
5711            #[cfg(not(target_arch = "wasm32"))]
5712            "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" =>
5713            {
5714                let id = self.arg_num(&args, 0, -1.0)? as i64;
5715                let key = self.arg_str(&args, 1, "");
5716                let val = self.arg_num(&args, 2, 0.0)? as f32;
5717                ai::bt_set(id, &key, val);
5718                return Ok(Value::Unit);
5719            },
5720            // bt_tick(handle) → chosen action name ("" if none)
5721            #[cfg(not(target_arch = "wasm32"))]
5722            "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" =>
5723            {
5724                let id = self.arg_num(&args, 0, -1.0)? as i64;
5725                return Ok(Value::Str(ai::bt_tick(id)));
5726            },
5727            // bt_status(handle) → 0 fail / 1 success / 2 running
5728            #[cfg(not(target_arch = "wasm32"))]
5729            "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" =>
5730            {
5731                let id = self.arg_num(&args, 0, -1.0)? as i64;
5732                return Ok(Value::Number(ai::bt_status(id) as f64));
5733            },
5734
5735            // ── game AI: miniature dialog LLM ────────────────────────────────
5736            // dialog_new([ctx, embed, hidden, seed]) → handle
5737            #[cfg(not(target_arch = "wasm32"))]
5738            "dialog_new" | "建对话模型" | "対話モデル作成" | "대화모델생성" | "สร้างโมเดลสนทนา" =>
5739            {
5740                let ctx = self.arg_num(&args, 0, 3.0)?.max(1.0) as usize;
5741                let embed = self.arg_num(&args, 1, 32.0)?.max(1.0) as usize;
5742                let hidden = self.arg_num(&args, 2, 64.0)?.max(1.0) as usize;
5743                let seed = self.arg_num(&args, 3, 1.0)? as u64;
5744                return Ok(Value::Number(
5745                    ai::dialog_new(ctx, embed, hidden, seed) as f64
5746                ));
5747            },
5748            // dialog_learn(handle, text) — add one utterance to the corpus
5749            #[cfg(not(target_arch = "wasm32"))]
5750            "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" =>
5751            {
5752                let id = self.arg_num(&args, 0, -1.0)? as i64;
5753                let text = self.arg_str(&args, 1, "");
5754                ai::dialog_learn(id, &text);
5755                return Ok(Value::Unit);
5756            },
5757            // dialog_load(handle, path) → lines added (-1 on error)
5758            #[cfg(not(target_arch = "wasm32"))]
5759            "dialog_load" | "对话载入" | "対話読込" | "대화불러오기" | "โหลดชุดสนทนา" =>
5760            {
5761                let id = self.arg_num(&args, 0, -1.0)? as i64;
5762                let path = self.arg_str(&args, 1, "");
5763                return Ok(Value::Number(ai::dialog_load(id, &path) as f64));
5764            },
5765            // dialog_train(handle[, epochs, lr]) → loss
5766            #[cfg(not(target_arch = "wasm32"))]
5767            "dialog_train" | "对话训练" | "対話訓練" | "대화훈련" | "ฝึกสนทนา" =>
5768            {
5769                let id = self.arg_num(&args, 0, -1.0)? as i64;
5770                let epochs = self.arg_num(&args, 1, 20.0)?.max(1.0) as usize;
5771                let lr = self.arg_num(&args, 2, 0.1)? as f32;
5772                return Ok(Value::Number(ai::dialog_train(id, epochs, lr) as f64));
5773            },
5774            // dialog_say(handle, prompt[, max_tokens, temperature]) → reply text
5775            #[cfg(not(target_arch = "wasm32"))]
5776            "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" =>
5777            {
5778                let id = self.arg_num(&args, 0, -1.0)? as i64;
5779                let prompt = self.arg_str(&args, 1, "");
5780                let max = self.arg_num(&args, 2, 24.0)?.max(1.0) as usize;
5781                let temp = self.arg_num(&args, 3, 0.8)? as f32;
5782                return Ok(Value::Str(ai::dialog_say(id, &prompt, max, temp)));
5783            },
5784            // dialog_save(handle, path) → bool
5785            #[cfg(not(target_arch = "wasm32"))]
5786            "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" =>
5787            {
5788                let id = self.arg_num(&args, 0, -1.0)? as i64;
5789                let path = self.arg_str(&args, 1, "model.llm");
5790                return Ok(Value::Bool(ai::dialog_save(id, &path)));
5791            },
5792            // dialog_load_model(path) → handle (-1 on failure)
5793            #[cfg(not(target_arch = "wasm32"))]
5794            "dialog_load_model"
5795            | "对话载模"
5796            | "対話モデル読込"
5797            | "대화모델불러오기"
5798            | "โหลดโมเดลสนทนา" => {
5799                let path = self.arg_str(&args, 0, "model.llm");
5800                return Ok(Value::Number(ai::dialog_load_model(&path) as f64));
5801            },
5802
5803            "write_file" | "เขียนไฟล์" => {
5804                #[cfg(target_arch = "wasm32")]
5805                return Ok(Value::Unit);
5806                #[cfg(not(target_arch = "wasm32"))]
5807                {
5808                    let path = self.arg_str(&args, 0, "");
5809                    let content = self.arg_str(&args, 1, "");
5810                    std::fs::write(&path, content.as_bytes())
5811                        .map_err(|e| EvalErr::from(format!("write_file '{path}': {e}")))?;
5812                    return Ok(Value::Unit);
5813                }
5814            },
5815            "print_file" | "พิมพ์ไฟล์" => {
5816                let content = self.arg_str(&args, 0, "");
5817                print!("{content}");
5818                return Ok(Value::Unit);
5819            },
5820
5821            // ── CLI arguments ─────────────────────────────────────────────────
5822            "get_args" | "รับอาร์กิวเมนต์" => {
5823                let v: Vec<Value> = std::env::args().map(Value::Str).collect();
5824                return Ok(Value::List(Rc::new(v)));
5825            },
5826
5827            // ── String utilities ──────────────────────────────────────────────
5828            "split" | "str_split" | "แยก" => {
5829                let s = self.arg_str(&args, 0, "");
5830                let sep = self.arg_str(&args, 1, "\n");
5831                let sep = if sep.is_empty() { "\n".into() } else { sep };
5832                let parts: Vec<Value> = s
5833                    .split(sep.as_str())
5834                    .map(|p| Value::Str(p.to_string()))
5835                    .collect();
5836                return Ok(Value::List(Rc::new(parts)));
5837            },
5838            "trim" | "str_trim" | "ตัดช่องว่าง" => {
5839                let s = self.arg_str(&args, 0, "");
5840                return Ok(Value::Str(s.trim().to_string()));
5841            },
5842            "starts_with" | "str_starts_with" | "เริ่มด้วย" => {
5843                let s = self.arg_str(&args, 0, "");
5844                let prefix = self.arg_str(&args, 1, "");
5845                return Ok(Value::Bool(s.starts_with(prefix.as_str())));
5846            },
5847            "ends_with" | "str_ends_with" | "ลงท้ายด้วย" => {
5848                let s = self.arg_str(&args, 0, "");
5849                let suffix = self.arg_str(&args, 1, "");
5850                return Ok(Value::Bool(s.ends_with(suffix.as_str())));
5851            },
5852            "str_replace" | "แทนสตริง" => {
5853                let s = self.arg_str(&args, 0, "");
5854                let from = self.arg_str(&args, 1, "");
5855                let to = self.arg_str(&args, 2, "");
5856                return Ok(Value::Str(s.replace(from.as_str(), to.as_str())));
5857            },
5858            "str_find" | "หาในสตริง" => {
5859                let s = self.arg_str(&args, 0, "");
5860                let needle = self.arg_str(&args, 1, "");
5861                // Return char index (not byte index) for consistency with substr
5862                let pos = s
5863                    .find(needle.as_str())
5864                    .map(|byte_i| s[..byte_i].chars().count() as f64)
5865                    .unwrap_or(-1.0);
5866                return Ok(Value::Number(pos));
5867            },
5868            "substr" | "str_slice" | "ส่วนสตริง" => {
5869                let s = self.arg_str(&args, 0, "");
5870                let start = self.arg_num(&args, 1, 0.0)? as usize;
5871                let len = args
5872                    .get(2)
5873                    .map(|v| self.to_number(v).unwrap_or(999999.0) as usize)
5874                    .unwrap_or_else(|| s.chars().count().saturating_sub(start));
5875                let chars: Vec<char> = s.chars().collect();
5876                let end = (start + len).min(chars.len());
5877                let slice: String = chars.get(start..end).unwrap_or(&[]).iter().collect();
5878                return Ok(Value::Str(slice));
5879            },
5880            "to_str" | "str" | "num_str" | "แปลงสตริง" => {
5881                let v = args.into_iter().next().unwrap_or(Value::Unit);
5882                return Ok(Value::Str(v.to_string()));
5883            },
5884            "str_repeat" | "ทำซ้ำสตริง" => {
5885                let s = self.arg_str(&args, 0, "");
5886                let n = self.arg_num(&args, 1, 1.0)? as usize;
5887                return Ok(Value::Str(s.repeat(n)));
5888            },
5889            "str_upper" => {
5890                let s = self.arg_str(&args, 0, "");
5891                return Ok(Value::Str(s.to_uppercase()));
5892            },
5893            "str_lower" => {
5894                let s = self.arg_str(&args, 0, "");
5895                return Ok(Value::Str(s.to_lowercase()));
5896            },
5897            "str_len" | "len" | "ความยาว" | "长度" | "長さ" | "길이" => {
5898                match args.first() {
5899                    Some(Value::Str(s)) => return Ok(Value::Number(s.chars().count() as f64)),
5900                    Some(Value::List(v)) => return Ok(Value::Number(v.len() as f64)),
5901                    _ => return Ok(Value::Number(0.0)),
5902                }
5903            },
5904
5905            // ── FNV-1a hash (deterministic, normalized 0.0–1.0) ──────────────
5906            "hash_str" | "แฮช" => {
5907                let s = self.arg_str(&args, 0, "");
5908                let mut h: u64 = 14695981039346656037_u64;
5909                for b in s.bytes() {
5910                    h ^= b as u64;
5911                    h = h.wrapping_mul(1099511628211);
5912                }
5913                return Ok(Value::Number((h & 0xFFFFFF) as f64 / 16777215.0));
5914            },
5915            "hash_int" | "แฮชจำนวน" => {
5916                let s = self.arg_str(&args, 0, "");
5917                let n = self.arg_num(&args, 1, 100.0)? as u64;
5918                let mut h: u64 = 14695981039346656037_u64;
5919                for b in s.bytes() {
5920                    h ^= b as u64;
5921                    h = h.wrapping_mul(1099511628211);
5922                }
5923                return Ok(Value::Number((h % n.max(1)) as f64));
5924            },
5925
5926            // ── List utilities ────────────────────────────────────────────────
5927            "list_new" | "รายการใหม่" | "新建列表" | "新規リスト" | "새목록" =>
5928            {
5929                return Ok(Value::List(Rc::new(Vec::new())));
5930            },
5931            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가" =>
5932            {
5933                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
5934                let val = args.get(1).cloned().unwrap_or(Value::Unit);
5935                if let Value::List(mut v) = lst {
5936                    Rc::make_mut(&mut v).push(val);
5937                    return Ok(Value::List(v));
5938                }
5939                return Ok(Value::List(Rc::new(vec![val])));
5940            },
5941            "list_get" | "รับรายการ" | "取元素" | "要素取得" | "요소가져오기" =>
5942            {
5943                // Borrow the list; clone only the element (was cloning the whole list).
5944                let i = self.arg_num(&args, 1, 0.0)? as usize;
5945                if let Some(Value::List(v)) = args.first() {
5946                    return Ok(v.get(i).cloned().unwrap_or(Value::Str(String::new())));
5947                }
5948                return Ok(Value::Str(String::new()));
5949            },
5950            // list_set(lst, idx, val) → new list with index replaced. Engine builtin
5951            // (O(n) one copy) to replace the O(n²) ling `ตั้งรายการ` that looped
5952            // list_push + list_get (each of which copied the whole list).
5953            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정" =>
5954            {
5955                let idx = self.arg_num(&args, 1, 0.0)? as usize;
5956                let mut ai = args.into_iter();
5957                let lst = ai.next().unwrap_or(Value::List(Rc::new(vec![])));
5958                ai.next(); // skip idx
5959                let val = ai.next().unwrap_or(Value::Unit);
5960                if let Value::List(mut v) = lst {
5961                    if idx < v.len() {
5962                        Rc::make_mut(&mut v)[idx] = val;
5963                    }
5964                    return Ok(Value::List(v));
5965                }
5966                return Ok(Value::List(Rc::new(vec![])));
5967            },
5968            "list_join" | "join" | "รวมรายการ" | "连接" | "連結" | "연결" =>
5969            {
5970                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
5971                let sep = args.get(1).map(|v| v.to_string()).unwrap_or_default();
5972                if let Value::List(v) = lst {
5973                    return Ok(Value::Str(
5974                        v.iter()
5975                            .map(|x| x.to_string())
5976                            .collect::<Vec<_>>()
5977                            .join(&sep),
5978                    ));
5979                }
5980                return Ok(Value::Str(String::new()));
5981            },
5982            // blob_f32("<deflate+base64>") / blob_i32(...) — decode an embedded,
5983            // losslessly-compressed numeric blob into a list. Produced by
5984            // `ling convert`; lets converted assets carry geometry/PCM/etc. compactly.
5985            #[cfg(not(target_arch = "wasm32"))]
5986            "blob_f32" | "blob_i32" => {
5987                let s = self.arg_str(&args, 0, "");
5988                let is_i32 = name == "blob_i32";
5989                match decode_blob(&s) {
5990                    Ok(bytes) => {
5991                        let mut out = Vec::with_capacity(bytes.len() / 4);
5992                        for ch in bytes.chunks_exact(4) {
5993                            let arr = [ch[0], ch[1], ch[2], ch[3]];
5994                            let n = if is_i32 {
5995                                i32::from_le_bytes(arr) as f64
5996                            } else {
5997                                f32::from_le_bytes(arr) as f64
5998                            };
5999                            out.push(Value::Number(n));
6000                        }
6001                        return Ok(Value::List(Rc::new(out)));
6002                    },
6003                    Err(e) => {
6004                        eprintln!("blob decode failed: {e}");
6005                        return Ok(Value::List(Rc::new(vec![])));
6006                    },
6007                }
6008            },
6009
6010            // ══════════════════════════════════════════════════════════════════
6011            // SVG EXPORT  (svg_begin / svg_rect / svg_circle / svg_line /
6012            //              svg_polyline / svg_text / svg_end / hsl_color)
6013            // Chinese aliases: 开始SVG 结束SVG SVG矩形 SVG圆形 SVG线段 SVG折线 SVG文本 HSL颜色
6014            // Thai aliases:    เริ่มSVG จบSVG SVGสี่เหลี่ยม SVGวงกลม SVGเส้น SVGเส้นหัก SVGข้อความ สีHSL
6015            // ══════════════════════════════════════════════════════════════════
6016            "svg_begin" | "开始SVG" | "เริ่มSVG" => {
6017                let path = self.arg_str(&args, 0, "output.svg");
6018                let width = self.arg_num(&args, 1, 800.0)?;
6019                let height = self.arg_num(&args, 2, 600.0)?;
6020                *self.svg.borrow_mut() = Some(SvgWriter::new(path, width, height));
6021                return Ok(Value::Unit);
6022            },
6023
6024            "svg_rect" | "SVG矩形" | "SVGสี่เหลี่ยม" => {
6025                let x = self.arg_num(&args, 0, 0.0)?;
6026                let y = self.arg_num(&args, 1, 0.0)?;
6027                let w = self.arg_num(&args, 2, 10.0)?;
6028                let h = self.arg_num(&args, 3, 10.0)?;
6029                let fill = self.arg_str(&args, 4, "#ffffff");
6030                if let Some(svg) = self.svg.borrow_mut().as_mut() {
6031                    svg.elements.push(format!(
6032                        "<rect x=\"{x:.1}\" y=\"{y:.1}\" width=\"{w:.1}\" \
6033                         height=\"{h:.1}\" fill=\"{fill}\"/>"
6034                    ));
6035                }
6036                return Ok(Value::Unit);
6037            },
6038
6039            "svg_circle" | "SVG圆形" | "SVGวงกลม" => {
6040                let cx = self.arg_num(&args, 0, 0.0)?;
6041                let cy = self.arg_num(&args, 1, 0.0)?;
6042                let r = self.arg_num(&args, 2, 5.0)?;
6043                let fill = self.arg_str(&args, 3, "#ffffff");
6044                if let Some(svg) = self.svg.borrow_mut().as_mut() {
6045                    svg.elements.push(format!(
6046                        "<circle cx=\"{cx:.1}\" cy=\"{cy:.1}\" r=\"{r:.1}\" fill=\"{fill}\"/>"
6047                    ));
6048                }
6049                return Ok(Value::Unit);
6050            },
6051
6052            "svg_line" | "SVG线段" | "SVGเส้น" => {
6053                let x1 = self.arg_num(&args, 0, 0.0)?;
6054                let y1 = self.arg_num(&args, 1, 0.0)?;
6055                let x2 = self.arg_num(&args, 2, 0.0)?;
6056                let y2 = self.arg_num(&args, 3, 0.0)?;
6057                let stroke = self.arg_str(&args, 4, "#ffffff");
6058                let sw = self.arg_num(&args, 5, 1.0)?;
6059                if let Some(svg) = self.svg.borrow_mut().as_mut() {
6060                    svg.elements.push(format!(
6061                        "<line x1=\"{x1:.1}\" y1=\"{y1:.1}\" x2=\"{x2:.1}\" y2=\"{y2:.1}\" \
6062                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
6063                    ));
6064                }
6065                return Ok(Value::Unit);
6066            },
6067
6068            "svg_polyline" | "SVG折线" | "SVGเส้นหัก" => {
6069                let pts = self.arg_str(&args, 0, "");
6070                let stroke = self.arg_str(&args, 1, "#ffffff");
6071                let sw = self.arg_num(&args, 2, 1.0)?;
6072                if let Some(svg) = self.svg.borrow_mut().as_mut() {
6073                    svg.elements.push(format!(
6074                        "<polyline points=\"{pts}\" fill=\"none\" \
6075                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
6076                    ));
6077                }
6078                return Ok(Value::Unit);
6079            },
6080
6081            "svg_text" | "SVG文本" | "SVGข้อความ" => {
6082                let x = self.arg_num(&args, 0, 0.0)?;
6083                let y = self.arg_num(&args, 1, 0.0)?;
6084                let text = self.arg_str(&args, 2, "");
6085                let fill = self.arg_str(&args, 3, "#ffffff");
6086                let size = self.arg_num(&args, 4, 12.0)?;
6087                if let Some(svg) = self.svg.borrow_mut().as_mut() {
6088                    let safe = text
6089                        .replace('&', "&amp;")
6090                        .replace('<', "&lt;")
6091                        .replace('>', "&gt;");
6092                    svg.elements.push(format!(
6093                        "<text x=\"{x:.1}\" y=\"{y:.1}\" fill=\"{fill}\" \
6094                         font-family=\"monospace\" font-size=\"{size:.0}\">{safe}</text>"
6095                    ));
6096                }
6097                return Ok(Value::Unit);
6098            },
6099
6100            "svg_end" | "结束SVG" | "จบSVG" => {
6101                {
6102                    let borrow = self.svg.borrow();
6103                    if let Some(svg) = borrow.as_ref() {
6104                        svg.save()
6105                            .map_err(|e| EvalErr::from(format!("svg_end: {e}")))?;
6106                    }
6107                }
6108                *self.svg.borrow_mut() = None;
6109                return Ok(Value::Unit);
6110            },
6111
6112            "hsl_color" | "HSL颜色" | "สีHSL" => {
6113                let h = self.arg_num(&args, 0, 0.0)?;
6114                let s = self.arg_num(&args, 1, 70.0)?;
6115                let l = self.arg_num(&args, 2, 50.0)?;
6116                return Ok(Value::Str(hsl_to_hex(h, s, l)));
6117            },
6118
6119            // ══════════════════════════════════════════════════════════════════
6120            // FFT / AUDIO ANALYSIS BUILTINS  (native only)
6121            // ══════════════════════════════════════════════════════════════════
6122
6123            // fft_push(samples_list) — feed raw audio samples and run FFT
6124            #[cfg(not(target_arch = "wasm32"))]
6125            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" =>
6126            {
6127                if let Some(Value::List(v)) = args.first() {
6128                    let samples: Vec<f32> = v
6129                        .iter()
6130                        .filter_map(|x| {
6131                            if let Value::Number(n) = x {
6132                                Some(*n as f32)
6133                            } else {
6134                                None
6135                            }
6136                        })
6137                        .collect();
6138                    self.fft.borrow_mut().push_samples(&samples);
6139                }
6140                return Ok(Value::Unit);
6141            },
6142
6143            // fft_bands(n) → list of n log-spaced magnitude bands (0..1)
6144            #[cfg(not(target_arch = "wasm32"))]
6145            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" =>
6146            {
6147                let n = self.arg_num(&args, 0, 32.0)? as usize;
6148                let bands = self.fft.borrow().freq_bands(n);
6149                *self.fft_bands_cache.borrow_mut() = bands.clone();
6150                return Ok(Value::List(Rc::new(
6151                    bands.into_iter().map(|v| Value::Number(v as f64)).collect(),
6152                )));
6153            },
6154
6155            // fft_beat() → bool
6156            #[cfg(not(target_arch = "wasm32"))]
6157            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" =>
6158            {
6159                return Ok(Value::Bool(self.fft.borrow().is_beat()));
6160            },
6161
6162            // fft_beat_ratio() → f64  (1.0 = at threshold, >1 = strong beat)
6163            #[cfg(not(target_arch = "wasm32"))]
6164            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" =>
6165            {
6166                return Ok(Value::Number(self.fft.borrow().beat_ratio() as f64));
6167            },
6168
6169            // fft_rms() → f64
6170            #[cfg(not(target_arch = "wasm32"))]
6171            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" => {
6172                return Ok(Value::Number(self.fft.borrow().rms() as f64));
6173            },
6174
6175            // fft_dominant_freq() → f64  in Hz
6176            #[cfg(not(target_arch = "wasm32"))]
6177            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" =>
6178            {
6179                return Ok(Value::Number(self.fft.borrow().dominant_freq() as f64));
6180            },
6181
6182            // ── wasm32 stubs: fft builtins are no-ops on web ───────────────
6183            #[cfg(target_arch = "wasm32")]
6184            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" =>
6185            {
6186                return Ok(Value::Unit);
6187            },
6188            #[cfg(target_arch = "wasm32")]
6189            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" =>
6190            {
6191                let n = self.arg_num(&args, 0, 32.0)? as usize;
6192                return Ok(Value::List(vec![Value::Number(0.0); n]));
6193            },
6194            #[cfg(target_arch = "wasm32")]
6195            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" =>
6196            {
6197                return Ok(Value::Bool(false));
6198            },
6199            #[cfg(target_arch = "wasm32")]
6200            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" =>
6201            {
6202                return Ok(Value::Number(1.0));
6203            },
6204            #[cfg(target_arch = "wasm32")]
6205            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" => {
6206                return Ok(Value::Number(0.0));
6207            },
6208            #[cfg(target_arch = "wasm32")]
6209            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" =>
6210            {
6211                return Ok(Value::Number(0.0));
6212            },
6213
6214            // ══════════════════════════════════════════════════════════════════
6215            // PROCEDURAL TEXTURE BLIT BUILTINS  (screen-space)
6216            // All: name(dst_x, dst_y, width, height, ...params, palette)
6217            // palette: "rainbow" | "fire" | "ocean" | "psychedelic" | "neon" | "forest"
6218            // ══════════════════════════════════════════════════════════════════
6219
6220            // tex_checkerboard(x, y, w, h, tiles, r1,g1,b1, r2,g2,b2)
6221            "tex_checkerboard" | "ลายตารางหมากรุก" => {
6222                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6223                let tiles = self.arg_num(&args, 4, 8.0)? as u32;
6224                let (r1, g1, b1) = (
6225                    self.arg_num(&args, 5, 255.)? as u32,
6226                    self.arg_num(&args, 6, 255.)? as u32,
6227                    self.arg_num(&args, 7, 255.)? as u32,
6228                );
6229                let (r2, g2, b2) = (
6230                    self.arg_num(&args, 8, 0.)? as u32,
6231                    self.arg_num(&args, 9, 0.)? as u32,
6232                    self.arg_num(&args, 10, 0.)? as u32,
6233                );
6234                let c1 = (r1 << 16) | (g1 << 8) | b1;
6235                let c2 = (r2 << 16) | (g2 << 8) | b2;
6236                let mut gfx = self.gfx.borrow_mut();
6237                let (bw, bh) = (gfx.width, gfx.height);
6238                for row in 0..th {
6239                    for col in 0..tw {
6240                        let cx = col as u32 * tiles / tw as u32;
6241                        let cy = row as u32 * tiles / th as u32;
6242                        let (dx, dy) = (tx + col, ty + row);
6243                        if dx < bw && dy < bh {
6244                            gfx.buffer[dy * bw + dx] = if (cx + cy) % 2 == 0 { c1 } else { c2 };
6245                        }
6246                    }
6247                }
6248                return Ok(Value::Unit);
6249            },
6250
6251            // tex_gradient(x, y, w, h, angle_deg, r1,g1,b1, r2,g2,b2)
6252            "tex_gradient" | "ลายไล่สี" => {
6253                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6254                let angle = self.arg_num(&args, 4, 0.0)? as f32;
6255                let (r1, g1, b1) = (
6256                    self.arg_num(&args, 5, 0.)? as f32 / 255.,
6257                    self.arg_num(&args, 6, 0.)? as f32 / 255.,
6258                    self.arg_num(&args, 7, 0.)? as f32 / 255.,
6259                );
6260                let (r2, g2, b2) = (
6261                    self.arg_num(&args, 8, 255.)? as f32 / 255.,
6262                    self.arg_num(&args, 9, 255.)? as f32 / 255.,
6263                    self.arg_num(&args, 10, 255.)? as f32 / 255.,
6264                );
6265                let (ca, sa) = (angle.to_radians().cos(), angle.to_radians().sin());
6266                let mut gfx = self.gfx.borrow_mut();
6267                let (bw, bh) = (gfx.width, gfx.height);
6268                for row in 0..th {
6269                    for col in 0..tw {
6270                        let nx = col as f32 / tw as f32 - 0.5;
6271                        let ny = row as f32 / th as f32 - 0.5;
6272                        let t = ((nx * ca + ny * sa + 0.707) / 1.414).clamp(0., 1.);
6273                        let (dx, dy) = (tx + col, ty + row);
6274                        if dx < bw && dy < bh {
6275                            gfx.buffer[dy * bw + dx] =
6276                                tex_rgb(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t);
6277                        }
6278                    }
6279                }
6280                return Ok(Value::Unit);
6281            },
6282
6283            // tex_noise(x, y, w, h, scale, octaves, seed, palette)
6284            "tex_noise" | "ลายนอยส์" => {
6285                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6286                let scale = self.arg_num(&args, 4, 4.0)? as f32;
6287                let octaves = self.arg_num(&args, 5, 4.0)? as u32;
6288                let seed = self.arg_num(&args, 6, 0.0)? as u32;
6289                let palette = self.arg_str(&args, 7, "rainbow");
6290                let mut gfx = self.gfx.borrow_mut();
6291                let (bw, bh) = (gfx.width, gfx.height);
6292                for row in 0..th {
6293                    for col in 0..tw {
6294                        let v = tex_fbm(
6295                            col as f32 * scale / tw as f32,
6296                            row as f32 * scale / th as f32,
6297                            octaves,
6298                            seed,
6299                        );
6300                        let [r, g, b] = tex_palette(&palette, v);
6301                        let (dx, dy) = (tx + col, ty + row);
6302                        if dx < bw && dy < bh {
6303                            gfx.buffer[dy * bw + dx] = tex_rgb(r, g, b);
6304                        }
6305                    }
6306                }
6307                return Ok(Value::Unit);
6308            },
6309
6310            // tex_freq_map(x, y, w, h, time, speed, palette)
6311            // Uses bands written by the last fft_bands() call.
6312            "tex_freq_map" | "ลายความถี่" => {
6313                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6314                let time = self.arg_num(&args, 4, 0.0)? as f32;
6315                let speed = self.arg_num(&args, 5, 0.3)? as f32;
6316                let palette = self.arg_str(&args, 6, "rainbow");
6317                let bands: Vec<f32> = {
6318                    let c = self.fft_bands_cache.borrow();
6319                    if c.is_empty() {
6320                        vec![0.0; 32]
6321                    } else {
6322                        c.clone()
6323                    }
6324                };
6325                let n = bands.len().max(1);
6326                let mut gfx = self.gfx.borrow_mut();
6327                let (bw, bh) = (gfx.width, gfx.height);
6328                for row in 0..th {
6329                    for col in 0..tw {
6330                        let band_idx = (col * n / tw.max(1)).min(n - 1);
6331                        let mag = bands[band_idx].clamp(0., 1.);
6332                        let fill_y = (mag * th as f32) as usize;
6333                        if row >= th.saturating_sub(fill_y) {
6334                            let t = (col as f32 / tw as f32 + time * speed) % 1.0;
6335                            let [r, g, b] = tex_palette(&palette, t);
6336                            let bright = mag * (1.0 - row as f32 / th as f32 * 0.5);
6337                            let (dx, dy) = (tx + col, ty + row);
6338                            if dx < bw && dy < bh {
6339                                gfx.buffer[dy * bw + dx] =
6340                                    tex_rgb(r * bright, g * bright, b * bright);
6341                            }
6342                        }
6343                    }
6344                }
6345                return Ok(Value::Unit);
6346            },
6347
6348            // tex_spiral(x, y, w, h, freq, bands, time, palette)
6349            "tex_spiral" | "ลายเกลียวหมุน" => {
6350                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6351                let freq = self.arg_num(&args, 4, 5.0)? as f32;
6352                let n_bands = self.arg_num(&args, 5, 8.0)? as f32;
6353                let time = self.arg_num(&args, 6, 0.0)? as f32;
6354                let palette = self.arg_str(&args, 7, "rainbow");
6355                let mut gfx = self.gfx.borrow_mut();
6356                let (bw, bh) = (gfx.width, gfx.height);
6357                for row in 0..th {
6358                    for col in 0..tw {
6359                        let nx = col as f32 / tw as f32 - 0.5;
6360                        let ny = row as f32 / th as f32 - 0.5;
6361                        let r = (nx * nx + ny * ny).sqrt();
6362                        let theta = ny.atan2(nx);
6363                        let t = ((r * freq - theta / std::f32::consts::TAU + time * 0.5) * n_bands
6364                            % 1.0)
6365                            .abs();
6366                        let [cr, cg, cb] = tex_palette(&palette, t);
6367                        let (dx, dy) = (tx + col, ty + row);
6368                        if dx < bw && dy < bh {
6369                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
6370                        }
6371                    }
6372                }
6373                return Ok(Value::Unit);
6374            },
6375
6376            // tex_ripple(x, y, w, h, freq, cx, cy, time, palette)
6377            "tex_ripple" | "ลายระลอก" => {
6378                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6379                let freq = self.arg_num(&args, 4, 10.0)? as f32;
6380                let rcx = self.arg_num(&args, 5, 0.5)? as f32;
6381                let rcy = self.arg_num(&args, 6, 0.5)? as f32;
6382                let time = self.arg_num(&args, 7, 0.0)? as f32;
6383                let palette = self.arg_str(&args, 8, "ocean");
6384                let mut gfx = self.gfx.borrow_mut();
6385                let (bw, bh) = (gfx.width, gfx.height);
6386                for row in 0..th {
6387                    for col in 0..tw {
6388                        let nx = col as f32 / tw as f32 - rcx;
6389                        let ny = row as f32 / th as f32 - rcy;
6390                        let r = (nx * nx + ny * ny).sqrt();
6391                        let t = ((r * freq - time) % 1.0).abs();
6392                        let [cr, cg, cb] = tex_palette(&palette, t);
6393                        let (dx, dy) = (tx + col, ty + row);
6394                        if dx < bw && dy < bh {
6395                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
6396                        }
6397                    }
6398                }
6399                return Ok(Value::Unit);
6400            },
6401
6402            // tex_mandelbrot(x, y, w, h, zoom, cx, cy, max_iter, palette)
6403            "tex_mandelbrot" | "ลายแมนเดลบรอต" => {
6404                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6405                let zoom = self.arg_num(&args, 4, 1.0)?;
6406                let mcx = self.arg_num(&args, 5, -0.5)?;
6407                let mcy = self.arg_num(&args, 6, 0.0)?;
6408                let max_iter = self.arg_num(&args, 7, 64.0)? as u32;
6409                let palette = self.arg_str(&args, 8, "psychedelic");
6410                let mut gfx = self.gfx.borrow_mut();
6411                let (bw, bh) = (gfx.width, gfx.height);
6412                for row in 0..th {
6413                    for col in 0..tw {
6414                        let zx0 = (col as f64 / tw as f64 - 0.5) / zoom + mcx;
6415                        let zy0 = (row as f64 / th as f64 - 0.5) / zoom + mcy;
6416                        let mut x = 0.0f64;
6417                        let mut y = 0.0f64;
6418                        let mut i = 0u32;
6419                        while i < max_iter && x * x + y * y < 4.0 {
6420                            let t = x * x - y * y + zx0;
6421                            y = 2.0 * x * y + zy0;
6422                            x = t;
6423                            i += 1;
6424                        }
6425                        let t = if i == max_iter {
6426                            0.0f32
6427                        } else {
6428                            (i as f32
6429                                - (x as f32 * x as f32 + y as f32 * y as f32).ln().ln()
6430                                    / 2.0f32.ln())
6431                                / max_iter as f32
6432                        };
6433                        let [cr, cg, cb] = tex_palette(&palette, t.clamp(0., 1.));
6434                        let (dx, dy) = (tx + col, ty + row);
6435                        if dx < bw && dy < bh {
6436                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
6437                        }
6438                    }
6439                }
6440                return Ok(Value::Unit);
6441            },
6442
6443            // tex_julia(x, y, w, h, c_re, c_im, max_iter, palette)
6444            "tex_julia" | "ลายจูเลีย" => {
6445                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6446                let c_re = self.arg_num(&args, 4, -0.7)?;
6447                let c_im = self.arg_num(&args, 5, 0.27)?;
6448                let max_iter = self.arg_num(&args, 6, 64.0)? as u32;
6449                let palette = self.arg_str(&args, 7, "neon");
6450                let mut gfx = self.gfx.borrow_mut();
6451                let (bw, bh) = (gfx.width, gfx.height);
6452                for row in 0..th {
6453                    for col in 0..tw {
6454                        let mut zx = (col as f64 / tw as f64 - 0.5) * 3.5;
6455                        let mut zy = (row as f64 / th as f64 - 0.5) * 3.5;
6456                        let mut i = 0u32;
6457                        while i < max_iter && zx * zx + zy * zy < 4.0 {
6458                            let t = zx * zx - zy * zy + c_re;
6459                            zy = 2.0 * zx * zy + c_im;
6460                            zx = t;
6461                            i += 1;
6462                        }
6463                        let t = i as f32 / max_iter as f32;
6464                        let [cr, cg, cb] = tex_palette(&palette, t);
6465                        let (dx, dy) = (tx + col, ty + row);
6466                        if dx < bw && dy < bh {
6467                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
6468                        }
6469                    }
6470                }
6471                return Ok(Value::Unit);
6472            },
6473
6474            // tex_voronoi(x, y, w, h, cells, seed, palette)
6475            "tex_voronoi" | "ลายโวโรนอย" => {
6476                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6477                let cells = self.arg_num(&args, 4, 16.0)? as u32;
6478                let seed = self.arg_num(&args, 5, 42.0)? as u32;
6479                let palette = self.arg_str(&args, 6, "rainbow");
6480                let pts: Vec<[f32; 2]> = (0..cells)
6481                    .map(|i| {
6482                        [
6483                            tex_hash(i as i32, 0, seed),
6484                            tex_hash(i as i32, 1, seed + 999),
6485                        ]
6486                    })
6487                    .collect();
6488                let mut gfx = self.gfx.borrow_mut();
6489                let (bw, bh) = (gfx.width, gfx.height);
6490                for row in 0..th {
6491                    for col in 0..tw {
6492                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
6493                        let (min_d, nearest) = pts.iter().enumerate().fold(
6494                            (f32::MAX, 0usize),
6495                            |(d, idx), (i, &[cx, cy])| {
6496                                let dd = (fx - cx).powi(2) + (fy - cy).powi(2);
6497                                if dd < d {
6498                                    (dd, i)
6499                                } else {
6500                                    (d, idx)
6501                                }
6502                            },
6503                        );
6504                        let t = (nearest as f32 / cells as f32 + min_d * 4.0) % 1.0;
6505                        let [cr, cg, cb] = tex_palette(&palette, t);
6506                        let (dx, dy) = (tx + col, ty + row);
6507                        if dx < bw && dy < bh {
6508                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
6509                        }
6510                    }
6511                }
6512                return Ok(Value::Unit);
6513            },
6514
6515            // tex_halftone(x, y, w, h, dot_size, time, palette)
6516            "tex_halftone" | "ลายฮาล์ฟโทน" => {
6517                let (tx, ty, tw, th) = self.tex_rect(&args)?;
6518                let dot_size = self.arg_num(&args, 4, 0.05)? as f32;
6519                let time = self.arg_num(&args, 5, 0.0)? as f32;
6520                let palette = self.arg_str(&args, 6, "rainbow");
6521                let mut gfx = self.gfx.borrow_mut();
6522                let (bw, bh) = (gfx.width, gfx.height);
6523                for row in 0..th {
6524                    for col in 0..tw {
6525                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
6526                        let gx = (fx / dot_size).floor();
6527                        let gy = (fy / dot_size).floor();
6528                        let lx = (fx / dot_size - gx - 0.5) * 2.0;
6529                        let ly = (fy / dot_size - gy - 0.5) * 2.0;
6530                        let r = (lx * lx + ly * ly).sqrt();
6531                        let t = (gx / (1.0 / dot_size) + time * 0.1) % 1.0;
6532                        let a = if r < 0.7 {
6533                            ((0.7 - r) / 0.7).clamp(0., 1.)
6534                        } else {
6535                            0.0
6536                        };
6537                        if a > 0.0 {
6538                            let [cr, cg, cb] = tex_palette(&palette, t);
6539                            let (dx, dy) = (tx + col, ty + row);
6540                            if dx < bw && dy < bh {
6541                                gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
6542                            }
6543                        }
6544                    }
6545                }
6546                return Ok(Value::Unit);
6547            },
6548
6549            // ══════════════════════════════════════════════════════════════════
6550            // RENDER / LIGHTING MODES  (holographic cel shading)
6551            // ══════════════════════════════════════════════════════════════════
6552            // set_shade_mode(m) — 0 flat · 1 cel · 2 holo (default)
6553            "set_shade_mode" | "设置着色" | "シェード設定" | "셰이드모드" | "ตั้งการแรเงา" =>
6554            {
6555                let m = self.arg_num(&args, 0, 2.0)? as u8;
6556                self.gfx.borrow_mut().shade_mode = m;
6557                return Ok(Value::Unit);
6558            },
6559            // set_cel_bands(n) — number of posterisation bands (>=2)
6560            "set_cel_bands" | "设置色阶" | "セル段数" | "셀밴드" | "ตั้งระดับสี" =>
6561            {
6562                let n = (self.arg_num(&args, 0, 4.0)? as u32).max(2);
6563                self.gfx.borrow_mut().shade.bands = n;
6564                return Ok(Value::Unit);
6565            },
6566            // set_shadow_color(r,g,b) — coloured-shadow tint, 0-255
6567            "set_shadow_color" | "设置阴影色" | "影の色" | "그림자색" | "ตั้งสีเงา" =>
6568            {
6569                let r = self.arg_num(&args, 0, 26.)? as f32 / 255.0;
6570                let g = self.arg_num(&args, 1, 33.)? as f32 / 255.0;
6571                let b = self.arg_num(&args, 2, 77.)? as f32 / 255.0;
6572                self.gfx.borrow_mut().shade.shadow = [r, g, b];
6573                return Ok(Value::Unit);
6574            },
6575            // set_rim(strength, r,g,b) — holographic fresnel edge glow
6576            // ══════════════════════════════════════════════════════════════════
6577            // CRYPTOGRAPHY (ling-crypto) — geo suite, hybrid PQ KEM, holographic
6578            // Bytes cross the language boundary as lowercase hex strings.
6579            // ══════════════════════════════════════════════════════════════════
6580            #[cfg(not(target_arch = "wasm32"))]
6581            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" =>
6582            {
6583                let s = self.arg_str(&args, 0, "");
6584                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
6585                    s.as_bytes(),
6586                ))));
6587            },
6588            #[cfg(target_arch = "wasm32")]
6589            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" =>
6590            {
6591                let s = self.arg_str(&args, 0, "");
6592                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
6593                    s.as_bytes(),
6594                ))));
6595            },
6596            // 3-D torus-knot fingerprint of any text/key → flat [x,y,z, x,y,z, …]
6597            #[cfg(not(target_arch = "wasm32"))]
6598            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" => {
6599                let s = self.arg_str(&args, 0, "");
6600                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
6601                let mut out = Vec::with_capacity(shape.points.len() * 3);
6602                for p in &shape.points {
6603                    out.push(Value::Number(p[0] as f64));
6604                    out.push(Value::Number(p[1] as f64));
6605                    out.push(Value::Number(p[2] as f64));
6606                }
6607                return Ok(Value::List(Rc::new(out)));
6608            },
6609            #[cfg(target_arch = "wasm32")]
6610            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" => {
6611                let s = self.arg_str(&args, 0, "");
6612                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
6613                let mut out = Vec::with_capacity(shape.points.len() * 3);
6614                for p in &shape.points {
6615                    out.push(Value::Number(p[0] as f64));
6616                    out.push(Value::Number(p[1] as f64));
6617                    out.push(Value::Number(p[2] as f64));
6618                }
6619                return Ok(Value::List(out));
6620            },
6621            #[cfg(not(target_arch = "wasm32"))]
6622            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" =>
6623            {
6624                let s = self.arg_str(&args, 0, "");
6625                return Ok(Value::Str(
6626                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
6627                ));
6628            },
6629            #[cfg(target_arch = "wasm32")]
6630            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" =>
6631            {
6632                let s = self.arg_str(&args, 0, "");
6633                return Ok(Value::Str(
6634                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
6635                ));
6636            },
6637            // KEM keypair (hybrid X25519+ML-KEM-768) → integer handle
6638            #[cfg(not(target_arch = "wasm32"))]
6639            "knot_keygen" | "hybrid_keygen" | "สร้างกุญแจปม" | "生成密钥" | "鍵生成" | "키생성" =>
6640            {
6641                self.crypto_ids.push(ling_crypto::KnotIdentity::generate());
6642                return Ok(Value::Number((self.crypto_ids.len() - 1) as f64));
6643            },
6644            #[cfg(not(target_arch = "wasm32"))]
6645            "knot_public" | "hybrid_public" | "กุญแจสาธารณะปม" | "公钥" | "公開鍵" | "공개키" =>
6646            {
6647                let h = self.arg_num(&args, 0, 0.0)? as usize;
6648                let pk = self
6649                    .crypto_ids
6650                    .get(h)
6651                    .map(|id| hex_encode(id.public_key()))
6652                    .unwrap_or_default();
6653                return Ok(Value::Str(pk));
6654            },
6655            // encapsulate(pubkey_hex) → [ciphertext_hex, shared_secret_hex]
6656            #[cfg(not(target_arch = "wasm32"))]
6657            "knot_encapsulate"
6658            | "hybrid_encapsulate"
6659            | "ห่อกุญแจปม"
6660            | "封装密钥"
6661            | "カプセル化"
6662            | "캡슐화" => {
6663                let pk = hex_decode(&self.arg_str(&args, 0, ""));
6664                match ling_crypto::geo::knot_encapsulate(&pk) {
6665                    Ok((ct, ss)) => {
6666                        return Ok(Value::List(Rc::new(vec![
6667                            Value::Str(hex_encode(&ct)),
6668                            Value::Str(hex_encode(&ss)),
6669                        ])))
6670                    },
6671                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
6672                }
6673            },
6674            // decapsulate(handle, ciphertext_hex) → shared_secret_hex
6675            #[cfg(not(target_arch = "wasm32"))]
6676            "knot_decapsulate"
6677            | "hybrid_decapsulate"
6678            | "แกะกุญแจปม"
6679            | "解封装密钥"
6680            | "カプセル解除"
6681            | "캡슐해제" => {
6682                let h = self.arg_num(&args, 0, 0.0)? as usize;
6683                let ct = hex_decode(&self.arg_str(&args, 1, ""));
6684                let ss = self
6685                    .crypto_ids
6686                    .get(h)
6687                    .and_then(|id| id.decapsulate(&ct).ok())
6688                    .map(|s| hex_encode(&s))
6689                    .unwrap_or_default();
6690                return Ok(Value::Str(ss));
6691            },
6692            // Authenticated encryption (XChaCha20-Poly1305) — seal(key_hex, text) → ct_hex
6693            #[cfg(not(target_arch = "wasm32"))]
6694            "crypto_seal" | "ผนึก" | "封印" | "封印する" | "봉인" => {
6695                let key = hex_to_32(&self.arg_str(&args, 0, ""));
6696                let pt = self.arg_str(&args, 1, "");
6697                match ling_crypto::geo::holo_seal(key, pt.as_bytes()) {
6698                    Ok(ct) => return Ok(Value::Str(hex_encode(&ct))),
6699                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
6700                }
6701            },
6702            #[cfg(not(target_arch = "wasm32"))]
6703            "crypto_open" | "เปิดผนึก" | "解封" | "封印解除" | "봉인해제" =>
6704            {
6705                let key = hex_to_32(&self.arg_str(&args, 0, ""));
6706                let ct = hex_decode(&self.arg_str(&args, 1, ""));
6707                match ling_crypto::geo::holo_open(key, &ct) {
6708                    Ok(pt) => return Ok(Value::Str(String::from_utf8_lossy(&pt).into_owned())),
6709                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
6710                }
6711            },
6712            // Holographic all-or-nothing transform — 4-D fragment coords [a,b,c,d, …]
6713            #[cfg(not(target_arch = "wasm32"))]
6714            "holo_points" | "จุดโฮโลแกรม" | "全息点" | "ホログラム点" | "홀로그램점" =>
6715            {
6716                let s = self.arg_str(&args, 0, "");
6717                let frags = ling_crypto::geo::scatter(s.as_bytes());
6718                let mut out = Vec::with_capacity(frags.len() * 4);
6719                for f in &frags {
6720                    for c in f.coord {
6721                        out.push(Value::Number(c as f64));
6722                    }
6723                }
6724                return Ok(Value::List(Rc::new(out)));
6725            },
6726            #[cfg(not(target_arch = "wasm32"))]
6727            "holo_fragment_count"
6728            | "จำนวนชิ้นโฮโลแกรม"
6729            | "全息碎片数"
6730            | "ホログラム断片数"
6731            | "홀로그램조각수" => {
6732                let s = self.arg_str(&args, 0, "");
6733                return Ok(Value::Number(
6734                    ling_crypto::geo::scatter(s.as_bytes()).len() as f64
6735                ));
6736            },
6737
6738            // ══════════════════════════════════════════════════════════════════
6739            // ling-ui — animation easings + holographic vector widgets + text I/O
6740            // ══════════════════════════════════════════════════════════════════
6741            "ease" => {
6742                let name = self.arg_str(&args, 0, "ease");
6743                let t = self.arg_num(&args, 1, 0.0)? as f32;
6744                return Ok(Value::Number(
6745                    ling_ui::Easing::from_name(&name).apply(t) as f64
6746                ));
6747            },
6748
6749            // ══════════════════════════════════════════════════════════════════
6750            // Anima — unified animation drivers (ling-animation). Organic 灵 +
6751            // mechanical 机 scalar drivers, callable per frame from a script.
6752            // ══════════════════════════════════════════════════════════════════
6753            "tween" | "补间" | "補間" | "트윈" | "แทรกค่า" => {
6754                let a = self.arg_num(&args, 0, 0.0)?;
6755                let b = self.arg_num(&args, 1, 0.0)?;
6756                let t = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 1.0);
6757                return Ok(Value::Number(a + (b - a) * t));
6758            },
6759            "tween_ease" | "缓动补间" | "緩和補間" | "이징트윈" | "แทรกนุ่ม" =>
6760            {
6761                let a = self.arg_num(&args, 0, 0.0)? as f32;
6762                let b = self.arg_num(&args, 1, 0.0)? as f32;
6763                let t = self.arg_num(&args, 2, 0.0)? as f32;
6764                let kind = self.arg_str(&args, 3, "linear");
6765                let e = ling_animation::EaseFunction::from_name(&kind);
6766                return Ok(Value::Number(
6767                    ling_animation::ease::tween_ease(&a, &b, t, e) as f64,
6768                ));
6769            },
6770            // ── Organic 灵 ──
6771            "breathe" | "呼吸" | "호흡" | "หายใจ" => {
6772                let t = self.arg_num(&args, 0, 0.0)? as f32;
6773                let rate = self.arg_num(&args, 1, 1.0)? as f32;
6774                let depth = self.arg_num(&args, 2, 0.1)? as f32;
6775                return Ok(Value::Number(
6776                    ling_animation::scalar::breathe(t, rate, depth) as f64,
6777                ));
6778            },
6779            "wobble" | "摆动" | "揺れ" | "흔들림" | "โยก" => {
6780                let t = self.arg_num(&args, 0, 0.0)? as f32;
6781                let freq = self.arg_num(&args, 1, 1.0)? as f32;
6782                let amp = self.arg_num(&args, 2, 1.0)? as f32;
6783                let phase = self.arg_num(&args, 3, 0.0)? as f32;
6784                return Ok(Value::Number(
6785                    ling_animation::scalar::wobble(t, freq, amp, phase) as f64,
6786                ));
6787            },
6788            "gait_phase" | "步相" | "歩相" | "걸음위상" | "เฟสก้าว" => {
6789                let t = self.arg_num(&args, 0, 0.0)? as f32;
6790                let speed = self.arg_num(&args, 1, 1.0)? as f32;
6791                return Ok(Value::Number(
6792                    ling_animation::scalar::gait_phase(t, speed) as f64
6793                ));
6794            },
6795            "gait_swing" | "步摆" | "歩振り" | "걸음흔들" | "ก้าวแกว่ง" =>
6796            {
6797                let t = self.arg_num(&args, 0, 0.0)? as f32;
6798                let speed = self.arg_num(&args, 1, 1.0)? as f32;
6799                let stride = self.arg_num(&args, 2, 1.0)? as f32;
6800                return Ok(Value::Number(
6801                    ling_animation::scalar::gait_swing(t, speed, stride) as f64,
6802                ));
6803            },
6804            "gait_lift" | "抬脚" | "足上げ" | "발들기" | "ยกเท้า" => {
6805                let t = self.arg_num(&args, 0, 0.0)? as f32;
6806                let speed = self.arg_num(&args, 1, 1.0)? as f32;
6807                let height = self.arg_num(&args, 2, 1.0)? as f32;
6808                return Ok(Value::Number(
6809                    ling_animation::scalar::gait_lift(t, speed, height) as f64,
6810                ));
6811            },
6812            "spring_to" | "弹向" | "バネ寄せ" | "스프링이동" | "สปริงไป" =>
6813            {
6814                let pos = self.arg_num(&args, 0, 0.0)? as f32;
6815                let vel = self.arg_num(&args, 1, 0.0)? as f32;
6816                let target = self.arg_num(&args, 2, 0.0)? as f32;
6817                let stiffness = self.arg_num(&args, 3, 120.0)? as f32;
6818                let damping = self.arg_num(&args, 4, 14.0)? as f32;
6819                let dt = self.arg_num(&args, 5, 1.0 / 60.0)? as f32;
6820                let (np, nv) =
6821                    ling_animation::scalar::spring_step(pos, vel, target, stiffness, damping, dt);
6822                return Ok(Value::List(Rc::new(vec![
6823                    Value::Number(np as f64),
6824                    Value::Number(nv as f64),
6825                ])));
6826            },
6827            "ik2" | "反解" | "逆運動" | "역운동" | "ไอเค2" => {
6828                let l1 = self.arg_num(&args, 0, 1.0)? as f32;
6829                let l2 = self.arg_num(&args, 1, 1.0)? as f32;
6830                let tx = self.arg_num(&args, 2, 0.0)? as f32;
6831                let ty = self.arg_num(&args, 3, 0.0)? as f32;
6832                let (sh, el) = ling_animation::scalar::two_bone_ik(l1, l2, tx, ty);
6833                return Ok(Value::List(Rc::new(vec![
6834                    Value::Number(sh as f64),
6835                    Value::Number(el as f64),
6836                ])));
6837            },
6838            // ── Mechanical 机 ──
6839            "gear_couple" | "齿轮联动" | "歯車連動" | "기어연동" | "เฟืองทด" =>
6840            {
6841                let angle = self.arg_num(&args, 0, 0.0)? as f32;
6842                let ti = self.arg_num(&args, 1, 1.0)? as f32;
6843                let to = self.arg_num(&args, 2, 1.0)? as f32;
6844                return Ok(Value::Number(
6845                    ling_animation::scalar::gear(angle, ti, to) as f64
6846                ));
6847            },
6848            "gear_train" | "齿轮组" | "歯車列" | "기어열" | "ชุดเฟือง" => {
6849                let angle = self.arg_num(&args, 0, 0.0)? as f32;
6850                let teeth: Vec<f32> = match args.get(1) {
6851                    Some(Value::List(items)) => items
6852                        .iter()
6853                        .filter_map(|v| {
6854                            if let Value::Number(n) = v {
6855                                Some(*n as f32)
6856                            } else {
6857                                None
6858                            }
6859                        })
6860                        .collect(),
6861                    _ => Vec::new(),
6862                };
6863                let out = ling_animation::mechanism::gear_train(angle, &teeth);
6864                return Ok(Value::List(Rc::new(
6865                    out.into_iter().map(|a| Value::Number(a as f64)).collect(),
6866                )));
6867            },
6868            "cam_lift" | "凸轮升程" | "カム揚程" | "캠리프트" | "ยกลูกเบี้ยว" =>
6869            {
6870                let angle = self.arg_num(&args, 0, 0.0)? as f32;
6871                let lift = self.arg_num(&args, 1, 1.0)? as f32;
6872                return Ok(Value::Number(
6873                    ling_animation::scalar::cam_lift(angle, lift) as f64
6874                ));
6875            },
6876            "piston" | "活塞" | "ピストン" | "피스톤" | "ลูกสูบ" => {
6877                let angle = self.arg_num(&args, 0, 0.0)? as f32;
6878                let crank = self.arg_num(&args, 1, 1.0)? as f32;
6879                let rod = self.arg_num(&args, 2, 2.0)? as f32;
6880                return Ok(Value::Number(
6881                    ling_animation::scalar::piston(angle, crank, rod) as f64,
6882                ));
6883            },
6884            "rack" | "齿条" | "ラック" | "랙" | "แร็ค" => {
6885                let angle = self.arg_num(&args, 0, 0.0)? as f32;
6886                let radius = self.arg_num(&args, 1, 1.0)? as f32;
6887                return Ok(Value::Number(
6888                    ling_animation::scalar::rack(angle, radius) as f64
6889                ));
6890            },
6891            #[cfg(not(target_arch = "wasm32"))]
6892            "mouse_x" => {
6893                let gfx = self.gfx.borrow();
6894                let v = gfx
6895                    .window
6896                    .as_ref()
6897                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
6898                    .map(|p| p.0 as f64)
6899                    .unwrap_or(0.0);
6900                return Ok(Value::Number(v));
6901            },
6902            #[cfg(target_arch = "wasm32")]
6903            "mouse_x" => {
6904                return Ok(Value::Number(0.0));
6905            },
6906            #[cfg(not(target_arch = "wasm32"))]
6907            "mouse_y" => {
6908                let gfx = self.gfx.borrow();
6909                let v = gfx
6910                    .window
6911                    .as_ref()
6912                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
6913                    .map(|p| p.1 as f64)
6914                    .unwrap_or(0.0);
6915                return Ok(Value::Number(v));
6916            },
6917            #[cfg(target_arch = "wasm32")]
6918            "mouse_y" => {
6919                return Ok(Value::Number(0.0));
6920            },
6921            #[cfg(not(target_arch = "wasm32"))]
6922            "mouse_down" => {
6923                let gfx = self.gfx.borrow();
6924                let d = gfx
6925                    .window
6926                    .as_ref()
6927                    .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
6928                    .unwrap_or(false);
6929                return Ok(Value::Bool(d));
6930            },
6931            #[cfg(target_arch = "wasm32")]
6932            "mouse_down" => {
6933                return Ok(Value::Bool(false));
6934            },
6935            #[cfg(not(target_arch = "wasm32"))]
6936            "mouse_down_right" | "เมาส์ขวา" => {
6937                let gfx = self.gfx.borrow();
6938                let d = gfx
6939                    .window
6940                    .as_ref()
6941                    .map(|w| w.get_mouse_down(minifb::MouseButton::Right))
6942                    .unwrap_or(false);
6943                return Ok(Value::Bool(d));
6944            },
6945            #[cfg(target_arch = "wasm32")]
6946            "mouse_down_right" | "เมาส์ขวา" => {
6947                return Ok(Value::Bool(false));
6948            },
6949            #[cfg(not(target_arch = "wasm32"))]
6950            "mouse_down_middle" | "เมาส์กลาง" => {
6951                let gfx = self.gfx.borrow();
6952                let d = gfx
6953                    .window
6954                    .as_ref()
6955                    .map(|w| w.get_mouse_down(minifb::MouseButton::Middle))
6956                    .unwrap_or(false);
6957                return Ok(Value::Bool(d));
6958            },
6959            #[cfg(target_arch = "wasm32")]
6960            "mouse_down_middle" | "เมาส์กลาง" => {
6961                return Ok(Value::Bool(false));
6962            },
6963            #[cfg(not(target_arch = "wasm32"))]
6964            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" =>
6965            {
6966                let x = self.arg_num(&args, 0, 0.0)? as f32;
6967                let y = self.arg_num(&args, 1, 0.0)? as f32;
6968                let w = self.arg_num(&args, 2, 0.0)? as f32;
6969                let h = self.arg_num(&args, 3, 0.0)? as f32;
6970                let gfx = self.gfx.borrow();
6971                let (mx, my) = gfx
6972                    .window
6973                    .as_ref()
6974                    .and_then(|win| win.get_mouse_pos(minifb::MouseMode::Clamp))
6975                    .unwrap_or((0.0, 0.0));
6976                return Ok(Value::Bool(ling_ui::holo::hit_rect(mx, my, x, y, w, h)));
6977            },
6978            #[cfg(target_arch = "wasm32")]
6979            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" =>
6980            {
6981                return Ok(Value::Bool(false));
6982            },
6983            // ui_text(x, y, scale, "string") — holographic vector text
6984            "ui_text" | "界面文字" | "UI文字" | "UI텍스트" | "ข้อความหน้าจอ" =>
6985            {
6986                let x = self.arg_num(&args, 0, 0.0)? as f32;
6987                let y = self.arg_num(&args, 1, 0.0)? as f32;
6988                let scale = self.arg_num(&args, 2, 16.0)? as f32;
6989                let s = self.arg_str(&args, 3, "");
6990                let segs = ling_ui::holo::text_lines(&s, x, y, scale * 0.62, scale, scale * 0.24);
6991                let mut gfx = self.gfx.borrow_mut();
6992                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
6993                for sg in segs {
6994                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
6995                }
6996                return Ok(Value::Unit);
6997            },
6998            // font_load("path.ttf") — load a vector font (outlines cached lazily as
6999            // cache/fonts/<stem>/<codepoint>.ling). Returns a handle, or -1 on failure.
7000            #[cfg(not(target_arch = "wasm32"))]
7001            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" =>
7002            {
7003                let path = self.arg_str(&args, 0, "");
7004                // Optional 2nd arg: variable-font weight (e.g. 600 for a solid, bold UI).
7005                let weight = match self.arg_num(&args, 1, 0.0)? {
7006                    w if w > 0.0 => Some(w as f32),
7007                    _ => None,
7008                };
7009                // Try the path as given, then relative to the script's directory.
7010                let mut loaded = ling_graphics::VectorFont::from_path_weight(&path, weight);
7011                if loaded.is_err() {
7012                    if let Some(dir) = &self.source_dir {
7013                        let joined = dir.join(&path);
7014                        loaded = ling_graphics::VectorFont::from_path_weight(
7015                            &joined.to_string_lossy(),
7016                            weight,
7017                        );
7018                    }
7019                }
7020                match loaded {
7021                    Ok(f) => {
7022                        let id = self.fonts.len();
7023                        self.fonts.push(f);
7024                        return Ok(Value::Number(id as f64));
7025                    },
7026                    Err(e) => {
7027                        eprintln!("font_load failed ({path}): {e}");
7028                        return Ok(Value::Number(-1.0));
7029                    },
7030                }
7031            },
7032            #[cfg(target_arch = "wasm32")]
7033            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" =>
7034            {
7035                // Web runtime does not load host TTF/OTF files yet.
7036                // Return -1 so scripts can fall back to ui_text.
7037                return Ok(Value::Number(-1.0));
7038            },
7039            // font_text(handle, x, y, px, "string") — anti-aliased *stroked* vector outline
7040            // in the current set_color / set_blend. (x,y) is the text box top-left.
7041            #[cfg(not(target_arch = "wasm32"))]
7042            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" =>
7043            {
7044                let id = self.arg_num(&args, 0, 0.0)? as i64;
7045                let x = self.arg_num(&args, 1, 0.0)? as f32;
7046                let y = self.arg_num(&args, 2, 0.0)? as f32;
7047                let px = self.arg_num(&args, 3, 16.0)? as f32;
7048                let s = self.arg_str(&args, 4, "");
7049                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
7050                    let strokes = self.font_layout_2d(id as usize, x, y, px, &s);
7051                    let mut gfx = self.gfx.borrow_mut();
7052                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
7053                    for pl in &strokes {
7054                        for seg in pl.windows(2) {
7055                            crate::gfx::raster::draw_line_aa(
7056                                &mut gfx.buffer,
7057                                w,
7058                                h,
7059                                color,
7060                                add,
7061                                seg[0][0],
7062                                seg[0][1],
7063                                seg[1][0],
7064                                seg[1][1],
7065                            );
7066                        }
7067                    }
7068                }
7069                return Ok(Value::Unit);
7070            },
7071            #[cfg(target_arch = "wasm32")]
7072            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" =>
7073            {
7074                return Ok(Value::Unit);
7075            },
7076            // font_text_fill(handle, x, y, px, "string") — anti-aliased *filled* vector glyphs.
7077            #[cfg(not(target_arch = "wasm32"))]
7078            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" =>
7079            {
7080                let id = self.arg_num(&args, 0, 0.0)? as i64;
7081                let x = self.arg_num(&args, 1, 0.0)? as f32;
7082                let y = self.arg_num(&args, 2, 0.0)? as f32;
7083                let px = self.arg_num(&args, 3, 16.0)? as f32;
7084                let s = self.arg_str(&args, 4, "");
7085                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
7086                    // fill each glyph independently so interior holes (winding) stay correct
7087                    let glyphs = self.font_layout_2d_glyphs(id as usize, x, y, px, &s);
7088                    let mut gfx = self.gfx.borrow_mut();
7089                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
7090                    for contours in &glyphs {
7091                        crate::gfx::raster::fill_contours_aa(
7092                            &mut gfx.buffer,
7093                            w,
7094                            h,
7095                            color,
7096                            add,
7097                            contours,
7098                        );
7099                    }
7100                }
7101                return Ok(Value::Unit);
7102            },
7103            #[cfg(target_arch = "wasm32")]
7104            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" =>
7105            {
7106                return Ok(Value::Unit);
7107            },
7108            // font_text_3d(handle, cx,cy,cz, ux,uy,uz, vx,vy,vz, size, "string")
7109            // — stroked vector text on a 3D plane: u = advance dir, v = up dir, size = world/em.
7110            //   Flows through the depth-sorted line pipeline, so it rotates with the camera (and 4D).
7111            #[cfg(not(target_arch = "wasm32"))]
7112            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" =>
7113            {
7114                let id = self.arg_num(&args, 0, 0.0)? as i64;
7115                let cx = self.arg_num(&args, 1, 0.0)? as f32;
7116                let cy = self.arg_num(&args, 2, 0.0)? as f32;
7117                let cz = self.arg_num(&args, 3, 0.0)? as f32;
7118                let ux = self.arg_num(&args, 4, 1.0)? as f32;
7119                let uy = self.arg_num(&args, 5, 0.0)? as f32;
7120                let uz = self.arg_num(&args, 6, 0.0)? as f32;
7121                let vx = self.arg_num(&args, 7, 0.0)? as f32;
7122                let vy = self.arg_num(&args, 8, 1.0)? as f32;
7123                let vz = self.arg_num(&args, 9, 0.0)? as f32;
7124                let size = self.arg_num(&args, 10, 1.0)? as f32;
7125                let s = self.arg_str(&args, 11, "");
7126                if id >= 0 && (id as usize) < self.fonts.len() && size > 0.0 {
7127                    // Build world-space polylines: world = C + (pen+ex)*size*U + ey*size*V
7128                    let font = &mut self.fonts[id as usize];
7129                    let asc = font.ascent();
7130                    let mut pen = 0.0f32;
7131                    let mut lines: Vec<[f32; 6]> = Vec::new();
7132                    for ch in s.chars() {
7133                        let go = font.glyph_outline(ch, 0.01);
7134                        for pl in &go.polylines {
7135                            for seg in pl.windows(2) {
7136                                let map = |p: [f32; 2]| {
7137                                    let a = pen + p[0];
7138                                    let b = p[1] - asc; // shift so the top of the cap sits near C
7139                                    [
7140                                        cx + a * size * ux + b * size * vx,
7141                                        cy + a * size * uy + b * size * vy,
7142                                        cz + a * size * uz + b * size * vz,
7143                                    ]
7144                                };
7145                                let p0 = map(seg[0]);
7146                                let p1 = map(seg[1]);
7147                                lines.push([p0[0], p0[1], p0[2], p1[0], p1[1], p1[2]]);
7148                            }
7149                        }
7150                        pen += go.advance;
7151                    }
7152                    let mut gfx = self.gfx.borrow_mut();
7153                    let color = gfx.color;
7154                    let near = -gfx.camera.zdist + 0.05;
7155                    for l in &lines {
7156                        let (mut ax, mut ay, mut az) = (l[0], l[1], l[2]);
7157                        let (mut bx, mut by, mut bz) = (l[3], l[4], l[5]);
7158                        let da = gfx.camera.depth(ax, ay, az);
7159                        let db = gfx.camera.depth(bx, by, bz);
7160                        if da <= near && db <= near {
7161                            continue;
7162                        }
7163                        if da <= near {
7164                            let t = (near - da) / (db - da);
7165                            ax += t * (bx - ax);
7166                            ay += t * (by - ay);
7167                            az += t * (bz - az);
7168                        } else if db <= near {
7169                            let t = (near - da) / (db - da);
7170                            bx = ax + t * (bx - ax);
7171                            by = ay + t * (by - ay);
7172                            bz = az + t * (bz - az);
7173                        }
7174                        let (sax, say, da2) = gfx.camera.project(ax, ay, az);
7175                        let (sbx, sby, db2) = gfx.camera.project(bx, by, bz);
7176                        let depth = (da2 + db2) / 2.0;
7177                        gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
7178                    }
7179                }
7180                return Ok(Value::Unit);
7181            },
7182            #[cfg(target_arch = "wasm32")]
7183            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" =>
7184            {
7185                return Ok(Value::Unit);
7186            },
7187            // font_width(handle, px, "string") — pixel width of a string in a loaded font.
7188            #[cfg(not(target_arch = "wasm32"))]
7189            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" =>
7190            {
7191                let id = self.arg_num(&args, 0, 0.0)? as i64;
7192                let px = self.arg_num(&args, 1, 16.0)? as f32;
7193                let s = self.arg_str(&args, 2, "");
7194                if id >= 0 && (id as usize) < self.fonts.len() {
7195                    return Ok(Value::Number(self.fonts[id as usize].measure(&s, px) as f64));
7196                }
7197                return Ok(Value::Number(0.0));
7198            },
7199            #[cfg(target_arch = "wasm32")]
7200            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" =>
7201            {
7202                return Ok(Value::Number(0.0));
7203            },
7204            // ui_frame(x,y,w,h, bracketLen) — sci-fi corner brackets
7205            "ui_frame" | "边框" | "フレーム枠" | "프레임틀" | "กรอบ" => {
7206                let x = self.arg_num(&args, 0, 0.0)? as f32;
7207                let y = self.arg_num(&args, 1, 0.0)? as f32;
7208                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
7209                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
7210                let l = self.arg_num(&args, 4, 14.0)? as f32;
7211                let segs = ling_ui::holo::corner_brackets(x, y, w0, h0, l);
7212                let mut gfx = self.gfx.borrow_mut();
7213                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
7214                for sg in segs {
7215                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
7216                }
7217                return Ok(Value::Unit);
7218            },
7219            // ui_bevel(x,y,w,h, bevel) — beveled holographic panel outline
7220            "ui_bevel" | "斜角框" | "ベベル枠" | "베벨틀" | "กรอบเฉียง" =>
7221            {
7222                let x = self.arg_num(&args, 0, 0.0)? as f32;
7223                let y = self.arg_num(&args, 1, 0.0)? as f32;
7224                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
7225                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
7226                let bv = self.arg_num(&args, 4, 10.0)? as f32;
7227                let segs = ling_ui::holo::beveled_rect(x, y, w0, h0, bv);
7228                let mut gfx = self.gfx.borrow_mut();
7229                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
7230                for sg in segs {
7231                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
7232                }
7233                return Ok(Value::Unit);
7234            },
7235
7236            // ══════════════════════════════════════════════════════════════════
7237            // VECTOR UI TOOLKIT  (crates/ling-ui/src/widgets.rs)
7238            // All widgets are vector + theme-coloured with an optional trailing
7239            // r,g,b override; interactive ones read the mouse and return state.
7240            // ══════════════════════════════════════════════════════════════════
7241            #[cfg(not(target_arch = "wasm32"))]
7242            "ui_theme" | "界面主题" | "UIテーマ" | "인터페이스테마" | "ธีมส่วนติดต่อ" =>
7243            {
7244                let cur = self.ui_theme;
7245                let primary = self.color_at(&args, 0, cur.primary);
7246                let accent = self.color_at(&args, 3, cur.accent);
7247                let track = self.color_at(&args, 6, cur.track);
7248                let warn = self.color_at(&args, 9, cur.warn);
7249                let text = self.color_at(&args, 12, cur.text);
7250                let bg = self.color_at(&args, 15, cur.bg);
7251                self.ui_theme = UiTheme { primary, accent, track, warn, text, bg };
7252                return Ok(Value::Unit);
7253            },
7254
7255            // ── HUD ──────────────────────────────────────────────────────────
7256            #[cfg(not(target_arch = "wasm32"))]
7257            "ui_radar" | "雷达" | "レーダー" | "레이더" | "เรดาร์" => {
7258                let cx = self.arg_num(&args, 0, 0.)? as f32;
7259                let cy = self.arg_num(&args, 1, 0.)? as f32;
7260                let r = self.arg_num(&args, 2, 60.)? as f32;
7261                let sweep = self.arg_num(&args, 3, 0.)? as f32;
7262                let th = self.ui_theme;
7263                let prim = self.color_at(&args, 4, th.primary);
7264                self.draw_ui(&ling_ui::widgets::radar(
7265                    cx, cy, r, sweep, prim, th.accent, th.track,
7266                ));
7267                return Ok(Value::Unit);
7268            },
7269            #[cfg(not(target_arch = "wasm32"))]
7270            "ui_compass" | "罗盘" | "コンパス" | "나침반" | "เข็มทิศ" => {
7271                let x = self.arg_num(&args, 0, 0.)? as f32;
7272                let y = self.arg_num(&args, 1, 0.)? as f32;
7273                let w0 = self.arg_num(&args, 2, 300.)? as f32;
7274                let h0 = self.arg_num(&args, 3, 24.)? as f32;
7275                let head = self.arg_num(&args, 4, 0.)? as f32;
7276                let th = self.ui_theme;
7277                let prim = self.color_at(&args, 5, th.primary);
7278                self.draw_ui(&ling_ui::widgets::compass(
7279                    x, y, w0, h0, head, prim, th.track,
7280                ));
7281                return Ok(Value::Unit);
7282            },
7283            #[cfg(not(target_arch = "wasm32"))]
7284            "ui_reticle" | "准星" | "照準" | "조준선" | "เป้าเล็ง" => {
7285                let cx = self.arg_num(&args, 0, 0.)? as f32;
7286                let cy = self.arg_num(&args, 1, 0.)? as f32;
7287                let r = self.arg_num(&args, 2, 30.)? as f32;
7288                let spread = self.arg_num(&args, 3, 0.)? as f32;
7289                let th = self.ui_theme;
7290                let prim = self.color_at(&args, 4, th.primary);
7291                self.draw_ui(&ling_ui::widgets::reticle(cx, cy, r, spread, prim));
7292                return Ok(Value::Unit);
7293            },
7294            #[cfg(not(target_arch = "wasm32"))]
7295            "ui_target" | "锁定框" | "ターゲット" | "표적" | "กรอบเป้า" =>
7296            {
7297                let x = self.arg_num(&args, 0, 0.)? as f32;
7298                let y = self.arg_num(&args, 1, 0.)? as f32;
7299                let w0 = self.arg_num(&args, 2, 80.)? as f32;
7300                let h0 = self.arg_num(&args, 3, 80.)? as f32;
7301                let lock = self.arg_num(&args, 4, 0.)? as f32;
7302                let th = self.ui_theme;
7303                let prim = self.color_at(&args, 5, th.primary);
7304                self.draw_ui(&ling_ui::widgets::target(
7305                    x, y, w0, h0, lock, prim, th.accent,
7306                ));
7307                return Ok(Value::Unit);
7308            },
7309            #[cfg(not(target_arch = "wasm32"))]
7310            "ui_panel" | "面板" | "パネル" | "패널" | "แผง" => {
7311                let x = self.arg_num(&args, 0, 0.)? as f32;
7312                let y = self.arg_num(&args, 1, 0.)? as f32;
7313                let w0 = self.arg_num(&args, 2, 200.)? as f32;
7314                let h0 = self.arg_num(&args, 3, 120.)? as f32;
7315                let bv = self.arg_num(&args, 4, 12.)? as f32;
7316                let th = self.ui_theme;
7317                let prim = self.color_at(&args, 5, th.primary);
7318                self.draw_ui(&ling_ui::widgets::panel(x, y, w0, h0, bv, prim, th.bg));
7319                return Ok(Value::Unit);
7320            },
7321            #[cfg(not(target_arch = "wasm32"))]
7322            "ui_scanlines" | "扫描线" | "走査線" | "스캔라인" | "เส้นสแกน" =>
7323            {
7324                let x = self.arg_num(&args, 0, 0.)? as f32;
7325                let y = self.arg_num(&args, 1, 0.)? as f32;
7326                let w0 = self.arg_num(&args, 2, 200.)? as f32;
7327                let h0 = self.arg_num(&args, 3, 120.)? as f32;
7328                let dens = self.arg_num(&args, 4, 24.)? as usize;
7329                let th = self.ui_theme;
7330                let line = self.color_at(&args, 5, th.track);
7331                self.draw_ui(&ling_ui::widgets::scanlines(x, y, w0, h0, dens, line));
7332                return Ok(Value::Unit);
7333            },
7334
7335            // ── Meters ───────────────────────────────────────────────────────
7336            #[cfg(not(target_arch = "wasm32"))]
7337            "ui_bar" | "进度条" | "バー" | "막대" | "แถบ" => {
7338                let x = self.arg_num(&args, 0, 0.)? as f32;
7339                let y = self.arg_num(&args, 1, 0.)? as f32;
7340                let w0 = self.arg_num(&args, 2, 160.)? as f32;
7341                let h0 = self.arg_num(&args, 3, 16.)? as f32;
7342                let val = self.arg_num(&args, 4, 0.)? as f32;
7343                let max = self.arg_num(&args, 5, 1.)? as f32;
7344                let th = self.ui_theme;
7345                let fill = self.color_at(&args, 6, th.primary);
7346                self.draw_ui(&ling_ui::widgets::bar(
7347                    x,
7348                    y,
7349                    w0,
7350                    h0,
7351                    val / max.max(1e-6),
7352                    fill,
7353                    th.track,
7354                ));
7355                return Ok(Value::Unit);
7356            },
7357            #[cfg(not(target_arch = "wasm32"))]
7358            "ui_segbar" | "分段条" | "分割バー" | "분할막대" | "แถบแบ่ง" =>
7359            {
7360                let x = self.arg_num(&args, 0, 0.)? as f32;
7361                let y = self.arg_num(&args, 1, 0.)? as f32;
7362                let w0 = self.arg_num(&args, 2, 160.)? as f32;
7363                let h0 = self.arg_num(&args, 3, 16.)? as f32;
7364                let val = self.arg_num(&args, 4, 0.)? as f32;
7365                let max = self.arg_num(&args, 5, 1.)? as f32;
7366                let segs = self.arg_num(&args, 6, 10.)? as usize;
7367                let th = self.ui_theme;
7368                let fill = self.color_at(&args, 7, th.primary);
7369                self.draw_ui(&ling_ui::widgets::segbar(
7370                    x,
7371                    y,
7372                    w0,
7373                    h0,
7374                    val / max.max(1e-6),
7375                    segs,
7376                    fill,
7377                    th.track,
7378                ));
7379                return Ok(Value::Unit);
7380            },
7381            #[cfg(not(target_arch = "wasm32"))]
7382            "ui_gauge" | "仪表" | "ゲージ" | "게이지" | "มาตรวัด" => {
7383                let cx = self.arg_num(&args, 0, 0.)? as f32;
7384                let cy = self.arg_num(&args, 1, 0.)? as f32;
7385                let r = self.arg_num(&args, 2, 50.)? as f32;
7386                let val = self.arg_num(&args, 3, 0.)? as f32;
7387                let max = self.arg_num(&args, 4, 1.)? as f32;
7388                let th = self.ui_theme;
7389                let needle = self.color_at(&args, 5, th.warn);
7390                self.draw_ui(&ling_ui::widgets::gauge(
7391                    cx,
7392                    cy,
7393                    r,
7394                    val / max.max(1e-6),
7395                    needle,
7396                    th.accent,
7397                    th.track,
7398                ));
7399                return Ok(Value::Unit);
7400            },
7401            #[cfg(not(target_arch = "wasm32"))]
7402            "ui_ring" | "环表" | "リングメーター" | "링미터" | "วงแหวนวัด" =>
7403            {
7404                let cx = self.arg_num(&args, 0, 0.)? as f32;
7405                let cy = self.arg_num(&args, 1, 0.)? as f32;
7406                let r = self.arg_num(&args, 2, 40.)? as f32;
7407                let val = self.arg_num(&args, 3, 0.)? as f32;
7408                let max = self.arg_num(&args, 4, 1.)? as f32;
7409                let th = self.ui_theme;
7410                let fill = self.color_at(&args, 5, th.primary);
7411                self.draw_ui(&ling_ui::widgets::ring(
7412                    cx,
7413                    cy,
7414                    r,
7415                    val / max.max(1e-6),
7416                    fill,
7417                    th.track,
7418                ));
7419                return Ok(Value::Unit);
7420            },
7421            #[cfg(not(target_arch = "wasm32"))]
7422            "ui_vu" | "音量条" | "VUメーター" | "음량막대" | "มาตรเสียง" =>
7423            {
7424                let x = self.arg_num(&args, 0, 0.)? as f32;
7425                let y = self.arg_num(&args, 1, 0.)? as f32;
7426                let w0 = self.arg_num(&args, 2, 160.)? as f32;
7427                let h0 = self.arg_num(&args, 3, 60.)? as f32;
7428                let levels = self.arg_list_f32(&args, 4);
7429                let th = self.ui_theme;
7430                let fill = self.color_at(&args, 5, th.primary);
7431                self.draw_ui(&ling_ui::widgets::vu(x, y, w0, h0, &levels, fill, th.warn));
7432                return Ok(Value::Unit);
7433            },
7434            #[cfg(not(target_arch = "wasm32"))]
7435            "ui_spark" | "迷你图" | "スパークライン" | "스파크라인" | "กราฟจิ๋ว" =>
7436            {
7437                let x = self.arg_num(&args, 0, 0.)? as f32;
7438                let y = self.arg_num(&args, 1, 0.)? as f32;
7439                let w0 = self.arg_num(&args, 2, 160.)? as f32;
7440                let h0 = self.arg_num(&args, 3, 40.)? as f32;
7441                let vals = self.arg_list_f32(&args, 4);
7442                let th = self.ui_theme;
7443                let line = self.color_at(&args, 5, th.accent);
7444                self.draw_ui(&ling_ui::widgets::spark(x, y, w0, h0, &vals, line));
7445                return Ok(Value::Unit);
7446            },
7447            #[cfg(not(target_arch = "wasm32"))]
7448            "ui_battery" | "电池" | "バッテリー" | "배터리" | "แบตเตอรี่" =>
7449            {
7450                let x = self.arg_num(&args, 0, 0.)? as f32;
7451                let y = self.arg_num(&args, 1, 0.)? as f32;
7452                let w0 = self.arg_num(&args, 2, 50.)? as f32;
7453                let h0 = self.arg_num(&args, 3, 22.)? as f32;
7454                let val = self.arg_num(&args, 4, 1.)? as f32;
7455                let max = self.arg_num(&args, 5, 1.)? as f32;
7456                let th = self.ui_theme;
7457                let fill = self.color_at(&args, 6, th.accent);
7458                self.draw_ui(&ling_ui::widgets::battery(
7459                    x,
7460                    y,
7461                    w0,
7462                    h0,
7463                    val / max.max(1e-6),
7464                    fill,
7465                    th.track,
7466                    th.warn,
7467                ));
7468                return Ok(Value::Unit);
7469            },
7470
7471            // ── Interface controls (interactive → return state) ──────────────
7472            #[cfg(not(target_arch = "wasm32"))]
7473            "ui_button" | "按钮" | "ボタン" | "버튼" | "ปุ่ม" => {
7474                let x = self.arg_num(&args, 0, 0.)? as f32;
7475                let y = self.arg_num(&args, 1, 0.)? as f32;
7476                let w0 = self.arg_num(&args, 2, 120.)? as f32;
7477                let h0 = self.arg_num(&args, 3, 40.)? as f32;
7478                let (mx, my, down) = self.mouse_now();
7479                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
7480                let clicked = hover && down && !self.mouse_was_down;
7481                let th = self.ui_theme;
7482                let prim = self.color_at(&args, 4, th.primary);
7483                self.draw_ui(&ling_ui::widgets::button(
7484                    x,
7485                    y,
7486                    w0,
7487                    h0,
7488                    hover,
7489                    down && hover,
7490                    prim,
7491                    th.bg,
7492                ));
7493                return Ok(Value::Number(if clicked { 1.0 } else { 0.0 }));
7494            },
7495            #[cfg(not(target_arch = "wasm32"))]
7496            "ui_toggle" | "开关" | "トグル" | "토글" | "สวิตช์" => {
7497                let x = self.arg_num(&args, 0, 0.)? as f32;
7498                let y = self.arg_num(&args, 1, 0.)? as f32;
7499                let w0 = self.arg_num(&args, 2, 52.)? as f32;
7500                let h0 = self.arg_num(&args, 3, 24.)? as f32;
7501                let mut state = self.arg_num(&args, 4, 0.)? > 0.5;
7502                let (mx, my, down) = self.mouse_now();
7503                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
7504                if hover && down && !self.mouse_was_down {
7505                    state = !state;
7506                }
7507                let th = self.ui_theme;
7508                let on = self.color_at(&args, 5, th.accent);
7509                self.draw_ui(&ling_ui::widgets::toggle(x, y, w0, h0, state, on, th.track));
7510                return Ok(Value::Number(if state { 1.0 } else { 0.0 }));
7511            },
7512            #[cfg(not(target_arch = "wasm32"))]
7513            "ui_slider" | "滑块" | "スライダー" | "슬라이더" | "แถบเลื่อน" =>
7514            {
7515                let x = self.arg_num(&args, 0, 0.)? as f32;
7516                let y = self.arg_num(&args, 1, 0.)? as f32;
7517                let w0 = self.arg_num(&args, 2, 160.)? as f32;
7518                let mut val = self.arg_num(&args, 3, 0.)? as f32;
7519                let mn = self.arg_num(&args, 4, 0.)? as f32;
7520                let mx_ = self.arg_num(&args, 5, 1.)? as f32;
7521                let (mx, my, down) = self.mouse_now();
7522                let hover = ling_ui::holo::hit_rect(mx, my, x - 8.0, y - 10.0, w0 + 16.0, 20.0);
7523                if hover && down {
7524                    let frac = ((mx - x) / w0).max(0.0).min(1.0);
7525                    val = mn + (mx_ - mn) * frac;
7526                }
7527                let frac = ((val - mn) / (mx_ - mn).abs().max(1e-6)).max(0.0).min(1.0);
7528                let th = self.ui_theme;
7529                let fill = self.color_at(&args, 6, th.primary);
7530                self.draw_ui(&ling_ui::widgets::slider(
7531                    x, y, w0, frac, hover, fill, th.track,
7532                ));
7533                return Ok(Value::Number(val as f64));
7534            },
7535            #[cfg(not(target_arch = "wasm32"))]
7536            "ui_checkbox" | "复选框" | "チェックボックス" | "체크박스" | "ช่องเลือก" =>
7537            {
7538                let x = self.arg_num(&args, 0, 0.)? as f32;
7539                let y = self.arg_num(&args, 1, 0.)? as f32;
7540                let s = self.arg_num(&args, 2, 20.)? as f32;
7541                let mut checked = self.arg_num(&args, 3, 0.)? > 0.5;
7542                let (mx, my, down) = self.mouse_now();
7543                let hover = ling_ui::holo::hit_rect(mx, my, x, y, s, s);
7544                if hover && down && !self.mouse_was_down {
7545                    checked = !checked;
7546                }
7547                let th = self.ui_theme;
7548                let prim = self.color_at(&args, 4, th.primary);
7549                self.draw_ui(&ling_ui::widgets::checkbox(
7550                    x, y, s, checked, hover, prim, th.track,
7551                ));
7552                return Ok(Value::Number(if checked { 1.0 } else { 0.0 }));
7553            },
7554            #[cfg(not(target_arch = "wasm32"))]
7555            "ui_tabs" | "标签页" | "タブ" | "탭" | "แท็บ" => {
7556                let x = self.arg_num(&args, 0, 0.)? as f32;
7557                let y = self.arg_num(&args, 1, 0.)? as f32;
7558                let w0 = self.arg_num(&args, 2, 240.)? as f32;
7559                let h0 = self.arg_num(&args, 3, 28.)? as f32;
7560                let count = self.arg_num(&args, 4, 3.)? as usize;
7561                let mut active = self.arg_num(&args, 5, 0.)? as i32;
7562                let (mx, my, down) = self.mouse_now();
7563                let mut hover = -1;
7564                if my >= y && my <= y + h0 && mx >= x && mx <= x + w0 && count > 0 {
7565                    hover = (((mx - x) / (w0 / count as f32)) as i32)
7566                        .max(0)
7567                        .min(count as i32 - 1);
7568                    if down && !self.mouse_was_down {
7569                        active = hover;
7570                    }
7571                }
7572                let th = self.ui_theme;
7573                let prim = self.color_at(&args, 6, th.primary);
7574                self.draw_ui(&ling_ui::widgets::tabs(
7575                    x,
7576                    y,
7577                    w0,
7578                    h0,
7579                    count,
7580                    active as usize,
7581                    hover,
7582                    prim,
7583                    th.track,
7584                ));
7585                return Ok(Value::Number(active as f64));
7586            },
7587            #[cfg(not(target_arch = "wasm32"))]
7588            "ui_progress" | "进度" | "プログレス" | "진행바" | "ความคืบหน้า" =>
7589            {
7590                let x = self.arg_num(&args, 0, 0.)? as f32;
7591                let y = self.arg_num(&args, 1, 0.)? as f32;
7592                let w0 = self.arg_num(&args, 2, 200.)? as f32;
7593                let h0 = self.arg_num(&args, 3, 12.)? as f32;
7594                let frac = self.arg_num(&args, 4, 0.)? as f32;
7595                let th = self.ui_theme;
7596                let fill = self.color_at(&args, 5, th.accent);
7597                self.draw_ui(&ling_ui::widgets::progress(
7598                    x, y, w0, h0, frac, fill, th.track,
7599                ));
7600                return Ok(Value::Unit);
7601            },
7602            #[cfg(not(target_arch = "wasm32"))]
7603            "ui_tooltip" | "提示框" | "ツールチップ" | "툴팁" | "คำแนะนำ" =>
7604            {
7605                let x = self.arg_num(&args, 0, 0.)? as f32;
7606                let y = self.arg_num(&args, 1, 0.)? as f32;
7607                let w0 = self.arg_num(&args, 2, 120.)? as f32;
7608                let h0 = self.arg_num(&args, 3, 28.)? as f32;
7609                let th = self.ui_theme;
7610                let prim = self.color_at(&args, 4, th.primary);
7611                self.draw_ui(&ling_ui::widgets::tooltip(x, y, w0, h0, prim, th.bg));
7612                return Ok(Value::Unit);
7613            },
7614            #[cfg(not(target_arch = "wasm32"))]
7615            "ui_stepper" | "步进器" | "ステッパー" | "스테퍼" | "ตัวปรับค่า" =>
7616            {
7617                let x = self.arg_num(&args, 0, 0.)? as f32;
7618                let y = self.arg_num(&args, 1, 0.)? as f32;
7619                let w0 = self.arg_num(&args, 2, 120.)? as f32;
7620                let h0 = self.arg_num(&args, 3, 28.)? as f32;
7621                let mut val = self.arg_num(&args, 4, 0.)? as f32;
7622                let step = self.arg_num(&args, 5, 1.)? as f32;
7623                let (mx, my, down) = self.mouse_now();
7624                let hm = ling_ui::holo::hit_rect(mx, my, x, y, h0, h0);
7625                let hp = ling_ui::holo::hit_rect(mx, my, x + w0 - h0, y, h0, h0);
7626                if down && !self.mouse_was_down {
7627                    if hm {
7628                        val -= step;
7629                    }
7630                    if hp {
7631                        val += step;
7632                    }
7633                }
7634                let th = self.ui_theme;
7635                let prim = self.color_at(&args, 6, th.primary);
7636                self.draw_ui(&ling_ui::widgets::stepper(
7637                    x, y, w0, h0, hm, hp, prim, th.track,
7638                ));
7639                return Ok(Value::Number(val as f64));
7640            },
7641
7642            // ── Game UI ──────────────────────────────────────────────────────
7643            #[cfg(not(target_arch = "wasm32"))]
7644            "ui_healthbar" | "血条" | "体力バー" | "체력바" | "แถบพลังชีวิต" =>
7645            {
7646                let x = self.arg_num(&args, 0, 0.)? as f32;
7647                let y = self.arg_num(&args, 1, 0.)? as f32;
7648                let w0 = self.arg_num(&args, 2, 180.)? as f32;
7649                let h0 = self.arg_num(&args, 3, 16.)? as f32;
7650                let val = self.arg_num(&args, 4, 1.)? as f32;
7651                let max = self.arg_num(&args, 5, 1.)? as f32;
7652                let pulse = self.arg_num(&args, 6, 0.)? as f32;
7653                let th = self.ui_theme;
7654                let full = self.color_at(&args, 7, th.accent);
7655                self.draw_ui(&ling_ui::widgets::healthbar(
7656                    x,
7657                    y,
7658                    w0,
7659                    h0,
7660                    val / max.max(1e-6),
7661                    pulse,
7662                    full,
7663                    th.warn,
7664                    th.track,
7665                ));
7666                return Ok(Value::Unit);
7667            },
7668            #[cfg(not(target_arch = "wasm32"))]
7669            "ui_cooldown" | "冷却" | "クールダウン" | "쿨다운" | "คูลดาวน์" =>
7670            {
7671                let cx = self.arg_num(&args, 0, 0.)? as f32;
7672                let cy = self.arg_num(&args, 1, 0.)? as f32;
7673                let r = self.arg_num(&args, 2, 28.)? as f32;
7674                let frac = self.arg_num(&args, 3, 0.)? as f32;
7675                let th = self.ui_theme;
7676                let fill = self.color_at(&args, 4, th.primary);
7677                self.draw_ui(&ling_ui::widgets::cooldown(cx, cy, r, frac, fill, th.track));
7678                return Ok(Value::Unit);
7679            },
7680            #[cfg(not(target_arch = "wasm32"))]
7681            "ui_counter" | "计数器" | "カウンター" | "카운터" | "ตัวนับ" => {
7682                let x = self.arg_num(&args, 0, 0.)? as f32;
7683                let y = self.arg_num(&args, 1, 0.)? as f32;
7684                let dw = self.arg_num(&args, 2, 14.)? as f32;
7685                let dh = self.arg_num(&args, 3, 24.)? as f32;
7686                let val = self.arg_num(&args, 4, 0.)? as i64;
7687                let digits = self.arg_num(&args, 5, 4.)? as usize;
7688                let th = self.ui_theme;
7689                let on = self.color_at(&args, 6, th.primary);
7690                let off = ling_ui::widgets::shade(th.track, 0.5);
7691                self.draw_ui(&ling_ui::widgets::counter(
7692                    x, y, dw, dh, val, digits, on, off,
7693                ));
7694                return Ok(Value::Unit);
7695            },
7696            #[cfg(not(target_arch = "wasm32"))]
7697            "ui_minimap" | "小地图" | "ミニマップ" | "미니맵" | "แผนที่ย่อ" =>
7698            {
7699                let x = self.arg_num(&args, 0, 0.)? as f32;
7700                let y = self.arg_num(&args, 1, 0.)? as f32;
7701                let w0 = self.arg_num(&args, 2, 140.)? as f32;
7702                let h0 = self.arg_num(&args, 3, 140.)? as f32;
7703                let th = self.ui_theme;
7704                let prim = self.color_at(&args, 4, th.primary);
7705                self.draw_ui(&ling_ui::widgets::minimap(x, y, w0, h0, prim, th.bg));
7706                return Ok(Value::Unit);
7707            },
7708            #[cfg(not(target_arch = "wasm32"))]
7709            "ui_dpad" | "方向键" | "方向パッド" | "방향패드" | "ปุ่มทิศทาง" =>
7710            {
7711                let cx = self.arg_num(&args, 0, 0.)? as f32;
7712                let cy = self.arg_num(&args, 1, 0.)? as f32;
7713                let r = self.arg_num(&args, 2, 50.)? as f32;
7714                let (mx, my, down) = self.mouse_now();
7715                let mut dir = 0;
7716                if down {
7717                    let (dx, dy) = (mx - cx, my - cy);
7718                    if dx * dx + dy * dy <= r * r {
7719                        if dx.abs() > dy.abs() {
7720                            dir = if dx > 0.0 { 2 } else { 4 };
7721                        } else {
7722                            dir = if dy > 0.0 { 3 } else { 1 };
7723                        }
7724                    }
7725                }
7726                let th = self.ui_theme;
7727                let prim = self.color_at(&args, 3, th.primary);
7728                self.draw_ui(&ling_ui::widgets::dpad(cx, cy, r, dir, prim, th.track));
7729                return Ok(Value::Number(dir as f64));
7730            },
7731            #[cfg(not(target_arch = "wasm32"))]
7732            "ui_slotgrid" | "物品格" | "スロットグリッド" | "슬롯격자" | "ช่องไอเทม" =>
7733            {
7734                let x = self.arg_num(&args, 0, 0.)? as f32;
7735                let y = self.arg_num(&args, 1, 0.)? as f32;
7736                let cols = self.arg_num(&args, 2, 4.)? as usize;
7737                let rows = self.arg_num(&args, 3, 1.)? as usize;
7738                let cell = self.arg_num(&args, 4, 36.)? as f32;
7739                let sel = self.arg_num(&args, 5, -1.)? as i32;
7740                let th = self.ui_theme;
7741                let prim = self.color_at(&args, 6, th.primary);
7742                self.draw_ui(&ling_ui::widgets::slotgrid(
7743                    x, y, cols, rows, cell, sel, prim, th.track,
7744                ));
7745                return Ok(Value::Unit);
7746            },
7747            #[cfg(not(target_arch = "wasm32"))]
7748            "ui_vignette" | "暗角" | "ビネット" | "비네트" | "ขอบมืด" => {
7749                let intensity = self.arg_num(&args, 0, 0.5)? as f32;
7750                let (w, h) = {
7751                    let g = self.gfx.borrow();
7752                    (g.width as f32, g.height as f32)
7753                };
7754                let th = self.ui_theme;
7755                let col = self.color_at(&args, 1, th.warn);
7756                self.draw_ui(&ling_ui::widgets::vignette(w, h, intensity, col));
7757                return Ok(Value::Unit);
7758            },
7759
7760            // ── Faux-3D in 2D space ──────────────────────────────────────────
7761            #[cfg(not(target_arch = "wasm32"))]
7762            "ui_gauge3d" | "立体仪表" | "立体ゲージ" | "입체게이지" | "มาตรวัด3มิติ" =>
7763            {
7764                let cx = self.arg_num(&args, 0, 0.)? as f32;
7765                let cy = self.arg_num(&args, 1, 0.)? as f32;
7766                let r = self.arg_num(&args, 2, 50.)? as f32;
7767                let val = self.arg_num(&args, 3, 0.)? as f32;
7768                let max = self.arg_num(&args, 4, 1.)? as f32;
7769                let spin = self.arg_num(&args, 5, 0.)? as f32;
7770                let th = self.ui_theme;
7771                let fill = self.color_at(&args, 6, th.primary);
7772                self.draw_ui(&ling_ui::widgets::gauge3d(
7773                    cx,
7774                    cy,
7775                    r,
7776                    val / max.max(1e-6),
7777                    spin,
7778                    fill,
7779                    th.track,
7780                ));
7781                return Ok(Value::Unit);
7782            },
7783            #[cfg(not(target_arch = "wasm32"))]
7784            "ui_panel3d" | "立体面板" | "立体パネル" | "입체패널" | "แผง3มิติ" =>
7785            {
7786                let x = self.arg_num(&args, 0, 0.)? as f32;
7787                let y = self.arg_num(&args, 1, 0.)? as f32;
7788                let w0 = self.arg_num(&args, 2, 200.)? as f32;
7789                let h0 = self.arg_num(&args, 3, 120.)? as f32;
7790                let depth = self.arg_num(&args, 4, 14.)? as f32;
7791                let th = self.ui_theme;
7792                let prim = self.color_at(&args, 5, th.primary);
7793                self.draw_ui(&ling_ui::widgets::panel3d(x, y, w0, h0, depth, prim, th.bg));
7794                return Ok(Value::Unit);
7795            },
7796            #[cfg(not(target_arch = "wasm32"))]
7797            "ui_radar3d" | "立体雷达" | "立体レーダー" | "입체레이더" | "เรดาร์3มิติ" =>
7798            {
7799                let cx = self.arg_num(&args, 0, 0.)? as f32;
7800                let cy = self.arg_num(&args, 1, 0.)? as f32;
7801                let r = self.arg_num(&args, 2, 60.)? as f32;
7802                let tilt = self.arg_num(&args, 3, 0.9)? as f32;
7803                let sweep = self.arg_num(&args, 4, 0.)? as f32;
7804                let th = self.ui_theme;
7805                let prim = self.color_at(&args, 5, th.primary);
7806                self.draw_ui(&ling_ui::widgets::radar3d(
7807                    cx, cy, r, tilt, sweep, prim, th.track,
7808                ));
7809                return Ok(Value::Unit);
7810            },
7811
7812            // ── Interface sounds ─────────────────────────────────────────────
7813            #[cfg(not(target_arch = "wasm32"))]
7814            "audio_blip" | "提示音" | "ビープ音" | "효과음" | "เสียงบี๊บ" =>
7815            {
7816                let freq = self.arg_num(&args, 0, 660.)? as f32;
7817                let dur = self.arg_num(&args, 1, 0.08)? as f32;
7818                let wave = Wave::from_name(&self.arg_str(&args, 2, "sine"));
7819                let amp = self.arg_num(&args, 3, 0.25)? as f32;
7820                if let Some(audio) = &self.audio {
7821                    audio.blip(freq, amp, dur, wave);
7822                }
7823                return Ok(Value::Unit);
7824            },
7825            #[cfg(not(target_arch = "wasm32"))]
7826            "ui_sound" | "界面音" | "UI音" | "인터페이스음" | "เสียงปุ่ม" =>
7827            {
7828                let name = self.arg_str(&args, 0, "click");
7829                if let Some(audio) = &self.audio {
7830                    match name.as_str() {
7831                        "hover" => audio.blip(880.0, 0.10, 0.04, Wave::Sine),
7832                        "confirm" => {
7833                            audio.blip(660.0, 0.22, 0.07, Wave::Square);
7834                            audio.blip(990.0, 0.18, 0.10, Wave::Square);
7835                        },
7836                        "error" => {
7837                            audio.blip(180.0, 0.30, 0.16, Wave::Saw);
7838                            audio.blip(140.0, 0.30, 0.18, Wave::Saw);
7839                        },
7840                        "toggle" => audio.blip(520.0, 0.22, 0.05, Wave::Triangle),
7841                        "tick" => audio.blip(1500.0, 0.12, 0.02, Wave::Square),
7842                        _ => audio.blip(720.0, 0.26, 0.05, Wave::Square), // "click"
7843                    }
7844                }
7845                return Ok(Value::Unit);
7846            },
7847
7848            // ══════════════════════════════════════════════════════════════════
7849            // MUSIC TOOLKIT  (crates/ling-music) — decode · analysis · GM synth ·
7850            // rhythm · karaoke. Analysis/decoding need no audio device; playback
7851            // and synthesis lazily start a dedicated music engine.
7852            // ══════════════════════════════════════════════════════════════════
7853
7854            // music_load(path) -> track handle (decodes WAV/FLAC/OGG/MP3/AAC)
7855            #[cfg(not(target_arch = "wasm32"))]
7856            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" =>
7857            {
7858                let path = self.arg_str(&args, 0, "");
7859                let resolved = if std::path::Path::new(&path).exists() {
7860                    path.clone()
7861                } else if let Some(d) = &self.source_dir {
7862                    d.join(&path).to_string_lossy().into_owned()
7863                } else {
7864                    path.clone()
7865                };
7866                match ling_music::load(&resolved) {
7867                    Ok(t) => {
7868                        let id = self.tracks.len();
7869                        self.tracks.push(t);
7870                        return Ok(Value::Number(id as f64));
7871                    },
7872                    Err(e) => {
7873                        eprintln!("music_load failed ({path}): {e}");
7874                        return Ok(Value::Number(-1.0));
7875                    },
7876                }
7877            },
7878            #[cfg(not(target_arch = "wasm32"))]
7879            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" =>
7880            {
7881                let id = self.arg_num(&args, 0, 0.0)? as i64;
7882                let d = self
7883                    .tracks
7884                    .get(id as usize)
7885                    .map(|t| t.duration)
7886                    .unwrap_or(0.0);
7887                return Ok(Value::Number(d as f64));
7888            },
7889            #[cfg(not(target_arch = "wasm32"))]
7890            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" =>
7891            {
7892                let id = self.arg_num(&args, 0, 0.0)? as i64;
7893                let b = self
7894                    .tracks
7895                    .get(id as usize)
7896                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
7897                    .unwrap_or(0.0);
7898                return Ok(Value::Number(b as f64));
7899            },
7900            #[cfg(not(target_arch = "wasm32"))]
7901            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" => {
7902                let id = self.arg_num(&args, 0, 0.0)? as i64;
7903                let k = self
7904                    .tracks
7905                    .get(id as usize)
7906                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
7907                    .unwrap_or_default();
7908                return Ok(Value::Str(k));
7909            },
7910            #[cfg(not(target_arch = "wasm32"))]
7911            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" =>
7912            {
7913                let id = self.arg_num(&args, 0, 0.0)? as i64;
7914                let v = self
7915                    .tracks
7916                    .get(id as usize)
7917                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
7918                    .unwrap_or_default();
7919                return Ok(Value::List(Rc::new(
7920                    v.into_iter().map(|x| Value::Number(x as f64)).collect(),
7921                )));
7922            },
7923            #[cfg(not(target_arch = "wasm32"))]
7924            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" =>
7925            {
7926                let id = self.arg_num(&args, 0, 0.0)? as i64;
7927                let beats = self
7928                    .tracks
7929                    .get(id as usize)
7930                    .map(|t| {
7931                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
7932                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
7933                    })
7934                    .unwrap_or_default();
7935                return Ok(Value::List(Rc::new(
7936                    beats.into_iter().map(|x| Value::Number(x as f64)).collect(),
7937                )));
7938            },
7939
7940            // ── playback ──
7941            #[cfg(not(target_arch = "wasm32"))]
7942            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" =>
7943            {
7944                let id = self.arg_num(&args, 0, 0.0)? as i64;
7945                if self.ensure_music() {
7946                    let track = self
7947                        .tracks
7948                        .get(id as usize)
7949                        .map(|t| (t.stereo.clone(), t.rate));
7950                    if let (Some((st, rate)), Some(m)) = (track, &self.music) {
7951                        m.set_track(st, rate);
7952                        m.play();
7953                    } else if let Some(m) = &self.music {
7954                        m.play();
7955                    }
7956                }
7957                return Ok(Value::Unit);
7958            },
7959            #[cfg(not(target_arch = "wasm32"))]
7960            "music_pause" | "暂停音乐" | "音楽一時停止" | "음악일시정지" | "หยุดเพลงชั่วคราว" =>
7961            {
7962                if let Some(m) = &self.music {
7963                    m.pause();
7964                }
7965                return Ok(Value::Unit);
7966            },
7967            #[cfg(not(target_arch = "wasm32"))]
7968            "music_stop" | "停止音乐" | "音楽停止" | "음악정지" | "หยุดเพลง" =>
7969            {
7970                if let Some(m) = &self.music {
7971                    m.stop();
7972                }
7973                return Ok(Value::Unit);
7974            },
7975            #[cfg(not(target_arch = "wasm32"))]
7976            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" =>
7977            {
7978                let sec = self.arg_num(&args, 0, 0.0)? as f32;
7979                if let Some(m) = &self.music {
7980                    m.seek(sec);
7981                }
7982                return Ok(Value::Unit);
7983            },
7984            #[cfg(not(target_arch = "wasm32"))]
7985            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" =>
7986            {
7987                let p = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
7988                return Ok(Value::Number(p as f64));
7989            },
7990            #[cfg(not(target_arch = "wasm32"))]
7991            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" =>
7992            {
7993                let v = self.arg_num(&args, 0, 0.8)? as f32;
7994                if self.ensure_music() {
7995                    if let Some(m) = &self.music {
7996                        m.set_volume(v);
7997                    }
7998                }
7999                return Ok(Value::Unit);
8000            },
8001
8002            // ── synthesis (GM-capable, patches from .ling files) ──
8003            #[cfg(not(target_arch = "wasm32"))]
8004            "music_patch" | "乐器音色" | "音色読込" | "악기패치" | "แพตช์เครื่องดนตรี" =>
8005            {
8006                let path = self.arg_str(&args, 0, "");
8007                let resolved = if std::path::Path::new(&path).exists() {
8008                    path.clone()
8009                } else if let Some(d) = &self.source_dir {
8010                    d.join(&path).to_string_lossy().into_owned()
8011                } else {
8012                    path.clone()
8013                };
8014                if !self.ensure_music() {
8015                    return Ok(Value::Number(-1.0));
8016                }
8017                match ling_music::patch::from_path(&resolved) {
8018                    Ok(p) => {
8019                        let id = self.music.as_ref().unwrap().add_patch(p);
8020                        return Ok(Value::Number(id as f64));
8021                    },
8022                    Err(e) => {
8023                        eprintln!("music_patch failed ({path}): {e}");
8024                        return Ok(Value::Number(-1.0));
8025                    },
8026                }
8027            },
8028            #[cfg(not(target_arch = "wasm32"))]
8029            "music_note" | "弹音符" | "音符演奏" | "음표연주" | "เล่นโน้ต" =>
8030            {
8031                let inst = self.arg_num(&args, 0, 0.0)? as usize;
8032                let midi = self.pitch_arg(&args, 1, 60);
8033                let dur = self.arg_num(&args, 2, 0.5)? as f32;
8034                let vel = self.arg_num(&args, 3, 0.9)? as f32;
8035                if self.ensure_music() {
8036                    if let Some(m) = &self.music {
8037                        m.note(inst, midi, vel, dur);
8038                    }
8039                }
8040                return Ok(Value::Unit);
8041            },
8042            #[cfg(not(target_arch = "wasm32"))]
8043            "music_note_on" | "音符开始" | "音符オン" | "음표켜기" | "โน้ตเริ่ม" =>
8044            {
8045                let inst = self.arg_num(&args, 0, 0.0)? as usize;
8046                let midi = self.pitch_arg(&args, 1, 60);
8047                let vel = self.arg_num(&args, 2, 0.9)? as f32;
8048                if self.ensure_music() {
8049                    if let Some(m) = &self.music {
8050                        m.note_on(inst, midi, vel);
8051                    }
8052                }
8053                return Ok(Value::Unit);
8054            },
8055            #[cfg(not(target_arch = "wasm32"))]
8056            "music_note_off" | "音符结束" | "音符オフ" | "음표끄기" | "โน้ตจบ" =>
8057            {
8058                let inst = self.arg_num(&args, 0, 0.0)? as usize;
8059                let midi = self.pitch_arg(&args, 1, 60);
8060                if let Some(m) = &self.music {
8061                    m.note_off(inst, midi);
8062                }
8063                return Ok(Value::Unit);
8064            },
8065
8066            // ── rhythm-game judging ──
8067            #[cfg(not(target_arch = "wasm32"))]
8068            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" =>
8069            {
8070                let delta_ms = self.arg_num(&args, 0, 9999.0)? as f32;
8071                return Ok(Value::Number(
8072                    ling_music::Grade::judge(delta_ms).index() as f64
8073                ));
8074            },
8075            #[cfg(not(target_arch = "wasm32"))]
8076            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" =>
8077            {
8078                let idx = self.arg_num(&args, 0, 4.0)? as i32;
8079                return Ok(Value::Str(
8080                    ling_music::Grade::from_index(idx).name().to_string(),
8081                ));
8082            },
8083
8084            // ── karaoke ──
8085            #[cfg(not(target_arch = "wasm32"))]
8086            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" =>
8087            {
8088                let path = self.arg_str(&args, 0, "");
8089                let resolved = if std::path::Path::new(&path).exists() {
8090                    path.clone()
8091                } else if let Some(d) = &self.source_dir {
8092                    d.join(&path).to_string_lossy().into_owned()
8093                } else {
8094                    path.clone()
8095                };
8096                match std::fs::read_to_string(&resolved) {
8097                    Ok(text) => {
8098                        let id = self.lyrics.len();
8099                        self.lyrics.push(ling_music::Lyrics::parse(&text));
8100                        return Ok(Value::Number(id as f64));
8101                    },
8102                    Err(e) => {
8103                        eprintln!("music_lrc failed ({path}): {e}");
8104                        return Ok(Value::Number(-1.0));
8105                    },
8106                }
8107            },
8108            #[cfg(not(target_arch = "wasm32"))]
8109            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" =>
8110            {
8111                let id = self.arg_num(&args, 0, 0.0)? as i64;
8112                let t = self.arg_num(&args, 1, 0.0)? as f32;
8113                let line = self
8114                    .lyrics
8115                    .get(id as usize)
8116                    .map(|l| l.line_at(t).to_string())
8117                    .unwrap_or_default();
8118                return Ok(Value::Str(line));
8119            },
8120            #[cfg(not(target_arch = "wasm32"))]
8121            "music_mic_pitch" | "麦克风音高" | "マイク音程" | "마이크음정" | "ระดับเสียงไมค์" =>
8122            {
8123                let hz = if let Some(mic) = self.mic.as_ref() {
8124                    let s = mic.latest_samples();
8125                    let rate = mic.sample_rate();
8126                    ling_music::pitch::detect(&s, rate).unwrap_or(0.0)
8127                } else {
8128                    0.0
8129                };
8130                return Ok(Value::Number(hz as f64));
8131            },
8132            #[cfg(not(target_arch = "wasm32"))]
8133            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" =>
8134            {
8135                let hz = self.arg_num(&args, 0, 0.0)? as f32;
8136                return Ok(Value::Str(ling_music::note::hz_to_name(hz)));
8137            },
8138            #[cfg(not(target_arch = "wasm32"))]
8139            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" =>
8140            {
8141                let midi = self.pitch_arg(&args, 0, 69);
8142                return Ok(Value::Number(
8143                    ling_music::note::midi_to_hz(midi as f32) as f64
8144                ));
8145            },
8146            #[cfg(not(target_arch = "wasm32"))]
8147            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" =>
8148            {
8149                let hz = self.arg_num(&args, 0, 0.0)? as f32;
8150                let target = self.arg_num(&args, 1, 0.0)? as f32;
8151                return Ok(Value::Number(
8152                    ling_music::karaoke::pitch_score(hz, target) as f64
8153                ));
8154            },
8155
8156            // ── MIDI (inaudible note source: drive coins, cues, etc.) ──
8157            #[cfg(not(target_arch = "wasm32"))]
8158            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" =>
8159            {
8160                let path = self.arg_str(&args, 0, "");
8161                let resolved = if std::path::Path::new(&path).exists() {
8162                    path.clone()
8163                } else if let Some(d) = &self.source_dir {
8164                    d.join(&path).to_string_lossy().into_owned()
8165                } else {
8166                    path.clone()
8167                };
8168                match ling_music::midi::load(&resolved) {
8169                    Ok(m) => {
8170                        let id = self.midis.len();
8171                        self.midis.push(m);
8172                        return Ok(Value::Number(id as f64));
8173                    },
8174                    Err(e) => {
8175                        eprintln!("music_midi_load failed ({path}): {e}");
8176                        return Ok(Value::Number(-1.0));
8177                    },
8178                }
8179            },
8180            #[cfg(not(target_arch = "wasm32"))]
8181            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" =>
8182            {
8183                let id = self.arg_num(&args, 0, 0.0)? as i64;
8184                let n = self
8185                    .midis
8186                    .get(id as usize)
8187                    .map(|m| m.notes.len())
8188                    .unwrap_or(0);
8189                return Ok(Value::Number(n as f64));
8190            },
8191            // music_midi_notes(id) -> flat [time, midi, time, midi, …]
8192            #[cfg(not(target_arch = "wasm32"))]
8193            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" =>
8194            {
8195                let id = self.arg_num(&args, 0, 0.0)? as i64;
8196                let mut out = Vec::new();
8197                if let Some(m) = self.midis.get(id as usize) {
8198                    for n in &m.notes {
8199                        out.push(Value::Number(n.time as f64));
8200                        out.push(Value::Number(n.midi as f64));
8201                    }
8202                }
8203                return Ok(Value::List(Rc::new(out)));
8204            },
8205            // music_midi_bars(id) -> flat [time, midi, dur, …] (for karaoke note bars)
8206            #[cfg(not(target_arch = "wasm32"))]
8207            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" =>
8208            {
8209                let id = self.arg_num(&args, 0, 0.0)? as i64;
8210                let mut out = Vec::new();
8211                if let Some(m) = self.midis.get(id as usize) {
8212                    for n in &m.notes {
8213                        out.push(Value::Number(n.time as f64));
8214                        out.push(Value::Number(n.midi as f64));
8215                        out.push(Value::Number(n.dur as f64));
8216                    }
8217                }
8218                return Ok(Value::List(Rc::new(out)));
8219            },
8220
8221            // music_fft(track_id, nbands) -> spectrum at the current playback position
8222            #[cfg(not(target_arch = "wasm32"))]
8223            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" =>
8224            {
8225                let id = self.arg_num(&args, 0, 0.0)? as i64;
8226                let nbands = self.arg_num(&args, 1, 16.0)? as usize;
8227                let pos = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
8228                if let Some(t) = self.tracks.get(id as usize) {
8229                    let idx = (pos * t.rate as f32) as usize;
8230                    let end = (idx + 2048).min(t.mono.len());
8231                    if end > idx + 64 {
8232                        self.fft.borrow_mut().push_samples(&t.mono[idx..end]);
8233                    }
8234                }
8235                let bands = self.fft.borrow().freq_bands(nbands);
8236                return Ok(Value::List(Rc::new(
8237                    bands.into_iter().map(|x| Value::Number(x as f64)).collect(),
8238                )));
8239            },
8240
8241            // ── stop every one-shot SFX/morph/sample voice (scene cleanup) ──
8242            #[cfg(not(target_arch = "wasm32"))]
8243            "audio_stop_sfx" | "停止音效" | "効果音停止" | "효과음정지" | "หยุดเอฟเฟกต์ทั้งหมด" =>
8244            {
8245                if let Some(a) = &self.audio {
8246                    a.stop_all_sfx();
8247                }
8248                return Ok(Value::Unit);
8249            },
8250            // ── spatial (2D/3D/4D) one-shot SFX ──
8251            #[cfg(not(target_arch = "wasm32"))]
8252            "audio_sfx" | "音效" | "空間効果音" | "공간효과음" | "เสียงเอฟเฟกต์" =>
8253            {
8254                let x = self.arg_num(&args, 0, 0.0)? as f32;
8255                let y = self.arg_num(&args, 1, 0.0)? as f32;
8256                let z = self.arg_num(&args, 2, 0.0)? as f32;
8257                let w = self.arg_num(&args, 3, 1.0)? as f32;
8258                let freq = self.arg_num(&args, 4, 440.0)? as f32;
8259                let amp = self.arg_num(&args, 5, 0.3)? as f32;
8260                let dur = self.arg_num(&args, 6, 0.15)? as f32;
8261                let wave = Wave::from_name(&self.arg_str(&args, 7, "sine"));
8262                if let Some(a) = &self.audio {
8263                    a.sfx(x, y, z, w, freq, amp, dur, wave);
8264                }
8265                return Ok(Value::Unit);
8266            },
8267            // ── YIN-YANG morph synth note: physical-model(light) ↔ FM/crush(dark) ──
8268            // โน้ตมอร์ฟ(x,y,z,w, freq, amp, dur, material, morph)
8269            //   material: 0 bowed-string · 1 plucked · 2 blown · 3 struck-metal
8270            //   morph:    0.0 light/acoustic .. 1.0 dark/digital
8271            #[cfg(not(target_arch = "wasm32"))]
8272            "morph_note" | "โน้ตมอร์ฟ" | "变形音" | "モーフ音" | "모프음" =>
8273            {
8274                let x = self.arg_num(&args, 0, 0.0)? as f32;
8275                let y = self.arg_num(&args, 1, 0.0)? as f32;
8276                let z = self.arg_num(&args, 2, 0.0)? as f32;
8277                let w = self.arg_num(&args, 3, 1.0)? as f32;
8278                let freq = self.arg_num(&args, 4, 220.0)? as f32;
8279                let amp = self.arg_num(&args, 5, 0.3)? as f32;
8280                let dur = self.arg_num(&args, 6, 0.6)? as f32;
8281                let material = self.arg_num(&args, 7, 0.0)?.clamp(0.0, 3.0) as u8;
8282                let morph = self.arg_num(&args, 8, 0.0)? as f32;
8283                if let Some(a) = &self.audio {
8284                    a.morph_note(x, y, z, w, freq, amp, dur, material, morph);
8285                }
8286                return Ok(Value::Unit);
8287            },
8288            // ── sample load / positional play / loop / stop ──
8289            #[cfg(not(target_arch = "wasm32"))]
8290            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" =>
8291            {
8292                let path = self.arg_str(&args, 0, "");
8293                let resolved = if std::path::Path::new(&path).exists() {
8294                    path.clone()
8295                } else if let Some(d) = &self.source_dir {
8296                    d.join(&path).to_string_lossy().into_owned()
8297                } else {
8298                    path.clone()
8299                };
8300                match ling_music::load(&resolved) {
8301                    Ok(t) => {
8302                        if let Some(a) = &self.audio {
8303                            return Ok(Value::Number(a.add_sample(t.mono, t.rate) as f64));
8304                        }
8305                        return Ok(Value::Number(-1.0));
8306                    },
8307                    Err(e) => {
8308                        eprintln!("audio_sample_load failed ({path}): {e}");
8309                        return Ok(Value::Number(-1.0));
8310                    },
8311                }
8312            },
8313            #[cfg(not(target_arch = "wasm32"))]
8314            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" =>
8315            {
8316                let id = self.arg_num(&args, 0, 0.0)? as usize;
8317                let x = self.arg_num(&args, 1, 0.0)? as f32;
8318                let y = self.arg_num(&args, 2, 0.0)? as f32;
8319                let z = self.arg_num(&args, 3, 0.0)? as f32;
8320                let w = self.arg_num(&args, 4, 1.0)? as f32;
8321                let vol = self.arg_num(&args, 5, 1.0)? as f32;
8322                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
8323                let v = self
8324                    .audio
8325                    .as_ref()
8326                    .map(|a| a.play_sample(id, x, y, z, w, vol, looping))
8327                    .unwrap_or(0);
8328                return Ok(Value::Number(v as f64));
8329            },
8330            #[cfg(not(target_arch = "wasm32"))]
8331            "audio_sample_stop" | "停止采样" | "サンプル停止" | "샘플정지" | "หยุดตัวอย่างเสียง" =>
8332            {
8333                let v = self.arg_num(&args, 0, 0.0)? as u32;
8334                if let Some(a) = &self.audio {
8335                    a.stop_sample(v);
8336                }
8337                return Ok(Value::Unit);
8338            },
8339            // ── master FX: delay / reverb / low-pass (underwater) ──
8340            #[cfg(not(target_arch = "wasm32"))]
8341            "audio_fx_delay" | "回声" | "ディレイ効果" | "딜레이" | "เสียงสะท้อน" =>
8342            {
8343                let time = self.arg_num(&args, 0, 0.3)? as f32;
8344                let fb = self.arg_num(&args, 1, 0.3)? as f32;
8345                let mix = self.arg_num(&args, 2, 0.3)? as f32;
8346                if let Some(a) = &self.audio {
8347                    a.fx_delay(time, fb, mix);
8348                }
8349                return Ok(Value::Unit);
8350            },
8351            #[cfg(not(target_arch = "wasm32"))]
8352            "audio_fx_reverb" | "混响" | "リバーブ" | "리버브" | "เสียงก้อง" =>
8353            {
8354                let mix = self.arg_num(&args, 0, 0.3)? as f32;
8355                if let Some(a) = &self.audio {
8356                    a.fx_reverb(mix);
8357                }
8358                return Ok(Value::Unit);
8359            },
8360            #[cfg(not(target_arch = "wasm32"))]
8361            "audio_fx_lowpass" | "低通滤波" | "ローパス" | "저역통과" | "กรองความถี่ต่ำ" =>
8362            {
8363                let cutoff = self.arg_num(&args, 0, 1.0)? as f32;
8364                if let Some(a) = &self.audio {
8365                    a.fx_lowpass(cutoff);
8366                }
8367                return Ok(Value::Unit);
8368            },
8369
8370            // ══════════════════════════════════════════════════════════════════
8371            // PHYSICS BUILTINS  (crates/ling-physics) — soft bodies, rigid+angular,
8372            // and a fast 2-D water/oil liquid sim mappable onto 3-D surfaces.
8373            // ══════════════════════════════════════════════════════════════════
8374
8375            // ── soft bodies (deformable bouncy balls) ──
8376            #[cfg(not(target_arch = "wasm32"))]
8377            "soft_ball" | "软球" | "ソフトボール" | "소프트볼" | "ลูกบอลนุ่ม" =>
8378            {
8379                let x = self.arg_num(&args, 0, 0.)? as f32;
8380                let y = self.arg_num(&args, 1, 0.)? as f32;
8381                let z = self.arg_num(&args, 2, 0.)? as f32;
8382                let r = self.arg_num(&args, 3, 1.0)? as f32;
8383                let b = ling_physics::soft::SoftBody::sphere(
8384                    ling_physics::Vec3::new(x, y, z),
8385                    r,
8386                    8,
8387                    12,
8388                    1.0,
8389                );
8390                let id = self.soft_bodies.len();
8391                self.soft_bodies.push(b);
8392                return Ok(Value::Number(id as f64));
8393            },
8394            #[cfg(not(target_arch = "wasm32"))]
8395            "soft_step" | "软体步进" | "ソフト更新" | "소프트스텝" | "ก้าวนุ่ม" =>
8396            {
8397                let id = self.arg_num(&args, 0, 0.)? as usize;
8398                let dt = self.arg_num(&args, 1, 0.016)? as f32;
8399                let gy = self.arg_num(&args, 2, 15.0)? as f32;
8400                if let Some(b) = self.soft_bodies.get_mut(id) {
8401                    b.integrate(dt, ling_physics::Vec3::new(0.0, gy, 0.0), 4);
8402                }
8403                return Ok(Value::Unit);
8404            },
8405            #[cfg(not(target_arch = "wasm32"))]
8406            "soft_bounce" | "软体落地" | "ソフト着地" | "소프트바운스" | "เด้งนุ่ม" =>
8407            {
8408                let id = self.arg_num(&args, 0, 0.)? as usize;
8409                let fy = self.arg_num(&args, 1, 0.)? as f32;
8410                let rest = self.arg_num(&args, 2, 0.5)? as f32;
8411                if let Some(b) = self.soft_bodies.get_mut(id) {
8412                    b.floor_collision(fy, rest);
8413                }
8414                return Ok(Value::Unit);
8415            },
8416            #[cfg(not(target_arch = "wasm32"))]
8417            "soft_contain" | "软体边界" | "ソフト箱" | "소프트경계" | "กล่องนุ่ม" =>
8418            {
8419                let id = self.arg_num(&args, 0, 0.)? as usize;
8420                let nx = self.arg_num(&args, 1, -5.)? as f32;
8421                let ny = self.arg_num(&args, 2, -5.)? as f32;
8422                let nz = self.arg_num(&args, 3, -5.)? as f32;
8423                let mx = self.arg_num(&args, 4, 5.)? as f32;
8424                let my = self.arg_num(&args, 5, 5.)? as f32;
8425                let mz = self.arg_num(&args, 6, 5.)? as f32;
8426                let rest = self.arg_num(&args, 7, 0.6)? as f32;
8427                if let Some(b) = self.soft_bodies.get_mut(id) {
8428                    b.contain(
8429                        ling_physics::Vec3::new(nx, ny, nz),
8430                        ling_physics::Vec3::new(mx, my, mz),
8431                        rest,
8432                    );
8433                }
8434                return Ok(Value::Unit);
8435            },
8436            #[cfg(not(target_arch = "wasm32"))]
8437            "soft_kick" | "软体踢" | "ソフト衝撃" | "소프트킥" | "เตะนุ่ม" =>
8438            {
8439                let id = self.arg_num(&args, 0, 0.)? as usize;
8440                let dx = self.arg_num(&args, 1, 0.)? as f32;
8441                let dy = self.arg_num(&args, 2, 0.)? as f32;
8442                let dz = self.arg_num(&args, 3, 0.)? as f32;
8443                let s = self.arg_num(&args, 4, 0.1)? as f32;
8444                if let Some(b) = self.soft_bodies.get_mut(id) {
8445                    b.kick(ling_physics::Vec3::new(dx, dy, dz), s);
8446                }
8447                return Ok(Value::Unit);
8448            },
8449            // soft_spin(id, ax, ay, az, rate) — add angular velocity about the axis
8450            // through the centroid (rate = rad/step; ≈ surface_speed / radius to roll)
8451            #[cfg(not(target_arch = "wasm32"))]
8452            "soft_spin" | "软体自旋" | "ソフト回転" | "소프트회전" | "หมุนนุ่ม" =>
8453            {
8454                let id = self.arg_num(&args, 0, 0.)? as usize;
8455                let ax = self.arg_num(&args, 1, 0.)? as f32;
8456                let ay = self.arg_num(&args, 2, 0.)? as f32;
8457                let az = self.arg_num(&args, 3, 0.)? as f32;
8458                let rate = self.arg_num(&args, 4, 0.1)? as f32;
8459                if let Some(b) = self.soft_bodies.get_mut(id) {
8460                    b.spin(ling_physics::Vec3::new(ax, ay, az), rate);
8461                }
8462                return Ok(Value::Unit);
8463            },
8464            #[cfg(not(target_arch = "wasm32"))]
8465            "soft_deform" | "形变量" | "変形量" | "변형량" | "ความบิดเบี้ยว" =>
8466            {
8467                let id = self.arg_num(&args, 0, 0.)? as usize;
8468                let d = self
8469                    .soft_bodies
8470                    .get(id)
8471                    .map(|b| b.deformation())
8472                    .unwrap_or(0.0);
8473                return Ok(Value::Number(d as f64));
8474            },
8475            // soft_angular_speed(id) -> magnitude of the body's angular velocity
8476            // (how fast it is tumbling/rolling), derived from its node velocities.
8477            #[cfg(not(target_arch = "wasm32"))]
8478            "soft_angular_speed"
8479            | "软体角速"
8480            | "ソフト角速度"
8481            | "소프트각속도"
8482            | "ความเร็วเชิงมุมนุ่ม" => {
8483                let id = self.arg_num(&args, 0, 0.)? as usize;
8484                let w = self
8485                    .soft_bodies
8486                    .get(id)
8487                    .map(|b| b.angular_speed())
8488                    .unwrap_or(0.0);
8489                return Ok(Value::Number(w as f64));
8490            },
8491            #[cfg(not(target_arch = "wasm32"))]
8492            "soft_centroid" | "软体质心" | "ソフト重心" | "소프트중심" | "จุดศูนย์กลางนุ่ม" =>
8493            {
8494                let id = self.arg_num(&args, 0, 0.)? as usize;
8495                let c = self
8496                    .soft_bodies
8497                    .get(id)
8498                    .map(|b| b.centroid())
8499                    .unwrap_or(ling_physics::Vec3::ZERO);
8500                return Ok(Value::List(Rc::new(vec![
8501                    Value::Number(c.x as f64),
8502                    Value::Number(c.y as f64),
8503                    Value::Number(c.z as f64),
8504                ])));
8505            },
8506            // soft_nodes(id) -> flat [x,y,z, x,y,z, …] for rendering the deformed mesh
8507            #[cfg(not(target_arch = "wasm32"))]
8508            "soft_nodes" | "软体节点" | "ソフト節点" | "소프트노드" | "จุดนุ่ม" =>
8509            {
8510                let id = self.arg_num(&args, 0, 0.)? as usize;
8511                let mut out = Vec::new();
8512                if let Some(b) = self.soft_bodies.get(id) {
8513                    for n in &b.nodes {
8514                        out.push(Value::Number(n.pos.x as f64));
8515                        out.push(Value::Number(n.pos.y as f64));
8516                        out.push(Value::Number(n.pos.z as f64));
8517                    }
8518                }
8519                return Ok(Value::List(Rc::new(out)));
8520            },
8521
8522            // ── rigid bodies with angular dynamics ──
8523            #[cfg(not(target_arch = "wasm32"))]
8524            "rb_add" | "刚体添加" | "剛体追加" | "강체추가" | "เพิ่มวัตถุแข็ง" =>
8525            {
8526                let x = self.arg_num(&args, 0, 0.)? as f32;
8527                let y = self.arg_num(&args, 1, 0.)? as f32;
8528                let z = self.arg_num(&args, 2, 0.)? as f32;
8529                let mass = self.arg_num(&args, 3, 1.0)? as f32;
8530                let mut b =
8531                    ling_physics::rigid::RigidBody::new(ling_physics::Vec3::new(x, y, z), mass);
8532                b.restitution = 0.6;
8533                return Ok(Value::Number(self.rigid_world.add(b) as f64));
8534            },
8535            #[cfg(not(target_arch = "wasm32"))]
8536            "rb_torque" | "扭矩" | "トルク" | "토크" | "แรงบิด" => {
8537                let i = self.arg_num(&args, 0, 0.)? as usize;
8538                let tx = self.arg_num(&args, 1, 0.)? as f32;
8539                let ty = self.arg_num(&args, 2, 0.)? as f32;
8540                let tz = self.arg_num(&args, 3, 0.)? as f32;
8541                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
8542                    b.apply_torque(ling_physics::Vec3::new(tx, ty, tz));
8543                }
8544                return Ok(Value::Unit);
8545            },
8546            #[cfg(not(target_arch = "wasm32"))]
8547            "rb_spin" | "自旋" | "スピン" | "스핀" | "หมุน" => {
8548                let i = self.arg_num(&args, 0, 0.)? as usize;
8549                let wx = self.arg_num(&args, 1, 0.)? as f32;
8550                let wy = self.arg_num(&args, 2, 0.)? as f32;
8551                let wz = self.arg_num(&args, 3, 0.)? as f32;
8552                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
8553                    b.apply_spin(ling_physics::Vec3::new(wx, wy, wz));
8554                }
8555                return Ok(Value::Unit);
8556            },
8557            #[cfg(not(target_arch = "wasm32"))]
8558            "rb_impulse" | "刚体冲量" | "剛体インパルス" | "강체충격" | "แรงดลแข็ง" =>
8559            {
8560                let i = self.arg_num(&args, 0, 0.)? as usize;
8561                let ix = self.arg_num(&args, 1, 0.)? as f32;
8562                let iy = self.arg_num(&args, 2, 0.)? as f32;
8563                let iz = self.arg_num(&args, 3, 0.)? as f32;
8564                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
8565                    b.apply_impulse(ling_physics::Vec3::new(ix, iy, iz));
8566                }
8567                return Ok(Value::Unit);
8568            },
8569            #[cfg(not(target_arch = "wasm32"))]
8570            "rb_floor" | "刚体落地" | "剛体着地" | "강체바닥" | "พื้นแข็ง" =>
8571            {
8572                let i = self.arg_num(&args, 0, 0.)? as usize;
8573                let fy = self.arg_num(&args, 1, 0.)? as f32;
8574                let rest = self.arg_num(&args, 2, 0.6)? as f32;
8575                let fric = self.arg_num(&args, 3, 0.6)? as f32;
8576                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
8577                    b.bounce_floor(fy, rest, fric);
8578                }
8579                return Ok(Value::Unit);
8580            },
8581            #[cfg(not(target_arch = "wasm32"))]
8582            "rb_gravity" | "刚体重力" | "剛体重力" | "강체중력" | "แรงโน้มถ่วงแข็ง" =>
8583            {
8584                let gx = self.arg_num(&args, 0, 0.)? as f32;
8585                let gy = self.arg_num(&args, 1, 9.81)? as f32;
8586                let gz = self.arg_num(&args, 2, 0.)? as f32;
8587                self.rigid_world.gravity = ling_physics::Vec3::new(gx, gy, gz);
8588                return Ok(Value::Unit);
8589            },
8590            #[cfg(not(target_arch = "wasm32"))]
8591            "rb_step" | "刚体步进" | "剛体更新" | "강체스텝" | "ก้าวแข็ง" =>
8592            {
8593                let dt = self.arg_num(&args, 0, 0.016)? as f32;
8594                self.rigid_world.step(dt);
8595                return Ok(Value::Unit);
8596            },
8597            #[cfg(not(target_arch = "wasm32"))]
8598            "rb_pos" | "刚体位置" | "剛体位置" | "강체위치" | "ตำแหน่งแข็ง" =>
8599            {
8600                let i = self.arg_num(&args, 0, 0.)? as usize;
8601                let p = self
8602                    .rigid_world
8603                    .bodies
8604                    .get(i)
8605                    .map(|b| b.pos)
8606                    .unwrap_or(ling_physics::Vec3::ZERO);
8607                return Ok(Value::List(Rc::new(vec![
8608                    Value::Number(p.x as f64),
8609                    Value::Number(p.y as f64),
8610                    Value::Number(p.z as f64),
8611                ])));
8612            },
8613            #[cfg(not(target_arch = "wasm32"))]
8614            "rb_rot" | "刚体旋转" | "剛体回転" | "강체회전" | "การหมุนแข็ง" =>
8615            {
8616                let i = self.arg_num(&args, 0, 0.)? as usize;
8617                let q = self
8618                    .rigid_world
8619                    .bodies
8620                    .get(i)
8621                    .map(|b| b.orientation)
8622                    .unwrap_or(ling_physics::Quat::IDENTITY);
8623                return Ok(Value::List(Rc::new(vec![
8624                    Value::Number(q.x as f64),
8625                    Value::Number(q.y as f64),
8626                    Value::Number(q.z as f64),
8627                    Value::Number(q.w as f64),
8628                ])));
8629            },
8630
8631            // ── native-res mesh (.lmesh): load once, draw fast (unlit, per-tri colour) ──
8632            #[cfg(not(target_arch = "wasm32"))]
8633            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" =>
8634            {
8635                let path = self.arg_str(&args, 0, "");
8636                let resolved = if std::path::Path::new(&path).exists() {
8637                    path.clone()
8638                } else if let Some(d) = &self.source_dir {
8639                    d.join(&path).to_string_lossy().into_owned()
8640                } else {
8641                    path.clone()
8642                };
8643                let bytes = match std::fs::read(&resolved) {
8644                    Ok(b) => b,
8645                    Err(e) => {
8646                        eprintln!("mesh_load failed ({path}): {e}");
8647                        return Ok(Value::Number(-1.0));
8648                    },
8649                };
8650                if bytes.len() < 16 || &bytes[0..4] != b"LMSH" {
8651                    eprintln!("mesh_load: bad header ({path})");
8652                    return Ok(Value::Number(-1.0));
8653                }
8654                let rd4 =
8655                    |o: usize| -> [u8; 4] { [bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]] };
8656                let height = f32::from_le_bytes(rd4(8));
8657                let ntri = u32::from_le_bytes(rd4(12)) as usize;
8658                let need = 16usize.saturating_add(ntri.saturating_mul(9 * 4 + 3));
8659                if bytes.len() < need {
8660                    eprintln!("mesh_load: truncated ({path})");
8661                    return Ok(Value::Number(-1.0));
8662                }
8663                let mut pos = Vec::with_capacity(ntri * 3);
8664                let mut col = Vec::with_capacity(ntri);
8665                let mut off = 16usize;
8666                for _ in 0..ntri {
8667                    for _k in 0..3 {
8668                        let x = f32::from_le_bytes(rd4(off));
8669                        let y = f32::from_le_bytes(rd4(off + 4));
8670                        let z = f32::from_le_bytes(rd4(off + 8));
8671                        off += 12;
8672                        pos.push([x, y, z]);
8673                    }
8674                    col.push([bytes[off], bytes[off + 1], bytes[off + 2]]);
8675                    off += 3;
8676                }
8677                eprintln!("mesh_load: {} ({} tris, h={:.2})", path, ntri, height);
8678                let id = self.meshes.len();
8679                self.meshes
8680                    .push(crate::gfx::shapes::ColorMesh { pos, col, height });
8681                return Ok(Value::Number(id as f64));
8682            },
8683            #[cfg(target_arch = "wasm32")]
8684            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" =>
8685            {
8686                // Native .lmesh loading is file-system based and not wired for wasm yet.
8687                // Return an invalid handle so scripts can choose a fallback path.
8688                return Ok(Value::Number(-1.0));
8689            },
8690            #[cfg(not(target_arch = "wasm32"))]
8691            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" =>
8692            {
8693                // ('วาดเมช' is taken by draw_mesh — use a distinct Thai alias)
8694                let id = self.arg_num(&args, 0, 0.)? as usize;
8695                let cx = self.arg_num(&args, 1, 0.)? as f32;
8696                let cy = self.arg_num(&args, 2, 0.)? as f32;
8697                let cz = self.arg_num(&args, 3, 0.)? as f32;
8698                let sc = self.arg_num(&args, 4, 1.)? as f32;
8699                let yaw = self.arg_num(&args, 5, 0.)? as f32;
8700                let sway = self.arg_num(&args, 6, 0.)? as f32;
8701                let arm = self.arg_num(&args, 7, 0.)? as f32;
8702                let lean = self.arg_num(&args, 8, 0.)? as f32;
8703                let leg = self.arg_num(&args, 9, 0.)? as f32;
8704                let tuck = self.arg_num(&args, 10, 0.)? as f32;
8705                if id < self.meshes.len() {
8706                    let m = &self.meshes[id];
8707                    let mut gfx = self.gfx.borrow_mut();
8708                    gfx.draw_color_mesh(m, cx, cy, cz, sc, yaw, sway, arm, lean, leg, tuck);
8709                }
8710                return Ok(Value::Unit);
8711            },
8712            #[cfg(target_arch = "wasm32")]
8713            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" =>
8714            {
8715                return Ok(Value::Unit);
8716            },
8717
8718            // ── liquid sim (water + oil, immiscible) ──
8719            "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" =>
8720            {
8721                let w = self.arg_num(&args, 0, 64.)? as usize;
8722                let h = self.arg_num(&args, 1, 64.)? as usize;
8723                let id = self.liquids.len();
8724                self.liquids
8725                    .push(ling_physics::liquid::LiquidGrid::new(w, h));
8726                return Ok(Value::Number(id as f64));
8727            },
8728            "liquid_set_colors" | "液体颜色" | "液体配色" | "액체색상" | "สีของเหลว" =>
8729            {
8730                let id = self.arg_num(&args, 0, 0.)? as usize;
8731                let wr = self.arg_num(&args, 1, 40.)? as f32;
8732                let wg = self.arg_num(&args, 2, 110.)? as f32;
8733                let wb = self.arg_num(&args, 3, 235.)? as f32;
8734                let or_ = self.arg_num(&args, 4, 240.)? as f32;
8735                let og = self.arg_num(&args, 5, 175.)? as f32;
8736                let ob = self.arg_num(&args, 6, 45.)? as f32;
8737                if let Some(g) = self.liquids.get_mut(id) {
8738                    g.set_colors(wr, wg, wb, or_, og, ob);
8739                }
8740                return Ok(Value::Unit);
8741            },
8742            "liquid_splat" | "液体注入" | "液体追加" | "액체분사" | "หยดของเหลว" =>
8743            {
8744                let id = self.arg_num(&args, 0, 0.)? as usize;
8745                let x = self.arg_num(&args, 1, 0.)? as f32;
8746                let y = self.arg_num(&args, 2, 0.)? as f32;
8747                let kind = self.arg_num(&args, 3, 0.)? as i32;
8748                let amt = self.arg_num(&args, 4, 1.0)? as f32;
8749                let rad = self.arg_num(&args, 5, 4.0)? as f32;
8750                if let Some(g) = self.liquids.get_mut(id) {
8751                    g.splat(x, y, kind, amt, rad);
8752                }
8753                return Ok(Value::Unit);
8754            },
8755            "liquid_gravity" | "液体重力" | "液体重力ベクトル" | "액체중력" | "แรงโน้มถ่วงเหลว" =>
8756            {
8757                let id = self.arg_num(&args, 0, 0.)? as usize;
8758                let gx = self.arg_num(&args, 1, 0.)? as f32;
8759                let gy = self.arg_num(&args, 2, 60.)? as f32;
8760                if let Some(g) = self.liquids.get_mut(id) {
8761                    g.set_gravity(gx, gy);
8762                }
8763                return Ok(Value::Unit);
8764            },
8765            "liquid_step" | "液体步进" | "液体更新" | "액체스텝" | "ก้าวของเหลว" =>
8766            {
8767                let id = self.arg_num(&args, 0, 0.)? as usize;
8768                let dt = self.arg_num(&args, 1, 0.016)? as f32;
8769                if let Some(g) = self.liquids.get_mut(id) {
8770                    g.step(dt);
8771                }
8772                return Ok(Value::Unit);
8773            },
8774            // liquid_step_all(dt) — advance EVERY liquid grid one tick, in parallel
8775            // across instances (rayon). Independent grids share no state, so this is
8776            // an embarrassingly-parallel batch: a scene with many liquid surfaces
8777            // steps in one call that scales across cores instead of N serial
8778            // `liquid_step` calls.
8779            "liquid_step_all" | "液体全步进" | "液体全更新" | "전체액체스텝" | "ก้าวของเหลวทั้งหมด" =>
8780            {
8781                let dt = self.arg_num(&args, 0, 0.016)? as f32;
8782                ling_physics::liquid::step_all(&mut self.liquids, dt);
8783                return Ok(Value::Unit);
8784            },
8785            // liquid_rainbow(id, on) — colour the fluid as a flowing ROYGBIV marble
8786            "liquid_rainbow" | "液体彩虹" | "液体虹" | "액체무지개" | "ของเหลวสายรุ้ง" =>
8787            {
8788                let id = self.arg_num(&args, 0, 0.)? as usize;
8789                let on = self.arg_num(&args, 1, 1.0)? > 0.5;
8790                if let Some(g) = self.liquids.get_mut(id) {
8791                    g.rainbow = on;
8792                }
8793                return Ok(Value::Unit);
8794            },
8795            // liquid_mix(id) -> 0 (oil/water separated) .. 1 (fully intermixed)
8796            "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" =>
8797            {
8798                let id = self.arg_num(&args, 0, 0.)? as usize;
8799                let m = self.liquids.get(id).map(|g| g.mix_amount()).unwrap_or(0.0);
8800                return Ok(Value::Number(m as f64));
8801            },
8802            // liquid_draw(id, sx, sy, scale) — fast flat 2-D blit of the colour field
8803            #[cfg(not(target_arch = "wasm32"))]
8804            "liquid_draw" | "绘制液体" | "液体描画" | "액체그리기" | "วาดของเหลว" =>
8805            {
8806                let id = self.arg_num(&args, 0, 0.)? as usize;
8807                let sx = self.arg_num(&args, 1, 0.)? as i32;
8808                let sy = self.arg_num(&args, 2, 0.)? as i32;
8809                let scale = (self.arg_num(&args, 3, 4.)? as i32).max(1);
8810                if id < self.liquids.len() {
8811                    let (gw, gh) = {
8812                        let g = &self.liquids[id];
8813                        (g.w, g.h)
8814                    };
8815                    let mut gfx = self.gfx.borrow_mut();
8816                    let (w, h) = (gfx.width as i32, gfx.height as i32);
8817                    let g = &self.liquids[id];
8818                    for cy in 0..gh {
8819                        for cx in 0..gw {
8820                            let col = g.sample_rgb(cx, cy);
8821                            let bx = sx + cx as i32 * scale;
8822                            let by = sy + cy as i32 * scale;
8823                            for dy in 0..scale {
8824                                for dx in 0..scale {
8825                                    let px = bx + dx;
8826                                    let py = by + dy;
8827                                    if px >= 0 && py >= 0 && px < w && py < h {
8828                                        gfx.buffer[(py * w + px) as usize] = col;
8829                                    }
8830                                }
8831                            }
8832                        }
8833                    }
8834                }
8835                return Ok(Value::Unit);
8836            },
8837            // liquid_draw_surface(id, kind, cx,cy,cz, radius, height)
8838            //   kind: 0 plane · 1 sphere · 2 cylinder · 3 cone · 4 dome
8839            "liquid_draw_surface" | "液体贴面" | "液体曲面" | "액체곡면" | "ของเหลวบนพื้นผิว" =>
8840            {
8841                #[cfg(not(target_arch = "wasm32"))]
8842                {
8843                let id = self.arg_num(&args, 0, 0.)? as usize;
8844                let kind = self.arg_num(&args, 1, 1.)? as i32;
8845                let cx = self.arg_num(&args, 2, 0.)? as f32;
8846                let cy = self.arg_num(&args, 3, 0.)? as f32;
8847                let cz = self.arg_num(&args, 4, 0.)? as f32;
8848                let radius = self.arg_num(&args, 5, 2.0)? as f32;
8849                let height = self.arg_num(&args, 6, 3.0)? as f32;
8850                if id < self.liquids.len() {
8851                    let (gw, gh) = {
8852                        let g = &self.liquids[id];
8853                        (g.w, g.h)
8854                    };
8855                    let mut gfx = self.gfx.borrow_mut();
8856                    let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
8857                    let cam = gfx.camera.clone();
8858                    let near = -cam.zdist + 0.05;
8859                    let g = &self.liquids[id];
8860                    let tau = std::f32::consts::TAU;
8861                    let pi = std::f32::consts::PI;
8862                    // surface point for a (u,v) in [0,1] on the chosen primitive
8863                    let sp = |u: f32, v: f32| -> [f32; 3] {
8864                        if kind == 0 {
8865                            [
8866                                cx + (u - 0.5) * 2.0 * radius,
8867                                cy,
8868                                cz + (v - 0.5) * 2.0 * radius,
8869                            ]
8870                        } else if kind == 2 {
8871                            let th = u * tau;
8872                            [
8873                                cx + th.cos() * radius,
8874                                cy + (v - 0.5) * height,
8875                                cz + th.sin() * radius,
8876                            ]
8877                        } else if kind == 3 {
8878                            let th = u * tau;
8879                            let rr = radius * (1.0 - v);
8880                            [
8881                                cx + th.cos() * rr,
8882                                cy + (v - 0.5) * height,
8883                                cz + th.sin() * rr,
8884                            ]
8885                        } else if kind == 4 {
8886                            let th = u * tau;
8887                            let ph = v * pi * 0.5;
8888                            [
8889                                cx + ph.sin() * th.cos() * radius,
8890                                cy - ph.cos() * radius,
8891                                cz + ph.sin() * th.sin() * radius,
8892                            ]
8893                        } else {
8894                            let th = u * tau;
8895                            let ph = v * pi;
8896                            [
8897                                cx + ph.sin() * th.cos() * radius,
8898                                cy + ph.cos() * radius,
8899                                cz + ph.sin() * th.sin() * radius,
8900                            ]
8901                        }
8902                    };
8903                    let nrm = |u: f32, v: f32| -> [f32; 3] {
8904                        if kind == 0 {
8905                            [0.0, -1.0, 0.0]
8906                        } else if kind == 2 {
8907                            let th = u * tau;
8908                            [th.cos(), 0.0, th.sin()]
8909                        } else if kind == 3 {
8910                            let th = u * tau;
8911                            let s = (radius / height.max(0.01)).atan();
8912                            [th.cos() * s.cos(), s.sin(), th.sin() * s.cos()]
8913                        } else if kind == 4 {
8914                            let th = u * tau;
8915                            let ph = v * pi * 0.5;
8916                            [ph.sin() * th.cos(), -ph.cos(), ph.sin() * th.sin()]
8917                        } else {
8918                            let th = u * tau;
8919                            let ph = v * pi;
8920                            [ph.sin() * th.cos(), ph.cos(), ph.sin() * th.sin()]
8921                        }
8922                    };
8923                    let gwf = gw as f32;
8924                    let ghf = gh as f32;
8925                    let mut cyc = 0usize;
8926                    while cyc < gh {
8927                        let mut cxc = 0usize;
8928                        while cxc < gw {
8929                            // cull by the cell centre's outward normal
8930                            let uc = (cxc as f32 + 0.5) / gwf;
8931                            let vc = (cyc as f32 + 0.5) / ghf;
8932                            let c = sp(uc, vc);
8933                            let n = nrm(uc, vc);
8934                            let dc = cam.depth(c[0], c[1], c[2]);
8935                            if dc > near {
8936                                let cull = kind != 0
8937                                    && cam.depth(
8938                                        c[0] + n[0] * 0.06,
8939                                        c[1] + n[1] * 0.06,
8940                                        c[2] + n[2] * 0.06,
8941                                    ) > dc;
8942                                if !cull {
8943                                    // project the 4 cell corners → a filled AA vector quad
8944                                    let u0 = cxc as f32 / gwf;
8945                                    let u1 = (cxc + 1) as f32 / gwf;
8946                                    let v0 = cyc as f32 / ghf;
8947                                    let v1 = (cyc + 1) as f32 / ghf;
8948                                    let q = [sp(u0, v0), sp(u1, v0), sp(u1, v1), sp(u0, v1)];
8949                                    let mut poly: Vec<[f32; 2]> = Vec::with_capacity(5);
8950                                    let mut ok = true;
8951                                    for p in &q {
8952                                        if cam.depth(p[0], p[1], p[2]) <= near {
8953                                            ok = false;
8954                                            break;
8955                                        }
8956                                        let (sx, sy, _) = cam.project(p[0], p[1], p[2]);
8957                                        poly.push([sx, sy]);
8958                                    }
8959                                    if ok {
8960                                        let p0 = poly[0];
8961                                        poly.push(p0);
8962                                        let col = g.sample_rgb(cxc, cyc);
8963                                        crate::gfx::raster::fill_contours_aa(
8964                                            &mut gfx.buffer,
8965                                            w,
8966                                            h,
8967                                            col,
8968                                            add,
8969                                            std::slice::from_ref(&poly),
8970                                        );
8971                                    }
8972                                }
8973                            }
8974                            cxc += 1;
8975                        }
8976                        cyc += 1;
8977                    }
8978                }
8979                }
8980                #[cfg(target_arch = "wasm32")]
8981                {
8982                    // WASM: liquid_draw_surface is a no-op for now (would need WebGL shader)
8983                    // The liquid simulation still runs, just not rendered to 3D surfaces
8984                }
8985                return Ok(Value::Unit);
8986            },
8987            // sparkle(x, y, w, h, count [, t]) — scatter twinkling vector star-sparkles
8988            // in a rect (snowglobe effect) in the current colour + blend mode.
8989            #[cfg(not(target_arch = "wasm32"))]
8990            "sparkle" | "闪光" | "きらめき" | "반짝임" | "ประกาย" => {
8991                let x = self.arg_num(&args, 0, 0.)? as f32;
8992                let y = self.arg_num(&args, 1, 0.)? as f32;
8993                let ww = self.arg_num(&args, 2, 200.)? as f32;
8994                let hh = self.arg_num(&args, 3, 200.)? as f32;
8995                let count = self.arg_num(&args, 4, 40.)? as i32;
8996                let t = self.arg_num(&args, 5, 0.)? as f32;
8997                let mut gfx = self.gfx.borrow_mut();
8998                let (w, h, add, color) = (gfx.width, gfx.height, gfx.blend == 1, gfx.color);
8999                let (cr, cg, cb) = (
9000                    (color >> 16 & 0xFF) as f32,
9001                    (color >> 8 & 0xFF) as f32,
9002                    (color & 0xFF) as f32,
9003                );
9004                let mut n = 0i32;
9005                while n < count {
9006                    let hsh = (n as u32).wrapping_mul(2654435761).wrapping_add(0x9E3779B9);
9007                    let u = ((hsh >> 8) & 1023) as f32 / 1023.0;
9008                    let v = ((hsh >> 18) & 1023) as f32 / 1023.0;
9009                    let phase = (hsh & 255) as f32 / 255.0;
9010                    let tw = (t * 3.0 + phase * 6.2831 + n as f32).sin() * 0.5 + 0.5;
9011                    let sz = 1.5 + tw * 5.0;
9012                    let px = x + u * ww;
9013                    let py = y + v * hh;
9014                    let b = tw * tw; // sharp twinkle
9015                    let col =
9016                        (((cr * b) as u32) << 16) | (((cg * b) as u32) << 8) | ((cb * b) as u32);
9017                    crate::gfx::raster::draw_line_aa(
9018                        &mut gfx.buffer,
9019                        w,
9020                        h,
9021                        col,
9022                        add,
9023                        px - sz,
9024                        py,
9025                        px + sz,
9026                        py,
9027                    );
9028                    crate::gfx::raster::draw_line_aa(
9029                        &mut gfx.buffer,
9030                        w,
9031                        h,
9032                        col,
9033                        add,
9034                        px,
9035                        py - sz,
9036                        px,
9037                        py + sz,
9038                    );
9039                    let d = sz * 0.55;
9040                    crate::gfx::raster::draw_line_aa(
9041                        &mut gfx.buffer,
9042                        w,
9043                        h,
9044                        col,
9045                        add,
9046                        px - d,
9047                        py - d,
9048                        px + d,
9049                        py + d,
9050                    );
9051                    crate::gfx::raster::draw_line_aa(
9052                        &mut gfx.buffer,
9053                        w,
9054                        h,
9055                        col,
9056                        add,
9057                        px - d,
9058                        py + d,
9059                        px + d,
9060                        py - d,
9061                    );
9062                    n += 1;
9063                }
9064                return Ok(Value::Unit);
9065            },
9066
9067            // ══════════════════════════════════════════════════════════════════
9068            // DIALOG BUILTINS  (crates/ling-game/src/dialog.rs) — cinematic,
9069            // typed-out, colour-coded text boxes. Markup: {n}name{/} {p}place{/}
9070            // {i}item{/}, \n newline, || page break.
9071            // ══════════════════════════════════════════════════════════════════
9072            #[cfg(not(target_arch = "wasm32"))]
9073            "dialog_show" | "对话显示" | "会話表示" | "대화표시" | "แสดงบทสนทนา" =>
9074            {
9075                let text = self.arg_str(&args, 0, "");
9076                let cps = self.arg_num(&args, 1, 32.0)? as f32;
9077                self.dialog = Some(ling_game::dialog::Dialog::new(&text, cps));
9078                return Ok(Value::Unit);
9079            },
9080            #[cfg(not(target_arch = "wasm32"))]
9081            "dialog_step" | "对话步进" | "会話更新" | "대화스텝" | "ก้าวบทสนทนา" =>
9082            {
9083                let dt = self.arg_num(&args, 0, 0.016)? as f32;
9084                if let Some(d) = self.dialog.as_mut() {
9085                    d.update(dt);
9086                }
9087                return Ok(Value::Unit);
9088            },
9089            #[cfg(not(target_arch = "wasm32"))]
9090            "dialog_advance" | "对话推进" | "会話送り" | "대화진행" | "เลื่อนบทสนทนา" =>
9091            {
9092                if let Some(d) = self.dialog.as_mut() {
9093                    d.advance();
9094                }
9095                return Ok(Value::Unit);
9096            },
9097            #[cfg(not(target_arch = "wasm32"))]
9098            "dialog_active" | "对话激活" | "会話中" | "대화중" | "บทสนทนาทำงาน" =>
9099            {
9100                let a = self
9101                    .dialog
9102                    .as_ref()
9103                    .map(|d| !d.is_closed())
9104                    .unwrap_or(false);
9105                return Ok(Value::Bool(a));
9106            },
9107            #[cfg(not(target_arch = "wasm32"))]
9108            "dialog_typing" | "对话打字" | "会話タイプ中" | "대화타이핑" | "กำลังพิมพ์บทสนทนา" =>
9109            {
9110                use ling_game::dialog::Dialog;
9111
9112                let a = self
9113                    .dialog
9114                    .as_ref()
9115                    .map(|d: &Dialog| !d.is_closed() && d.is_typing())
9116                    .unwrap_or(false);
9117                return Ok(Value::Bool(a));
9118            },
9119            #[cfg(not(target_arch = "wasm32"))]
9120            "dialog_close" | "对话关闭" | "会話閉じる" | "대화닫기" | "ปิดบทสนทนา" =>
9121            {
9122                self.dialog = None;
9123                return Ok(Value::Unit);
9124            },
9125            // dialog_color(role, r, g, b) — role: 0 text · 1 name · 2 place · 3 item
9126            #[cfg(not(target_arch = "wasm32"))]
9127            "dialog_color" | "对话颜色" | "会話色" | "대화색" | "สีบทสนทนา" =>
9128            {
9129                let role = (self.arg_num(&args, 0, 0.0)? as usize).min(3);
9130                let r = self.arg_num(&args, 1, 255.0)? as u32 & 0xFF;
9131                let g = self.arg_num(&args, 2, 255.0)? as u32 & 0xFF;
9132                let b = self.arg_num(&args, 3, 255.0)? as u32 & 0xFF;
9133                self.dialog_colors[role] = (r << 16) | (g << 8) | b;
9134                return Ok(Value::Unit);
9135            },
9136            // dialog_draw(x, y, w, h [, font_handle]) — draw the box + typed text
9137            #[cfg(not(target_arch = "wasm32"))]
9138            "dialog_draw" | "对话绘制" | "会話描画" | "대화그리기" | "วาดบทสนทนา" =>
9139            {
9140                let x = self.arg_num(&args, 0, 40.0)? as f32;
9141                let y = self.arg_num(&args, 1, 0.0)? as f32;
9142                let ww = self.arg_num(&args, 2, 720.0)? as f32;
9143                let hh = self.arg_num(&args, 3, 150.0)? as f32;
9144                let font = self.arg_num(&args, 4, -1.0)? as i64;
9145                let t = (crate::runtime::now_secs() - self.start_time_secs) as f32;
9146                self.render_dialog(x, y, ww, hh, font, t);
9147                return Ok(Value::Unit);
9148            },
9149
9150            // text_poll() — fold newly-typed keys into the input buffer, return it
9151            #[cfg(not(target_arch = "wasm32"))]
9152            "text_poll" => {
9153                let keys = {
9154                    let gfx = self.gfx.borrow();
9155                    gfx.window
9156                        .as_ref()
9157                        .map(|w| w.get_keys_pressed(minifb::KeyRepeat::No))
9158                        .unwrap_or_default()
9159                };
9160                for k in keys {
9161                    if k == minifb::Key::Backspace {
9162                        self.text_buffer.pop();
9163                    } else if let Some(c) = key_char(k) {
9164                        self.text_buffer.push(c);
9165                    }
9166                }
9167                return Ok(Value::Str(self.text_buffer.clone()));
9168            },
9169            #[cfg(target_arch = "wasm32")]
9170            "text_poll" => {
9171                return Ok(Value::Str(self.text_buffer.clone()));
9172            },
9173            "text_get" => return Ok(Value::Str(self.text_buffer.clone())),
9174            "text_set" => {
9175                self.text_buffer = self.arg_str(&args, 0, "");
9176                return Ok(Value::Unit);
9177            },
9178            "text_clear" => {
9179                self.text_buffer.clear();
9180                return Ok(Value::Unit);
9181            },
9182            // record_frame() — append the current framebuffer as a PPM, return frame #
9183            #[cfg(not(target_arch = "wasm32"))]
9184            "record_frame" => {
9185                let n = self.record_n;
9186                let (buf, w, h) = {
9187                    let gfx = self.gfx.borrow();
9188                    (gfx.buffer.clone(), gfx.width, gfx.height)
9189                };
9190                let _ = std::fs::create_dir_all("recordings");
9191                let mut out = Vec::with_capacity(w * h * 3 + 32);
9192                out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes());
9193                for px in &buf {
9194                    let p = *px;
9195                    out.push((p >> 16) as u8);
9196                    out.push((p >> 8) as u8);
9197                    out.push(p as u8);
9198                }
9199                let _ = std::fs::write(format!("recordings/frame_{n:05}.ppm"), out);
9200                self.record_n += 1;
9201                return Ok(Value::Number(n as f64));
9202            },
9203            "record_count" => return Ok(Value::Number(self.record_n as f64)),
9204            // ── screenshot(mode) → PNG in ./screenshots/ with timestamp + mode + size ──
9205            #[cfg(not(target_arch = "wasm32"))]
9206            "screenshot" | "บันทึกภาพ" => {
9207                let mode = self.arg_str(&args, 0, "game");
9208                let (buf, w, h) = {
9209                    let gfx = self.gfx.borrow();
9210                    (gfx.buffer.clone(), gfx.width, gfx.height)
9211                };
9212                let _ = std::fs::create_dir_all("screenshots");
9213                let ts = std::time::SystemTime::now()
9214                    .duration_since(std::time::UNIX_EPOCH)
9215                    .map(|d| d.as_secs())
9216                    .unwrap_or(0);
9217                let safe: String = mode
9218                    .chars()
9219                    .map(|c| if c.is_alphanumeric() { c } else { '_' })
9220                    .collect();
9221                let path = format!("screenshots/ss_{ts}_{safe}_{w}x{h}.png");
9222                let mut rgb = Vec::with_capacity(w * h * 3);
9223                for px in &buf {
9224                    let p = *px;
9225                    rgb.push((p >> 16) as u8);
9226                    rgb.push((p >> 8) as u8);
9227                    rgb.push(p as u8);
9228                }
9229                if let Some(img) = image::RgbImage::from_raw(w as u32, h as u32, rgb) {
9230                    let _ = img.save(&path);
9231                }
9232                return Ok(Value::Str(path));
9233            },
9234            // ── microphone → crypto donut ──
9235            // mic_capture() — append the latest mic samples to the record buffer
9236            // (call each frame while recording). Returns the buffer length.
9237            #[cfg(not(target_arch = "wasm32"))]
9238            "mic_capture" => {
9239                if let Some(mic) = self.mic.as_ref() {
9240                    let s = mic.latest_samples();
9241                    self.mic_buffer.extend_from_slice(&s);
9242                    let cap = 96_000usize; // ~2 s @ 48 kHz
9243                    if self.mic_buffer.len() > cap {
9244                        let drop = self.mic_buffer.len() - cap;
9245                        self.mic_buffer.drain(0..drop);
9246                    }
9247                }
9248                return Ok(Value::Number(self.mic_buffer.len() as f64));
9249            },
9250            // mic_seed() — SHA3-256 hex of the recorded audio, usable as a donut seed
9251            #[cfg(not(target_arch = "wasm32"))]
9252            "mic_seed" => {
9253                let mut bytes = Vec::with_capacity(self.mic_buffer.len() * 4);
9254                for f in &self.mic_buffer {
9255                    bytes.extend_from_slice(&f.to_le_bytes());
9256                }
9257                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(&bytes))));
9258            },
9259            #[cfg(not(target_arch = "wasm32"))]
9260            "mic_clear" => {
9261                self.mic_buffer.clear();
9262                return Ok(Value::Number(0.0));
9263            },
9264            // flush the 3-D depth queue onto the framebuffer WITHOUT presenting,
9265            // so 2-D UI drawn afterwards overlays the 3-D scene.
9266            #[cfg(not(target_arch = "wasm32"))]
9267            "flush_3d" | "render_3d" => {
9268                let mut gfx = self.gfx.borrow_mut();
9269                if !gfx.depth_queue.is_empty() {
9270                    let w = gfx.width;
9271                    let h = gfx.height;
9272                    let dt = gfx.depth_test;
9273                    let reset_z = gfx.zbuf_needs_clear;
9274                    let (bm, ba) = (gfx.blend, gfx.alpha);
9275                    let queue = std::mem::take(&mut gfx.depth_queue);
9276                    {
9277                        let g = &mut *gfx;
9278                        let z = if dt { Some(&mut g.depth_buf) } else { None };
9279                        queue.flush(&mut g.buffer, z, reset_z, w, h);
9280                    }
9281                    gfx.zbuf_needs_clear = false;
9282                    gfx.depth_queue.set_state(bm, ba); // keep active blend/alpha across the mid-frame flush
9283                }
9284                return Ok(Value::Unit);
9285            },
9286            #[cfg(target_arch = "wasm32")]
9287            "flush_3d" | "render_3d" => {
9288                let mut gfx = self.gfx.borrow_mut();
9289                if !gfx.depth_queue.is_empty() {
9290                    let w = gfx.width;
9291                    let h = gfx.height;
9292                    let dt = gfx.depth_test;
9293                    let reset_z = gfx.zbuf_needs_clear;
9294                    let (bm, ba) = (gfx.blend, gfx.alpha);
9295                    let queue = std::mem::take(&mut gfx.depth_queue);
9296                    {
9297                        let g = &mut *gfx;
9298                        let z = if dt { Some(&mut g.depth_buf) } else { None };
9299                        queue.flush(&mut g.buffer, z, reset_z, w, h);
9300                    }
9301                    gfx.zbuf_needs_clear = false;
9302                    gfx.depth_queue.set_state(bm, ba);
9303                }
9304                return Ok(Value::Unit);
9305            },
9306
9307            // Viscous full-screen distortion (warp/pucker/bloat, edge-wrapped). Call
9308            // after the 3-D flush and before the UI so only the world layer warps.
9309            #[cfg(not(target_arch = "wasm32"))]
9310            "screen_distort" | "บิดจอ" | "屏幕扭曲" | "画面歪み" | "화면왜곡" =>
9311            {
9312                let amount = self.arg_num(&args, 0, 8.0)? as f32;
9313                let t = self.arg_num(&args, 1, 0.0)? as f32;
9314                // optional `step` (default 1 = full res): 2 = half-res block warp
9315                // (~4× fewer warp computes, slightly softer — suits a liquid look).
9316                let step = self.arg_num(&args, 2, 1.0)?.max(1.0) as usize;
9317                let _d = std::time::Instant::now();
9318                self.gfx.borrow_mut().distort(amount, t, step);
9319                ling_phase_add(phase::DISTORT, _d.elapsed().as_nanos());
9320                return Ok(Value::Unit);
9321            },
9322
9323            "set_rim" | "设置边缘光" | "リム設定" | "림라이트" | "ตั้งขอบเรือง" =>
9324            {
9325                let s = self.arg_num(&args, 0, 0.6)? as f32;
9326                let r = self.arg_num(&args, 1, 115.)? as f32 / 255.0;
9327                let g = self.arg_num(&args, 2, 217.)? as f32 / 255.0;
9328                let b = self.arg_num(&args, 3, 255.)? as f32 / 255.0;
9329                let mut gfx = self.gfx.borrow_mut();
9330                gfx.shade.rim = s;
9331                gfx.shade.rim_color = [r, g, b];
9332                return Ok(Value::Unit);
9333            },
9334
9335            // ══════════════════════════════════════════════════════════════════
9336            // 3-D PRIMITIVES  (src/gfx/shapes.rs)  — "Inkscape for 3-D"
9337            //   shape(cx,cy,cz,  sx,sy,sz,  rx,ry,rz,  mode,  e0,e1,e2)
9338            //     centre (cx,cy,cz), per-axis scale, Euler rotation (radians),
9339            //     mode: 0 filled · 1 wireframe · 2 both,
9340            //     e0..e2: shape-specific (segments / sides / ratio …).
9341            //   Pen colour (set_color) drives fill lighting and wireframe colour.
9342            // ══════════════════════════════════════════════════════════════════
9343            n if crate::gfx::shapes::canon(n).is_some() => {
9344                let kind = crate::gfx::shapes::canon(n).unwrap();
9345                let cx = self.arg_num(&args, 0, 0.)? as f32;
9346                let cy = self.arg_num(&args, 1, 0.)? as f32;
9347                let cz = self.arg_num(&args, 2, 0.)? as f32;
9348                let sx = self.arg_num(&args, 3, 1.)? as f32;
9349                let sy = self.arg_num(&args, 4, 1.)? as f32;
9350                let sz = self.arg_num(&args, 5, 1.)? as f32;
9351                let rx = self.arg_num(&args, 6, 0.)? as f32;
9352                let ry = self.arg_num(&args, 7, 0.)? as f32;
9353                let rz = self.arg_num(&args, 8, 0.)? as f32;
9354                let mode = self.arg_num(&args, 9, 0.)? as i32;
9355                let e0 = self.arg_num(&args, 10, 0.)? as f32;
9356                let e1 = self.arg_num(&args, 11, 0.)? as f32;
9357                let e2 = self.arg_num(&args, 12, 0.)? as f32;
9358                if let Some(mesh) = crate::gfx::shapes::build(
9359                    kind,
9360                    [cx, cy, cz, sx, sy, sz, rx, ry, rz],
9361                    e0,
9362                    e1,
9363                    e2,
9364                ) {
9365                    let mut gfx = self.gfx.borrow_mut();
9366                    gfx.emit_mesh(&mesh, mode);
9367                }
9368                return Ok(Value::Unit);
9369            },
9370
9371            _ => {},
9372        }
9373
9374        // `form` struct constructor: positional `Name(v0, v1, ...)`.
9375        if let Some(field_names) = self.structs.get(name).cloned() {
9376            if args.len() != field_names.len() {
9377                return Err(EvalErr::from(format!(
9378                    "{name} expects {} field(s), got {}",
9379                    field_names.len(),
9380                    args.len()
9381                )));
9382            }
9383            let fields = field_names.into_iter().zip(args).collect();
9384            return Ok(Value::Struct { name: name.to_string(), fields });
9385        }
9386
9387        // `choose` enum variant constructor: `Variant(...)` or `Enum::Variant(...)`.
9388        if let Some((enum_name, arity)) = self.enum_variants.get(name).cloned() {
9389            if args.len() != arity {
9390                return Err(EvalErr::from(format!(
9391                    "{name} expects {arity} value(s), got {}",
9392                    args.len()
9393                )));
9394            }
9395            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
9396            return Ok(Value::Variant { enum_name, variant, payload: args });
9397        }
9398
9399        #[cfg(target_arch = "wasm32")]
9400        if let Some(v) = wasm_unsupported_builtin(name) {
9401            return Ok(v);
9402        }
9403
9404        Err(EvalErr::from(format!("unknown function '{name}'")))
9405    }
9406
9407    fn call_value(&mut self, v: Value, args: Vec<Value>) -> EvalResult {
9408        match v {
9409            Value::Fn(params, body, mut captured) => {
9410                for (p, a) in params.iter().zip(args) {
9411                    captured.insert(p.clone(), a);
9412                }
9413                match self.framed("<closure>", |me| me.exec_block(&body, &mut captured)) {
9414                    Ok(v) => Ok(v.unwrap_or(Value::Unit)),
9415                    Err(EvalErr::Return(v)) => Ok(v),
9416                    Err(e) => Err(e),
9417                }
9418            },
9419            other => Err(EvalErr::from(format!("cannot call {:?}", other))),
9420        }
9421    }
9422
9423    fn call_method(&self, recv: Value, method: &str, args: Vec<Value>) -> EvalResult {
9424        match (&recv, method) {
9425            (Value::Str(s), "is_empty" | "是空") => Ok(Value::Bool(s.is_empty())),
9426            (Value::Str(s), "len" | "长") => Ok(Value::Number(s.len() as f64)),
9427            (Value::Str(s), "to_string" | "转文") => Ok(Value::Str(s.clone())),
9428            (Value::Str(s), "contains" | "包含") => {
9429                if let Some(Value::Str(sub)) = args.first() {
9430                    Ok(Value::Bool(s.contains(sub.as_str())))
9431                } else {
9432                    Ok(Value::Bool(false))
9433                }
9434            },
9435            (Value::Str(s), "push_str" | "推_文") => {
9436                let mut s2 = s.clone();
9437                if let Some(Value::Str(a)) = args.first() {
9438                    s2.push_str(a);
9439                }
9440                Ok(Value::Str(s2))
9441            },
9442            (Value::List(v), "len" | "长") => Ok(Value::Number(v.len() as f64)),
9443            (Value::List(v), "push" | "推") => {
9444                let mut v2: Vec<Value> = (**v).clone();
9445                if let Some(a) = args.first() {
9446                    v2.push(a.clone());
9447                }
9448                Ok(Value::List(Rc::new(v2)))
9449            },
9450            // `form` field access: `point.x` (no-arg method == field read).
9451            (Value::Struct { fields, .. }, _) if args.is_empty() => fields
9452                .iter()
9453                .find(|(k, _)| k == method)
9454                .map(|(_, v)| v.clone())
9455                .ok_or_else(|| EvalErr::from(format!("no field '{method}' on {recv}"))),
9456            // Enum introspection: `.tag` → variant name, `.is(Name)` not needed for now.
9457            (Value::Variant { variant, .. }, "tag" | "标签" | "タグ" | "태그" | "ป้าย")
9458                if args.is_empty() =>
9459            {
9460                Ok(Value::Str(variant.clone()))
9461            },
9462            (Value::Ok(inner), _) | (Value::Err(inner), _) => Ok(*inner.clone()),
9463            _ => Err(EvalErr::from(format!("no method '{method}' on {recv}"))),
9464        }
9465    }
9466
9467    // ─── Pattern matching ─────────────────────────────────────────────────────
9468
9469    fn match_pattern(&self, pat: &Pattern, val: &Value) -> Option<Env> {
9470        match (pat, val) {
9471            (Pattern::Wildcard, _) => Some(new_env()),
9472            (Pattern::Str(s), Value::Str(v)) if s == v => Some(new_env()),
9473            (Pattern::Number(n), Value::Number(v)) if (n - v).abs() < 1e-12 => Some(new_env()),
9474            (Pattern::Bool(b), Value::Bool(v)) if b == v => Some(new_env()),
9475            (Pattern::Ident(name), _) => {
9476                let mut e = new_env();
9477                e.insert(name.clone(), val.clone());
9478                Some(e)
9479            },
9480            (Pattern::Constructor(ctor, inner_pat), _) => {
9481                let (matches, inner_val) = match (ctor.as_str(), val) {
9482                    ("ok" | "好", Value::Ok(v)) => (true, Some(v.as_ref().clone())),
9483                    ("bad" | "坏", Value::Err(v)) => (true, Some(v.as_ref().clone())),
9484                    ("ok" | "好", v) if !matches!(v, Value::Err(_)) => (true, Some(v.clone())),
9485                    _ => (false, None),
9486                };
9487                if !matches {
9488                    return None;
9489                }
9490                match (inner_pat, inner_val) {
9491                    (Some(p), Some(v)) => self.match_pattern(p, &v),
9492                    (None, _) => Some(new_env()),
9493                    (Some(p), None) => self.match_pattern(p, &Value::Unit),
9494                }
9495            },
9496            // User enum variant pattern: `Circle(r)`, `Pair(a, b)`, nullary `Origin()`.
9497            (Pattern::Variant(vname, sub_pats), Value::Variant { variant, payload, .. }) => {
9498                if vname != variant || sub_pats.len() != payload.len() {
9499                    return None;
9500                }
9501                let mut bindings = new_env();
9502                for (p, v) in sub_pats.iter().zip(payload.iter()) {
9503                    bindings.extend(self.match_pattern(p, v)?);
9504                }
9505                Some(bindings)
9506            },
9507            // A zero-payload variant pattern also matches the bare result-style `ok`/`bad`
9508            // values so `Ok()`-style patterns keep working uniformly.
9509            (Pattern::Variant(vname, sub), Value::Ok(v)) if (vname == "ok" || vname == "好") => {
9510                match sub.as_slice() {
9511                    [] => Some(new_env()),
9512                    [p] => self.match_pattern(p, v),
9513                    _ => None,
9514                }
9515            },
9516            (Pattern::Variant(vname, sub), Value::Err(v))
9517                if (vname == "bad" || vname == "坏" || vname == "err") =>
9518            {
9519                match sub.as_slice() {
9520                    [] => Some(new_env()),
9521                    [p] => self.match_pattern(p, v),
9522                    _ => None,
9523                }
9524            },
9525            _ => None,
9526        }
9527    }
9528
9529    // ─── Utilities ───────────────────────────────────────────────────────────
9530
9531    fn value_to_iter(&self, val: Value) -> Result<Vec<Value>, EvalErr> {
9532        match val {
9533            Value::List(v) => Ok(Rc::try_unwrap(v).unwrap_or_else(|rc| (*rc).clone())),
9534            Value::Str(s) => Ok(s.chars().map(|c| Value::Str(c.to_string())).collect()),
9535            Value::Number(n) => Ok((0..n as i64).map(|i| Value::Number(i as f64)).collect()),
9536            other => Err(EvalErr::from(format!("cannot iterate over {:?}", other))),
9537        }
9538    }
9539
9540    pub(crate) fn is_truthy(&self, val: &Value) -> bool {
9541        match val {
9542            Value::Bool(b) => *b,
9543            Value::Unit => false,
9544            Value::Number(n) => *n != 0.0,
9545            Value::Str(s) => !s.is_empty(),
9546            Value::List(v) => !v.is_empty(),
9547            Value::Ok(_) => true,
9548            Value::Err(_) => false,
9549            Value::Fn(_, _, _) => true,
9550            Value::Struct { .. } => true,
9551            Value::Variant { .. } => true,
9552        }
9553    }
9554
9555    fn to_number(&self, val: &Value) -> Result<f64, EvalErr> {
9556        match val {
9557            Value::Number(n) => Ok(*n),
9558            Value::Str(s) => s
9559                .parse()
9560                .map_err(|_| EvalErr::from(format!("cannot convert '{s}' to number"))),
9561            other => Err(EvalErr::from(format!("expected number, got {:?}", other))),
9562        }
9563    }
9564
9565    /// Get the n-th argument as f64, falling back to `default` if missing.
9566    fn arg_num(&self, args: &[Value], n: usize, default: f64) -> Result<f64, EvalErr> {
9567        match args.get(n) {
9568            Some(v) => self.to_number(v),
9569            None => Ok(default),
9570        }
9571    }
9572
9573    fn arg_str(&self, args: &[Value], n: usize, default: &str) -> String {
9574        args.get(n)
9575            .map(|v| v.to_string())
9576            .unwrap_or_else(|| default.to_string())
9577    }
9578
9579    /// Read a list-of-numbers argument as `Vec<f32>` (empty if absent/not a list).
9580    #[allow(dead_code)]
9581    fn arg_list_f32(&self, args: &[Value], n: usize) -> Vec<f32> {
9582        match args.get(n) {
9583            Some(Value::List(v)) => v
9584                .iter()
9585                .filter_map(|x| match x {
9586                    Value::Number(n) => Some(*n as f32),
9587                    _ => None,
9588                })
9589                .collect(),
9590            _ => Vec::new(),
9591        }
9592    }
9593
9594    /// Optional `r,g,b` colour override starting at arg `i` → packed 0x00RRGGBB,
9595    /// or `default` if those three numeric args aren't present.
9596    #[cfg(not(target_arch = "wasm32"))]
9597    fn color_at(&self, args: &[Value], i: usize, default: u32) -> u32 {
9598        match (args.get(i), args.get(i + 1), args.get(i + 2)) {
9599            (Some(a), Some(b), Some(c)) => {
9600                match (self.to_number(a), self.to_number(b), self.to_number(c)) {
9601                    (Ok(r), Ok(g), Ok(bl)) => {
9602                        ((r as u32 & 0xFF) << 16) | ((g as u32 & 0xFF) << 8) | (bl as u32 & 0xFF)
9603                    },
9604                    _ => default,
9605                }
9606            },
9607            _ => default,
9608        }
9609    }
9610
9611    /// A pitch argument: a note-name string (`"C4"`, `"A#3"`) or a numeric MIDI value.
9612    #[cfg(not(target_arch = "wasm32"))]
9613    fn pitch_arg(&self, args: &[Value], i: usize, default: i32) -> i32 {
9614        match args.get(i) {
9615            Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(default),
9616            Some(Value::Number(n)) => *n as i32,
9617            _ => default,
9618        }
9619    }
9620
9621    /// Current mouse position + left-button-down (native window only).
9622    #[cfg(not(target_arch = "wasm32"))]
9623    fn mouse_now(&self) -> (f32, f32, bool) {
9624        let gfx = self.gfx.borrow();
9625        let (mx, my) = gfx
9626            .window
9627            .as_ref()
9628            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
9629            .unwrap_or((0.0, 0.0));
9630        let down = gfx
9631            .window
9632            .as_ref()
9633            .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
9634            .unwrap_or(false);
9635        (mx, my, down)
9636    }
9637
9638    /// Rasterize a UI [`ling_ui::widgets::Draw`] into the framebuffer: filled
9639    /// polygons via the AA scanline fill, polylines via AA lines, honouring the
9640    /// current blend mode.
9641    #[cfg(not(target_arch = "wasm32"))]
9642    fn draw_ui(&self, d: &ling_ui::widgets::Draw) {
9643        let mut gfx = self.gfx.borrow_mut();
9644        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
9645        for (c, poly) in &d.fills {
9646            crate::gfx::raster::fill_contours_aa(
9647                &mut gfx.buffer,
9648                w,
9649                h,
9650                *c,
9651                add,
9652                std::slice::from_ref(poly),
9653            );
9654        }
9655        for (c, pl) in &d.strokes {
9656            for s in pl.windows(2) {
9657                crate::gfx::raster::draw_line_aa(
9658                    &mut gfx.buffer,
9659                    w,
9660                    h,
9661                    *c,
9662                    add,
9663                    s[0][0],
9664                    s[0][1],
9665                    s[1][0],
9666                    s[1][1],
9667                );
9668            }
9669        }
9670    }
9671
9672    /// Parse (dst_x, dst_y, width, height) from the first four args of a tex_* builtin.
9673    fn tex_rect(&self, args: &[Value]) -> Result<(usize, usize, usize, usize), EvalErr> {
9674        let tx = self.arg_num(args, 0, 0.0)? as usize;
9675        let ty = self.arg_num(args, 1, 0.0)? as usize;
9676        let tw = self.arg_num(args, 2, 256.0)? as usize;
9677        let th = self.arg_num(args, 3, 256.0)? as usize;
9678        Ok((tx, ty, tw.max(1), th.max(1)))
9679    }
9680
9681    pub(crate) fn apply_binop(&self, op: &BinOp, l: Value, r: Value) -> EvalResult {
9682        match op {
9683            BinOp::Add => match (l, r) {
9684                (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
9685                (Value::Str(a), Value::Str(b)) => Ok(Value::Str(a + &b)),
9686                (Value::Str(a), b) => Ok(Value::Str(a + &b.to_string())),
9687                (a, Value::Str(b)) => Ok(Value::Str(a.to_string() + &b)),
9688                (a, b) => Err(EvalErr::from(format!("cannot add {:?} and {:?}", a, b))),
9689            },
9690            BinOp::Sub => Ok(Value::Number(self.to_number(&l)? - self.to_number(&r)?)),
9691            BinOp::Mul => Ok(Value::Number(self.to_number(&l)? * self.to_number(&r)?)),
9692            BinOp::Div => Ok(Value::Number(self.to_number(&l)? / self.to_number(&r)?)),
9693            BinOp::Rem => Ok(Value::Number(self.to_number(&l)? % self.to_number(&r)?)),
9694            BinOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
9695            BinOp::Ne => Ok(Value::Bool(!values_equal(&l, &r))),
9696            BinOp::Lt => Ok(Value::Bool(self.to_number(&l)? < self.to_number(&r)?)),
9697            BinOp::Gt => Ok(Value::Bool(self.to_number(&l)? > self.to_number(&r)?)),
9698            BinOp::Le => Ok(Value::Bool(self.to_number(&l)? <= self.to_number(&r)?)),
9699            BinOp::Ge => Ok(Value::Bool(self.to_number(&l)? >= self.to_number(&r)?)),
9700            BinOp::And => Ok(Value::Bool(self.is_truthy(&l) && self.is_truthy(&r))),
9701            BinOp::Or => Ok(Value::Bool(self.is_truthy(&l) || self.is_truthy(&r))),
9702        }
9703    }
9704
9705    fn builtin_format(&self, args: &[Value]) -> Result<String, EvalErr> {
9706        if args.is_empty() {
9707            return Ok(String::new());
9708        }
9709        let fmt = match &args[0] {
9710            Value::Str(s) => s.clone(),
9711            other => return Ok(other.to_string()),
9712        };
9713
9714        let mut result = String::new();
9715        let mut arg_idx = 1usize;
9716        let mut chars = fmt.chars().peekable();
9717        while let Some(c) = chars.next() {
9718            if c == '{' {
9719                if chars.peek() == Some(&'}') {
9720                    chars.next();
9721                    if arg_idx < args.len() {
9722                        result.push_str(&args[arg_idx].to_string());
9723                        arg_idx += 1;
9724                    }
9725                } else {
9726                    let mut spec = String::new();
9727                    for ch in chars.by_ref() {
9728                        if ch == '}' {
9729                            break;
9730                        }
9731                        spec.push(ch);
9732                    }
9733                    if arg_idx < args.len() {
9734                        if spec.starts_with(":.") {
9735                            if let Value::Number(n) = &args[arg_idx] {
9736                                let prec: usize =
9737                                    spec[2..].trim_end_matches('f').parse().unwrap_or(2);
9738                                result.push_str(&format!("{:.prec$}", n));
9739                                arg_idx += 1;
9740                                continue;
9741                            }
9742                        }
9743                        result.push_str(&args[arg_idx].to_string());
9744                        arg_idx += 1;
9745                    }
9746                }
9747            } else {
9748                result.push(c);
9749            }
9750        }
9751        Ok(result)
9752    }
9753}
9754
9755#[cfg(not(target_arch = "wasm32"))]
9756/// Map a friendly button name (any vendor / d-pad alias) to a gamepad button.
9757#[cfg(not(target_arch = "wasm32"))]
9758fn parse_pad_button(name: &str) -> Option<ling_input::GamepadButton> {
9759    use ling_input::GamepadButton as B;
9760    Some(match name.to_ascii_lowercase().as_str() {
9761        "a" | "south" | "cross" => B::South,
9762        "b" | "east" | "circle" => B::East,
9763        "x" | "west" | "square" => B::West,
9764        "y" | "north" | "triangle" => B::North,
9765        "lb" | "l1" | "left_shoulder" => B::LeftShoulder,
9766        "rb" | "r1" | "right_shoulder" => B::RightShoulder,
9767        "lt" | "l2" | "left_trigger" => B::LeftTrigger,
9768        "rt" | "r2" | "right_trigger" => B::RightTrigger,
9769        "start" | "menu" | "options" => B::Start,
9770        "select" | "back" | "share" | "view" => B::Select,
9771        "guide" | "home" => B::Guide,
9772        "l3" | "left_stick" => B::LeftStick,
9773        "r3" | "right_stick" => B::RightStick,
9774        "up" | "dpad_up" => B::DpadUp,
9775        "down" | "dpad_down" => B::DpadDown,
9776        "left" | "dpad_left" => B::DpadLeft,
9777        "right" | "dpad_right" => B::DpadRight,
9778        _ => return None,
9779    })
9780}
9781
9782#[cfg(not(target_arch = "wasm32"))]
9783fn str_to_minifb_key(name: &str) -> Option<minifb::Key> {
9784    use minifb::Key;
9785    Some(match name {
9786        "numpad0" | "kp0" => Key::NumPad0,
9787        "numpad1" | "kp1" => Key::NumPad1,
9788        "numpad2" | "kp2" => Key::NumPad2,
9789        "numpad3" | "kp3" => Key::NumPad3,
9790        "numpad4" | "kp4" => Key::NumPad4,
9791        "numpad5" | "kp5" => Key::NumPad5,
9792        "numpad6" | "kp6" => Key::NumPad6,
9793        "numpad7" | "kp7" => Key::NumPad7,
9794        "numpad8" | "kp8" => Key::NumPad8,
9795        "numpad9" | "kp9" => Key::NumPad9,
9796        "numpad+" | "kp+" => Key::NumPadPlus,
9797        "numpad-" | "kp-" => Key::NumPadMinus,
9798        "numpad*" | "kp*" => Key::NumPadAsterisk,
9799        "numpad/" | "kp/" => Key::NumPadSlash,
9800        "left" => Key::Left,
9801        "right" => Key::Right,
9802        "up" => Key::Up,
9803        "down" => Key::Down,
9804        "space" => Key::Space,
9805        "enter" => Key::Enter,
9806        "escape" => Key::Escape,
9807        "pageup" => Key::PageUp,
9808        "pagedown" => Key::PageDown,
9809        "lshift" | "leftshift" => Key::LeftShift,
9810        "rshift" | "rightshift" => Key::RightShift,
9811        "lctrl" | "leftctrl" => Key::LeftCtrl,
9812        "rctrl" | "rightctrl" => Key::RightCtrl,
9813        "lalt" | "leftalt" => Key::LeftAlt,
9814        "ralt" | "rightalt" => Key::RightAlt,
9815        "tab" => Key::Tab,
9816        "backspace" => Key::Backspace,
9817        "delete" => Key::Delete,
9818        "insert" => Key::Insert,
9819        "home" => Key::Home,
9820        "end" => Key::End,
9821        "a" => Key::A,
9822        "b" => Key::B,
9823        "c" => Key::C,
9824        "d" => Key::D,
9825        "e" => Key::E,
9826        "f" => Key::F,
9827        "g" => Key::G,
9828        "h" => Key::H,
9829        "i" => Key::I,
9830        "j" => Key::J,
9831        "k" => Key::K,
9832        "l" => Key::L,
9833        "m" => Key::M,
9834        "n" => Key::N,
9835        "o" => Key::O,
9836        "p" => Key::P,
9837        "q" => Key::Q,
9838        "r" => Key::R,
9839        "s" => Key::S,
9840        "t" => Key::T,
9841        "u" => Key::U,
9842        "v" => Key::V,
9843        "w" => Key::W,
9844        "x" => Key::X,
9845        "y" => Key::Y,
9846        "z" => Key::Z,
9847        "0" => Key::Key0,
9848        "1" => Key::Key1,
9849        "2" => Key::Key2,
9850        "3" => Key::Key3,
9851        "4" => Key::Key4,
9852        "5" => Key::Key5,
9853        "6" => Key::Key6,
9854        "7" => Key::Key7,
9855        "8" => Key::Key8,
9856        "9" => Key::Key9,
9857        _ => return None,
9858    })
9859}
9860
9861pub(crate) fn values_equal(a: &Value, b: &Value) -> bool {
9862    match (a, b) {
9863        (Value::Number(x), Value::Number(y)) => (x - y).abs() < 1e-12,
9864        (Value::Str(x), Value::Str(y)) => x == y,
9865        (Value::Bool(x), Value::Bool(y)) => x == y,
9866        (Value::Unit, Value::Unit) => true,
9867        _ => false,
9868    }
9869}
9870
9871// Rasteriser functions live in crate::gfx::raster — imported at top of file.
9872
9873// ── Window platform helpers ────────────────────────────────────────────────────
9874
9875/// Hide the console window that the OS auto-attaches to console-subsystem
9876/// processes. No-op on non-Windows and when no console is present.
9877#[cfg(not(target_arch = "wasm32"))]
9878fn hide_console_window() {
9879    #[cfg(windows)]
9880    unsafe {
9881        extern "system" {
9882            fn GetConsoleWindow() -> isize;
9883            fn ShowWindow(hwnd: isize, nCmdShow: i32) -> i32;
9884        }
9885        let hwnd = GetConsoleWindow();
9886        if hwnd != 0 {
9887            ShowWindow(hwnd, 0); // SW_HIDE = 0
9888        }
9889    }
9890}
9891
9892/// Strip *all* window chrome from `hwnd` and make it cover the whole primary
9893/// monitor (0,0 → screen_w × screen_h), above the taskbar. This turns the
9894/// minifb window into a true borderless-fullscreen surface: no title bar, no
9895/// frame, no resize grips — there is no visible window "handle" left.
9896#[cfg(all(not(target_arch = "wasm32"), windows))]
9897fn make_borderless_fullscreen(hwnd: isize, screen_w: i32, screen_h: i32) {
9898    if hwnd == 0 {
9899        return;
9900    }
9901    unsafe {
9902        extern "system" {
9903            fn SetWindowLongPtrW(hwnd: isize, index: i32, new: isize) -> isize;
9904            fn SetWindowPos(
9905                hwnd: isize,
9906                insert_after: isize,
9907                x: i32,
9908                y: i32,
9909                cx: i32,
9910                cy: i32,
9911                flags: u32,
9912            ) -> i32;
9913            fn ShowWindow(hwnd: isize, cmd: i32) -> i32;
9914        }
9915        const GWL_STYLE: i32 = -16;
9916        const GWL_EXSTYLE: i32 = -20;
9917        // WS_POPUP (0x80000000) | WS_VISIBLE (0x10000000) — a bare top-level
9918        // window with no caption, border, or system menu.
9919        SetWindowLongPtrW(hwnd, GWL_STYLE, 0x9000_0000isize);
9920        // Clear extended edges (WS_EX_WINDOWEDGE / CLIENTEDGE / DLGMODALFRAME).
9921        SetWindowLongPtrW(hwnd, GWL_EXSTYLE, 0);
9922        // HWND_TOPMOST = -1; SWP_FRAMECHANGED (0x0020) | SWP_SHOWWINDOW (0x0040).
9923        SetWindowPos(hwnd, -1isize, 0, 0, screen_w, screen_h, 0x0020 | 0x0040);
9924        ShowWindow(hwnd, 3); // SW_MAXIMIZE-equivalent paint; 3 = SW_SHOWMAXIMIZED
9925    }
9926}
9927
9928/// Primary-monitor resolution and refresh rate as `(width, height, hz)`.
9929/// `hz` falls back to 60 when the driver reports an unknown/`default` rate.
9930#[cfg(all(not(target_arch = "wasm32"), windows))]
9931fn monitor_info() -> (i32, i32, i32) {
9932    unsafe {
9933        extern "system" {
9934            fn GetSystemMetrics(index: i32) -> i32;
9935            fn GetDC(hwnd: isize) -> isize;
9936            fn ReleaseDC(hwnd: isize, hdc: isize) -> i32;
9937            fn GetDeviceCaps(hdc: isize, index: i32) -> i32;
9938        }
9939        let w = GetSystemMetrics(0).max(1); // SM_CXSCREEN
9940        let h = GetSystemMetrics(1).max(1); // SM_CYSCREEN
9941        let hdc = GetDC(0);
9942        let mut hz = if hdc != 0 { GetDeviceCaps(hdc, 116) } else { 0 }; // VREFRESH
9943        if hdc != 0 {
9944            ReleaseDC(0, hdc);
9945        }
9946        if hz <= 1 {
9947            hz = 60; // 0 or 1 means "device default" → assume 60 Hz
9948        }
9949        (w, h, hz)
9950    }
9951}
9952
9953/// Non-Windows native fallback: resolution from [`native_screen_size`]; refresh
9954/// from the active X11/RandR mode (so a 144 Hz panel drives the loop at 144),
9955/// falling back to 60 Hz when it can't be detected (Wayland, headless, macOS).
9956#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
9957fn monitor_info() -> (i32, i32, i32) {
9958    let (w, h) = native_screen_size();
9959    (w as i32, h as i32, linux_refresh_hz().unwrap_or(60))
9960}
9961
9962/// Active display refresh rate via `xrandr`. The current mode's rate is the
9963/// number flagged with `*` in `xrandr` output (e.g. `1920x1080 144.00*+`).
9964#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
9965fn linux_refresh_hz() -> Option<i32> {
9966    let out = std::process::Command::new("xrandr").arg("--current").output().ok()?;
9967    if !out.status.success() {
9968        return None;
9969    }
9970    let text = String::from_utf8_lossy(&out.stdout);
9971    // Find the token carrying `*` (the active rate) and round it to an integer.
9972    text.split_whitespace()
9973        .find(|tok| tok.contains('*'))
9974        .and_then(|tok| tok.trim_matches(|c: char| !c.is_ascii_digit() && c != '.').parse::<f64>().ok())
9975        .map(|hz| hz.round() as i32)
9976        .filter(|&hz| hz >= 24 && hz <= 1000)
9977}
9978
9979/// WASM fallback: the canvas is the display surface; assume 60 Hz.
9980#[cfg(target_arch = "wasm32")]
9981fn monitor_info() -> (i32, i32, i32) {
9982    let (w, h) = crate::gfx::webgl::canvas_size();
9983    (w as i32, h as i32, 60)
9984}
9985
9986/// Query the primary display resolution on non-Windows platforms.
9987/// Falls back to 1920×1080 if the size cannot be determined.
9988#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
9989fn native_screen_size() -> (f64, f64) {
9990    // On Linux/macOS we don't have an easy dependency-free call; return a
9991    // sensible default. Callers can always pass explicit dimensions.
9992    (1920.0, 1080.0)
9993}
9994
9995// ════════════════════════════════════════════════════════════════════════════
9996// Builtin call profiler  (env-gated, near-zero cost when off)
9997//
9998//   LING_PROFILE=1            enable per-builtin call-count + inclusive-time tally
9999//   LING_PROFILE_EVERY=N      print the report every N frames (default 240)
10000//
10001// Every builtin call funnels through `Interp::call_named` (JIT via `ling_builtin`,
10002// tree-walker directly), so this captures the full render/physics/audio builtin
10003// hot-path. Report is sorted by total time and prints calls, calls/frame,
10004// total_ms and ms/frame — the top-down "what's making so many calls" view.
10005// ════════════════════════════════════════════════════════════════════════════
10006struct LingProfileState {
10007    enabled: bool,
10008    every: u64,
10009    frames: u64,
10010    calls: std::collections::HashMap<String, (u64, u128)>, // name -> (count, nanos)
10011}
10012
10013thread_local! {
10014    static LING_PROFILE: std::cell::RefCell<LingProfileState> = std::cell::RefCell::new({
10015        let enabled = std::env::var("LING_PROFILE").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
10016        let every = std::env::var("LING_PROFILE_EVERY").ok()
10017            .and_then(|v| v.parse::<u64>().ok()).filter(|&n| n > 0).unwrap_or(240);
10018        if enabled {
10019            eprintln!("[ling-profile] ON — report every {every} frames (set LING_PROFILE_EVERY to change)");
10020        }
10021        LingProfileState { enabled, every, frames: 0, calls: std::collections::HashMap::new() }
10022    });
10023}
10024
10025#[inline]
10026fn ling_profile_enabled() -> bool {
10027    LING_PROFILE.with(|p| p.borrow().enabled)
10028}
10029
10030thread_local! {
10031    static LING_FPS: std::cell::RefCell<(bool, f64, u32, f64)> = std::cell::RefCell::new(
10032        (std::env::var("LING_FPS").map(|v| v != "0" && !v.is_empty()).unwrap_or(false), 0.0, 0, 0.0)
10033    );
10034}
10035
10036#[cfg(not(target_arch = "wasm32"))]
10037fn ling_fps_tick() {
10038    LING_FPS.with(|s| {
10039        let mut s = s.borrow_mut();
10040        if !s.0 { return; }
10041        let now = crate::runtime::now_secs();
10042        if s.1 > 0.0 {
10043            s.3 += now - s.1;
10044            s.2 += 1;
10045            if s.2 >= 120 {
10046                let avg = s.3 / s.2 as f64;
10047                eprintln!("[fps] {:.1} fps  ({:.2} ms/frame, wall, {} frames)", 1.0 / avg, avg * 1000.0, s.2);
10048                s.2 = 0;
10049                s.3 = 0.0;
10050            }
10051        }
10052        s.1 = now;
10053    });
10054}
10055
10056// Coarse render-pipeline timers (set LING_PHASE=1). Each accumulates wall-time
10057// per frame at flush/present granularity, so the cost is negligible. Reports the
10058// software-rasteriser breakdown the builtin profiler can't separate (the work is
10059// all inside the `present`/`flush_3d` builtins).
10060thread_local! {
10061    static LING_PHASE: std::cell::RefCell<(bool, u64, [u128; 5])> = std::cell::RefCell::new(
10062        (std::env::var_os("LING_PHASE").is_some(), 0, [0; 5])
10063    );
10064}
10065
10066/// Phase indices for [`ling_phase_add`].
10067pub mod phase {
10068    pub const FLUSH: usize = 0;
10069    pub const TOON: usize = 1;
10070    pub const BLIT: usize = 2;
10071    pub const DISTORT: usize = 3;
10072    pub const SORT: usize = 4;
10073}
10074
10075#[cfg(not(target_arch = "wasm32"))]
10076#[inline]
10077pub fn ling_phase_add(idx: usize, nanos: u128) {
10078    LING_PHASE.with(|p| {
10079        let mut p = p.borrow_mut();
10080        if p.0 {
10081            p.2[idx] += nanos;
10082        }
10083    });
10084}
10085
10086#[cfg(not(target_arch = "wasm32"))]
10087fn ling_phase_frame() {
10088    LING_PHASE.with(|p| {
10089        let mut p = p.borrow_mut();
10090        if !p.0 { return; }
10091        p.1 += 1;
10092        if p.1 >= 120 {
10093            let f = p.1 as f64;
10094            let ms = |i: usize| p.2[i] as f64 / 1e6 / f;
10095            eprintln!(
10096                "[phase] sort={:.2} flush={:.2} toon={:.2} blit={:.2} distort={:.2} ms/frame",
10097                ms(phase::SORT), ms(phase::FLUSH), ms(phase::TOON), ms(phase::BLIT), ms(phase::DISTORT)
10098            );
10099            p.1 = 0;
10100            p.2 = [0; 5];
10101        }
10102    });
10103}
10104
10105fn ling_profile_record(name: &str, nanos: u128) {
10106    // Frame boundary = a present() call.
10107    let is_frame = matches!(
10108        name,
10109        "present" | "แสดงผล" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시"
10110    );
10111    LING_PROFILE.with(|p| {
10112        let mut p = p.borrow_mut();
10113        let e = p.calls.entry(name.to_string()).or_insert((0, 0));
10114        e.0 += 1;
10115        e.1 += nanos;
10116        if is_frame {
10117            p.frames += 1;
10118            if p.frames % p.every == 0 {
10119                ling_profile_print(&p);
10120            }
10121        }
10122    });
10123}
10124
10125fn ling_profile_print(p: &LingProfileState) {
10126    let mut rows: Vec<(&String, u64, u128)> =
10127        p.calls.iter().map(|(n, (c, ns))| (n, *c, *ns)).collect();
10128    rows.sort_by(|a, b| b.2.cmp(&a.2)); // by total time desc
10129    let total_ns: u128 = p.calls.values().map(|(_, ns)| *ns).sum();
10130    let total_calls: u64 = p.calls.values().map(|(c, _)| *c).sum();
10131    let fr = p.frames.max(1) as f64;
10132    eprintln!(
10133        "\n┌─ LING PROFILE ── frames={} ─ builtin calls by total inclusive time ─────────────",
10134        p.frames
10135    );
10136    eprintln!(
10137        "│ {:<24} {:>9} {:>9} {:>10} {:>9} {:>6}",
10138        "builtin", "calls", "calls/fr", "total_ms", "ms/frame", "%time"
10139    );
10140    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
10141    for (name, count, ns) in rows.iter().take(30) {
10142        let ms = *ns as f64 / 1e6;
10143        let pct = if total_ns > 0 { *ns as f64 / total_ns as f64 * 100.0 } else { 0.0 };
10144        eprintln!(
10145            "│ {:<24} {:>9} {:>9.1} {:>10.1} {:>9.3} {:>5.1}%",
10146            truncate_name(name),
10147            count,
10148            *count as f64 / fr,
10149            ms,
10150            ms / fr,
10151            pct
10152        );
10153    }
10154    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
10155    eprintln!(
10156        "│ TOTAL {} builtin calls, {:.1} ms over {} frames  →  {:.0} calls/frame, {:.2} ms/frame in builtins",
10157        total_calls,
10158        total_ns as f64 / 1e6,
10159        p.frames,
10160        total_calls as f64 / fr,
10161        total_ns as f64 / 1e6 / fr
10162    );
10163    eprintln!("└──────────────────────────────────────────────────────────────────────────────");
10164}
10165
10166/// Trim a builtin name to fit the report column (counts chars, good enough for
10167/// the mixed-script names).
10168fn truncate_name(s: &str) -> String {
10169    let max = 24;
10170    if s.chars().count() <= max {
10171        s.to_string()
10172    } else {
10173        let mut t: String = s.chars().take(max - 1).collect();
10174        t.push('…');
10175        t
10176    }
10177}