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(all(not(target_arch = "wasm32"), feature = "llm"))]
5mod llm;
6#[cfg(all(not(target_arch = "wasm32"), feature = "vision"))]
7mod vision;
8#[cfg(not(target_arch = "wasm32"))]
9mod gamepad;
10#[cfg(target_arch = "wasm32")]
11mod input_web;
12#[cfg(not(target_arch = "wasm32"))]
13pub(crate) mod jit_abi;
14
15/// Initialize the AOT/JIT runtime. Must be called before any AOT-compiled code.
16/// Creates a new interpreter instance for runtime function dispatch.
17#[cfg(not(target_arch = "wasm32"))]
18pub fn init_aot_runtime() {
19    let interp = Interpreter::new();
20    jit_abi::init(interp);
21}
22
23/// Returns seconds since Unix epoch. On wasm32 uses `js_sys::Date::now()`
24/// (milliseconds / 1000); on native uses `SystemTime`.
25pub fn now_secs() -> f64 {
26    #[cfg(target_arch = "wasm32")]
27    {
28        js_sys::Date::now() / 1000.0
29    }
30    #[cfg(not(target_arch = "wasm32"))]
31    {
32        std::time::SystemTime::now()
33            .duration_since(std::time::UNIX_EPOCH)
34            .map(|d| d.as_secs_f64())
35            .unwrap_or(0.0)
36    }
37}
38
39/// Global hue-cycle for wireframe line strokes (enabled via `set_line_hue_cycle`).
40/// Stored as f64 bits: the cycle rate (radians/sec) and the epoch-seconds baseline
41/// captured when it was last (re)enabled. A rate of 0 disables the effect.
42static LINE_HUE_RATE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
43static LINE_HUE_START: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
44
45/// Enable/disable the wireframe line hue-cycle. `rate` is in radians/sec (0 = off).
46pub fn set_line_hue_rate(rate: f64) {
47    LINE_HUE_RATE.store(rate.to_bits(), std::sync::atomic::Ordering::Relaxed);
48    LINE_HUE_START.store(now_secs().to_bits(), std::sync::atomic::Ordering::Relaxed);
49}
50
51/// Current hue phase (radians) for line strokes, or `None` when the cycle is off.
52/// Elapsed is computed in f64 (epoch seconds are huge) before the caller casts to
53/// f32, so `sin` keeps precision across a long session.
54pub fn line_hue_phase() -> Option<f64> {
55    let rate = f64::from_bits(LINE_HUE_RATE.load(std::sync::atomic::Ordering::Relaxed));
56    if rate <= 0.0 {
57        return None;
58    }
59    let start = f64::from_bits(LINE_HUE_START.load(std::sync::atomic::Ordering::Relaxed));
60    Some((now_secs() - start) * rate)
61}
62
63// Wasm-only module registry: seeded by JS before `run_program` is called so
64// that `use "path"` statements resolve without a real filesystem.
65#[cfg(target_arch = "wasm32")]
66thread_local! {
67    static WASM_MODULES: std::cell::RefCell<std::collections::HashMap<String, String>> =
68        std::cell::RefCell::new(std::collections::HashMap::new());
69}
70
71/// Register a module source for wasm32 `use` resolution.
72/// Called from JS via `wasm_bindgen` before `run_program`.
73#[cfg(target_arch = "wasm32")]
74pub fn register_wasm_module(path: &str, source: &str) {
75    WASM_MODULES.with(|m| m.borrow_mut().insert(path.to_string(), source.to_string()));
76}
77
78/// Look up a registered module source on wasm32.
79#[cfg(target_arch = "wasm32")]
80pub(crate) fn get_wasm_module(path: &str) -> Option<String> {
81    WASM_MODULES.with(|m| m.borrow().get(path).cloned())
82}
83
84#[cfg(target_arch = "wasm32")]
85#[inline]
86fn wasm_sleep_ms(ms: i32) {
87    if ms <= 0 {
88        return;
89    }
90
91    // Keep this in Rust/js_sys so wasm-bindgen doesn't emit `require(...)`
92    // snippets, which break worker/no-modules output.
93    let global = js_sys::global();
94    let has_sab = js_sys::Reflect::has(
95        &global,
96        &wasm_bindgen::JsValue::from_str("SharedArrayBuffer"),
97    )
98    .unwrap_or(false);
99    let has_atomics =
100        js_sys::Reflect::has(&global, &wasm_bindgen::JsValue::from_str("Atomics")).unwrap_or(false);
101    if has_sab && has_atomics {
102        let sab = js_sys::SharedArrayBuffer::new(4);
103        let i32a = js_sys::Int32Array::new(&sab);
104        if js_sys::Atomics::wait_with_timeout(&i32a, 0, 0, ms as f64).is_ok() {
105            return;
106        }
107    }
108
109    let end = js_sys::Date::now() + ms as f64;
110    while js_sys::Date::now() < end {}
111}
112
113#[cfg(target_arch = "wasm32")]
114fn wasm_fetch_sync(
115    path: &str,
116    response_type: &str,
117    return_expr: &str,
118) -> Result<wasm_bindgen::JsValue, String> {
119    let quoted = js_sys::JSON::stringify(&wasm_bindgen::JsValue::from_str(path))
120        .ok()
121        .and_then(|s| s.as_string())
122        .unwrap_or_else(|| "\"\"".to_string());
123
124    let script = format!(
125        "(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}})()"
126    );
127
128    js_sys::eval(&script).map_err(|e| {
129        e.as_string()
130            .unwrap_or_else(|| format!("JS eval failed: {:?}", e))
131    })
132}
133
134#[cfg(target_arch = "wasm32")]
135fn wasm_fetch_bytes(path: &str) -> Result<Vec<u8>, String> {
136    let value = wasm_fetch_sync(
137        path,
138        "arraybuffer",
139        "new Uint8Array(xhr.response || new ArrayBuffer(0))",
140    )?;
141    let arr = js_sys::Uint8Array::new(&value);
142    let mut out = vec![0u8; arr.length() as usize];
143    arr.copy_to(&mut out);
144    Ok(out)
145}
146
147#[cfg(target_arch = "wasm32")]
148fn wasm_fetch_text(path: &str) -> Result<String, String> {
149    let value = wasm_fetch_sync(path, "text", "String(xhr.responseText || '')")?;
150    Ok(value.as_string().unwrap_or_default())
151}
152
153#[cfg(not(target_arch = "wasm32"))]
154mod net;
155#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
156pub mod web;
157use crate::gfx::{GfxState, Light};
158use crate::parser::ast::*;
159#[cfg(target_arch = "wasm32")]
160use js_sys;
161use std::cell::RefCell;
162use std::collections::HashMap;
163use std::rc::Rc;
164// `raster` is wasm-safe (pure CPU framebuffer), so `draw_line` is available on
165// web too; `fill_triangle` is only reached from native-gated 3-D fill paths.
166use crate::gfx::raster::draw_line;
167#[cfg(not(target_arch = "wasm32"))]
168use crate::gfx::raster::fill_triangle;
169#[cfg(not(target_arch = "wasm32"))]
170use ling_audio::{AudioEngine, ToneParams, Wave};
171
172#[cfg(not(target_arch = "wasm32"))]
173use ling_audio::FftAnalyzer;
174
175#[cfg(not(target_arch = "wasm32"))]
176use ling_mic;
177
178// ─── Values ──────────────────────────────────────────────────────────────────
179
180#[derive(Debug, Clone)]
181pub enum Value {
182    Str(String),
183    Number(f64),
184    Bool(bool),
185    Unit,
186    List(Rc<Vec<Value>>),
187    Ok(Box<Value>),
188    Err(Box<Value>),
189    Fn(Vec<String>, Vec<Stmt>, Env),
190    /// `form` record instance — ordered named fields.
191    Struct {
192        name: String,
193        fields: Vec<(String, Value)>,
194    },
195    /// `choose` enum instance — variant tag plus ordered payload.
196    Variant {
197        enum_name: String,
198        variant: String,
199        payload: Vec<Value>,
200    },
201}
202
203// Interpreter-hot maps use a fast non-crypto hasher: short Thai identifier keys
204// are hashed on every variable access and builtin dispatch, where SipHash dominates.
205use rustc_hash::FxHashMap;
206type Env = FxHashMap<String, Value>;
207
208#[inline]
209fn new_env() -> Env {
210    FxHashMap::default()
211}
212
213impl std::fmt::Display for Value {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        match self {
216            Value::Str(s) => write!(f, "{s}"),
217            Value::Number(n) => {
218                if n.fract() == 0.0 && n.abs() < 1e15 {
219                    write!(f, "{}", *n as i64)
220                } else {
221                    write!(f, "{n}")
222                }
223            },
224            Value::Bool(b) => write!(f, "{b}"),
225            Value::Unit => write!(f, "()"),
226            Value::List(v) => {
227                write!(f, "[")?;
228                for (i, x) in v.iter().enumerate() {
229                    if i > 0 {
230                        write!(f, ", ")?;
231                    }
232                    write!(f, "{x}")?;
233                }
234                write!(f, "]")
235            },
236            Value::Ok(v) => write!(f, "Ok({v})"),
237            Value::Err(v) => write!(f, "Err({v})"),
238            Value::Fn(_, _, _) => write!(f, "<fn>"),
239            Value::Struct { name, fields } => {
240                write!(f, "{name} {{ ")?;
241                for (i, (k, v)) in fields.iter().enumerate() {
242                    if i > 0 {
243                        write!(f, ", ")?;
244                    }
245                    write!(f, "{k}: {v}")?;
246                }
247                write!(f, " }}")
248            },
249            Value::Variant { variant, payload, .. } => {
250                write!(f, "{variant}")?;
251                if !payload.is_empty() {
252                    write!(f, "(")?;
253                    for (i, v) in payload.iter().enumerate() {
254                        if i > 0 {
255                            write!(f, ", ")?;
256                        }
257                        write!(f, "{v}")?;
258                    }
259                    write!(f, ")")?;
260                }
261                Ok(())
262            },
263        }
264    }
265}
266
267#[cfg(target_arch = "wasm32")]
268fn wasm_unsupported_builtin(name: &str) -> Option<Value> {
269    Some(match name {
270        // interface blips (native-only today)
271        "audio_blip"
272        | "提示音"
273        | "ビープ音"
274        | "효과음"
275        | "เสียงบี๊บ"
276        | "ui_sound"
277        | "界面音"
278        | "UI音"
279        | "인터페이스음"
280        | "เสียงปุ่ม"
281        | "audio_stop_sfx"
282        | "停止音效"
283        | "効果音停止"
284        | "효과음정지"
285        | "หยุดเอฟเฟกต์ทั้งหมด" | "بوق_کوتاه" | "نغمة_قصيرة" | "ביפ" | "بلپ_آواز" => Value::Unit,
286
287        // music loading / analysis / playback / midi (native-only today)
288        "music_load"
289        | "载入音乐"
290        | "音楽読込"
291        | "음악로드"
292        | "โหลดเพลง"
293        | "music_patch"
294        | "乐器音色"
295        | "音色読込"
296        | "악기패치"
297        | "แพตช์เครื่องดนตรี"
298        | "music_lrc"
299        | "载入歌词"
300        | "歌詞読込"
301        | "가사로드"
302        | "โหลดเนื้อเพลง"
303        | "music_midi_load"
304        | "载入MIDI"
305        | "MIDI読込"
306        | "미디로드"
307        | "โหลดมิดี" | "بارگذاری_موسیقی" | "تحميل_الموسيقى" | "טעינת_מוזיקה" | "موسیقی_لوڈ" => Value::Number(-1.0),
308
309        "music_duration"
310        | "音乐时长"
311        | "音楽長さ"
312        | "음악길이"
313        | "ความยาวเพลง"
314        | "music_bpm"
315        | "节拍速度"
316        | "テンポ"
317        | "템포"
318        | "จังหวะต่อนาที"
319        | "music_pos"
320        | "音乐位置"
321        | "音楽位置"
322        | "음악위치"
323        | "ตำแหน่งเพลง"
324        | "music_mic_pitch"
325        | "麦克风音高"
326        | "マイク音程"
327        | "마이크음정"
328        | "ระดับเสียงไมค์"
329        | "music_hz"
330        | "音符频率"
331        | "音符周波数"
332        | "음표주파수"
333        | "ความถี่โน้ต"
334        | "music_pitch_score"
335        | "音准评分"
336        | "音程スコア"
337        | "음정점수"
338        | "คะแนนเสียง"
339        | "music_judge"
340        | "判定"
341        | "判定する"
342        | "판정"
343        | "ตัดสินจังหวะ"
344        | "music_midi_count"
345        | "MIDI数量"
346        | "MIDI数"
347        | "미디수"
348        | "จำนวนมิดี" | "مدت_موسیقی" | "مدة_الموسيقى" | "משך_מוזיקה" | "موسیقی_دورانیہ" => Value::Number(0.0),
349
350        "music_key"
351        | "调性"
352        | "調性"
353        | "조성"
354        | "คีย์เพลง"
355        | "music_lyric"
356        | "当前歌词"
357        | "現在歌詞"
358        | "현재가사"
359        | "เนื้อเพลงปัจจุบัน"
360        | "music_note_name"
361        | "音名"
362        | "音名称"
363        | "음이름"
364        | "ชื่อโน้ต"
365        | "music_grade_name"
366        | "判定名"
367        | "判定名称"
368        | "판정이름"
369        | "ชื่อการตัดสิน" | "گام_موسیقی" | "مقام_الموسيقى" | "סולם_מוזיקלי" | "موسیقی_کلید" => Value::Str(String::new()),
370
371        "music_onsets"
372        | "音符起点"
373        | "オンセット"
374        | "온셋"
375        | "จุดเริ่มเสียง"
376        | "music_beat_grid"
377        | "节拍网格"
378        | "ビートグリッド"
379        | "비트그리드"
380        | "กริดจังหวะ"
381        | "music_midi_notes"
382        | "MIDI音符"
383        | "MIDIノート"
384        | "미디음표"
385        | "โน้ตมิดี"
386        | "music_midi_bars"
387        | "MIDI音条"
388        | "MIDIバー"
389        | "미디바"
390        | "แท่งมิดี"
391        | "music_fft"
392        | "音乐频谱"
393        | "音楽スペクトル"
394        | "음악스펙트럼"
395        | "สเปกตรัมเพลง" | "آغازهای_نت" | "بدايات_النغمات" | "התחלות_תווים" | "نوٹ_شروعات" => Value::List(Vec::new().into()),
396
397        "music_play"
398        | "播放音乐"
399        | "音楽再生"
400        | "음악재생"
401        | "เล่นเพลง"
402        | "music_pause"
403        | "暂停音乐"
404        | "音楽一時停止"
405        | "음악일시정지"
406        | "หยุดเพลงชั่วคราว"
407        | "music_stop"
408        | "停止音乐"
409        | "音楽停止"
410        | "음악정지"
411        | "หยุดเพลง"
412        | "music_seek"
413        | "定位音乐"
414        | "音楽シーク"
415        | "음악탐색"
416        | "ค้นหาเพลง"
417        | "music_volume"
418        | "音乐音量"
419        | "音楽音量"
420        | "음악음량"
421        | "ระดับเพลง"
422        | "music_note"
423        | "弹音符"
424        | "音符演奏"
425        | "음표연주"
426        | "เล่นโน้ต"
427        | "music_note_on"
428        | "音符开始"
429        | "音符オン"
430        | "음표켜기"
431        | "โน้ตเริ่ม"
432        | "music_note_off"
433        | "音符结束"
434        | "音符オフ"
435        | "음표끄기"
436        | "โน้ตจบ" | "پخش_موسیقی" | "شغّل_الموسيقى" | "נגן_מוזיקה" | "موسیقی_چلاؤ" => Value::Unit,
437
438        // liquid sim — return handle 0 for new, Unit for everything else
439        "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" | "مایع_جدید" | "سائل_جديد" | "נוזל_חדש" | "نیا_مائع" => {
440            Value::Number(0.0)
441        },
442        "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" | "ترکیب_مایع" | "مزج_سائل" | "ערבוב_נוזל" | "مائع_ملاؤ" => {
443            Value::Number(0.0)
444        },
445        "liquid_set_colors"
446        | "液体颜色"
447        | "液体配色"
448        | "액체색상"
449        | "สีของเหลว"
450        | "liquid_splat"
451        | "液体注入"
452        | "液体追加"
453        | "액체분사"
454        | "หยดของเหลว"
455        | "liquid_gravity"
456        | "液体重力"
457        | "液体重力ベクトル"
458        | "액체중력"
459        | "แรงโน้มถ่วงเหลว"
460        | "liquid_step"
461        | "液体步进"
462        | "液体更新"
463        | "액체스텝"
464        | "ก้าวของเหลว"
465        | "liquid_step_all"
466        | "液体全步进"
467        | "液体全更新"
468        | "전체액체스텝"
469        | "ก้าวของเหลวทั้งหมด"
470        | "liquid_rainbow"
471        | "液体彩虹"
472        | "液体虹"
473        | "액체무지개"
474        | "ของเหลวสายรุ้ง"
475        | "liquid_draw"
476        | "绘制液体"
477        | "液体描画"
478        | "액체그리기"
479        | "วาดของเหลว"
480        | "liquid_draw_surface"
481        | "液体贴面"
482        | "液体曲面"
483        | "액체곡면"
484        | "ของเหลวบนพื้นผิว" | "تنظیم_رنگ_مایع" | "عيّن_ألوان_السائل" | "קבע_צבעי_נוזל" | "مائع_رنگ_مقرر_کرو" => Value::Unit,
485
486        // ── game AI: neural networks ─────────────────────────────────────────
487        "nn_new"
488        | "建神经网"
489        | "ニューラル作成"
490        | "신경망생성"
491        | "สร้างโครงข่าย"
492        | "nn_load"
493        | "载入网"
494        | "網読込"
495        | "신경망불러오기"
496        | "โหลดโครงข่าย" | "شبکه_جدید" | "شبكة_جديدة" | "רשת_חדשה" | "نئی_نیورل_نیٹ" => Value::Number(-1.0),
497        "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" | "پیش‌روی_شبکه" | "تمرير_أمامي" | "העברה_קדימה" | "فارورڈ_پاس" => {
498            Value::List(Vec::new().into())
499        },
500        "nn_train"
501        | "训练网"
502        | "ニューラル学習"
503        | "신경망학습"
504        | "ฝึกโครงข่าย"
505        | "nn_dense"
506        | "密集层"
507        | "密層追加"
508        | "밀집층"
509        | "ชั้นหนาแน่น" | "آموزش_شبکه" | "درّب_الشبكة" | "אמן_רשת" | "نیٹ_ٹریننگ" => Value::Number(0.0),
510        "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" | "ذخیره_شبکه" | "احفظ_الشبكة" | "שמור_רשת" | "نیٹ_محفوظ_کرو" => {
511            Value::Bool(false)
512        },
513
514        // ── game AI: behavior trees ─────────────────────────────────────────
515        "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" | "ساخت_درخت_رفتار" | "ابنِ_شجرة_السلوك" | "בנה_עץ_התנהגות" | "بی_ٹی_تعمیر" => {
516            Value::Number(-1.0)
517        },
518        "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" | "تیک_درخت_رفتار" | "نبضة_شجرة_السلوك" | "טיק_עץ_התנהגות" | "بی_ٹی_ٹک" => {
519            Value::Str(String::new())
520        },
521        "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" | "وضعیت_درخت_رفتار" | "حالة_شجرة_السلوك" | "סטטוס_עץ_התנהגות" | "بی_ٹی_حالت" => {
522            Value::Number(0.0)
523        },
524        "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" | "تنظیم_واقعیت" | "عيّن_حقيقة" | "קבע_עובדה" | "بی_ٹی_سیٹ" => {
525            Value::Unit
526        },
527
528        // ── game AI: dialog LLM ─────────────────────────────────────────────
529        "dialog_new"
530        | "建对话模型"
531        | "対話モデル作成"
532        | "대화모델생성"
533        | "สร้างโมเดลสนทนา"
534        | "dialog_load_model"
535        | "对话载模"
536        | "対話モデル読込"
537        | "대화모델불러오기"
538        | "โหลดโมเดลสนทนา"
539        | "dialog_train"
540        | "对话训练"
541        | "対話訓練"
542        | "대화훈련"
543        | "ฝึกสนทนา"
544        | "dialog_load"
545        | "对话载入"
546        | "対話読込"
547        | "대화불러오기"
548        | "โหลดชุดสนทนา" | "مدل_گفتگوی_جدید" | "نموذج_حوار_جديد" | "מודל_דיאלוג_חדש" | "نیا_مکالمہ_ماڈل" => Value::Number(-1.0),
549        "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" | "بگو" | "قل" | "אמור" | "کہو" => {
550            Value::Str(String::new())
551        },
552        "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" | "ذخیره_مدل_گفتگو" | "احفظ_نموذج_الحوار" | "שמור_מודל_דיאלוג" | "مکالمہ_ماڈل_محفوظ" => {
553            Value::Bool(false)
554        },
555        "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" | "یادگیری_گفتگو" | "تعلّم_الحوار" | "למד_דיאלוג" | "مکالمہ_سیکھو" => {
556            Value::Unit
557        },
558
559        // ── networking ──────────────────────────────────────────────────────
560        "net_connect"
561        | "联网"
562        | "ネット接続"
563        | "네트연결"
564        | "เชื่อมเน็ต"
565        | "net_listen"
566        | "监听"
567        | "待機"
568        | "리슨"
569        | "รอรับ"
570        | "net_send"
571        | "发送"
572        | "送信"
573        | "전송"
574        | "ส่ง" | "اتصال_شبکه" | "اتصل_بالشبكة" | "התחבר_לרשת" | "نیٹ_کنیکٹ" => Value::Number(-1.0),
575        "net_recv"
576        | "接收"
577        | "受信"
578        | "수신"
579        | "รับ"
580        | "net_status"
581        | "连接状态"
582        | "接続状態"
583        | "연결상태"
584        | "สถานะการเชื่อม" | "دریافت_شبکه" | "استقبل_من_الشبكة" | "קבל_מרשת" | "نیٹ_وصول" => Value::Str(String::new()),
585        "net_discover" | "发现" | "探索" | "검색" | "ค้นหาเครือข่าย" | "کشف_شبکه" | "اكتشف_الشبكة" | "גלה_רשת" | "نیٹ_دریافت" => {
586            Value::List(Vec::new().into())
587        },
588        "net_close"
589        | "断开"
590        | "切断"
591        | "연결종료"
592        | "ตัดเชื่อม"
593        | "net_test"
594        | "测连接"
595        | "接続テスト"
596        | "연결테스트"
597        | "ทดสอบเน็ต" | "بستن_شبکه" | "أغلق_الشبكة" | "סגור_רשת" | "نیٹ_بند" => Value::Number(0.0),
598
599        // catch-all: any other native-only builtin silently no-ops on wasm32
600        _ => Value::Unit,
601    })
602}
603
604// ─── Control flow ────────────────────────────────────────────────────────────
605
606#[derive(Debug)]
607pub(crate) enum EvalErr {
608    Runtime(String),
609    Return(Value),
610    #[allow(dead_code)] // reserved for future `break` statement support
611    Break,
612}
613
614impl From<String> for EvalErr {
615    fn from(s: String) -> Self {
616        EvalErr::Runtime(s)
617    }
618}
619
620type EvalResult = Result<Value, EvalErr>;
621
622/// RFC 4648 base32 encode (no padding) — used for TOTP secrets.
623#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
624fn base32_encode(data: &[u8]) -> String {
625    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
626    let mut out = String::new();
627    let mut buffer: u32 = 0;
628    let mut bits = 0u32;
629    for &b in data {
630        buffer = (buffer << 8) | b as u32;
631        bits += 8;
632        while bits >= 5 {
633            bits -= 5;
634            out.push(ALPHABET[((buffer >> bits) & 0x1f) as usize] as char);
635        }
636    }
637    if bits > 0 {
638        out.push(ALPHABET[((buffer << (5 - bits)) & 0x1f) as usize] as char);
639    }
640    out
641}
642
643/// RFC 4648 base32 decode (case-insensitive, ignores padding/whitespace).
644#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
645fn base32_decode(s: &str) -> Option<Vec<u8>> {
646    let mut buffer: u32 = 0;
647    let mut bits = 0u32;
648    let mut out = Vec::new();
649    for c in s.chars() {
650        if c == '=' || c.is_whitespace() {
651            continue;
652        }
653        let v = match c.to_ascii_uppercase() {
654            'A'..='Z' => c.to_ascii_uppercase() as u32 - 'A' as u32,
655            '2'..='7' => c as u32 - '2' as u32 + 26,
656            _ => return None,
657        };
658        buffer = (buffer << 5) | v;
659        bits += 5;
660        if bits >= 8 {
661            bits -= 8;
662            out.push((buffer >> bits) as u8);
663        }
664    }
665    Some(out)
666}
667
668/// One RFC 6238 TOTP code (HMAC-SHA1, 6 digits) for the given 30s time step.
669#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
670fn totp_code(secret_b32: &str, step: u64) -> Option<String> {
671    let key = base32_decode(secret_b32)?;
672    let msg = step.to_be_bytes();
673    let mac = hmac_sha1(&key, &msg);
674    let offset = (mac[19] & 0x0f) as usize;
675    let bin = ((mac[offset] as u32 & 0x7f) << 24)
676        | ((mac[offset + 1] as u32) << 16)
677        | ((mac[offset + 2] as u32) << 8)
678        | (mac[offset + 3] as u32);
679    Some(format!("{:06}", bin % 1_000_000))
680}
681
682/// TOTP verify with a ±1 step window (tolerates minor clock skew).
683#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
684fn totp_check(secret_b32: &str, code: &str) -> bool {
685    if code.len() != 6 || !code.bytes().all(|b| b.is_ascii_digit()) {
686        return false;
687    }
688    let now_step = (crate::runtime::now_secs() as u64) / 30;
689    for delta in [-1i64, 0, 1] {
690        let step = (now_step as i64 + delta) as u64;
691        if let Some(expected) = totp_code(secret_b32, step) {
692            // constant-time-ish compare (fixed 6-char length)
693            if expected.as_bytes().ct_eq_str(code.as_bytes()) {
694                return true;
695            }
696        }
697    }
698    false
699}
700
701/// HMAC-SHA1 built on the `hmac`+`sha1` crates (both via the web feature).
702#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
703fn hmac_sha1(key: &[u8], msg: &[u8]) -> [u8; 20] {
704    use hmac::{Mac, SimpleHmac};
705    let mut mac = SimpleHmac::<sha1::Sha1>::new_from_slice(key).expect("hmac key");
706    mac.update(msg);
707    let out = mac.finalize().into_bytes();
708    let mut arr = [0u8; 20];
709    arr.copy_from_slice(&out);
710    arr
711}
712
713/// Tiny fixed-length constant-time byte compare helper for TOTP codes.
714#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
715trait CtEqStr {
716    fn ct_eq_str(&self, other: &[u8]) -> bool;
717}
718#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
719impl CtEqStr for [u8] {
720    fn ct_eq_str(&self, other: &[u8]) -> bool {
721        if self.len() != other.len() {
722            return false;
723        }
724        let mut diff = 0u8;
725        for (a, b) in self.iter().zip(other.iter()) {
726            diff |= a ^ b;
727        }
728        diff == 0
729    }
730}
731
732/// Percent-decodes a URL query component (`+` → space, `%41` → `A`).
733fn url_decode(s: &str) -> String {
734    let bytes = s.as_bytes();
735    let mut out = Vec::with_capacity(bytes.len());
736    let mut i = 0;
737    while i < bytes.len() {
738        match bytes[i] {
739            b'+' => {
740                out.push(b' ');
741                i += 1;
742            },
743            b'%' if i + 2 < bytes.len() => {
744                let hi = (bytes[i + 1] as char).to_digit(16);
745                let lo = (bytes[i + 2] as char).to_digit(16);
746                if let (Some(h), Some(l)) = (hi, lo) {
747                    out.push((h * 16 + l) as u8);
748                    i += 3;
749                } else {
750                    out.push(bytes[i]);
751                    i += 1;
752                }
753            },
754            b => {
755                out.push(b);
756                i += 1;
757            },
758        }
759    }
760    String::from_utf8_lossy(&out).into_owned()
761}
762
763/// Maps Ling values to owned rusqlite parameter values for `db_exec`/`db_query`.
764#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
765fn values_to_sql_params(args: &[Value]) -> Vec<ling_http::rusqlite::types::Value> {
766    use ling_http::rusqlite::types::Value as Sql;
767    args.iter()
768        .map(|v| match v {
769            Value::Number(n) if n.fract() == 0.0 && n.abs() < 9e15 => Sql::Integer(*n as i64),
770            Value::Number(n) => Sql::Real(*n),
771            Value::Bool(b) => Sql::Integer(*b as i64),
772            other => Sql::Text(other.to_string()),
773        })
774        .collect()
775}
776
777// GfxState is now defined in crate::gfx — see src/gfx/mod.rs.
778
779// ─── SVG writer ───────────────────────────────────────────────────────────────
780
781struct SvgWriter {
782    path: String,
783    width: f64,
784    height: f64,
785    elements: Vec<String>,
786}
787
788impl SvgWriter {
789    fn new(path: String, width: f64, height: f64) -> Self {
790        let bg = format!("<rect width=\"{width}\" height=\"{height}\" fill=\"#0a0a0a\"/>");
791        Self { path, width, height, elements: vec![bg] }
792    }
793
794    fn save(&self) -> std::io::Result<()> {
795        let w = self.width;
796        let h = self.height;
797        let mut out = format!(
798            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
799             <svg xmlns=\"http://www.w3.org/2000/svg\" \
800             width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\">\n"
801        );
802        for elem in &self.elements {
803            out.push_str("  ");
804            out.push_str(elem);
805            out.push('\n');
806        }
807        out.push_str("</svg>\n");
808        // Create parent directory if it doesn't exist.
809        if let Some(parent) = std::path::Path::new(&self.path).parent() {
810            if !parent.as_os_str().is_empty() {
811                let _ = std::fs::create_dir_all(parent);
812            }
813        }
814        std::fs::write(&self.path, out.as_bytes())
815    }
816}
817
818fn hsl_to_hex(h: f64, s: f64, l: f64) -> String {
819    let s = s / 100.0;
820    let l = l / 100.0;
821    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
822    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
823    let m = l - c / 2.0;
824    let (r1, g1, b1) = if h < 60.0 {
825        (c, x, 0.0)
826    } else if h < 120.0 {
827        (x, c, 0.0)
828    } else if h < 180.0 {
829        (0.0, c, x)
830    } else if h < 240.0 {
831        (0.0, x, c)
832    } else if h < 300.0 {
833        (x, 0.0, c)
834    } else {
835        (c, 0.0, x)
836    };
837    let r = ((r1 + m) * 255.0).round() as u8;
838    let g = ((g1 + m) * 255.0).round() as u8;
839    let b = ((b1 + m) * 255.0).round() as u8;
840    format!("#{r:02x}{g:02x}{b:02x}")
841}
842
843// ─── Procedural texture helpers ───────────────────────────────────────────────
844
845fn tex_hash(x: i32, y: i32, seed: u32) -> f32 {
846    let mut h = (x as u32)
847        .wrapping_add((y as u32).wrapping_mul(2654435769))
848        .wrapping_add(seed.wrapping_mul(1234567891));
849    h ^= h >> 16;
850    h = h.wrapping_mul(0x45d9f3b);
851    h ^= h >> 16;
852    h as f32 / u32::MAX as f32
853}
854
855fn tex_vnoise(x: f32, y: f32, seed: u32) -> f32 {
856    let xi = x.floor() as i32;
857    let yi = y.floor() as i32;
858    let sm = |t: f32| t * t * (3.0 - 2.0 * t);
859    let xf = sm(x - xi as f32);
860    let yf = sm(y - yi as f32);
861    let a = tex_hash(xi, yi, seed);
862    let b = tex_hash(xi + 1, yi, seed);
863    let c = tex_hash(xi, yi + 1, seed);
864    let d = tex_hash(xi + 1, yi + 1, seed);
865    a + (b - a) * xf + (c - a) * yf + (a - b - c + d) * xf * yf
866}
867
868fn tex_fbm(x: f32, y: f32, octaves: u32, seed: u32) -> f32 {
869    let mut v = 0.0f32;
870    let mut amp = 0.5f32;
871    let mut f = 1.0f32;
872    for i in 0..octaves {
873        v += tex_vnoise(x * f, y * f, seed.wrapping_add(i * 7919)) * amp;
874        amp *= 0.5;
875        f *= 2.0;
876    }
877    v
878}
879
880fn tex_palette(name: &str, t: f32) -> [f32; 3] {
881    let (a, b, c, d): ([f32; 3], [f32; 3], [f32; 3], [f32; 3]) = match name {
882        "fire" => (
883            [0.8, 0.4, 0.1],
884            [0.7, 0.3, 0.1],
885            [1.0, 0.5, 0.3],
886            [0.0, 0.5, 0.8],
887        ),
888        "ocean" => (
889            [0.1, 0.4, 0.7],
890            [0.3, 0.3, 0.4],
891            [0.8, 1.0, 0.5],
892            [0.3, 0.0, 0.6],
893        ),
894        "psychedelic" => (
895            [0.5, 0.5, 0.5],
896            [0.8, 0.8, 0.8],
897            [1.0, 1.3, 0.7],
898            [0.0, 0.15, 0.3],
899        ),
900        "neon" => (
901            [0.5, 0.5, 0.5],
902            [0.5, 0.5, 0.5],
903            [2.0, 1.0, 0.0],
904            [0.5, 0.2, 0.25],
905        ),
906        "forest" => (
907            [0.3, 0.5, 0.2],
908            [0.2, 0.3, 0.1],
909            [1.0, 0.5, 0.8],
910            [0.1, 0.3, 0.6],
911        ),
912        _ => (
913            [0.5, 0.5, 0.5],
914            [0.5, 0.5, 0.5],
915            [1.0, 1.0, 1.0],
916            [0.0, 0.333, 0.667],
917        ),
918    };
919    [0, 1, 2]
920        .map(|i| (a[i] + b[i] * (std::f32::consts::TAU * (c[i] * t + d[i])).cos()).clamp(0.0, 1.0))
921}
922
923/// Map a physical key to a typed character for ling-ui text input (lowercase).
924#[cfg(not(target_arch = "wasm32"))]
925// Full US-QWERTY keyboard → char, shift-aware. `key_char` only ever emits
926// printable ASCII (never Tab/Enter/control bytes — those keys have no case
927// here at all) so anything read through `text_poll` is safe by construction
928// to drop straight into the game's tab/comma/semicolon/pipe-framed wire
929// protocols without needing per-keystroke sanitization.
930fn key_char(k: minifb::Key, shift: bool) -> Option<char> {
931    use minifb::Key::*;
932    let base = match k {
933        A => 'a',
934        B => 'b',
935        C => 'c',
936        D => 'd',
937        E => 'e',
938        F => 'f',
939        G => 'g',
940        H => 'h',
941        I => 'i',
942        J => 'j',
943        K => 'k',
944        L => 'l',
945        M => 'm',
946        N => 'n',
947        O => 'o',
948        P => 'p',
949        Q => 'q',
950        R => 'r',
951        S => 's',
952        T => 't',
953        U => 'u',
954        V => 'v',
955        W => 'w',
956        X => 'x',
957        Y => 'y',
958        Z => 'z',
959        Key0 => '0',
960        Key1 => '1',
961        Key2 => '2',
962        Key3 => '3',
963        Key4 => '4',
964        Key5 => '5',
965        Key6 => '6',
966        Key7 => '7',
967        Key8 => '8',
968        Key9 => '9',
969        Space => ' ',
970        Minus => '-',
971        Equal => '=',
972        Period => '.',
973        Comma => ',',
974        Slash => '/',
975        Backslash => '\\',
976        Semicolon => ';',
977        Apostrophe => '\'',
978        LeftBracket => '[',
979        RightBracket => ']',
980        Backquote => '`',
981        _ => return None,
982    };
983    if !shift {
984        return Some(base);
985    }
986    Some(match base {
987        'a'..='z' => base.to_ascii_uppercase(),
988        '0' => ')',
989        '1' => '!',
990        '2' => '@',
991        '3' => '#',
992        '4' => '$',
993        '5' => '%',
994        '6' => '^',
995        '7' => '&',
996        '8' => '*',
997        '9' => '(',
998        '-' => '_',
999        '=' => '+',
1000        '.' => '>',
1001        ',' => '<',
1002        '/' => '?',
1003        '\\' => '|',
1004        ';' => ':',
1005        '\'' => '"',
1006        '[' => '{',
1007        ']' => '}',
1008        '`' => '~',
1009        other => other,
1010    })
1011}
1012
1013/// Win32 virtual-key code → char, mirroring `key_char` exactly but keyed by
1014/// VK code instead of `minifb::Key` — used by the `GetAsyncKeyState` input
1015/// path (see `os_key_down` / the `topmost_window` fallback in `text_poll`).
1016#[cfg(all(not(target_arch = "wasm32"), windows))]
1017fn vk_char(vk: i32, shift: bool) -> Option<char> {
1018    let base = match vk {
1019        0x41..=0x5A => (b'a' + (vk - 0x41) as u8) as char, // 'A'..'Z'
1020        0x30..=0x39 => (b'0' + (vk - 0x30) as u8) as char, // '0'..'9'
1021        0x20 => ' ',  // VK_SPACE
1022        0xBD => '-',  // VK_OEM_MINUS
1023        0xBB => '=',  // VK_OEM_PLUS (unshifted '=')
1024        0xBE => '.',  // VK_OEM_PERIOD
1025        0xBC => ',',  // VK_OEM_COMMA
1026        0xBF => '/',  // VK_OEM_2
1027        0xDC => '\\', // VK_OEM_5
1028        0xBA => ';',  // VK_OEM_1
1029        0xDE => '\'', // VK_OEM_7
1030        0xDB => '[',  // VK_OEM_4
1031        0xDD => ']',  // VK_OEM_6
1032        0xC0 => '`',  // VK_OEM_3
1033        _ => return None,
1034    };
1035    if !shift {
1036        return Some(base);
1037    }
1038    Some(match base {
1039        'a'..='z' => base.to_ascii_uppercase(),
1040        '0' => ')',
1041        '1' => '!',
1042        '2' => '@',
1043        '3' => '#',
1044        '4' => '$',
1045        '5' => '%',
1046        '6' => '^',
1047        '7' => '&',
1048        '8' => '*',
1049        '9' => '(',
1050        '-' => '_',
1051        '=' => '+',
1052        '.' => '>',
1053        ',' => '<',
1054        '/' => '?',
1055        '\\' => '|',
1056        ';' => ':',
1057        '\'' => '"',
1058        '[' => '{',
1059        ']' => '}',
1060        '`' => '~',
1061        other => other,
1062    })
1063}
1064
1065/// The VK codes `text_poll`'s `GetAsyncKeyState` fallback scans each frame —
1066/// every key `vk_char` can turn into a character.
1067#[cfg(all(not(target_arch = "wasm32"), windows))]
1068const TEXT_POLL_VKS: &[i32] = &[
1069    0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
1070    0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x30, 0x31, 0x32, 0x33,
1071    0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x20, 0xBD, 0xBB, 0xBE, 0xBC, 0xBF, 0xDC, 0xBA, 0xDE,
1072    0xDB, 0xDD, 0xC0,
1073];
1074/// VK_BACK — polled separately from `TEXT_POLL_VKS` since it edits (pops)
1075/// rather than appends.
1076#[cfg(all(not(target_arch = "wasm32"), windows))]
1077const VK_BACK: i32 = 0x08;
1078#[cfg(all(not(target_arch = "wasm32"), windows))]
1079const VK_SHIFT: i32 = 0x10;
1080
1081/// Read the OS's live key-state table directly — unlike minifb's
1082/// `is_key_down`/`get_keys_pressed` (populated from `WM_KEYDOWN`, which only
1083/// arrives at a window holding real Win32 keyboard focus), this works even
1084/// when the borderless-fullscreen window is topmost/visually in front but
1085/// didn't actually win the OS focus fight (Windows' foreground-lock — see
1086/// `force_window_focus`). High bit of `GetAsyncKeyState` = currently down.
1087#[cfg(all(not(target_arch = "wasm32"), windows))]
1088fn os_key_down(vk: i32) -> bool {
1089    unsafe {
1090        extern "system" {
1091            fn GetAsyncKeyState(vkey: i32) -> i16;
1092        }
1093        (GetAsyncKeyState(vk) as u16 & 0x8000) != 0
1094    }
1095}
1096
1097/// True when `hwnd` is the actual OS foreground window. `GetAsyncKeyState`
1098/// (see `os_key_down`) reads the global key-state table regardless of which
1099/// window is focused, so the `topmost_window` input fallback must check this
1100/// before trusting it — otherwise alt-tabbing away from the topmost/
1101/// fullscreen window to type in some other app would still feed keystrokes
1102/// into the game sitting behind it, since nothing about GetAsyncKeyState
1103/// itself would notice the window lost focus.
1104#[cfg(all(not(target_arch = "wasm32"), windows))]
1105fn window_is_foreground(hwnd: isize) -> bool {
1106    unsafe {
1107        extern "system" {
1108            fn GetForegroundWindow() -> isize;
1109        }
1110        hwnd != 0 && GetForegroundWindow() == hwnd
1111    }
1112}
1113
1114/// Delay (seconds) a key must stay held before it starts auto-repeating —
1115/// matches the initial "hesitation" of a normal OS text field.
1116#[cfg(all(not(target_arch = "wasm32"), windows))]
1117const KEY_REPEAT_DELAY: f64 = 0.45;
1118/// Interval (seconds) between repeats once a held key is auto-repeating.
1119#[cfg(all(not(target_arch = "wasm32"), windows))]
1120const KEY_REPEAT_RATE: f64 = 0.045;
1121
1122/// Edge/repeat detector for the `GetAsyncKeyState` text-input fallback:
1123/// fires true on the initial press, then again every `KEY_REPEAT_RATE`
1124/// seconds once the key has been held past `KEY_REPEAT_DELAY` — the same
1125/// press-then-hold-to-repeat behavior minifb's `KeyRepeat::Yes` gives the
1126/// normal (focused-window) path, which this fallback otherwise lacks since
1127/// it does its own from-scratch edge detection per VK code.
1128#[cfg(all(not(target_arch = "wasm32"), windows))]
1129fn key_repeat_fire(now: f64, down: bool, was_down: bool, down_since: &mut f64, last_fire: &mut f64) -> bool {
1130    if !down {
1131        return false;
1132    }
1133    if !was_down {
1134        *down_since = now;
1135        *last_fire = now;
1136        return true;
1137    }
1138    if now - *down_since >= KEY_REPEAT_DELAY && now - *last_fire >= KEY_REPEAT_RATE {
1139        *last_fire = now;
1140        return true;
1141    }
1142    false
1143}
1144
1145/// Map the same key-name strings `key_down`/`key_pressed` already accept
1146/// (see `str_to_minifb_key`) to Win32 virtual-key codes, for the
1147/// `GetAsyncKeyState` fallback path.
1148#[cfg(all(not(target_arch = "wasm32"), windows))]
1149fn str_to_vk(name: &str) -> Option<i32> {
1150    Some(match name {
1151        "numpad0" | "kp0" => 0x60,
1152        "numpad1" | "kp1" => 0x61,
1153        "numpad2" | "kp2" => 0x62,
1154        "numpad3" | "kp3" => 0x63,
1155        "numpad4" | "kp4" => 0x64,
1156        "numpad5" | "kp5" => 0x65,
1157        "numpad6" | "kp6" => 0x66,
1158        "numpad7" | "kp7" => 0x67,
1159        "numpad8" | "kp8" => 0x68,
1160        "numpad9" | "kp9" => 0x69,
1161        "numpad+" | "kp+" => 0x6B,
1162        "numpad-" | "kp-" => 0x6D,
1163        "numpad*" | "kp*" => 0x6A,
1164        "numpad/" | "kp/" => 0x6F,
1165        "left" => 0x25,
1166        "up" => 0x26,
1167        "right" => 0x27,
1168        "down" => 0x28,
1169        "space" => 0x20,
1170        "enter" => 0x0D,
1171        "escape" => 0x1B,
1172        "pageup" => 0x21,
1173        "pagedown" => 0x22,
1174        "lshift" | "leftshift" => 0xA0,
1175        "rshift" | "rightshift" => 0xA1,
1176        "lctrl" | "leftctrl" => 0xA2,
1177        "rctrl" | "rightctrl" => 0xA3,
1178        "lalt" | "leftalt" => 0xA4,
1179        "ralt" | "rightalt" => 0xA5,
1180        "tab" => 0x09,
1181        "backspace" => VK_BACK,
1182        "delete" => 0x2E,
1183        "insert" => 0x2D,
1184        "home" => 0x24,
1185        "end" => 0x23,
1186        "a" => 0x41,
1187        "b" => 0x42,
1188        "c" => 0x43,
1189        "d" => 0x44,
1190        "e" => 0x45,
1191        "f" => 0x46,
1192        "g" => 0x47,
1193        "h" => 0x48,
1194        "i" => 0x49,
1195        "j" => 0x4A,
1196        "k" => 0x4B,
1197        "l" => 0x4C,
1198        "m" => 0x4D,
1199        "n" => 0x4E,
1200        "o" => 0x4F,
1201        "p" => 0x50,
1202        "q" => 0x51,
1203        "r" => 0x52,
1204        "s" => 0x53,
1205        "t" => 0x54,
1206        "u" => 0x55,
1207        "v" => 0x56,
1208        "w" => 0x57,
1209        "x" => 0x58,
1210        "y" => 0x59,
1211        "z" => 0x5A,
1212        "0" => 0x30,
1213        "1" => 0x31,
1214        "2" => 0x32,
1215        "3" => 0x33,
1216        "4" => 0x34,
1217        "5" => 0x35,
1218        "6" => 0x36,
1219        "7" => 0x37,
1220        "8" => 0x38,
1221        "9" => 0x39,
1222        _ => return None,
1223    })
1224}
1225
1226/// Lowercase-hex encode bytes (the wire format for crypto values in Ling).
1227fn hex_encode(bytes: &[u8]) -> String {
1228    let mut s = String::with_capacity(bytes.len() * 2);
1229    for b in bytes {
1230        s.push_str(&format!("{b:02x}"));
1231    }
1232    s
1233}
1234
1235/// Decode a `ling convert` blob: base64 → zlib-inflate → raw little-endian bytes.
1236#[cfg(not(target_arch = "wasm32"))]
1237fn decode_blob(s: &str) -> Result<Vec<u8>, String> {
1238    use base64::Engine as _;
1239    use std::io::Read as _;
1240    let comp = base64::engine::general_purpose::STANDARD
1241        .decode(s.trim())
1242        .map_err(|e| format!("base64: {e}"))?;
1243    let mut out = Vec::new();
1244    flate2::read::ZlibDecoder::new(&comp[..])
1245        .read_to_end(&mut out)
1246        .map_err(|e| format!("inflate: {e}"))?;
1247    Ok(out)
1248}
1249
1250/// Decode a lowercase/uppercase hex string to bytes (ignores malformed tail).
1251#[cfg(not(target_arch = "wasm32"))]
1252fn hex_decode(s: &str) -> Vec<u8> {
1253    let s = s.trim();
1254    (0..s.len() / 2)
1255        .filter_map(|i| u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok())
1256        .collect()
1257}
1258
1259/// Decode a hex string into a fixed 32-byte key (zero-padded / truncated).
1260#[cfg(not(target_arch = "wasm32"))]
1261fn hex_to_32(s: &str) -> [u8; 32] {
1262    let v = hex_decode(s);
1263    let mut out = [0u8; 32];
1264    let n = v.len().min(32);
1265    out[..n].copy_from_slice(&v[..n]);
1266    out
1267}
1268
1269/// Builds a minimal, valid PDF with one page per input image (decoded via
1270/// `image`, page sized to the image's own pixel dimensions), backing the
1271/// `pdf_from_images` builtin. No PDF crate: each page is three objects
1272/// (Page / Contents / Image XObject) hand-written directly, with the image
1273/// stream flate-compressed raw RGB8 (`/Filter /FlateDecode /ColorSpace
1274/// /DeviceRGB /BitsPerComponent 8`) — no separate re-encoding step beyond
1275/// what `flate2` (already a dependency) does for the stream itself.
1276#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1277fn build_pdf_from_images(paths: &[String], out_path: &str) -> Result<(), Box<dyn std::error::Error>> {
1278    use std::io::Write as _;
1279
1280    struct PageImg {
1281        w: u32,
1282        h: u32,
1283        compressed: Vec<u8>,
1284    }
1285
1286    let mut pages = Vec::with_capacity(paths.len());
1287    for p in paths {
1288        let img = image::open(p)?.to_rgb8();
1289        let (w, h) = (img.width(), img.height());
1290        let mut enc = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1291        enc.write_all(img.as_raw())?;
1292        pages.push(PageImg { w, h, compressed: enc.finish()? });
1293    }
1294
1295    let n = pages.len();
1296    // Object numbers: 1=Catalog, 2=Pages, then per page i (0-indexed):
1297    // 3+3i=Page, 4+3i=Contents, 5+3i=Image XObject.
1298    let page_nums: Vec<u32> = (0..n).map(|i| 3 + (i as u32) * 3).collect();
1299    let total_objs = 2 + n * 3;
1300    let mut off = vec![0usize; total_objs + 1]; // 1-based; off[0] unused
1301
1302    let mut buf: Vec<u8> = Vec::new();
1303    buf.extend_from_slice(b"%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
1304
1305    off[1] = buf.len();
1306    buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1307
1308    off[2] = buf.len();
1309    let kids = page_nums.iter().map(|n| format!("{n} 0 R")).collect::<Vec<_>>().join(" ");
1310    buf.extend_from_slice(format!("2 0 obj\n<< /Type /Pages /Kids [{kids}] /Count {n} >>\nendobj\n").as_bytes());
1311
1312    for (i, page) in pages.iter().enumerate() {
1313        let page_num = page_nums[i];
1314        let content_num = page_num + 1;
1315        let image_num = page_num + 2;
1316        let (w, h) = (page.w, page.h);
1317
1318        off[page_num as usize] = buf.len();
1319        buf.extend_from_slice(format!(
1320            "{page_num} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}] /Resources << /XObject << /Im0 {image_num} 0 R >> >> /Contents {content_num} 0 R >>\nendobj\n"
1321        ).as_bytes());
1322
1323        let content = format!("q {w} 0 0 {h} 0 0 cm /Im0 Do Q");
1324        off[content_num as usize] = buf.len();
1325        buf.extend_from_slice(format!(
1326            "{content_num} 0 obj\n<< /Length {} >>\nstream\n{content}\nendstream\nendobj\n",
1327            content.len()
1328        ).as_bytes());
1329
1330        off[image_num as usize] = buf.len();
1331        buf.extend_from_slice(format!(
1332            "{image_num} 0 obj\n<< /Type /XObject /Subtype /Image /Width {w} /Height {h} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length {} >>\nstream\n",
1333            page.compressed.len()
1334        ).as_bytes());
1335        buf.extend_from_slice(&page.compressed);
1336        buf.extend_from_slice(b"\nendstream\nendobj\n");
1337    }
1338
1339    let xref_offset = buf.len();
1340    buf.extend_from_slice(format!("xref\n0 {}\n", total_objs + 1).as_bytes());
1341    buf.extend_from_slice(b"0000000000 65535 f \n");
1342    for entry in off.iter().skip(1) {
1343        buf.extend_from_slice(format!("{entry:010} 00000 n \n").as_bytes());
1344    }
1345    buf.extend_from_slice(
1346        format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF", total_objs + 1).as_bytes(),
1347    );
1348
1349    std::fs::write(out_path, buf)?;
1350    Ok(())
1351}
1352
1353fn tex_rgb(r: f32, g: f32, b: f32) -> u32 {
1354    ((r * 255.0) as u32) << 16 | ((g * 255.0) as u32) << 8 | (b * 255.0) as u32
1355}
1356
1357// ─── 3D Perlin Noise (Improved Perlin 2002) ───────────────────────────────────
1358
1359const PERM: [u8; 512] = [
1360    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
1361    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
1362    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
1363    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
1364    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
1365    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
1366    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
1367    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
1368    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
1369    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
1370    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180,
1371    198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185,
1372    134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223,
1373    140, 161, 137, 13, 191, 230, 66, 104, 153, 199, 167, 147, 99, 179, 92,
1374    // Duplicate for wrap-around indexing
1375    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
1376    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
1377    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
1378    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
1379    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
1380    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
1381    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
1382    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
1383    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
1384    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
1385    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174,
1386];
1387
1388fn fade(t: f32) -> f32 {
1389    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
1390}
1391
1392fn grad(hash: u8, x: f32, y: f32, z: f32) -> f32 {
1393    let h = hash & 15;
1394    let u = if h < 8 { x } else { y };
1395    let v = if h < 8 { y } else { z };
1396    (if (h & 1) == 0 { u } else { -u }) + (if (h & 2) == 0 { v } else { -v })
1397}
1398
1399fn perlin3(x: f32, y: f32, z: f32) -> f32 {
1400    let xi = (x.floor() as i32) & 255;
1401    let yi = (y.floor() as i32) & 255;
1402    let zi = (z.floor() as i32) & 255;
1403
1404    let xf = x - x.floor();
1405    let yf = y - y.floor();
1406    let zf = z - z.floor();
1407
1408    let u = fade(xf);
1409    let v = fade(yf);
1410    let w = fade(zf);
1411
1412    let p0 = PERM[xi as usize] as usize;
1413    let p1 = PERM[((xi + 1) & 255) as usize] as usize;
1414    let pa = PERM[(p0 + yi as usize) & 255] as usize;
1415    let pb = PERM[(p0 + ((yi + 1) & 255) as usize) & 255] as usize;
1416    let pc = PERM[(p1 + yi as usize) & 255] as usize;
1417    let pd = PERM[(p1 + ((yi + 1) & 255) as usize) & 255] as usize;
1418
1419    let g000 = grad(PERM[(pa + zi as usize) & 255], xf, yf, zf);
1420    let g001 = grad(
1421        PERM[(pa + ((zi + 1) & 255) as usize) & 255],
1422        xf,
1423        yf,
1424        zf - 1.0,
1425    );
1426    let g010 = grad(PERM[(pb + zi as usize) & 255], xf, yf - 1.0, zf);
1427    let g011 = grad(
1428        PERM[(pb + ((zi + 1) & 255) as usize) & 255],
1429        xf,
1430        yf - 1.0,
1431        zf - 1.0,
1432    );
1433    let g100 = grad(PERM[(pc + zi as usize) & 255], xf - 1.0, yf, zf);
1434    let g101 = grad(
1435        PERM[(pc + ((zi + 1) & 255) as usize) & 255],
1436        xf - 1.0,
1437        yf,
1438        zf - 1.0,
1439    );
1440    let g110 = grad(PERM[(pd + zi as usize) & 255], xf - 1.0, yf - 1.0, zf);
1441    let g111 = grad(
1442        PERM[(pd + ((zi + 1) & 255) as usize) & 255],
1443        xf - 1.0,
1444        yf - 1.0,
1445        zf - 1.0,
1446    );
1447
1448    let l00 = g000 + u * (g100 - g000);
1449    let l01 = g001 + u * (g101 - g001);
1450    let l10 = g010 + u * (g110 - g010);
1451    let l11 = g011 + u * (g111 - g011);
1452
1453    let l0 = l00 + v * (l10 - l00);
1454    let l1 = l01 + v * (l11 - l01);
1455
1456    l0 + w * (l1 - l0)
1457}
1458
1459fn fast_rand_f64(state: &mut u64) -> f64 {
1460    *state = state
1461        .wrapping_mul(6364136223846793005)
1462        .wrapping_add(1442695040888963407);
1463    ((*state >> 32) as u32) as f64 / 4294967296.0
1464}
1465
1466// ─── Circle Drawing Primitives ────────────────────────────────────────────────
1467
1468/// Write one pixel into the framebuffer (normal or additive blend).
1469#[inline]
1470fn put_px(buf: &mut [u32], idx: usize, color: u32, blend: u8) {
1471    if idx >= buf.len() {
1472        return;
1473    }
1474    if blend == 0 {
1475        buf[idx] = color;
1476    } else {
1477        let old = buf[idx];
1478        let r = (((old >> 16) & 255) + ((color >> 16) & 255)).min(255);
1479        let g = (((old >> 8) & 255) + ((color >> 8) & 255)).min(255);
1480        let b = ((old & 255) + (color & 255)).min(255);
1481        buf[idx] = (r << 16) | (g << 8) | b;
1482    }
1483}
1484
1485/// Pack three float colour channels (0..255) into a 0x00RRGGBB word, clamping.
1486#[inline]
1487fn rgb(r: f64, g: f64, b: f64) -> u32 {
1488    let r = (r as i64).clamp(0, 255) as u32;
1489    let g = (g as i64).clamp(0, 255) as u32;
1490    let b = (b as i64).clamp(0, 255) as u32;
1491    (r << 16) | (g << 8) | b
1492}
1493
1494#[allow(clippy::too_many_arguments)]
1495fn draw_circle_outline(
1496    buf: &mut [u32],
1497    w: i32,
1498    h: i32,
1499    cx: i32,
1500    cy: i32,
1501    r: i32,
1502    color: u32,
1503    blend: u8,
1504) {
1505    let r = r.clamp(0, 20000); // guard against overflow / runaway from tiny depths
1506    if r == 0 {
1507        return;
1508    }
1509    let mut x = 0;
1510    let mut y = r;
1511    let mut d = 3 - 2 * r;
1512    while x <= y {
1513        plot_circle_points(buf, w, h, cx, cy, x, y, color, blend);
1514        if d < 0 {
1515            d += 4 * x + 6;
1516        } else {
1517            d += 4 * (x - y) + 10;
1518            y -= 1;
1519        }
1520        x += 1;
1521    }
1522}
1523
1524#[allow(clippy::too_many_arguments)]
1525fn plot_circle_points(
1526    buf: &mut [u32],
1527    w: i32,
1528    h: i32,
1529    cx: i32,
1530    cy: i32,
1531    x: i32,
1532    y: i32,
1533    color: u32,
1534    blend: u8,
1535) {
1536    let points = [
1537        (cx + x, cy + y),
1538        (cx - x, cy + y),
1539        (cx + x, cy - y),
1540        (cx - x, cy - y),
1541        (cx + y, cy + x),
1542        (cx - y, cy + x),
1543        (cx + y, cy - x),
1544        (cx - y, cy - x),
1545    ];
1546    for &(px, py) in &points {
1547        if px >= 0 && px < w && py >= 0 && py < h {
1548            put_px(buf, (py * w + px) as usize, color, blend);
1549        }
1550    }
1551}
1552
1553#[allow(clippy::too_many_arguments)]
1554fn draw_circle_filled(
1555    buf: &mut [u32],
1556    w: i32,
1557    h: i32,
1558    cx: i32,
1559    cy: i32,
1560    r: i32,
1561    color: u32,
1562    blend: u8,
1563) {
1564    if r <= 0 {
1565        return;
1566    }
1567    for dy in -r..=r {
1568        let dx_max = ((r * r - dy * dy) as f64).sqrt() as i32;
1569        let py = cy + dy;
1570        if py < 0 || py >= h {
1571            continue;
1572        }
1573        for dx in -dx_max..=dx_max {
1574            let px = cx + dx;
1575            if px >= 0 && px < w {
1576                put_px(buf, (py * w + px) as usize, color, blend);
1577            }
1578        }
1579    }
1580}
1581
1582#[cfg(test)]
1583mod draw_tests {
1584    use super::*;
1585
1586    #[test]
1587    fn filled_circle_actually_writes_pixels() {
1588        let mut buf = vec![0u32; 100 * 100];
1589        draw_circle_filled(&mut buf, 100, 100, 50, 50, 10, 0xFF00FF, 0);
1590        assert_eq!(buf[50 * 100 + 50], 0xFF00FF, "centre pixel must be filled");
1591        assert_eq!(buf[0], 0, "far corner must stay clear");
1592        let n = buf.iter().filter(|&&p| p != 0).count();
1593        assert!(n > 200 && n < 500, "r=10 disc area ≈ 314, got {n}");
1594    }
1595
1596    #[test]
1597    fn circle_outline_writes_a_ring() {
1598        let mut buf = vec![0u32; 100 * 100];
1599        draw_circle_outline(&mut buf, 100, 100, 50, 50, 20, 0x00FF00, 0);
1600        assert_eq!(buf[50 * 100 + 50], 0, "outline must NOT fill the centre");
1601        assert!(
1602            buf.iter().any(|&p| p == 0x00FF00),
1603            "outline must draw a ring"
1604        );
1605    }
1606
1607    #[test]
1608    fn additive_blend_accumulates_channels() {
1609        let mut buf = vec![0x202020u32; 1];
1610        put_px(&mut buf, 0, 0x404040, 1);
1611        assert_eq!(buf[0], 0x606060);
1612    }
1613}
1614
1615// ─── Interpreter ─────────────────────────────────────────────────────────────
1616
1617/// Customizable colour palette for the vector UI toolkit (packed 0x00RRGGBB).
1618/// `ui_theme(...)` sets it; every widget falls back to these and accepts a
1619/// trailing `r,g,b` override.
1620#[derive(Clone, Copy)]
1621pub struct UiTheme {
1622    pub primary: u32,
1623    pub accent: u32,
1624    pub track: u32,
1625    pub warn: u32,
1626    pub text: u32,
1627    pub bg: u32,
1628}
1629
1630impl Default for UiTheme {
1631    fn default() -> Self {
1632        Self {
1633            primary: 0x00D2FF, // holographic cyan
1634            accent: 0x28FFB4,  // mint
1635            track: 0x2C3E64,   // dim slate
1636            warn: 0xFF5A5A,    // alert red
1637            text: 0xBEEBFF,    // pale cyan
1638            bg: 0x0A1018,      // near-black panel
1639        }
1640    }
1641}
1642
1643#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1644pub struct Interpreter {
1645    globals: HashMap<String, Expr>,
1646    /// Globals evaluated ONCE at program start (immutable after load).
1647    /// call_named clones this instead of re-evaluating every global per call.
1648    global_seed: Env,
1649    functions: FxHashMap<String, Rc<FnDef>>,
1650    /// `form` definitions: struct name → ordered field names.
1651    pub(crate) structs: HashMap<String, Vec<String>>,
1652    /// `choose` variants: variant name (bare and `Enum::Variant`) → (enum name, arity).
1653    enum_variants: HashMap<String, (String, usize)>,
1654    _modules: HashMap<String, Vec<FnDef>>,
1655    gfx: RefCell<GfxState>,
1656    svg: RefCell<Option<SvgWriter>>,
1657    /// Directory of the primary source file, for relative `use` resolution.
1658    pub source_dir: Option<std::path::PathBuf>,
1659    /// Files already loaded — prevents circular imports.
1660    loaded_files: std::collections::HashSet<String>,
1661    /// Optional audio engine — `None` if no audio device is available.
1662    #[cfg(not(target_arch = "wasm32"))]
1663    audio: Option<AudioEngine>,
1664    #[cfg(not(target_arch = "wasm32"))]
1665    fft: RefCell<FftAnalyzer>,
1666    fft_bands_cache: RefCell<Vec<f32>>,
1667    /// Real-time clock — seconds since Unix epoch at startup (f64 works on both
1668    /// native and wasm32; Instant is not available on wasm32).
1669    start_time_secs: f64,
1670    /// Frame counter — incremented at each present()
1671    frame_num: u64,
1672    /// Target framerate used to pace `present()` on wasm32.
1673    #[cfg(target_arch = "wasm32")]
1674    wasm_target_fps: f64,
1675    /// Next frame deadline (ms since epoch) for wasm frame pacing.
1676    #[cfg(target_arch = "wasm32")]
1677    wasm_next_present_ms: f64,
1678    /// Random state for rand() builtin (xorshift)
1679    rand_state: u64,
1680    /// Microphone input (Phase 1 audio reactivity)
1681    #[cfg(not(target_arch = "wasm32"))]
1682    mic: Option<ling_mic::MicInput>,
1683    /// Persistent KEM keypairs (knot / hybrid identities), referenced by handle.
1684    #[cfg(not(target_arch = "wasm32"))]
1685    crypto_ids: Vec<ling_crypto::KnotIdentity>,
1686    /// Persistent Ed25519 signing keypairs, referenced by handle.
1687    #[cfg(not(target_arch = "wasm32"))]
1688    ed25519_ids: Vec<ling_crypto::Ed25519Keypair>,
1689    /// Persistent ML-DSA-65 (post-quantum) signing keypairs, referenced by handle.
1690    #[cfg(not(target_arch = "wasm32"))]
1691    mldsa_ids: Vec<ling_crypto::MlDsa65Keypair>,
1692    /// Editable text-input buffer (ling-ui text fields).
1693    text_buffer: String,
1694    /// Frame counter for record_frame().
1695    record_n: u32,
1696    /// Accumulated microphone samples (for turning sound into crypto donuts).
1697    #[cfg(not(target_arch = "wasm32"))]
1698    mic_buffer: Vec<f32>,
1699    /// Loaded vector UI fonts, referenced by handle (index) from `font_load`.
1700    #[cfg(not(target_arch = "wasm32"))]
1701    fonts: Vec<ling_graphics::VectorFont>,
1702    /// Loaded raster images (PNG/etc.), referenced by handle (index) from
1703    /// `image_load` — read pixel-by-pixel via `image_pixel_r/g/b/a`.
1704    images: Vec<image::RgbaImage>,
1705    /// Customizable UI colour palette (set via `ui_theme`).
1706    ui_theme: UiTheme,
1707    /// Left-mouse state on the previous frame — for widget click-edge detection.
1708    mouse_was_down: bool,
1709    /// Live music engine (decode playback + GM synth) — lazily initialised.
1710    #[cfg(not(target_arch = "wasm32"))]
1711    music: Option<ling_music::MusicEngine>,
1712    #[cfg(not(target_arch = "wasm32"))]
1713    music_init: bool,
1714    /// Decoded tracks (for analysis + playback), by `music_load` handle.
1715    tracks: Vec<ling_music::DecodedAudio>,
1716    /// Parsed `.lrc` lyrics, by `music_lrc` handle.
1717    lyrics: Vec<ling_music::Lyrics>,
1718    /// Parsed MIDI songs, by `music_midi_load` handle.
1719    midis: Vec<ling_music::MidiSong>,
1720    /// Soft bodies (deformable balls), by `soft_ball` handle.
1721    soft_bodies: Vec<ling_physics::soft::SoftBody>,
1722    /// Rigid-body world (angular dynamics), shared by `rb_*`.
1723    rigid_world: ling_physics::rigid::PhysicsWorld,
1724    /// Liquid grids (water/oil), by `liquid_new` handle.
1725    liquids: Vec<ling_physics::liquid::LiquidGrid>,
1726    meshes: Vec<crate::gfx::shapes::ColorMesh>,
1727    /// Loaded glTF models (skeleton + skin weights + animations), by `mesh_load` handle.
1728    gltf_models: std::cell::RefCell<Vec<ling_physics::gltf::GltfModel>>,
1729    /// Active cinematic dialog box (Ocarina/Majora-style), if any.
1730    dialog: Option<ling_game::dialog::Dialog>,
1731    /// Dialog highlight colours by role: text, name, place, item (0x00RRGGBB).
1732    dialog_colors: [u32; 4],
1733    /// Active user-function call frames (names), for error tracebacks.
1734    frames: Vec<String>,
1735    /// Snapshot of `frames` captured the moment a runtime error first arose
1736    /// (the deepest call). Consumed by `take_error_trace`.
1737    error_trace: Option<Vec<String>>,
1738    /// Unified input (gamepads/joysticks/VR/touch via the ling-input
1739    /// "Sensorium"). Lazily initialised on the first `pad_*` builtin call;
1740    /// `None` if no native input backend is available.
1741    #[cfg(not(target_arch = "wasm32"))]
1742    input: RefCell<Option<InputState>>,
1743    /// Routes registered by `http_route(method, path, handler)`, consumed
1744    /// by `http_serve`. Lives here (not a global, unlike `net`) because the
1745    /// handler is a `Value::Fn` closure — `Value` holds `Rc` and so can't
1746    /// safely live in a `static`.
1747    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1748    http_routes: Vec<(String, String, Value)>,
1749    /// SQLite handle opened by `db_open` — plain rusqlite Connection, no
1750    /// pool: the interpreter is single-threaded, so one connection is both
1751    /// sufficient and contention-free.
1752    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1753    db: Option<ling_http::rusqlite::Connection>,
1754    /// `(url_prefix, disk_dir)` pairs registered by `http_static`, consumed by
1755    /// `http_serve` — served as raw bytes, bypassing the String-only Value bridge.
1756    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1757    http_static_dirs: Vec<(String, String)>,
1758    /// Background jobs started by `http_post_async`, polled by `http_job_poll`.
1759    /// A plain `Arc<Mutex<..>>` handle (not `Value`), so it's fine to touch from
1760    /// the background tokio task that fills in each job's result.
1761    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1762    async_jobs: web::AsyncJobs,
1763    /// Google sign-in exchanges started by `oauth_google_start`, polled by
1764    /// `oauth_google_done`/`_error`/`_sub`/`_email`/`_name`. Separate from
1765    /// `async_jobs` because a login result has three fields, not one string.
1766    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1767    oauth_jobs: web::OAuthJobs,
1768}
1769
1770/// Live gamepad input state: a ling-input hub fed by the native `gilrs` backend.
1771#[cfg(not(target_arch = "wasm32"))]
1772struct InputState {
1773    sensorium: ling_input::Sensorium,
1774    backend: ling_input::backend::GilrsBackend,
1775}
1776
1777impl Default for Interpreter {
1778    fn default() -> Self {
1779        Self::new()
1780    }
1781}
1782
1783impl Interpreter {
1784    pub fn new() -> Self {
1785        #[cfg(not(target_arch = "wasm32"))]
1786        let audio = AudioEngine::new()
1787            .map_err(|e| eprintln!("audio init failed (no sound): {e}"))
1788            .ok();
1789        Self {
1790            globals: HashMap::new(),
1791            global_seed: new_env(),
1792            functions: FxHashMap::default(),
1793            structs: HashMap::new(),
1794            enum_variants: HashMap::new(),
1795            _modules: HashMap::new(),
1796            gfx: RefCell::new(GfxState::new()),
1797            svg: RefCell::new(None),
1798            source_dir: None,
1799            loaded_files: std::collections::HashSet::new(),
1800            #[cfg(not(target_arch = "wasm32"))]
1801            audio,
1802            #[cfg(not(target_arch = "wasm32"))]
1803            fft: RefCell::new(FftAnalyzer::new(2048, 44100)),
1804            fft_bands_cache: RefCell::new(vec![]),
1805            start_time_secs: crate::runtime::now_secs(),
1806            frame_num: 0,
1807            #[cfg(target_arch = "wasm32")]
1808            wasm_target_fps: 60.0,
1809            #[cfg(target_arch = "wasm32")]
1810            wasm_next_present_ms: 0.0,
1811            rand_state: 0x123456789ABCDEF,
1812            #[cfg(not(target_arch = "wasm32"))]
1813            mic: None,
1814            #[cfg(not(target_arch = "wasm32"))]
1815            crypto_ids: Vec::new(),
1816            #[cfg(not(target_arch = "wasm32"))]
1817            ed25519_ids: Vec::new(),
1818            #[cfg(not(target_arch = "wasm32"))]
1819            mldsa_ids: Vec::new(),
1820            text_buffer: String::new(),
1821            record_n: 0,
1822            #[cfg(not(target_arch = "wasm32"))]
1823            mic_buffer: Vec::new(),
1824            #[cfg(not(target_arch = "wasm32"))]
1825            fonts: Vec::new(),
1826            images: Vec::new(),
1827            ui_theme: UiTheme::default(),
1828            mouse_was_down: false,
1829            #[cfg(not(target_arch = "wasm32"))]
1830            music: None,
1831            #[cfg(not(target_arch = "wasm32"))]
1832            music_init: false,
1833            tracks: Vec::new(),
1834            lyrics: Vec::new(),
1835            midis: Vec::new(),
1836            soft_bodies: Vec::new(),
1837            rigid_world: ling_physics::rigid::PhysicsWorld::new(),
1838            liquids: Vec::new(),
1839            meshes: Vec::new(),
1840            gltf_models: std::cell::RefCell::new(Vec::new()),
1841            dialog: None,
1842            dialog_colors: [0xE6F2FF, 0xFFD24A, 0x4AD2FF, 0x6CFF8C], // text · name · place · item
1843            frames: Vec::new(),
1844            error_trace: None,
1845            #[cfg(not(target_arch = "wasm32"))]
1846            input: RefCell::new(None),
1847            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1848            http_routes: Vec::new(),
1849            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1850            db: None,
1851            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1852            http_static_dirs: Vec::new(),
1853            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1854            async_jobs: web::AsyncJobs::new(),
1855            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1856            oauth_jobs: web::OAuthJobs::new(),
1857        }
1858    }
1859
1860    /// Lazily initialise the input system and advance it one frame; returns the
1861    /// number of connected gamepads. Call once per game-loop iteration (like a
1862    /// window update) before reading `pad_*` state.
1863    #[cfg(not(target_arch = "wasm32"))]
1864    fn pad_poll(&self) -> usize {
1865        let mut slot = self.input.borrow_mut();
1866        if slot.is_none() {
1867            match ling_input::backend::GilrsBackend::new() {
1868                Ok(backend) => {
1869                    *slot = Some(InputState { sensorium: ling_input::Sensorium::new(4), backend });
1870                },
1871                Err(_) => return 0,
1872            }
1873        }
1874        let st = slot.as_mut().unwrap();
1875        st.sensorium.begin_frame();
1876        st.sensorium.pump(&mut st.backend);
1877        st.sensorium.update(1.0 / 60.0);
1878        st.sensorium.devices.count()
1879    }
1880
1881    /// Read player `slot`'s gamepad with `f`, or return `default` if there is no
1882    /// input system / no such pad.
1883    #[cfg(not(target_arch = "wasm32"))]
1884    fn with_pad<T>(&self, slot: usize, default: T, f: impl FnOnce(&ling_input::Gamepad) -> T) -> T {
1885        let inp = self.input.borrow();
1886        match inp.as_ref().and_then(|s| s.sensorium.player(slot)) {
1887            Some(p) => f(p),
1888            None => default,
1889        }
1890    }
1891
1892    /// Take the call-stack snapshot captured at the deepest runtime error, if any.
1893    /// Frames are ordered outermost-first (entry point first, failing call last).
1894    pub fn take_error_trace(&mut self) -> Vec<String> {
1895        self.error_trace.take().unwrap_or_default()
1896    }
1897
1898    #[cfg(target_arch = "wasm32")]
1899    fn wasm_pace_frame(&mut self) {
1900        let fps = self.wasm_target_fps.max(1.0);
1901        let frame_ms = 1000.0 / fps;
1902        let now = js_sys::Date::now();
1903        if self.wasm_next_present_ms <= 0.0 {
1904            self.wasm_next_present_ms = now + frame_ms;
1905            return;
1906        }
1907
1908        let wait_ms = (self.wasm_next_present_ms - now).floor() as i32;
1909        if wait_ms > 0 {
1910            wasm_sleep_ms(wait_ms);
1911        }
1912
1913        let after = js_sys::Date::now();
1914        if after > self.wasm_next_present_ms + frame_ms * 3.0 {
1915            self.wasm_next_present_ms = after + frame_ms;
1916        } else {
1917            self.wasm_next_present_ms += frame_ms;
1918        }
1919    }
1920
1921    /// Run `body`, recording `name` as a call frame and snapshotting the stack on
1922    /// the first runtime error so a traceback can be reported.
1923    fn framed<T, F>(&mut self, name: &str, body: F) -> Result<T, EvalErr>
1924    where
1925        F: FnOnce(&mut Self) -> Result<T, EvalErr>,
1926    {
1927        self.frames.push(name.to_string());
1928        let result = body(self);
1929        if matches!(result, Err(EvalErr::Runtime(_))) && self.error_trace.is_none() {
1930            self.error_trace = Some(self.frames.clone());
1931        }
1932        self.frames.pop();
1933        result
1934    }
1935
1936    /// Render the active dialog box: beveled frame + dark fill, then the visible
1937    /// (typewriter-revealed) text word-wrapped with colour-coded runs, plus a
1938    /// blinking advance arrow once the page is fully typed.
1939    #[cfg(not(target_arch = "wasm32"))]
1940    fn render_dialog(&mut self, x: f32, y: f32, w: f32, h: f32, font: i64, t: f32) {
1941        let (runs, typing) = match &self.dialog {
1942            Some(d) if !d.is_closed() => {
1943                let runs: Vec<(String, usize, bool)> = d
1944                    .visible_runs()
1945                    .into_iter()
1946                    .map(|r| (r.text, r.role.index(), r.newline_before))
1947                    .collect();
1948                (runs, d.is_typing())
1949            },
1950            _ => return,
1951        };
1952        let colors = self.dialog_colors;
1953        // ── frame + fill ──
1954        let b = 12.0;
1955        let corners: Vec<[f32; 2]> = vec![
1956            [x + b, y],
1957            [x + w - b, y],
1958            [x + w, y + b],
1959            [x + w, y + h - b],
1960            [x + w - b, y + h],
1961            [x + b, y + h],
1962            [x, y + h - b],
1963            [x, y + b],
1964            [x + b, y],
1965        ];
1966        {
1967            let mut gfx = self.gfx.borrow_mut();
1968            let (bw, bh) = (gfx.width, gfx.height);
1969            crate::gfx::raster::fill_contours_aa(
1970                &mut gfx.buffer,
1971                bw,
1972                bh,
1973                0x0A1018,
1974                false,
1975                std::slice::from_ref(&corners),
1976            );
1977            for seg in corners.windows(2) {
1978                crate::gfx::raster::draw_line_aa(
1979                    &mut gfx.buffer,
1980                    bw,
1981                    bh,
1982                    0x00D2FF,
1983                    false,
1984                    seg[0][0],
1985                    seg[0][1],
1986                    seg[1][0],
1987                    seg[1][1],
1988                );
1989            }
1990        }
1991        // ── word-wrapped, colour-coded text ──
1992        let px = 22.0f32;
1993        let pad = 20.0f32;
1994        let line_h = px * 1.45;
1995        let mut cx = x + pad;
1996        let mut cy = y + pad;
1997        let use_font = font >= 0 && (font as usize) < self.fonts.len();
1998        for (text, role, nl) in &runs {
1999            if *nl {
2000                cx = x + pad;
2001                cy += line_h;
2002            }
2003            for word in text.split_inclusive(' ') {
2004                let wpx = if use_font {
2005                    self.fonts[font as usize].measure(word, px)
2006                } else {
2007                    ling_ui::holo::text_width(word, px * 0.6, px * 0.24)
2008                };
2009                if cx + wpx > x + w - pad && cx > x + pad + 1.0 {
2010                    cx = x + pad;
2011                    cy += line_h;
2012                }
2013                if cy + line_h > y + h {
2014                    break;
2015                }
2016                let col = colors[(*role).min(3)];
2017                if use_font {
2018                    let glyphs = self.font_layout_2d_glyphs(font as usize, cx, cy, px, word);
2019                    let mut gfx = self.gfx.borrow_mut();
2020                    let (bw, bh, add) = (gfx.width, gfx.height, gfx.blend == 1);
2021                    for contours in &glyphs {
2022                        crate::gfx::raster::fill_contours_aa(
2023                            &mut gfx.buffer,
2024                            bw,
2025                            bh,
2026                            col,
2027                            add,
2028                            contours,
2029                        );
2030                    }
2031                } else {
2032                    let segs = ling_ui::holo::text_lines(word, cx, cy, px * 0.6, px, px * 0.24);
2033                    let mut gfx = self.gfx.borrow_mut();
2034                    let (bw, bh) = (gfx.width, gfx.height);
2035                    for s in segs {
2036                        draw_line(&mut gfx.buffer, bw, bh, col, s[0], s[1], s[2], s[3]);
2037                    }
2038                }
2039                cx += wpx;
2040            }
2041        }
2042        // ── blinking advance arrow when fully typed ──
2043        if !typing && (t * 3.0).sin() > 0.0 {
2044            let ax = x + w - 26.0;
2045            let ay = y + h - 22.0;
2046            let mut gfx = self.gfx.borrow_mut();
2047            let (bw, bh) = (gfx.width, gfx.height);
2048            crate::gfx::raster::fill_contours_aa(
2049                &mut gfx.buffer,
2050                bw,
2051                bh,
2052                0x00D2FF,
2053                false,
2054                std::slice::from_ref(&vec![
2055                    [ax - 7.0, ay],
2056                    [ax + 7.0, ay],
2057                    [ax, ay + 9.0],
2058                    [ax - 7.0, ay],
2059                ]),
2060            );
2061        }
2062    }
2063
2064    /// Lazily start the music engine on first use (playback/synth need a device;
2065    /// analysis/decoding do not). Returns `false` if no audio device is available.
2066    #[cfg(not(target_arch = "wasm32"))]
2067    fn ensure_music(&mut self) -> bool {
2068        if self.music.is_some() {
2069            return true;
2070        }
2071        if self.music_init {
2072            return false;
2073        }
2074        self.music_init = true;
2075        match ling_music::MusicEngine::new() {
2076            Ok(m) => {
2077                self.music = Some(m);
2078                true
2079            },
2080            Err(e) => {
2081                eprintln!("music engine init failed (no music playback): {e}");
2082                false
2083            },
2084        }
2085    }
2086
2087    #[cfg(target_arch = "wasm32")]
2088    fn wasm_resolve_source_path(&self, path: &str) -> String {
2089        let p = path.trim();
2090        if p.is_empty() {
2091            return String::new();
2092        }
2093        if p.contains("://") || p.starts_with('/') || p.starts_with("./") || p.starts_with("../") {
2094            return p.to_string();
2095        }
2096        if let Some(d) = &self.source_dir {
2097            let base = d.to_string_lossy().replace('\\', "/");
2098            if !base.is_empty() {
2099                return format!(
2100                    "{}/{}",
2101                    base.trim_end_matches('/'),
2102                    p.trim_start_matches("./")
2103                );
2104            }
2105        }
2106        p.to_string()
2107    }
2108
2109    #[cfg(target_arch = "wasm32")]
2110    fn wasm_music_builtin(&mut self, name: &str, args: &[Value]) -> Result<Option<Value>, EvalErr> {
2111        match name {
2112            // music_load(path) -> track handle (decode from fetched bytes)
2113            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" | "بارگذاری_موسیقی" | "تحميل_الموسيقى" | "טעינת_מוזיקה" | "موسیقی_لوڈ" | "charger_musique" | "musik_laden" | "загрузить_музыку" =>
2114            {
2115                let path = self.arg_str(args, 0, "");
2116                let resolved = self.wasm_resolve_source_path(&path);
2117                match wasm_fetch_bytes(&resolved)
2118                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
2119                {
2120                    Ok(t) => {
2121                        let id = self.tracks.len();
2122                        self.tracks.push(t);
2123                        return Ok(Some(Value::Number(id as f64)));
2124                    },
2125                    Err(e) => {
2126                        eprintln!("music_load failed ({path}): {e}");
2127                        return Ok(Some(Value::Number(-1.0)));
2128                    },
2129                }
2130            },
2131            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" | "مدت_موسیقی" | "مدة_الموسيقى" | "משך_מוזיקה" | "موسیقی_دورانیہ" | "durée_musique" | "musik_dauer" | "длительность_музыки" =>
2132            {
2133                let id = self.arg_num(args, 0, 0.0)? as i64;
2134                let d = self
2135                    .tracks
2136                    .get(id as usize)
2137                    .map(|t| t.duration)
2138                    .unwrap_or(0.0);
2139                return Ok(Some(Value::Number(d as f64)));
2140            },
2141            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" | "ضربان_در_دقیقه" | "نبضات_بالدقيقة" | "פעימות_לדקה" | "بی_پی_ایم" | "bpm_musique" | "musik_bpm" | "музыка_bpm" =>
2142            {
2143                let id = self.arg_num(args, 0, 0.0)? as i64;
2144                let b = self
2145                    .tracks
2146                    .get(id as usize)
2147                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
2148                    .unwrap_or(0.0);
2149                return Ok(Some(Value::Number(b as f64)));
2150            },
2151            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" | "گام_موسیقی" | "مقام_الموسيقى" | "סולם_מוזיקלי" | "موسیقی_کلید" | "tonalité_musique" | "musik_tonart" | "тональность_музыки" => {
2152                let id = self.arg_num(args, 0, 0.0)? as i64;
2153                let k = self
2154                    .tracks
2155                    .get(id as usize)
2156                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
2157                    .unwrap_or_default();
2158                return Ok(Some(Value::Str(k)));
2159            },
2160            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" | "آغازهای_نت" | "بدايات_النغمات" | "התחלות_תווים" | "نوٹ_شروعات" | "attaques_musique" | "musik_einsätze" | "атаки_музыки" =>
2161            {
2162                let id = self.arg_num(args, 0, 0.0)? as i64;
2163                let v = self
2164                    .tracks
2165                    .get(id as usize)
2166                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
2167                    .unwrap_or_default();
2168                return Ok(Some(Value::List(
2169                    v.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2170                )));
2171            },
2172            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" | "شبکه_ضرب" | "شبكة_الإيقاع" | "רשת_פעימות" | "بیٹ_گرڈ" | "grille_temps_musique" | "musik_taktraster" | "сетка_ритма_музыки" =>
2173            {
2174                let id = self.arg_num(args, 0, 0.0)? as i64;
2175                let beats = self
2176                    .tracks
2177                    .get(id as usize)
2178                    .map(|t| {
2179                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
2180                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
2181                    })
2182                    .unwrap_or_default();
2183                return Ok(Some(Value::List(
2184                    beats.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2185                )));
2186            },
2187            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" | "بارگذاری_متن_ترانه" | "تحميل_كلمات_الأغنية" | "טעינת_מילות_שיר" | "گیت_متن_لوڈ" | "lrc_musique" | "musik_lrc" | "lrc_музыки" =>
2188            {
2189                let path = self.arg_str(args, 0, "");
2190                let resolved = self.wasm_resolve_source_path(&path);
2191                match wasm_fetch_text(&resolved) {
2192                    Ok(text) => {
2193                        let id = self.lyrics.len();
2194                        self.lyrics.push(ling_music::Lyrics::parse(&text));
2195                        return Ok(Some(Value::Number(id as f64)));
2196                    },
2197                    Err(e) => {
2198                        eprintln!("music_lrc failed ({path}): {e}");
2199                        return Ok(Some(Value::Number(-1.0)));
2200                    },
2201                }
2202            },
2203            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" | "متن_ترانه_فعلی" | "كلمات_الأغنية_الحالية" | "מילות_שיר_נוכחיות" | "موجودہ_گیت_متن" | "paroles_musique" | "musik_liedtext" | "текст_песни" =>
2204            {
2205                let id = self.arg_num(args, 0, 0.0)? as i64;
2206                let t = self.arg_num(args, 1, 0.0)? as f32;
2207                let line = self
2208                    .lyrics
2209                    .get(id as usize)
2210                    .map(|l| l.line_at(t).to_string())
2211                    .unwrap_or_default();
2212                return Ok(Some(Value::Str(line)));
2213            },
2214            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" | "بارگذاری_MIDI" | "تحميل_MIDI" | "טעינת_MIDI" | "MIDI_لوڈ" | "charger_midi_musique" | "musik_midi_laden" | "загрузить_midi_музыки" =>
2215            {
2216                let path = self.arg_str(args, 0, "");
2217                let resolved = self.wasm_resolve_source_path(&path);
2218                match wasm_fetch_bytes(&resolved).and_then(|bytes| {
2219                    ling_music::midi::from_bytes(&bytes).map_err(|e| e.to_string())
2220                }) {
2221                    Ok(m) => {
2222                        let id = self.midis.len();
2223                        self.midis.push(m);
2224                        return Ok(Some(Value::Number(id as f64)));
2225                    },
2226                    Err(e) => {
2227                        eprintln!("music_midi_load failed ({path}): {e}");
2228                        return Ok(Some(Value::Number(-1.0)));
2229                    },
2230                }
2231            },
2232            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" | "تعداد_MIDI" | "عدد_MIDI" | "מספר_MIDI" | "MIDI_تعداد" | "nombre_midi_musique" | "musik_midi_anzahl" | "число_midi_музыки" =>
2233            {
2234                let id = self.arg_num(args, 0, 0.0)? as i64;
2235                let n = self
2236                    .midis
2237                    .get(id as usize)
2238                    .map(|m| m.notes.len())
2239                    .unwrap_or(0);
2240                return Ok(Some(Value::Number(n as f64)));
2241            },
2242            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" | "نت‌های_MIDI" | "نغمات_MIDI" | "תווי_MIDI" | "MIDI_نوٹس" | "notes_midi_musique" | "musik_midi_noten" | "ноты_midi_музыки" =>
2243            {
2244                let id = self.arg_num(args, 0, 0.0)? as i64;
2245                let mut out = Vec::new();
2246                if let Some(m) = self.midis.get(id as usize) {
2247                    for n in &m.notes {
2248                        out.push(Value::Number(n.time as f64));
2249                        out.push(Value::Number(n.midi as f64));
2250                    }
2251                }
2252                return Ok(Some(Value::List(out.into())));
2253            },
2254            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" | "میله‌های_MIDI" | "أعمدة_MIDI" | "עמודות_MIDI" | "MIDI_بارز" | "mesures_midi_musique" | "musik_midi_takte" | "такты_midi_музыки" =>
2255            {
2256                let id = self.arg_num(args, 0, 0.0)? as i64;
2257                let mut out = Vec::new();
2258                if let Some(m) = self.midis.get(id as usize) {
2259                    for n in &m.notes {
2260                        out.push(Value::Number(n.time as f64));
2261                        out.push(Value::Number(n.midi as f64));
2262                        out.push(Value::Number(n.dur as f64));
2263                    }
2264                }
2265                return Ok(Some(Value::List(out.into())));
2266            },
2267            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" | "داوری_ضرب" | "حكم_الإيقاع" | "שיפוט_קצב" | "بیٹ_فیصلہ" | "juger_musique" | "musik_bewerten" | "оценить_музыку" =>
2268            {
2269                let delta_ms = self.arg_num(args, 0, 9999.0)? as f32;
2270                return Ok(Some(Value::Number(
2271                    ling_music::Grade::judge(delta_ms).index() as f64,
2272                )));
2273            },
2274            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" | "نام_رتبه" | "اسم_التقييم" | "שם_דירוג" | "گریڈ_نام" | "nom_grade_musique" | "musik_bewertungsname" | "имя_оценки_музыки" =>
2275            {
2276                let idx = self.arg_num(args, 0, 4.0)? as i32;
2277                return Ok(Some(Value::Str(
2278                    ling_music::Grade::from_index(idx).name().to_string(),
2279                )));
2280            },
2281            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" | "نام_نت" | "اسم_النغمة" | "שם_תו" | "نوٹ_نام" | "nom_note_musique" | "musik_notenname" | "имя_ноты_музыки" =>
2282            {
2283                let hz = self.arg_num(args, 0, 0.0)? as f32;
2284                return Ok(Some(Value::Str(ling_music::note::hz_to_name(hz))));
2285            },
2286            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" | "فرکانس_نت" | "تردد_النغمة" | "תדר_תו" | "نوٹ_ہرٹز" | "hz_musique" | "musik_hz" | "музыка_гц" =>
2287            {
2288                let midi = match args.get(0) {
2289                    Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(69),
2290                    Some(Value::Number(n)) => *n as i32,
2291                    _ => 69,
2292                };
2293                return Ok(Some(Value::Number(
2294                    ling_music::note::midi_to_hz(midi as f32) as f64,
2295                )));
2296            },
2297            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" | "امتیاز_زیروبمی" | "درجة_طبقة_الصوت" | "ציון_גובה_צליל" | "پچ_اسکور" | "score_hauteur_musique" | "musik_tonhöhen_punktzahl" | "счёт_высоты_тона" =>
2298            {
2299                let hz = self.arg_num(args, 0, 0.0)? as f32;
2300                let target = self.arg_num(args, 1, 0.0)? as f32;
2301                return Ok(Some(Value::Number(
2302                    ling_music::karaoke::pitch_score(hz, target) as f64,
2303                )));
2304            },
2305
2306            // ── Playback ──────────────────────────────────────────────────────
2307            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" | "پخش_موسیقی" | "شغّل_الموسيقى" | "נגן_מוזיקה" | "موسیقی_چلاؤ" | "jouer_musique" | "musik_abspielen" | "играть_музыку" =>
2308            {
2309                let id = self.arg_num(args, 0, 0.0)? as usize;
2310                if let Some(t) = self.tracks.get(id) {
2311                    crate::gfx::audio_web::play_music(id, &t.stereo, t.channels, t.rate, 1.0);
2312                }
2313                return Ok(Some(Value::Unit));
2314            },
2315            "music_pause"
2316            | "暂停音乐"
2317            | "音楽一時停止"
2318            | "음악일시정지"
2319            | "หยุดเพลงชั่วคราว"
2320            | "music_stop"
2321            | "停止音乐"
2322            | "音楽停止"
2323            | "음악정지"
2324            | "หยุดเพลง" | "مکث_موسیقی" | "ألبث_الموسيقى" | "השהה_מוזיקה" | "موسیقی_روکو_مؤقت" | "pause_musique" | "musik_pausieren" | "пауза_музыки" => {
2325                let id = self.arg_num(args, 0, 0.0)? as usize;
2326                crate::gfx::audio_web::stop_music(id);
2327                return Ok(Some(Value::Unit));
2328            },
2329            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" | "جستجوی_موسیقی" | "ابحث_في_الموسيقى" | "חפש_במוזיקה" | "موسیقی_تلاش" | "chercher_musique" | "musik_suchen" | "перемотать_музыку" =>
2330            {
2331                // Seek is not straightforward on AudioBufferSourceNode; no-op for now.
2332                return Ok(Some(Value::Unit));
2333            },
2334            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" | "موقعیت_موسیقی" | "موضع_الموسيقى" | "מיקום_מוזיקה" | "موسیقی_مقام" | "position_musique" | "musik_position" | "позиция_музыки" =>
2335            {
2336                return Ok(Some(Value::Number(
2337                    crate::gfx::audio_web::current_music_position(),
2338                )));
2339            },
2340            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" | "بلندی_موسیقی" | "مستوى_الموسيقى" | "עוצמת_מוזיקה" | "موسیقی_شدت" | "volume_musique" | "musik_lautstärke" | "громкость_музыки" =>
2341            {
2342                let vol = self.arg_num(args, 0, 0.8)? as f32;
2343                // Apply to the most-recently started slot (slot 0 is typical).
2344                crate::gfx::audio_web::set_music_volume(0, vol);
2345                return Ok(Some(Value::Unit));
2346            },
2347
2348            // ── FFT bands at current playback position ─────────────────────
2349            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" | "طیف_موسیقی" | "طيف_الموسيقى" | "ספקטרום_מוזיקה" | "میوزک_اسپیکٹرم" | "fft_musique" | "musik_fft" | "fft_музыки" =>
2350            {
2351                let id = self.arg_num(args, 0, 0.0)? as usize;
2352                let nbands = self.arg_num(args, 1, 16.0)? as usize;
2353                let pos = crate::gfx::audio_web::current_music_position() as f32;
2354                let bands = if let Some(t) = self.tracks.get(id) {
2355                    ling_music::analysis::fft_bands_at_pos(&t.mono, t.rate, pos, nbands)
2356                } else {
2357                    vec![0.0f32; nbands]
2358                };
2359                return Ok(Some(Value::List(
2360                    bands.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2361                )));
2362            },
2363
2364            _ => {},
2365        }
2366        Ok(None)
2367    }
2368
2369    /// Lay out `text` for font `id` at size `px`, returning every glyph contour as
2370    /// a screen-space polyline (x→right, y→down). `(x, y)` is the text box top-left;
2371    /// the baseline is placed `ascent*px` below it. Curves are flattened to 0.3 px.
2372    #[cfg(not(target_arch = "wasm32"))]
2373    fn font_layout_2d(
2374        &mut self,
2375        id: usize,
2376        x: f32,
2377        y: f32,
2378        px: f32,
2379        text: &str,
2380    ) -> Vec<Vec<[f32; 2]>> {
2381        let mut out = Vec::new();
2382        for g in self.font_layout_2d_glyphs(id, x, y, px, text) {
2383            out.extend(g);
2384        }
2385        out
2386    }
2387
2388    /// Same as [`font_layout_2d`] but grouped per glyph (so a fill can apply the
2389    /// non-zero winding rule within each glyph, preserving interior holes).
2390    #[cfg(not(target_arch = "wasm32"))]
2391    fn font_layout_2d_glyphs(
2392        &mut self,
2393        id: usize,
2394        x: f32,
2395        y: f32,
2396        px: f32,
2397        text: &str,
2398    ) -> Vec<Vec<Vec<[f32; 2]>>> {
2399        let font = &mut self.fonts[id];
2400        let asc = font.ascent();
2401        let tol = 0.3 / px;
2402        let mut pen = 0.0f32;
2403        let mut glyphs = Vec::new();
2404        for ch in text.chars() {
2405            let go = font.glyph_outline(ch, tol);
2406            let mut contours = Vec::with_capacity(go.polylines.len());
2407            for pl in &go.polylines {
2408                let mapped: Vec<[f32; 2]> = pl
2409                    .iter()
2410                    .map(|p| [x + (pen + p[0]) * px, y + (asc - p[1]) * px])
2411                    .collect();
2412                contours.push(mapped);
2413            }
2414            glyphs.push(contours);
2415            pen += go.advance;
2416        }
2417        glyphs
2418    }
2419
2420    /// Register every item (functions, structs, globals) and evaluate the
2421    /// non-`do` globals into `global_seed`, WITHOUT running the entry. Used to
2422    /// prime the JIT's fallback interpreter so cranelift-skipped (oversized)
2423    /// functions can still be interpreted with full access to globals + peers.
2424    pub fn register_program(&mut self, program: &Program) -> Result<(), String> {
2425        for item in &program.items {
2426            self.register_item("", item)?;
2427        }
2428        let mut env = new_env();
2429        let non_do: Vec<_> = self
2430            .globals
2431            .iter()
2432            .filter(|(_, e)| !matches!(e, Expr::Do(_)))
2433            .map(|(k, e)| (k.clone(), e.clone()))
2434            .collect();
2435        let mut pending: Vec<(String, Expr)> = Vec::new();
2436        for (k, expr) in &non_do {
2437            let mut tmp = new_env();
2438            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
2439                env.insert(k.clone(), v);
2440            } else {
2441                pending.push((k.clone(), expr.clone()));
2442            }
2443        }
2444        for (k, expr) in &pending {
2445            let mut tmp = env.clone();
2446            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
2447                env.insert(k.clone(), v);
2448            }
2449        }
2450        self.global_seed = env;
2451        Ok(())
2452    }
2453
2454    pub fn run_program(&mut self, program: &Program) -> Result<(), String> {
2455        self.register_program(program)?;
2456        let entry = self
2457            .find_entry()
2458            .ok_or("no entry point — need `bind start = do {...}` or `ผูก เริ่ม = ทำ {...}`")?;
2459        let mut env = self.global_seed.clone();
2460        self.framed("start", |me| me.eval_expr(&entry, &mut env))
2461            .map(|_| ())
2462            .map_err(|e| match e {
2463                EvalErr::Runtime(s) => s,
2464                EvalErr::Return(_) => "unexpected top-level return".to_string(),
2465                EvalErr::Break => "unexpected break at top level".to_string(),
2466            })
2467    }
2468
2469    fn register_item(&mut self, ns: &str, item: &Item) -> Result<(), String> {
2470        match item {
2471            Item::Bind(name, expr) => {
2472                let key = if ns.is_empty() {
2473                    name.clone()
2474                } else {
2475                    format!("{ns}::{name}")
2476                };
2477                self.globals.insert(key, expr.clone());
2478            },
2479            Item::Fn(def) => {
2480                let key = if ns.is_empty() {
2481                    def.name.clone()
2482                } else {
2483                    format!("{ns}::{}", def.name)
2484                };
2485                self.functions.insert(key, Rc::new(def.clone()));
2486            },
2487            Item::Mod(name, body) => {
2488                let child_ns = if ns.is_empty() {
2489                    name.clone()
2490                } else {
2491                    format!("{ns}::{name}")
2492                };
2493                for child in body {
2494                    self.register_item(&child_ns, child)?;
2495                }
2496            },
2497            Item::TypeAlias(_, _) => {},
2498            Item::Struct(name, fields) => {
2499                self.structs.insert(name.clone(), fields.clone());
2500                if !ns.is_empty() {
2501                    self.structs.insert(format!("{ns}::{name}"), fields.clone());
2502                }
2503            },
2504            Item::Enum(name, variants) => {
2505                for v in variants {
2506                    self.enum_variants
2507                        .insert(v.name.clone(), (name.clone(), v.arity));
2508                    self.enum_variants
2509                        .insert(format!("{name}::{}", v.name), (name.clone(), v.arity));
2510                    if !ns.is_empty() {
2511                        self.enum_variants
2512                            .insert(format!("{ns}::{name}::{}", v.name), (name.clone(), v.arity));
2513                    }
2514                }
2515            },
2516            Item::Use { path, alias } => {
2517                self.load_module(path, alias.as_deref(), ns)?;
2518            },
2519        }
2520        Ok(())
2521    }
2522
2523    /// Resolve `path` relative to `source_dir`, load and parse it, then
2524    /// register all its definitions.  If `alias` is given, every name is
2525    /// prefixed with `<parent_ns>::<alias>`.  Circular imports are silently
2526    /// skipped.
2527    fn load_module(
2528        &mut self,
2529        path: &str,
2530        alias: Option<&str>,
2531        parent_ns: &str,
2532    ) -> Result<(), String> {
2533        // ── Wasm32: no filesystem — use the pre-registered module registry ──
2534        #[cfg(target_arch = "wasm32")]
2535        let (source, sub_dir) = {
2536            // Skip if already loaded (circular import guard)
2537            if self.loaded_files.contains(path) {
2538                return Ok(());
2539            }
2540            self.loaded_files.insert(path.to_string());
2541
2542            let src = crate::runtime::get_wasm_module(path)
2543                .or_else(|| crate::runtime::get_wasm_module(&format!("{}.ling", path)))
2544                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
2545            (src, None::<std::path::PathBuf>)
2546        };
2547
2548        // ── Native: resolve against filesystem ──
2549        #[cfg(not(target_arch = "wasm32"))]
2550        let (source, sub_dir) = {
2551            let base_dir = self
2552                .source_dir
2553                .clone()
2554                .unwrap_or_else(|| std::path::PathBuf::from("."));
2555            let raw = std::path::Path::new(path);
2556            let candidates: Vec<std::path::PathBuf> = vec![
2557                base_dir.join(format!("{}.ling", path)),
2558                base_dir.join(format!("{}.灵", path)),
2559                base_dir.join(format!("{}.령", path)),
2560                base_dir.join(format!("{}.霊", path)),
2561                base_dir.join(format!("{}.ลิง", path)),
2562                base_dir.join(raw),
2563                std::path::PathBuf::from(format!("{}.ling", path)),
2564                std::path::PathBuf::from(path),
2565            ];
2566
2567            let resolved = candidates
2568                .into_iter()
2569                .find(|p| p.exists())
2570                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
2571
2572            let canonical = resolved
2573                .canonicalize()
2574                .unwrap_or_else(|_| resolved.clone())
2575                .to_string_lossy()
2576                .to_string();
2577
2578            // Skip if already loaded (circular import guard)
2579            if self.loaded_files.contains(&canonical) {
2580                return Ok(());
2581            }
2582            self.loaded_files.insert(canonical.clone());
2583
2584            let src = std::fs::read_to_string(&resolved)
2585                .map_err(|e| format!("use: failed to read '{path}': {e}"))?;
2586            let dir = resolved.parent().map(|p| p.to_path_buf());
2587            (src, dir)
2588        };
2589
2590        let program = crate::parser::parse(&source)
2591            .map_err(|e| format!("use: parse error in '{path}': {e}"))?;
2592
2593        // Compute target namespace: parent_ns :: alias (or just alias, or just parent_ns)
2594        let target_ns = match (parent_ns.is_empty(), alias) {
2595            (_, Some(a)) if !parent_ns.is_empty() => format!("{parent_ns}::{a}"),
2596            (_, Some(a)) => a.to_string(),
2597            (false, None) => parent_ns.to_string(),
2598            (true, None) => String::new(),
2599        };
2600
2601        // Save/restore source_dir for nested relative imports
2602        let prev_dir = self.source_dir.clone();
2603        self.source_dir = sub_dir;
2604
2605        for item in &program.items {
2606            self.register_item(&target_ns, item)?;
2607        }
2608
2609        self.source_dir = prev_dir;
2610        Ok(())
2611    }
2612
2613    fn find_entry(&self) -> Option<Expr> {
2614        // Known entry-point names across supported human languages.
2615        for key in crate::entry::ENTRY_NAMES {
2616            if let Some(e) = self.globals.get(*key) {
2617                return Some(e.clone());
2618            }
2619        }
2620        self.globals
2621            .values()
2622            .find(|e| matches!(e, Expr::Do(_)))
2623            .cloned()
2624    }
2625
2626    // ─── Expression evaluation ────────────────────────────────────────────────
2627
2628    fn eval_expr(&mut self, expr: &Expr, env: &mut Env) -> EvalResult {
2629        match expr {
2630            Expr::Str(s) => Ok(Value::Str(s.clone())),
2631            Expr::Number(n) => Ok(Value::Number(*n)),
2632            Expr::Bool(b) => Ok(Value::Bool(*b)),
2633            Expr::Unit => Ok(Value::Unit),
2634            Expr::Array(elems) => {
2635                let vs: Vec<_> = elems
2636                    .iter()
2637                    .map(|e| self.eval_expr(e, env))
2638                    .collect::<Result<_, _>>()?;
2639                Ok(Value::List(Rc::new(vs)))
2640            },
2641
2642            Expr::Ident(name) => self.lookup(name, env),
2643
2644            Expr::Path(segs) => {
2645                if segs.len() == 1 {
2646                    return self.lookup(&segs[0], env);
2647                }
2648                Ok(Value::Str(segs.join("::")))
2649            },
2650
2651            Expr::Ref(inner) => self.eval_expr(inner, env),
2652            Expr::Await(inner) => self.eval_expr(inner, env),
2653
2654            Expr::Do(stmts) => {
2655                let mut local = env.clone();
2656                Ok(self.exec_block(stmts, &mut local)?.unwrap_or(Value::Unit))
2657            },
2658
2659            Expr::BinOp(op, lhs, rhs) => {
2660                let l = self.eval_expr(lhs, env)?;
2661                let r = self.eval_expr(rhs, env)?;
2662                self.apply_binop(op, l, r)
2663            },
2664
2665            Expr::If { cond, then, elseifs, else_body } => {
2666                let cond_val = self.eval_expr(cond, env)?;
2667                if self.is_truthy(&cond_val) {
2668                    return Ok(self.exec_block(then, env)?.unwrap_or(Value::Unit));
2669                }
2670                for (ei_cond, ei_body) in elseifs {
2671                    let ei_cond_val = self.eval_expr(ei_cond, env)?;
2672                    if self.is_truthy(&ei_cond_val) {
2673                        return Ok(self.exec_block(ei_body, env)?.unwrap_or(Value::Unit));
2674                    }
2675                }
2676                if let Some(eb) = else_body {
2677                    return Ok(self.exec_block(eb, env)?.unwrap_or(Value::Unit));
2678                }
2679                Ok(Value::Unit)
2680            },
2681
2682            Expr::While { cond, body } => {
2683                // Run the body directly in the *outer* env so that
2684                // `bind counter = counter + 1` persists across iterations,
2685                // which is the expected behaviour in a scripting language.
2686                loop {
2687                    let cv = self.eval_expr(cond, env)?;
2688                    if !self.is_truthy(&cv) {
2689                        break;
2690                    }
2691                    match self.exec_block(body, env) {
2692                        Ok(_) => {},
2693                        Err(EvalErr::Break) => break,
2694                        Err(e) => return Err(e),
2695                    }
2696                }
2697                Ok(Value::Unit)
2698            },
2699
2700            Expr::For { var, iter, body } => {
2701                let iter_val = self.eval_expr(iter, env)?;
2702                let items = self.value_to_iter(iter_val)?;
2703                for item in items {
2704                    let mut local = env.clone();
2705                    local.insert(var.clone(), item);
2706                    match self.exec_block(body, &mut local) {
2707                        Ok(_) => {},
2708                        Err(EvalErr::Break) => break,
2709                        Err(e) => return Err(e),
2710                    }
2711                }
2712                Ok(Value::Unit)
2713            },
2714
2715            Expr::Match(subject, arms) => {
2716                let subj = self.eval_expr(subject, env)?;
2717                for arm in arms {
2718                    if let Some(bindings) = self.match_pattern(&arm.pattern, &subj) {
2719                        let mut local = env.clone();
2720                        local.extend(bindings);
2721                        return self.eval_expr(&arm.body, &mut local);
2722                    }
2723                }
2724                Ok(Value::Unit)
2725            },
2726
2727            Expr::Range(lo, hi) => {
2728                let lo_v = self.eval_expr(lo, env)?;
2729                let hi_v = self.eval_expr(hi, env)?;
2730                let lo_n = self.to_number(&lo_v)? as i64;
2731                let hi_n = self.to_number(&hi_v)? as i64;
2732                Ok(Value::List(Rc::new(
2733                    (lo_n..hi_n).map(|i| Value::Number(i as f64)).collect(),
2734                )))
2735            },
2736
2737            Expr::Index(base, idx) => {
2738                let b = self.eval_expr(base, env)?;
2739                let i = self.eval_expr(idx, env)?;
2740                let n = self.to_number(&i)? as usize;
2741                match b {
2742                    Value::List(v) => v
2743                        .get(n)
2744                        .cloned()
2745                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
2746                    Value::Str(s) => s
2747                        .chars()
2748                        .nth(n)
2749                        .map(|c| Value::Str(c.to_string()))
2750                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
2751                    other => Err(EvalErr::from(format!("cannot index {:?}", other))),
2752                }
2753            },
2754
2755            Expr::Call(callee, args) => {
2756                let arg_vals: Vec<Value> = args
2757                    .iter()
2758                    .map(|a| self.eval_expr(a, env))
2759                    .collect::<Result<_, _>>()?;
2760                match callee.as_ref() {
2761                    Expr::Ident(name) => self.call_named(name, arg_vals, env),
2762                    Expr::Path(segs) => self.call_named(&segs.join("::"), arg_vals, env),
2763                    _ => {
2764                        let v = self.eval_expr(callee, env)?;
2765                        self.call_value(v, arg_vals)
2766                    },
2767                }
2768            },
2769
2770            Expr::MethodCall { receiver, method, args } => {
2771                let recv = self.eval_expr(receiver, env)?;
2772                let arg_vals: Vec<Value> = args
2773                    .iter()
2774                    .map(|a| self.eval_expr(a, env))
2775                    .collect::<Result<_, _>>()?;
2776                self.call_method(recv, method, arg_vals)
2777            },
2778
2779            Expr::Closure(params, body) => Ok(Value::Fn(
2780                params.clone(),
2781                vec![Stmt::Expr(*body.clone())],
2782                env.clone(),
2783            )),
2784
2785            Expr::Asm(_) => Ok(Value::Unit),
2786        }
2787    }
2788
2789    // ─── Block execution ─────────────────────────────────────────────────────
2790
2791    fn exec_block(&mut self, stmts: &[Stmt], env: &mut Env) -> Result<Option<Value>, EvalErr> {
2792        let mut last: Option<Value> = None;
2793        for stmt in stmts {
2794            match stmt {
2795                Stmt::Bind(name, expr) => {
2796                    match self.try_inplace_list_update(name, expr, env)? {
2797                        Some(v) => env.insert(name.clone(), v),
2798                        None => {
2799                            let v = self.eval_expr(expr, env)?;
2800                            env.insert(name.clone(), v)
2801                        },
2802                    };
2803                    last = None;
2804                },
2805                Stmt::Return(expr) => {
2806                    let v = self.eval_expr(expr, env)?;
2807                    return Err(EvalErr::Return(v));
2808                },
2809                Stmt::Expr(expr) => {
2810                    last = Some(self.eval_expr(expr, env)?);
2811                },
2812            }
2813        }
2814        Ok(last)
2815    }
2816
2817    /// Fast path for `bind v = list_push(v, x)` / `bind v = list_set(v, i, x)`:
2818    /// the binding aliases the same list being rebuilt, so the env copy keeps the
2819    /// `Rc` shared and `make_mut` copies the whole vector every call. Taking the
2820    /// value out of env first leaves the `Rc` unique (unless truly aliased
2821    /// elsewhere, where copy-on-write still applies), turning O(n) into O(1).
2822    /// Returns `None` to fall back to normal evaluation.
2823    fn try_inplace_list_update(
2824        &mut self,
2825        name: &str,
2826        expr: &Expr,
2827        env: &mut Env,
2828    ) -> Result<Option<Value>, EvalErr> {
2829        let Expr::Call(callee, args) = expr else { return Ok(None) };
2830        let Expr::Ident(fname) = callee.as_ref() else { return Ok(None) };
2831        let is_push = matches!(
2832            fname.as_str(),
2833            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가"
2834        );
2835        let is_set = matches!(
2836            fname.as_str(),
2837            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정"
2838        );
2839        if !is_push && !is_set {
2840            return Ok(None);
2841        }
2842        // First arg must be the same variable we are binding, and the builtin
2843        // must not be shadowed by a user function.
2844        match args.first() {
2845            Some(Expr::Ident(a0)) if a0 == name => {},
2846            _ => return Ok(None),
2847        }
2848        if self.functions.contains_key(fname.as_str()) {
2849            return Ok(None);
2850        }
2851        if is_push {
2852            if args.len() != 2 {
2853                return Ok(None);
2854            }
2855            let val = self.eval_expr(&args[1], env)?;
2856            match env.remove(name) {
2857                Some(Value::List(mut v)) => {
2858                    Rc::make_mut(&mut v).push(val);
2859                    Ok(Some(Value::List(v)))
2860                },
2861                other => {
2862                    if let Some(o) = other {
2863                        env.insert(name.to_string(), o);
2864                    }
2865                    Ok(None)
2866                },
2867            }
2868        } else {
2869            if args.len() != 3 {
2870                return Ok(None);
2871            }
2872            let idx_v = self.eval_expr(&args[1], env)?;
2873            let idx = self.to_number(&idx_v).unwrap_or(0.0) as usize;
2874            let val = self.eval_expr(&args[2], env)?;
2875            match env.remove(name) {
2876                Some(Value::List(mut v)) => {
2877                    if idx < v.len() {
2878                        Rc::make_mut(&mut v)[idx] = val;
2879                    }
2880                    Ok(Some(Value::List(v)))
2881                },
2882                other => {
2883                    if let Some(o) = other {
2884                        env.insert(name.to_string(), o);
2885                    }
2886                    Ok(None)
2887                },
2888            }
2889        }
2890    }
2891
2892    // ─── Dispatch helpers ─────────────────────────────────────────────────────
2893
2894    fn lookup(&self, name: &str, env: &Env) -> EvalResult {
2895        if let Some(v) = env.get(name) {
2896            return Ok(v.clone());
2897        }
2898        // Globals are an immutable load-time snapshot shared by every call frame;
2899        // a function reads them here instead of receiving a per-call clone.
2900        if let Some(v) = self.global_seed.get(name) {
2901            return Ok(v.clone());
2902        }
2903        if self.functions.contains_key(name) {
2904            let def = &self.functions[name];
2905            return Ok(Value::Fn(def.params.clone(), def.body.clone(), new_env()));
2906        }
2907        // Bare nullary enum variant used as a value (e.g. `bind p = Origin`).
2908        if let Some((enum_name, 0)) = self.enum_variants.get(name).cloned() {
2909            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
2910            return Ok(Value::Variant { enum_name, variant, payload: Vec::new() });
2911        }
2912        // Math constants usable as plain identifiers (e.g. `sin(pi)`)
2913        match name {
2914            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" | "پی" | "باي" | "פאי" | "پائی" | "пи" => {
2915                return Ok(Value::Number(std::f64::consts::PI))
2916            },
2917            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" | "تاو" | "טאו" | "ٹاؤ" | "тау" => {
2918                return Ok(Value::Number(std::f64::consts::TAU))
2919            },
2920            _ => {},
2921        }
2922        Err(EvalErr::from(format!("undefined: '{name}'")))
2923    }
2924
2925    /// Profiling wrapper around the real dispatch. Zero overhead unless
2926    /// `LING_PROFILE` is set (one thread-local bool check per call). When on,
2927    /// it tallies per-name call count + inclusive time and, on each frame
2928    /// boundary (`present`), prints a sorted top-down report every
2929    /// `LING_PROFILE_EVERY` frames (default 240). Both the JIT (`ling_builtin` →
2930    /// here) and the tree-walker route through this, so it sees every builtin —
2931    /// in JIT mode user fns are native, so it's a clean builtin/render/physics
2932    /// profile with no nesting double-count.
2933    pub(crate) fn call_named(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2934        if !ling_profile_enabled() {
2935            return self.call_named_inner(name, args, env);
2936        }
2937        let t0 = crate::runtime::now_secs();
2938        let r = self.call_named_inner(name, args, env);
2939        ling_profile_record(
2940            name,
2941            ((crate::runtime::now_secs() - t0) * 1_000_000_000.0) as u128,
2942        );
2943        r
2944    }
2945
2946    fn call_named_inner(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2947        // A user-defined function shadows any builtin of the same name, matching
2948        // the JIT/AOT backends (which always resolve a defined function first).
2949        if let Some(def) = self.functions.get(name).cloned() {
2950            let mut call_env =
2951                FxHashMap::with_capacity_and_hasher(def.params.len(), Default::default());
2952            let _ = env; // call-site locals are intentionally NOT visible to fns
2953            for (param, arg) in def.params.iter().zip(args) {
2954                call_env.insert(param.clone(), arg);
2955            }
2956            return match self.framed(name, |me| me.exec_block(&def.body, &mut call_env)) {
2957                Ok(v) => Ok(v.unwrap_or(Value::Unit)),
2958                Err(EvalErr::Return(v)) => Ok(v),
2959                Err(e) => Err(e),
2960            };
2961        }
2962
2963        #[cfg(target_arch = "wasm32")]
2964        if let Some(v) = self.wasm_music_builtin(name, &args)? {
2965            return Ok(v);
2966        }
2967
2968        match name {
2969            // Module global read emitted by the MIR backend: resolve against the
2970            // evaluated global snapshot (functions see globals read-only).
2971            "__ling_global" => {
2972                if let Some(Value::Str(g)) = args.first() {
2973                    if let Some(v) = self.global_seed.get(g.as_str()) {
2974                        return Ok(v.clone());
2975                    }
2976                }
2977                return Ok(Value::Unit);
2978            },
2979            // ── Print ──
2980            "print" | "println" | "印" | "打印" | "印刷" | "พิมพ์" | "출력" | "вывести"
2981            | "imprimir" | "afficher" | "چاپ" | "اطبع" | "הדפס" | "چھاپو" | "drucken" | "печать" => {
2982                let s = args
2983                    .iter()
2984                    .map(|v| v.to_string())
2985                    .collect::<Vec<_>>()
2986                    .join("");
2987                println!("{s}");
2988                return Ok(Value::Unit);
2989            },
2990            // print_color(colorIdx, text...) — ANSI-coloured console line.
2991            //   colorIdx 0..7 → bright fg (90+idx): 1=red 2=green 3=yellow 4=blue 6=cyan 7=white.
2992            "print_color" | "พิมพ์สี" | "چاپ_رنگی" | "اطبع_بلون" | "הדפס_בצבע" | "رنگین_چھاپو" => {
2993                #[cfg(windows)]
2994                {
2995                    use std::sync::Once;
2996                    static VT: Once = Once::new();
2997                    VT.call_once(|| {
2998                        extern "system" {
2999                            fn GetStdHandle(n: u32) -> *mut std::ffi::c_void;
3000                            fn GetConsoleMode(h: *mut std::ffi::c_void, m: *mut u32) -> i32;
3001                            fn SetConsoleMode(h: *mut std::ffi::c_void, m: u32) -> i32;
3002                        }
3003                        unsafe {
3004                            let h = GetStdHandle(0xFFFF_FFF5u32); // STD_OUTPUT_HANDLE (-11)
3005                            let mut mode = 0u32;
3006                            if GetConsoleMode(h, &mut mode) != 0 {
3007                                SetConsoleMode(h, mode | 0x0004); // ENABLE_VIRTUAL_TERMINAL_PROCESSING
3008                            }
3009                        }
3010                    });
3011                }
3012                let col = self.arg_num(&args, 0, 7.0)? as i64;
3013                let s = args
3014                    .iter()
3015                    .skip(1)
3016                    .map(|v| v.to_string())
3017                    .collect::<Vec<_>>()
3018                    .join("");
3019                let code = 90 + col.clamp(0, 7);
3020                println!("\x1b[1;{code}m{s}\x1b[0m");
3021                return Ok(Value::Unit);
3022            },
3023            // ── Format ──
3024            "format"
3025            | "格式"
3026            | "フォーマット"
3027            | "서식"
3028            | "รูปแบบ"
3029            | "форматировать"
3030            | "formatear"
3031            | "formater" | "قالب‌بندی" | "نسّق" | "פרמט" | "فارمیٹ" | "formatieren" => {
3032                return Ok(Value::Str(self.builtin_format(&args)?));
3033            },
3034            // ── String join / concatenation ──
3035            "格式::拼接" | "format::join" => match args.first() {
3036                Some(Value::List(items)) => {
3037                    return Ok(Value::Str(items.iter().map(|v| v.to_string()).collect()));
3038                },
3039                _ => return Ok(Value::Str(self.builtin_format(&args)?)),
3040            },
3041            // ── Result constructors ──
3042            "ok" | "好" | "良し" | "좋아" | "โอเค" | "تایید" | "تمام" | "בסדר" | "ٹھیک" | "bon" | "gut" | "хорошо" => {
3043                let val = args.into_iter().next().unwrap_or(Value::Unit);
3044                return Ok(Value::Ok(Box::new(val)));
3045            },
3046            "bad" | "坏" | "err" | "悪い" | "나쁨" | "ผิด" | "بد" | "سيء" | "רע" | "برا" | "mauvais" | "schlecht" | "плохо" => {
3047                let val = args.into_iter().next().unwrap_or(Value::Unit);
3048                return Ok(Value::Err(Box::new(val)));
3049            },
3050            // ── Vec constructors ──
3051            "向量::从" | "Vec::from" => {
3052                if let Some(Value::List(v)) = args.first() {
3053                    return Ok(Value::List(v.clone()));
3054                }
3055                return Ok(Value::List(Rc::new(args)));
3056            },
3057            "向量::有容量" | "Vec::with_capacity" => {
3058                return Ok(Value::List(Rc::new(Vec::new())))
3059            },
3060            // ── Timer stubs ──
3061            "计时::获取当前小时" | "Timer::hour" => return Ok(Value::Number(14.0)),
3062            "计时::现在" | "Timer::now" => return Ok(Value::Number(1000.0)),
3063            // ── Sleep ──
3064            "sleep" | "หยุด" | "นอน" | "sleep_ms" | "睡眠" | "眠る" | "スリープ" | "잠자기"
3065            | "잠" | "流水::睡眠" | "Flow::sleep" | "خواب" | "نم" | "שינה" | "سو_جاؤ" | "dormir" | "schlafen" | "спать" => {
3066                if let Some(ms_val) = args.first() {
3067                    if let Ok(ms) = self.to_number(ms_val) {
3068                        #[cfg(target_arch = "wasm32")]
3069                        wasm_sleep_ms(ms.max(0.0) as i32);
3070                        #[cfg(not(target_arch = "wasm32"))]
3071                        std::thread::sleep(std::time::Duration::from_millis(ms as u64));
3072                    }
3073                }
3074                return Ok(Value::Unit);
3075            },
3076            // ── Flow::parallel stub ──
3077            "流水::并行" | "Flow::parallel" => {
3078                if let Some(Value::Fn(params, body, mut cap)) = args.first().cloned() {
3079                    let _ = params;
3080                    match self.exec_block(&body, &mut cap) {
3081                        Ok(Some(v)) => return Ok(v),
3082                        Ok(None) => return Ok(Value::Unit),
3083                        Err(EvalErr::Return(v)) => return Ok(v),
3084                        Err(e) => return Err(e),
3085                    }
3086                }
3087                return Ok(Value::Unit);
3088            },
3089
3090            // ══════════════════════════════════════════════════════════════════
3091            // MATH BUILTINS  (all args and results are f64)
3092            // Thai aliases: ไซน์ โคไซน์ แทนเจนต์ รากที่สอง ค่าสัมบูรณ์
3093            //               ปัดลง ปัดขึ้น ปัดเศษ ตัดทศนิยม ต่ำสุด สูงสุด
3094            //               จำกัด ยกกำลัง ลอการิทึม พาย
3095            // ══════════════════════════════════════════════════════════════════
3096
3097            // ── Trigonometry (input in radians) ──
3098            "sin" | "ไซน์" | "正弦" | "サイン" | "사인" | "سینوس" | "جا" | "סינוס" | "سائن" | "sinus" | "синус" => {
3099                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sin()));
3100            },
3101            "cos" | "โคไซน์" | "余弦" | "コサイン" | "코사인" | "کسینوس" | "جتا" | "קוסינוס" | "کوسائن" | "cosinus" | "kosinus" | "косинус" => {
3102                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cos()));
3103            },
3104
3105            // ── Hyperbolic functions ──
3106            // Hyperbolic tangent
3107            "tanh" | "tanhf" | "双曲正切" | "双曲線正接" | "쌍곡탄젠트" => {
3108                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tanh()));
3109            },
3110
3111            "tan" | "แทนเจนต์" | "正切" | "タンジェント" | "탄젠트" | "تانژانت" | "ظا" | "טנגנס" | "ٹینجنٹ" | "tangente" | "tangens" | "тангенс" => {
3112                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tan()));
3113            },
3114            "asin" | "arcsin" | "反正弦" | "アークサイン" | "아크사인" | "อาร์กไซน์" | "آرک‌سینوس" | "قوس_جا" | "ארקסינוס" | "آرک_سائن" | "arcsinus" | "arkussinus" | "арксинус" =>
3115            {
3116                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.asin()));
3117            },
3118            "acos" | "arccos" | "反余弦" | "アークコサイン" | "아크코사인" | "อาร์กโคไซน์" | "آرک‌کسینوس" | "قوس_جتا" | "ארקוקוסינוס" | "آرک_کوسائن" | "arccosinus" | "arkuskosinus" | "арккосинус" =>
3119            {
3120                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.acos()));
3121            },
3122            "atan" | "arctan" | "反正切" | "アークタンジェント" | "아크탄젠트" | "อาร์กแทนเจนต์" | "آرک‌تانژانت" | "قوس_ظا" | "ארקטנגנס" | "آرک_ٹینجنٹ" | "arctangente" | "arkustangens" | "арктангенс" =>
3123            {
3124                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.atan()));
3125            },
3126            "atan2" | "arctan2" | "反正切2" | "アークタンジェント2" | "아크탄젠트2" =>
3127            {
3128                let y = self.arg_num(&args, 0, 0.0)?;
3129                let x = self.arg_num(&args, 1, 1.0)?;
3130                return Ok(Value::Number(y.atan2(x)));
3131            },
3132
3133            // ── Roots / powers ──
3134            "sqrt" | "รากที่สอง" | "平方根" | "根" | "제곱근" | "جذر" | "שורש" | "racine_carrée" | "quadratwurzel" | "корень" => {
3135                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sqrt()));
3136            },
3137            "cbrt" | "立方根" | "세제곱근" | "รากที่สาม" | "ریشه_سوم" | "جذر_تكعيبي" | "שורש_שלישי" | "مکعب_جذر" | "racine_cubique" | "kubikwurzel" | "кубический_корень" => {
3138                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cbrt()));
3139            },
3140            "pow" | "ยกกำลัง" | "幂" | "べき乗" | "거듭제곱" | "توان" | "أس" | "חזקה" | "قوت" | "puissance" | "potenz" | "степень" => {
3141                let base = self.arg_num(&args, 0, 0.0)?;
3142                let exp = self.arg_num(&args, 1, 1.0)?;
3143                return Ok(Value::Number(base.powf(exp)));
3144            },
3145            "exp" | "指数" | "指数関数" | "지수" => {
3146                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.exp()));
3147            },
3148            "hypot" | "斜边" | "斜辺" | "빗변" => {
3149                let x = self.arg_num(&args, 0, 0.0)?;
3150                let y = self.arg_num(&args, 1, 0.0)?;
3151                return Ok(Value::Number(x.hypot(y)));
3152            },
3153
3154            // ── Logarithms ──
3155            "ln" | "log" | "ลอการิทึม" | "对数" | "対数" | "로그" | "لگاریتم_طبیعی" | "لوغاريتم_طبيعي" | "לוגריתם_טבעי" | "فطری_لوگ" => {
3156                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.ln()));
3157            },
3158            "log2" | "对数2" | "対数2" | "로그2" => {
3159                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log2()));
3160            },
3161            "log10" | "对数10" | "対数10" | "로그10" => {
3162                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log10()));
3163            },
3164
3165            // ── Rounding / truncation ──
3166            "abs" | "ค่าสัมบูรณ์" | "绝对值" | "绝对" | "絶対値" | "절댓값" | "절대값" | "قدرمطلق" | "مطلق" | "ערך_מוחלט" | "مطلق_قدر" | "valeur_absolue" | "betrag" | "модуль_числа" =>
3167            {
3168                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.abs()));
3169            },
3170            "floor" | "ปัดลง" | "向下取整" | "下整" | "床関数" | "내림" | "کف" | "أرضية" | "רצפה" | "فرش" | "plancher" | "abrunden" | "вниз" => {
3171                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.floor()));
3172            },
3173            "ceil" | "ปัดขึ้น" | "向上取整" | "上整" | "天井関数" | "올림" | "سقف" | "תקרה" | "چھت" | "plafond" | "aufrunden" | "вверх" =>
3174            {
3175                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.ceil()));
3176            },
3177            "round" | "ปัดเศษ" | "四舍五入" | "四舍" | "四捨五入" | "반올림" | "گرد_کردن" | "تقريب" | "עיגול" | "گول" | "arrondir" | "runden" | "округлить" =>
3178            {
3179                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.round()));
3180            },
3181            "trunc"
3182            | "int"
3183            | "ตัดทศนิยม"
3184            | "取整"
3185            | "整数化"
3186            | "整数"
3187            | "截整"
3188            | "정수화"
3189            | "정수"
3190            | "切り捨て"
3191            | "버림" | "برش" | "اقتطاع" | "קיטום" | "کٹائی" | "tronquer" | "abschneiden" | "усечь" => {
3192                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.trunc()));
3193            },
3194            "fract" | "小数部分" | "小数部" | "소수부" => {
3195                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.fract()));
3196            },
3197
3198            // ── min / max / clamp ──
3199            "min" | "ต่ำสุด" | "最小" | "최솟값" | "کمینه" | "أصغر" | "מינימום" | "کم_ترین" | "minimum" | "минимум" => {
3200                let a = self.arg_num(&args, 0, 0.0)?;
3201                let b = self.arg_num(&args, 1, 0.0)?;
3202                return Ok(Value::Number(a.min(b)));
3203            },
3204            "max" | "สูงสุด" | "最大" | "최댓값" | "بیشینه" | "أكبر" | "מקסימום" | "زیادہ_ترین" | "maximum" | "максимум" => {
3205                let a = self.arg_num(&args, 0, 0.0)?;
3206                let b = self.arg_num(&args, 1, 0.0)?;
3207                return Ok(Value::Number(a.max(b)));
3208            },
3209            "clamp" | "จำกัด" | "截取" | "範囲制限" | "범위제한" | "محدود" | "قيّد" | "הגבל" | "limiter" | "begrenzen" | "ограничить" => {
3210                let x = self.arg_num(&args, 0, 0.0)?;
3211                let lo = self.arg_num(&args, 1, 0.0)?;
3212                let hi = self.arg_num(&args, 2, 1.0)?;
3213                return Ok(Value::Number(x.clamp(lo, hi)));
3214            },
3215
3216            // ── Constants (also accessible as plain identifiers via lookup) ──
3217            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" | "پی" | "باي" | "פאי" | "پائی" | "пи" => {
3218                return Ok(Value::Number(std::f64::consts::PI))
3219            },
3220            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" | "تاو" | "טאו" | "ٹاؤ" | "тау" => {
3221                return Ok(Value::Number(std::f64::consts::TAU))
3222            },
3223
3224            // ══════════════════════════════════════════════════════════════════
3225            // PHASE 1: DMT TRIP CODER FEATURES
3226            // ══════════════════════════════════════════════════════════════════
3227
3228            // ── Step 1: Noise Functions ──
3229            "vnoise" | "noise2" | "นอยส์2ดี" | "柏林噪声2D" | "バリューノイズ2D" | "값노이즈2D" | "نویز_برداری" | "ضجيج_متجه" | "רעש_וקטורי" | "ویکٹر_نوائز" | "bruit_v" | "v_rauschen" | "шум_v" =>
3230            {
3231                let x = self.arg_num(&args, 0, 0.0)? as f32;
3232                let y = self.arg_num(&args, 1, 0.0)? as f32;
3233                let seed = self.arg_num(&args, 2, 0.0)? as u32;
3234                return Ok(Value::Number(tex_vnoise(x, y, seed) as f64));
3235            },
3236
3237            "fbm" | "นอยส์ออร์แกนิก" | "分形噪声" | "フラクタルノイズ" | "프랙탈노이즈" | "نویز_فراکتالی" | "ضجيج_عضوي" | "רעש_פרקטלי" | "فریکٹل_نوائز" =>
3238            {
3239                let x = self.arg_num(&args, 0, 0.0)? as f32;
3240                let y = self.arg_num(&args, 1, 0.0)? as f32;
3241                let octaves = self.arg_num(&args, 2, 4.0)? as u32;
3242                let seed = self.arg_num(&args, 3, 0.0)? as u32;
3243                return Ok(Value::Number(tex_fbm(x, y, octaves, seed) as f64));
3244            },
3245
3246            "perlin"
3247            | "perlin3"
3248            | "เพอร์ลิน3ดี"
3249            | "柏林噪声3D"
3250            | "パーリンノイズ3D"
3251            | "펄린노이즈3D" | "نویز_پرلین" | "بيرلين" | "רעש_פרלין" | "پرلن_نوائز" | "перлин" => {
3252                let x = self.arg_num(&args, 0, 0.0)? as f32;
3253                let y = self.arg_num(&args, 1, 0.0)? as f32;
3254                let z = self.arg_num(&args, 2, 0.0)? as f32;
3255                return Ok(Value::Number(perlin3(x, y, z) as f64));
3256            },
3257
3258            // ── Step 2: Math Ergonomics ──
3259            "lerp" | "ค่าระหว่าง" | "线性插值" | "線形補間" | "선형보간" | "میان‌یابی" | "استيفاء" | "אינטרפולציה" | "درمیانی_قدر" | "interpoler" | "interpolieren" | "интерполировать" =>
3260            {
3261                let a = self.arg_num(&args, 0, 0.0)?;
3262                let b = self.arg_num(&args, 1, 1.0)?;
3263                let t = self.arg_num(&args, 2, 0.0)?;
3264                return Ok(Value::Number(a + (b - a) * t));
3265            },
3266
3267            "smoothstep" | "เปลี่ยนแบบนุ่ม" | "平滑步进" | "スムーズステップ" | "스무스스텝" | "گام_نرم" | "تدرج_ناعم" | "מדרגה_חלקה" | "ہموار_قدم" | "lissage" | "glättung" | "сглаживание" =>
3268            {
3269                let lo = self.arg_num(&args, 0, 0.0)?;
3270                let hi = self.arg_num(&args, 1, 1.0)?;
3271                let x = self.arg_num(&args, 2, 0.5)?;
3272                let t = ((x - lo) / (hi - lo)).clamp(0.0, 1.0);
3273                return Ok(Value::Number(t * t * (3.0 - 2.0 * t)));
3274            },
3275
3276            "rand" | "สุ่ม" | "随机" | "乱数" | "난수" | "تصادفی" | "عشوائي" | "אקראי" | "بے_ترتیب" | "aléatoire" | "zufall" | "случайное" => {
3277                let val = fast_rand_f64(&mut self.rand_state);
3278                return Ok(Value::Number(val));
3279            },
3280
3281            "sign" | "เครื่องหมาย" | "符号" | "符号関数" | "부호" | "علامت" | "إشارة" | "סימן" | "نشان" | "signe" | "vorzeichen" | "знак" => {
3282                let x = self.arg_num(&args, 0, 0.0)?;
3283                return Ok(Value::Number(x.signum()));
3284            },
3285
3286            "hsv_to_rgb" | "เอชเอสวีเป็นRGB" | "HSV转RGB" | "HSV変換RGB" | "HSV변환RGB" | "HSV_به_RGB" | "HSV_إلى_RGB" | "HSV_ל_RGB" | "HSV_سے_RGB" | "hsv_vers_rgb" | "hsv_zu_rgb" | "hsv_в_rgb" =>
3287            {
3288                let h = self.arg_num(&args, 0, 0.0)?; // 0-360
3289                let s = self.arg_num(&args, 1, 1.0)?; // 0-1
3290                let v = self.arg_num(&args, 2, 1.0)?; // 0-1
3291                let c = v * s;
3292                let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
3293                let m = v - c;
3294                let (r1, g1, b1) = if h < 60.0 {
3295                    (c, x, 0.0)
3296                } else if h < 120.0 {
3297                    (x, c, 0.0)
3298                } else if h < 180.0 {
3299                    (0.0, c, x)
3300                } else if h < 240.0 {
3301                    (0.0, x, c)
3302                } else if h < 300.0 {
3303                    (x, 0.0, c)
3304                } else {
3305                    (c, 0.0, x)
3306                };
3307                let r = ((r1 + m) * 255.0).round();
3308                let g = ((g1 + m) * 255.0).round();
3309                let b = ((b1 + m) * 255.0).round();
3310                return Ok(Value::List(Rc::new(vec![
3311                    Value::Number(r),
3312                    Value::Number(g),
3313                    Value::Number(b),
3314                ])));
3315            },
3316
3317            "lerp_color" | "ไล่สี" | "颜色插值" | "色補間" | "색보간" | "میان‌یابی_رنگ" | "استيفاء_اللون" | "אינטרפולציית_צבע" | "رنگ_درمیانی_قدر" | "interpoler_couleur" | "farbe_interpolieren" | "интерполировать_цвет" => {
3318                let r1 = self.arg_num(&args, 0, 0.0)?;
3319                let g1 = self.arg_num(&args, 1, 0.0)?;
3320                let b1 = self.arg_num(&args, 2, 0.0)?;
3321                let r2 = self.arg_num(&args, 3, 255.0)?;
3322                let g2 = self.arg_num(&args, 4, 255.0)?;
3323                let b2 = self.arg_num(&args, 5, 255.0)?;
3324                let t = self.arg_num(&args, 6, 0.0)?;
3325                let r = r1 + (r2 - r1) * t;
3326                let g = g1 + (g2 - g1) * t;
3327                let b = b1 + (b2 - b1) * t;
3328                let c = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
3329                self.gfx.borrow_mut().color = c;
3330                return Ok(Value::Unit);
3331            },
3332
3333            // ── Step 3: Real-Time Clock ──
3334            "time_now" | "เวลาปัจจุบัน" | "当前时间" | "経過時間" | "현재시간" | "زمان_اکنون" | "الوقت_الآن" | "הזמן_עכשיו" | "ابھی_کا_وقت" | "temps_actuel" | "aktuelle_zeit" | "текущее_время" =>
3335            {
3336                return Ok(Value::Number(
3337                    crate::runtime::now_secs() - self.start_time_secs,
3338                ));
3339            },
3340
3341            // Wall-clock seconds since the Unix epoch (real date/time). Lets a
3342            // program defer deterministic-yet-evolving generation to the actual
3343            // datetime — same clock → same world, advancing as real time passes.
3344            "epoch_now" | "เวลาโลก" | "datetime" | "现在时刻" | "現在時刻" | "현재시각" | "مهر_زمانی_اکنون" | "طابع_الوقت_الآن" | "חותמת_זמן_עכשיו" | "ایپاک_وقت" =>
3345            {
3346                return Ok(Value::Number(crate::runtime::now_secs()));
3347            },
3348
3349            "frame_count" | "เฟรม" | "帧数" | "フレーム数" | "프레임수" | "شمار_فریم" | "عدد_الإطارات" | "ספירת_פריימים" | "فریم_شمار" | "nombre_images" | "bildanzahl" | "число_кадров" => {
3350                return Ok(Value::Number(self.frame_num as f64));
3351            },
3352
3353            // ── Step 4: Microphone Input ──
3354            "mic_open" | "เปิดไมค์" | "开麦克风" | "マイク開く" | "마이크열기" | "باز_کردن_میکروفون" | "افتح_الميكروفون" | "פתח_מיקרופון" | "مائیکروفون_کھولو" | "ouvrir_micro" | "mikrofon_öffnen" | "открыть_микрофон" =>
3355            {
3356                #[cfg(not(target_arch = "wasm32"))]
3357                {
3358                    match ling_mic::MicInput::open(Default::default()) {
3359                        Ok(mic) => {
3360                            let _ = mic.start(|_samples: &[f32]| {}); // No-op callback
3361                            self.mic = Some(mic);
3362                            return Ok(Value::Number(1.0)); // opened
3363                        },
3364                        // No device / permission denied → graceful: don't crash the game loop.
3365                        // Returns 0.0; mic_rms/mic_peak return 0.0 while self.mic is None.
3366                        Err(_e) => {
3367                            self.mic = None;
3368                            return Ok(Value::Number(0.0));
3369                        },
3370                    }
3371                }
3372                #[cfg(target_arch = "wasm32")]
3373                return Ok(Value::Unit);
3374            },
3375
3376            "mic_rms" | "เสียงRMS" | "麦克风音量" | "マイクRMS" | "마이크RMS" | "RMS_میکروفون" | "RMS_الميكروفون" | "RMS_מיקרופון" | "مائیکروفون_RMS" | "rms_micro" | "mikrofon_rms" | "rms_микрофона" =>
3377            {
3378                #[cfg(not(target_arch = "wasm32"))]
3379                {
3380                    let rms = self
3381                        .mic
3382                        .as_ref()
3383                        .map(|m: &ling_mic::MicInput| m.rms())
3384                        .unwrap_or(0.0);
3385                    return Ok(Value::Number(rms as f64));
3386                }
3387                #[cfg(target_arch = "wasm32")]
3388                return Ok(Value::Number(0.0));
3389            },
3390
3391            "mic_peak" | "เสียงพีค" | "麦克风峰值" | "マイクピーク" | "마이크피크" | "اوج_میکروفون" | "ذروة_الميكروفون" | "שיא_מיקרופון" | "مائیکروفون_چوٹی" | "crête_micro" | "mikrofon_spitze" | "пик_микрофона" =>
3392            {
3393                #[cfg(not(target_arch = "wasm32"))]
3394                {
3395                    let peak = self
3396                        .mic
3397                        .as_ref()
3398                        .map(|m: &ling_mic::MicInput| m.peak())
3399                        .unwrap_or(0.0);
3400                    return Ok(Value::Number(peak as f64));
3401                }
3402                #[cfg(target_arch = "wasm32")]
3403                return Ok(Value::Number(0.0));
3404            },
3405
3406            "mic_fft" | "วิเคราะห์เสียงสด" | "实时频谱" | "リアルタイムFFT" | "실시간FFT" | "FFT_میکروفون" | "FFT_الميكروفون" | "FFT_מיקרופון" | "مائیکروفون_FFT" | "fft_micro" | "mikrofon_fft" | "fft_микрофона" =>
3407            {
3408                #[cfg(not(target_arch = "wasm32"))]
3409                {
3410                    let n = self.arg_num(&args, 0, 8.0)? as usize;
3411                    if let Some(mic) = self.mic.as_ref() {
3412                        let samples = mic.latest_samples();
3413                        self.fft.borrow_mut().push_samples(&samples);
3414                    }
3415                    let bands = self.fft.borrow().freq_bands(n);
3416                    let result: Vec<Value> =
3417                        bands.iter().map(|&v| Value::Number(v as f64)).collect();
3418                    return Ok(Value::List(Rc::new(result)));
3419                }
3420                #[cfg(target_arch = "wasm32")]
3421                return Ok(Value::List(Vec::new().into()));
3422            },
3423
3424            // ── Step 5: Additive Blend Mode ──
3425            "set_blend" | "โหมดผสม" | "混合模式" | "ブレンドモード" | "블렌드모드" | "تنظیم_ترکیب" | "عيّن_المزج" | "קבע_מיזוג" | "بلینڈ_مقرر_کرو" | "définir_mélange" | "mischmodus_setzen" | "задать_смешивание" =>
3426            {
3427                let mode = self.arg_num(&args, 0, 0.0)? as u8;
3428                let mut gfx = self.gfx.borrow_mut();
3429                gfx.blend = mode;
3430                let a = gfx.alpha;
3431                gfx.depth_queue.set_state(mode, a); // 3-D queue captures blend for subsequent pushes
3432                return Ok(Value::Unit);
3433            },
3434
3435            // set_antialias(on) — smooth wireframe strokes (lines / edges / arcs /
3436            // circle outlines) via Xiaolin-Wu coverage. Default OFF = crisp,
3437            // opaque, aliased pixels; pass 1 to opt into smooth edges.
3438            "set_antialias" | "ตั้งลบรอยหยัก" | "抗锯齿" | "アンチエイリアス" | "안티에일리어싱" | "تنظیم_ضدلبه‌دندانه" | "عيّن_مضاد_التسنن" | "קבע_החלקת_קצוות" | "اینٹی_الائیسنگ_مقرر_کرو" =>
3439            {
3440                let on = self.arg_num(&args, 0, 1.0)? > 0.5;
3441                self.gfx.borrow_mut().antialias = on;
3442                return Ok(Value::Unit);
3443            },
3444            // get_antialias() -> bool — current wireframe anti-aliasing state.
3445            "get_antialias"
3446            | "อ่านลบรอยหยัก"
3447            | "读取抗锯齿"
3448            | "アンチエイリアス取得"
3449            | "안티에일리어싱상태" | "خواندن_ضدلبه‌دندانه" | "اقرأ_مضاد_التسنن" | "קרא_החלקת_קצוות" | "اینٹی_الائیسنگ_پڑھو" => {
3450                return Ok(Value::Bool(self.gfx.borrow().antialias));
3451            },
3452
3453            // set_font_antialias(on) — smooth `font_text`/`font_text_fill` glyph
3454            // edges, independent of `set_antialias` (which only covers wireframe
3455            // strokes). Default OFF = crisp, hard-edged text; pass 1 to opt in.
3456            "set_font_antialias" | "글꼴안티에일리어싱" => {
3457                let on = self.arg_num(&args, 0, 1.0)? > 0.5;
3458                self.gfx.borrow_mut().font_antialias = on;
3459                return Ok(Value::Unit);
3460            },
3461            // get_font_antialias() -> bool — current font anti-aliasing state.
3462            "get_font_antialias" | "글꼴안티에일리어싱상태" => {
3463                return Ok(Value::Bool(self.gfx.borrow().font_antialias));
3464            },
3465
3466            // ── Step 6: Circle Primitives ──
3467            "draw_circle" | "วาดวงกลม" | "画圆" | "円描画" | "원그리기" | "رسم_دایره" | "ارسم_دائرة" | "צייר_עיגול" | "دائرہ_کھینچو" | "dessiner_cercle" | "kreis_zeichnen" | "рисовать_круг" =>
3468            {
3469                let cx = self.arg_num(&args, 0, 0.0)? as i32;
3470                let cy = self.arg_num(&args, 1, 0.0)? as i32;
3471                let r = self.arg_num(&args, 2, 10.0)? as i32;
3472                let mut gfx = self.gfx.borrow_mut();
3473                let (w, h, color, blend) =
3474                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
3475                if gfx.antialias {
3476                    let (uw, uh) = (gfx.width, gfx.height);
3477                    let segs = ((r.max(1) as u32) * 4).clamp(24, 512);
3478                    crate::gfx::raster::draw_arc(
3479                        &mut gfx.buffer,
3480                        uw,
3481                        uh,
3482                        color,
3483                        true,
3484                        blend == 1,
3485                        cx as f32,
3486                        cy as f32,
3487                        r as f32,
3488                        0.0,
3489                        std::f32::consts::TAU,
3490                        segs,
3491                    );
3492                } else {
3493                    draw_circle_outline(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
3494                }
3495                return Ok(Value::Unit);
3496            },
3497
3498            "draw_filled_circle"
3499            | "draw_disc"
3500            | "วาดวงกลมทึบ"
3501            | "画实心圆"
3502            | "塗りつぶし円"
3503            | "원채우기" | "رسم_دایره_توپر" | "ارسم_دائرة_ممتلئة" | "צייר_עיגול_מלא" | "بھرا_دائرہ_کھینچو" => {
3504                let cx = self.arg_num(&args, 0, 0.0)? as i32;
3505                let cy = self.arg_num(&args, 1, 0.0)? as i32;
3506                let r = self.arg_num(&args, 2, 10.0)? as i32;
3507                let mut gfx = self.gfx.borrow_mut();
3508                let (w, h, color, blend) =
3509                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
3510                draw_circle_filled(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
3511                return Ok(Value::Unit);
3512            },
3513
3514            // draw_arc(cx, cy, r, a0, a1 [, segments]) — stroke a circular arc in
3515            // the pen colour (full circle when a1-a0 = TAU). Honors the antialias
3516            // flag; opaque by default (additive when blend = 1).
3517            "draw_arc" | "arc" | "วาดส่วนโค้ง" | "画弧" | "円弧描画" | "호그리기" | "رسم_کمان" | "ارسم_قوسا" | "צייר_קשת" | "آرک_کھینچو" =>
3518            {
3519                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3520                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3521                let r = self.arg_num(&args, 2, 10.0)? as f32;
3522                let a0 = self.arg_num(&args, 3, 0.0)? as f32;
3523                let a1 = self.arg_num(&args, 4, std::f64::consts::TAU)? as f32;
3524                let default_segs = ((r.abs() * (a1 - a0).abs()).ceil() as u32).clamp(8, 1024);
3525                let segs = self.arg_num(&args, 5, default_segs as f64)? as u32;
3526                let mut gfx = self.gfx.borrow_mut();
3527                let color = gfx.color;
3528                #[cfg(not(target_arch = "wasm32"))]
3529                {
3530                    let (uw, uh, aa, add) = (gfx.width, gfx.height, gfx.antialias, gfx.blend == 1);
3531                    crate::gfx::raster::draw_arc(
3532                        &mut gfx.buffer,
3533                        uw,
3534                        uh,
3535                        color,
3536                        aa,
3537                        add,
3538                        cx,
3539                        cy,
3540                        r,
3541                        a0,
3542                        a1,
3543                        segs,
3544                    );
3545                }
3546                #[cfg(target_arch = "wasm32")]
3547                {
3548                    let segs_f = segs.max(1);
3549                    let step = (a1 - a0) / segs_f as f32;
3550                    let mut px = cx + r * a0.cos();
3551                    let mut py = cy + r * a0.sin();
3552                    let mut i = 1u32;
3553                    while i <= segs_f {
3554                        let a = a0 + step * i as f32;
3555                        let nx = cx + r * a.cos();
3556                        let ny = cy + r * a.sin();
3557                        gfx.depth_queue.push_line(0.0, color, px, py, nx, ny);
3558                        px = nx;
3559                        py = ny;
3560                        i += 1;
3561                    }
3562                }
3563                return Ok(Value::Unit);
3564            },
3565
3566            // ── Step 7: Transparent fills, gradient surfaces & colored shadows ──
3567            // These all write straight into the software framebuffer (gfx.buffer)
3568            // on both native and web, so no target gating is needed.
3569
3570            // set_alpha(a) — pen opacity 0..1 for the alpha-blended fills below.
3571            "set_alpha" | "ตั้งความโปร่งใส" | "设透明" | "アルファ設定" | "투명도설정" | "تنظیم_شفافیت" | "عيّن_الشفافية" | "קבע_שקיפות" | "شفافیت_مقرر_کرو" | "définir_alpha" | "alpha_setzen" | "задать_альфа" =>
3572            {
3573                let a = self.arg_num(&args, 0, 1.0)? as f32;
3574                let mut gfx = self.gfx.borrow_mut();
3575                gfx.alpha = a.clamp(0.0, 1.0);
3576                let (m, al) = (gfx.blend, gfx.alpha);
3577                gfx.depth_queue.set_state(m, al); // 3-D queue captures alpha for subsequent pushes
3578                return Ok(Value::Unit);
3579            },
3580
3581            // mesh_hue(radians) — hue-rotate the baked per-tri colours of every
3582            // subsequent mesh_draw (.lmesh). 0 resets. Cheap: one matrix per call.
3583            "mesh_hue" | "หมุนสีเมช" | "فام_مش" | "صبغة_الشبكة" | "גוון_רשת" | "میش_ہیو" =>
3584            {
3585                let h = self.arg_num(&args, 0, 0.0)? as f32;
3586                let g = self.arg_num(&args, 1, 1.0)? as f32;
3587                let mut gfx = self.gfx.borrow_mut();
3588                gfx.mesh_hue = h;
3589                gfx.mesh_hue_gain = g.max(0.0);
3590                return Ok(Value::Unit);
3591            },
3592
3593            // set_frame_blur(amount 0..0.95) — afterimage trails: blend the previous
3594            // presented frame into each new one. 0 = off (also frees the ghost buffer).
3595            "set_frame_blur" | "frame_blur" | "เบลอเฟรม" | "تنظیم_تاری_فریم" | "عيّن_ضبابية_الإطار" | "קבע_טשטוש_פריים" | "فریم_بلر_مقرر_کرو" =>
3596            {
3597                let a = self.arg_num(&args, 0, 0.0)? as f32;
3598                let mut gfx = self.gfx.borrow_mut();
3599                gfx.frame_blur = a.clamp(0.0, 0.95);
3600                if gfx.frame_blur <= 0.0 {
3601                    gfx.prev_frame = Vec::new();
3602                }
3603                return Ok(Value::Unit);
3604            },
3605
3606            // set_line_hue_cycle(rate) — rapidly cycle the hue of ALL wireframe line
3607            // strokes (draw_line / draw_line_3d). `rate` in radians/sec; 0 = off.
3608            // Process-global so a single call covers every stroke, every frame.
3609            "set_line_hue_cycle" | "ตั้งวนสีเส้น" | "تنظیم_چرخه_فام_خط" | "عيّن_دورة_صبغة_الخط" | "קבע_מחזור_גוון_קו" | "لائن_ہیو_سائیکل_مقرر_کرو" => {
3610                let rate = self.arg_num(&args, 0, 0.0)?;
3611                crate::runtime::set_line_hue_rate(rate);
3612                return Ok(Value::Unit);
3613            },
3614
3615            // set_color_space(mode) — 0 = legacy sRGB compositing (default),
3616            // 1 = gamma-correct linear-light compositing (blend in linear, store
3617            // sRGB) so alpha and gradients don't darken/shift hue.
3618            "set_color_space" | "ปริภูมิสี" | "色彩空间" | "色空間" | "색공간" | "تنظیم_فضای_رنگ" | "عيّن_فضاء_اللون" | "קבע_מרחב_צבע" | "کلر_اسپیس_مقرر_کرو" | "définir_espace_couleur" | "farbraum_setzen" | "задать_цветовое_пространство" =>
3619            {
3620                let m = self.arg_num(&args, 0, 0.0)? as i64;
3621                self.gfx.borrow_mut().linear_blend = m != 0;
3622                return Ok(Value::Unit);
3623            },
3624
3625            // set_gradient_space(mode) — 1 = perceptual OkLab gradient interp
3626            // (default), 0 = legacy sRGB. Affects grad_triangle / grad_rect.
3627            "set_gradient_space" | "ปริภูมิไล่สี" | "渐变空间" | "グラデ空間" | "그라데이션공간" | "تنظیم_فضای_گرادیان" | "عيّن_فضاء_التدرج" | "קבע_מרחב_גרדיאנט" | "گریڈینٹ_اسپیس_مقرر_کرو" | "définir_espace_dégradé" | "verlaufsraum_setzen" | "задать_пространство_градиента" =>
3628            {
3629                let m = self.arg_num(&args, 0, 1.0)? as i64;
3630                self.gfx.borrow_mut().grad_oklab = m != 0;
3631                return Ok(Value::Unit);
3632            },
3633
3634            // mix_color(r0,g0,b0, r1,g1,b1, t) — set the pen colour to the
3635            // perceptual OkLab blend of two colours (t in 0..1). Far nicer
3636            // mid-tones than a raw RGB lerp.
3637            "mix_color" | "ผสมสี" | "混合颜色" | "色混合" | "색혼합" | "ترکیب_رنگ" | "امزج_اللون" | "ערבב_צבע" | "رنگ_ملاؤ" | "mélanger_couleur" | "farbe_mischen" | "смешать_цвет" => {
3638                let c0 = rgb(
3639                    self.arg_num(&args, 0, 0.0)?,
3640                    self.arg_num(&args, 1, 0.0)?,
3641                    self.arg_num(&args, 2, 0.0)?,
3642                );
3643                let c1 = rgb(
3644                    self.arg_num(&args, 3, 255.0)?,
3645                    self.arg_num(&args, 4, 255.0)?,
3646                    self.arg_num(&args, 5, 255.0)?,
3647                );
3648                let t = self.arg_num(&args, 6, 0.5)? as f32;
3649                self.gfx.borrow_mut().color = crate::gfx::color::mix_oklab(c0, c1, t);
3650                return Ok(Value::Unit);
3651            },
3652
3653            // set_depth_test(on) — enable the per-pixel z-buffer for the deferred
3654            // 3-D/queued draws (correct interpenetration) instead of painter's-
3655            // only sort. 0 = off (default), non-zero = on.
3656            "set_depth_test" | "ทดสอบความลึก" | "深度测试" | "深度テスト" | "깊이테스트" | "تنظیم_آزمون_عمق" | "عيّن_اختبار_العمق" | "קבע_בדיקת_עומק" | "ڈیپتھ_ٹیسٹ_مقرر_کرو" | "définir_test_profondeur" | "tiefentest_setzen" | "задать_тест_глубины" =>
3657            {
3658                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
3659                self.gfx.borrow_mut().depth_test = on;
3660                return Ok(Value::Unit);
3661            },
3662
3663            // set_flat_shade(on) / ตั้งแฟลตเชด — perf test: skip all per-triangle/mesh
3664            // lighting (compute_lit_color) and draw with the raw pen colour.
3665            "set_flat_shade" | "ตั้งแฟลตเชด" | "平面着色" | "フラット着色" | "평면음영" | "تنظیم_سایه‌پردازی_تخت" | "عيّن_تظليلا_مسطحا" | "קבע_הצללה_שטוחה" | "فلیٹ_شیڈ_مقرر_کرو" =>
3666            {
3667                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
3668                self.gfx.borrow_mut().flat_shade = on;
3669                return Ok(Value::Unit);
3670            },
3671
3672            // set_normal_override(x,y,z) - force subsequent triangle/mesh lighting
3673            // to use a stylized world-space normal until reset_normal_override().
3674            "set_normal_override" =>
3675            {
3676                let x = self.arg_num(&args, 0, 0.0)? as f32;
3677                let y = self.arg_num(&args, 1, -1.0)? as f32;
3678                let z = self.arg_num(&args, 2, 0.0)? as f32;
3679                self.gfx.borrow_mut().normal_override = Some([x, y, z]);
3680                return Ok(Value::Unit);
3681            },
3682
3683            "reset_normal_override" =>
3684            {
3685                self.gfx.borrow_mut().normal_override = None;
3686                return Ok(Value::Unit);
3687            },
3688
3689            // clear_depth() / ล้างความลึก — force the z-buffer to clear on the next
3690            // flush. `เติม` already does this; call explicitly to start a fresh
3691            // depth pass mid-frame (e.g. a separate overlay scene).
3692            "clear_depth" | "ล้างความลึก" | "清深度" | "深度クリア" | "깊이지우기" | "پاک‌کردن_عمق" | "امسح_العمق" | "נקה_עומק" | "گہرائی_صاف_کرو" =>
3693            {
3694                self.gfx.borrow_mut().zbuf_needs_clear = true;
3695                return Ok(Value::Unit);
3696            },
3697
3698            // depth_blur(focus, range, radius) / เบลอความลึก — depth-of-field post
3699            // pass over the framebuffer using the z-buffer: sharp at camera-space
3700            // depth `focus`, blurred up to `radius` px as depth departs by `range`.
3701            // Background (no geometry) blurs fully. Call AFTER `flush_3d` (so the
3702            // z-buffer is populated) and BEFORE `present`. Needs `set_depth_test(1)`.
3703            "depth_blur" | "เบลอความลึก" | "dof" | "depth_of_field" | "景深" | "تاری_عمق" | "ضبابية_العمق" | "טשטוש_עומק" | "ڈیپتھ_بلر" =>
3704            {
3705                let focus = self.arg_num(&args, 0, 30.0)? as f32;
3706                let range = self.arg_num(&args, 1, 60.0)? as f32;
3707                let radius = self.arg_num(&args, 2, 3.0)?.max(0.0) as usize;
3708                // oil [0..1] — oil-slick treatment of the blurred zone:
3709                // iridescent chroma fringe + hue swirl (water / heat haze).
3710                let oil = self.arg_num(&args, 3, 0.0)? as f32;
3711                let mut gfx = self.gfx.borrow_mut();
3712                let w = gfx.width;
3713                let h = gfx.height;
3714                if gfx.depth_buf.len() == w * h {
3715                    let g = &mut *gfx;
3716                    crate::gfx::raster::depth_of_field(
3717                        &mut g.buffer,
3718                        &g.depth_buf,
3719                        w,
3720                        h,
3721                        focus,
3722                        range,
3723                        radius,
3724                        oil,
3725                    );
3726                }
3727                return Ok(Value::Unit);
3728            },
3729
3730            // light_pool(x, y, z, radius, r, g, b, intensity) / แอ่งแสง —
3731            // volumetric light splash: a soft additive radial vector gradient on
3732            // the floor at height y — the coloured pool a light throws on the
3733            // ground (underwater-light look). Smooth transparent edge, distance-
3734            // fog aware. Colours 0-255; intensity ~0.2-1.5.
3735            "light_pool" | "แอ่งแสง" | "光池" | "ライトプール" | "빛웅덩이" | "برکه_نور" | "بركة_ضوء" | "בריכת_אור" | "روشنی_تالاب" | "bassin_lumière" | "lichtpfütze" | "лужа_света" =>
3736            {
3737                let x = self.arg_num(&args, 0, 0.0)? as f32;
3738                let y = self.arg_num(&args, 1, 0.0)? as f32;
3739                let z = self.arg_num(&args, 2, 0.0)? as f32;
3740                let radius = self.arg_num(&args, 3, 20.0)? as f32;
3741                let r = self.arg_num(&args, 4, 255.0)? as f32 / 255.0;
3742                let g = self.arg_num(&args, 5, 255.0)? as f32 / 255.0;
3743                let b = self.arg_num(&args, 6, 255.0)? as f32 / 255.0;
3744                let inten = self.arg_num(&args, 7, 1.0)? as f32;
3745                self.gfx
3746                    .borrow_mut()
3747                    .emit_light_pool(x, y, z, radius, [r, g, b], inten);
3748                return Ok(Value::Unit);
3749            },
3750
3751            // light_beam(x, y, z, floor_y, radius, r, g, b, intensity) / ลำแสงไฟ —
3752            // volumetric god-ray shaft: a soft additive double-cone from the
3753            // light position down to the floor plane, spreading to `radius`.
3754            // Pair with light_pool at the base. Colours 0-255.
3755            "light_beam" | "ลำแสงไฟ" | "光柱" | "ライトビーム" | "빛기둥" | "پرتو_نور" | "شعاع_ضوء" | "קרן_אור" | "روشنی_شعاع" | "faisceau_lumière" | "lichtstrahl" | "луч_света" =>
3756            {
3757                let x = self.arg_num(&args, 0, 0.0)? as f32;
3758                let y = self.arg_num(&args, 1, 0.0)? as f32;
3759                let z = self.arg_num(&args, 2, 0.0)? as f32;
3760                let fy = self.arg_num(&args, 3, 0.0)? as f32;
3761                let radius = self.arg_num(&args, 4, 14.0)? as f32;
3762                let r = self.arg_num(&args, 5, 255.0)? as f32 / 255.0;
3763                let g = self.arg_num(&args, 6, 255.0)? as f32 / 255.0;
3764                let b = self.arg_num(&args, 7, 255.0)? as f32 / 255.0;
3765                let inten = self.arg_num(&args, 8, 1.0)? as f32;
3766                self.gfx
3767                    .borrow_mut()
3768                    .emit_light_beam(x, y, z, fy, radius, [r, g, b], inten);
3769                return Ok(Value::Unit);
3770            },
3771
3772            // grad_triangle(x0,y0,r0,g0,b0, x1,y1,r1,g1,b1, x2,y2,r2,g2,b2)
3773            // Smooth per-vertex gradient triangle — a cheap lit surface: put the
3774            // bright colour on the vertex facing the light. Honours set_alpha.
3775            "grad_triangle" | "สามเหลี่ยมไล่สี" | "渐变三角" | "グラデ三角" | "그라데삼각" | "مثلث_گرادیان" | "مثلث_متدرج" | "משולש_גרדיאנט" | "گریڈینٹ_مثلث" | "triangle_dégradé" | "dreieck_verlauf" | "градиент_треугольник" =>
3776            {
3777                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3778                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3779                let c0 = rgb(
3780                    self.arg_num(&args, 2, 255.0)?,
3781                    self.arg_num(&args, 3, 255.0)?,
3782                    self.arg_num(&args, 4, 255.0)?,
3783                );
3784                let x1 = self.arg_num(&args, 5, 0.0)? as f32;
3785                let y1 = self.arg_num(&args, 6, 0.0)? as f32;
3786                let c1 = rgb(
3787                    self.arg_num(&args, 7, 255.0)?,
3788                    self.arg_num(&args, 8, 255.0)?,
3789                    self.arg_num(&args, 9, 255.0)?,
3790                );
3791                let x2 = self.arg_num(&args, 10, 0.0)? as f32;
3792                let y2 = self.arg_num(&args, 11, 0.0)? as f32;
3793                let c2 = rgb(
3794                    self.arg_num(&args, 12, 255.0)?,
3795                    self.arg_num(&args, 13, 255.0)?,
3796                    self.arg_num(&args, 14, 255.0)?,
3797                );
3798                let mut gfx = self.gfx.borrow_mut();
3799                let (w, h, alpha, mode, lin, ok) = (
3800                    gfx.width,
3801                    gfx.height,
3802                    gfx.alpha,
3803                    gfx.blend,
3804                    gfx.linear_blend,
3805                    gfx.grad_oklab,
3806                );
3807                crate::gfx::raster::fill_triangle_grad(
3808                    &mut gfx.buffer,
3809                    w,
3810                    h,
3811                    alpha,
3812                    mode,
3813                    lin,
3814                    ok,
3815                    x0,
3816                    y0,
3817                    c0,
3818                    x1,
3819                    y1,
3820                    c1,
3821                    x2,
3822                    y2,
3823                    c2,
3824                );
3825                return Ok(Value::Unit);
3826            },
3827
3828            // grad_rect(x,y,w,h, r0,g0,b0, r1,g1,b1, dir) — linear-gradient rect.
3829            // dir 0 = horizontal (left→right), else vertical (top→bottom).
3830            "grad_rect" | "สี่เหลี่ยมไล่สี" | "渐变矩形" | "グラデ矩形" | "그라데사각" | "مستطیل_گرادیان" | "مستطيل_متدرج" | "מלבן_גרדיאנט" | "گریڈینٹ_مستطیل" | "rectangle_dégradé" | "rechteck_verlauf" | "градиент_прямоугольник" =>
3831            {
3832                let x = self.arg_num(&args, 0, 0.0)? as f32;
3833                let y = self.arg_num(&args, 1, 0.0)? as f32;
3834                let rw = self.arg_num(&args, 2, 0.0)? as f32;
3835                let rh = self.arg_num(&args, 3, 0.0)? as f32;
3836                let c0 = rgb(
3837                    self.arg_num(&args, 4, 255.0)?,
3838                    self.arg_num(&args, 5, 255.0)?,
3839                    self.arg_num(&args, 6, 255.0)?,
3840                );
3841                let c1 = rgb(
3842                    self.arg_num(&args, 7, 0.0)?,
3843                    self.arg_num(&args, 8, 0.0)?,
3844                    self.arg_num(&args, 9, 0.0)?,
3845                );
3846                let dir = self.arg_num(&args, 10, 1.0)? as u8;
3847                let mut gfx = self.gfx.borrow_mut();
3848                let (w, h, alpha, mode, lin, ok) = (
3849                    gfx.width,
3850                    gfx.height,
3851                    gfx.alpha,
3852                    gfx.blend,
3853                    gfx.linear_blend,
3854                    gfx.grad_oklab,
3855                );
3856                crate::gfx::raster::fill_rect_grad(
3857                    &mut gfx.buffer,
3858                    w,
3859                    h,
3860                    alpha,
3861                    mode,
3862                    lin,
3863                    ok,
3864                    x,
3865                    y,
3866                    rw,
3867                    rh,
3868                    c0,
3869                    c1,
3870                    dir,
3871                );
3872                return Ok(Value::Unit);
3873            },
3874
3875            // shadow_blob(cx,cy, rx,ry, alpha) — soft colored shadow ellipse in
3876            // the current pen colour. Dark colour = normal shadow; any hue = a
3877            // tinted/coloured shadow. Edge softness comes from shadow_params.
3878            "shadow_blob" | "เงาวงรี" | "阴影斑" | "影ブロブ" | "그림자블롭" | "لکه_سایه" | "بقعة_ظل" | "כתם_צל" | "سایہ_دھبہ" | "tache_ombre" | "schattenklecks" | "пятно_тени" =>
3879            {
3880                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3881                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3882                let rx = self.arg_num(&args, 2, 16.0)? as f32;
3883                let ry = self.arg_num(&args, 3, 8.0)? as f32;
3884                let a = self.arg_num(&args, 4, 0.5)? as f32;
3885                let mut gfx = self.gfx.borrow_mut();
3886                let (w, h, color, soft, mode, lin) = (
3887                    gfx.width,
3888                    gfx.height,
3889                    gfx.color,
3890                    gfx.shadow.soft,
3891                    gfx.blend,
3892                    gfx.linear_blend,
3893                );
3894                crate::gfx::raster::fill_disc_soft(
3895                    &mut gfx.buffer,
3896                    w,
3897                    h,
3898                    cx,
3899                    cy,
3900                    rx,
3901                    ry,
3902                    color,
3903                    a,
3904                    soft,
3905                    mode,
3906                    lin,
3907                );
3908                return Ok(Value::Unit);
3909            },
3910
3911            // cast_shadow(cx,cy, height) — height-driven contact shadow in the
3912            // current pen colour. Closer to the surface (small height) = smaller,
3913            // darker, sharper; farther (large height) = bigger, fainter, softer.
3914            // Tune the ramp with shadow_params.
3915            "cast_shadow" | "ทอดเงา" | "投射阴影" | "影を落とす" | "그림자드리우기" | "افکندن_سایه" | "ألقِ_ظلا" | "הטל_צל" | "سایہ_ڈالو" | "projeter_ombre" | "schatten_werfen" | "отбросить_тень" =>
3916            {
3917                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3918                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3919                let height = (self.arg_num(&args, 2, 0.0)? as f32).max(0.0);
3920                let mut gfx = self.gfx.borrow_mut();
3921                let sp = gfx.shadow;
3922                let radius = (sp.base + sp.grow * height).max(0.5);
3923                let alpha = (sp.alpha - sp.fade * height).clamp(0.04, 1.0);
3924                let soft = (sp.soft + height * 0.004).clamp(0.0, 0.95);
3925                let (w, h, color, mode, lin) = (
3926                    gfx.width,
3927                    gfx.height,
3928                    gfx.color,
3929                    gfx.blend,
3930                    gfx.linear_blend,
3931                );
3932                crate::gfx::raster::fill_disc_soft(
3933                    &mut gfx.buffer,
3934                    w,
3935                    h,
3936                    cx,
3937                    cy,
3938                    radius,
3939                    radius * 0.62,
3940                    color,
3941                    alpha,
3942                    soft,
3943                    mode,
3944                    lin,
3945                );
3946                return Ok(Value::Unit);
3947            },
3948
3949            // shadow_params(base, grow, alpha, fade, soft) — tune cast_shadow.
3950            // Each arg defaults to the current value, so you can set just one.
3951            "shadow_params" | "ตั้งค่าเงา" | "阴影参数" | "影設定" | "그림자설정" | "پارامترهای_سایه" | "معاملات_الظل" | "פרמטרי_צל" | "سایہ_پیرامیٹرز" | "paramètres_ombre" | "schattenparameter" | "параметры_тени" =>
3952            {
3953                let cur = self.gfx.borrow().shadow;
3954                let base = self.arg_num(&args, 0, cur.base as f64)? as f32;
3955                let grow = self.arg_num(&args, 1, cur.grow as f64)? as f32;
3956                let alpha = self.arg_num(&args, 2, cur.alpha as f64)? as f32;
3957                let fade = self.arg_num(&args, 3, cur.fade as f64)? as f32;
3958                let soft = self.arg_num(&args, 4, cur.soft as f64)? as f32;
3959                self.gfx.borrow_mut().shadow =
3960                    crate::gfx::ShadowParams { base, grow, alpha, fade, soft };
3961                return Ok(Value::Unit);
3962            },
3963
3964            // depth_triangle(x0,y0, x1,y1, x2,y2, z) — queue a depth-sorted tri in
3965            // the current colour. Drawn back-to-front (painter's algorithm) at
3966            // present(); larger z = farther away. Lets 2-D sprites/quads sort by
3967            // depth the same way 3-D faces do.
3968            "depth_triangle" | "สามเหลี่ยมเรียงลึก" | "深度三角" | "深度三角形" | "깊이삼각" | "مثلث_عمق" | "مثلث_العمق" | "משולש_עומק" | "گہرائی_مثلث" | "triangle_profondeur" | "tiefendreieck" | "глубина_треугольник" =>
3969            {
3970                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3971                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3972                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3973                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3974                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
3975                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
3976                let z = self.arg_num(&args, 6, 0.0)? as f32;
3977                let mut gfx = self.gfx.borrow_mut();
3978                let color = gfx.color;
3979                gfx.depth_queue
3980                    .push_triangle(z, color, x0, y0, x1, y1, x2, y2);
3981                return Ok(Value::Unit);
3982            },
3983
3984            // depth_line(x0,y0, x1,y1, z) — queue a depth-sorted line in the
3985            // current colour (same painter's queue as depth_triangle).
3986            "depth_line" | "เส้นเรียงลึก" | "深度线" | "深度線" | "깊이선" | "خط_عمق" | "خط_العمق" | "קו_עומק" | "گہرائی_لکیر" | "ligne_profondeur" | "tiefenlinie" | "глубина_линия" =>
3987            {
3988                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3989                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3990                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3991                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3992                let z = self.arg_num(&args, 4, 0.0)? as f32;
3993                let mut gfx = self.gfx.borrow_mut();
3994                let color = gfx.color;
3995                gfx.depth_queue.push_line(z, color, x0, y0, x1, y1);
3996                return Ok(Value::Unit);
3997            },
3998
3999            // ══════════════════════════════════════════════════════════════════
4000            // GRAPHICS BUILTINS
4001            // Thai names first, then English aliases.
4002            // ══════════════════════════════════════════════════════════════════
4003
4004            // ── เปิดหน้าต่าง(width, height, title) — open_window ──
4005            "เปิดหน้าต่าง" | "open_window" | "gfx_window" | "开窗" | "ウィンドウ開く" | "창열기" | "باز_کردن_پنجره" | "افتح_نافذة" | "פתח_חלון" | "ونڈو_کھولو" =>
4006            {
4007                let w = self.arg_num(&args, 0, 800.0)? as usize;
4008                let h = self.arg_num(&args, 1, 600.0)? as usize;
4009                #[cfg(not(target_arch = "wasm32"))]
4010                {
4011                    let title = args
4012                        .get(2)
4013                        .map(|v| v.to_string())
4014                        .unwrap_or_else(|| "Ling".into());
4015                    let mut gfx = self.gfx.borrow_mut();
4016                    let mut win = minifb::Window::new(
4017                        &title,
4018                        w,
4019                        h,
4020                        minifb::WindowOptions {
4021                            resize: false,
4022                            scale: minifb::Scale::X1,
4023                            ..Default::default()
4024                        },
4025                    )
4026                    .map_err(|e| EvalErr::from(format!("cannot open window: {e}")))?;
4027                    apply_frame_pacing(&mut win, gfx.vsync);
4028                    gfx.buffer = vec![0u32; w * h];
4029                    gfx.width = w;
4030                    gfx.height = h;
4031                    gfx.window = Some(win);
4032                    gfx.topmost_window = false;
4033                    gfx.sync_projection();
4034                }
4035                #[cfg(target_arch = "wasm32")]
4036                {
4037                    let mut gfx = self.gfx.borrow_mut();
4038                    gfx.width = w;
4039                    gfx.height = h;
4040                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
4041                    gfx.sync_projection();
4042                    crate::gfx::webgl::resize(w as u32, h as u32);
4043                }
4044                return Ok(Value::Unit);
4045            },
4046
4047            // ── เติม(r, g, b) — fill / clear screen with colour ──
4048            "เติม" | "fill" | "gfx_fill" | "clear" | "填" | "塗り潰し" | "채우기" | "清"
4049            | "消去" | "지우기" | "پر_کن" | "املأ" | "מלא" | "بھرو" => {
4050                let r = self.arg_num(&args, 0, 0.0)? as u32;
4051                let g = self.arg_num(&args, 1, 0.0)? as u32;
4052                let b = self.arg_num(&args, 2, 0.0)? as u32;
4053                #[cfg(not(target_arch = "wasm32"))]
4054                {
4055                    let c = (r << 16) | (g << 8) | b;
4056                    let mut gfx = self.gfx.borrow_mut();
4057                    gfx.buffer.fill(c);
4058                    gfx.zbuf_needs_clear = true; // clear color ⇒ clear depth next flush
4059                    gfx.edge_set.clear(); // reset shared-edge dedup for new frame
4060                }
4061                #[cfg(target_arch = "wasm32")]
4062                {
4063                    let mut gfx = self.gfx.borrow_mut();
4064                    gfx.fill_r = r as f32 / 255.0;
4065                    gfx.fill_g = g as f32 / 255.0;
4066                    gfx.fill_b = b as f32 / 255.0;
4067                    let c = (r << 16) | (g << 8) | b;
4068                    gfx.buffer.fill(c);
4069                    gfx.zbuf_needs_clear = true;
4070                    gfx.edge_set.clear();
4071                }
4072                return Ok(Value::Unit);
4073            },
4074
4075            // ── set_color_hsl(h, s, l) — set drawing colour from HSL ──
4076            // h: 0–360 degrees, s: 0–100 saturation, l: 0–100 lightness
4077            "set_color_hsl" | "颜色HSL" | "色相" | "HSL色" | "HSL색설정" | "สีHSLวาด" | "تنظیم_رنگ_HSL" | "عيّن_اللون_HSL" | "קבע_צבע_HSL" | "HSL_رنگ_مقرر_کرو" | "définir_couleur_hsl" | "farbe_hsl_setzen" | "задать_цвет_hsl" =>
4078            {
4079                let h = self.arg_num(&args, 0, 0.0)?;
4080                let s = self.arg_num(&args, 1, 70.0)?;
4081                let l = self.arg_num(&args, 2, 50.0)?;
4082                let hex = hsl_to_hex(h, s, l);
4083                let r = u32::from_str_radix(&hex[1..3], 16).unwrap_or(255);
4084                let g = u32::from_str_radix(&hex[3..5], 16).unwrap_or(255);
4085                let b = u32::from_str_radix(&hex[5..7], 16).unwrap_or(255);
4086                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
4087                return Ok(Value::Unit);
4088            },
4089
4090            // ── สีดินสอ(r, g, b) — set drawing colour ──
4091            "สีดินสอ" | "set_color" | "gfx_color" | "color" | "设色" | "色設定" | "색설정" | "تنظیم_رنگ" | "عيّن_اللون" | "קבע_צבע" | "رنگ_مقرر_کرو" =>
4092            {
4093                let r = self.arg_num(&args, 0, 255.0)? as u32;
4094                let g = self.arg_num(&args, 1, 255.0)? as u32;
4095                let b = self.arg_num(&args, 2, 255.0)? as u32;
4096                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
4097                return Ok(Value::Unit);
4098            },
4099
4100            // ── วาดสามเหลี่ยม(x1,y1, x2,y2, x3,y3) — draw filled triangle ──
4101            "วาดสามเหลี่ยม"
4102            | "draw_triangle"
4103            | "gfx_triangle"
4104            | "triangle"
4105            | "画三角"
4106            | "三角形描画"
4107            | "삼각형그리기" | "رسم_مثلث" | "ارسم_مثلثا" | "צייר_משולש" | "مثلث_کھینچو" => {
4108                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
4109                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
4110                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
4111                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
4112                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
4113                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
4114                let mut gfx = self.gfx.borrow_mut();
4115                let color = gfx.color;
4116                #[cfg(not(target_arch = "wasm32"))]
4117                {
4118                    let w = gfx.width;
4119                    let h = gfx.height;
4120                    fill_triangle(&mut gfx.buffer, w, h, color, x0, y0, x1, y1, x2, y2);
4121                }
4122                #[cfg(target_arch = "wasm32")]
4123                gfx.depth_queue
4124                    .push_triangle(0.0, color, x0, y0, x1, y1, x2, y2);
4125                return Ok(Value::Unit);
4126            },
4127
4128            // ── วาดเส้น(x1,y1, x2,y2) — draw line ──
4129            "วาดเส้น" | "draw_line" | "gfx_line" | "line" | "画线" | "線描く" | "선그리기" | "رسم_خط" | "ارسم_خط" | "צייר_קו" | "لکیر_کھینچو" =>
4130            {
4131                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
4132                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
4133                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
4134                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
4135                let mut gfx = self.gfx.borrow_mut();
4136                let color = gfx.color;
4137                #[cfg(not(target_arch = "wasm32"))]
4138                {
4139                    let w = gfx.width;
4140                    let h = gfx.height;
4141                    let aa = gfx.antialias;
4142                    let add = gfx.blend == 1;
4143                    if aa {
4144                        crate::gfx::raster::draw_line_aa(
4145                            &mut gfx.buffer,
4146                            w,
4147                            h,
4148                            color,
4149                            add,
4150                            x0,
4151                            y0,
4152                            x1,
4153                            y1,
4154                        );
4155                    } else {
4156                        draw_line(&mut gfx.buffer, w, h, color, x0, y0, x1, y1);
4157                    }
4158                }
4159                #[cfg(target_arch = "wasm32")]
4160                gfx.depth_queue.push_line(0.0, color, x0, y0, x1, y1);
4161                return Ok(Value::Unit);
4162            },
4163
4164            // ── วาดจุด(x, y) — plot a single pixel ──
4165            "วาดจุด" | "draw_pixel" | "gfx_pixel" | "pixel" | "画点" | "点描く" | "점그리기" | "رسم_نقطه" | "ارسم_نقطة" | "צייר_פיקסל" | "پکسل_کھینچو" =>
4166            {
4167                let px = self.arg_num(&args, 0, 0.0)? as i32;
4168                let py = self.arg_num(&args, 1, 0.0)? as i32;
4169                #[cfg(not(target_arch = "wasm32"))]
4170                {
4171                    let mut gfx = self.gfx.borrow_mut();
4172                    let color = gfx.color;
4173                    let w = gfx.width;
4174                    let h = gfx.height;
4175                    if px >= 0 && py >= 0 && (px as usize) < w && (py as usize) < h {
4176                        gfx.buffer[py as usize * w + px as usize] = color;
4177                    }
4178                }
4179                #[cfg(target_arch = "wasm32")]
4180                {
4181                    // Render pixel as a 1×1 square via two triangles.
4182                    let mut gfx = self.gfx.borrow_mut();
4183                    let color = gfx.color;
4184                    let x = px as f32;
4185                    let y = py as f32;
4186                    gfx.depth_queue
4187                        .push_triangle(0.0, color, x, y, x + 1.0, y, x + 1.0, y + 1.0);
4188                    gfx.depth_queue
4189                        .push_triangle(0.0, color, x, y, x + 1.0, y + 1.0, x, y + 1.0);
4190                }
4191                return Ok(Value::Unit);
4192            },
4193
4194            // ── แสดงผล() — flush depth queue, then present frame to screen ──
4195            "แสดงผล" | "present" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시" | "نمایش" | "اعرض" | "הצג" | "دکھاؤ" =>
4196            {
4197                // Click-edge widgets (ui_button etc.) compare THIS frame's
4198                // mouse_now() against `mouse_was_down` to detect a fresh
4199                // press. That comparison only works if mouse_was_down
4200                // reflects what the script itself observed this frame — i.e.
4201                // the state from BEFORE update_with_buffer below pulls in new
4202                // OS events. Capturing it after (as the "freshest" read)
4203                // would mean a just-arrived click is already baked into
4204                // mouse_was_down by the time next frame's ui_button compares
4205                // against it, so `down && !mouse_was_down` is never true and
4206                // clicks never register at all. Declared at the top of the
4207                // match arm (not inside the block below) so it survives past
4208                // the wasm32/non-wasm32 split further down.
4209                #[cfg(not(target_arch = "wasm32"))]
4210                let pre_update_mouse_down = self
4211                    .gfx
4212                    .borrow()
4213                    .window
4214                    .as_ref()
4215                    .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
4216                    .unwrap_or(false);
4217                #[cfg(not(target_arch = "wasm32"))]
4218                {
4219                    ling_fps_tick();
4220                    ling_phase_frame();
4221                    // Flush depth queue and present — release borrow before reading mouse.
4222                    {
4223                        let mut gfx = self.gfx.borrow_mut();
4224                        if !gfx.depth_queue.is_empty() {
4225                            let w = gfx.width;
4226                            let h = gfx.height;
4227                            let dt = gfx.depth_test;
4228                            let reset_z = gfx.zbuf_needs_clear;
4229                            let (bm, ba) = (gfx.blend, gfx.alpha);
4230                            let aa = gfx.antialias;
4231                            let queue = std::mem::take(&mut gfx.depth_queue);
4232                            {
4233                                let g = &mut *gfx;
4234                                let z = if dt { Some(&mut g.depth_buf) } else { None };
4235                                queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
4236                            }
4237                            gfx.depth_queue.set_state(bm, ba);
4238                            gfx.zbuf_needs_clear = false;
4239                        }
4240                        let _t = std::time::Instant::now();
4241                        if !gfx.post_done {
4242                            gfx.toon_post_process();
4243                        }
4244                        gfx.post_done = false;
4245                        ling_phase_add(phase::TOON, _t.elapsed().as_nanos());
4246                        let w = gfx.width;
4247                        let h = gfx.height;
4248                        let g = &mut *gfx;
4249                        if g.frame_blur > 0.0 {
4250                            // Afterimage trails: previous frame decays by `frame_blur`
4251                            // per frame and composites with MAX — fresh content stays
4252                            // full-brightness, ghosts fade out over time.
4253                            // retention 0.98 @60fps ≈ trails last ~2.6 s.
4254                            let a = (g.frame_blur.clamp(0.0, 0.995) * 256.0) as u32;
4255                            if g.prev_frame.len() != g.buffer.len() {
4256                                g.prev_frame = g.buffer.clone();
4257                            }
4258                            for (dst, prev) in g.buffer.iter_mut().zip(g.prev_frame.iter_mut()) {
4259                                let c = *dst;
4260                                let pv = *prev;
4261                                let pr = (((pv >> 16) & 0xFF) * a) >> 8;
4262                                let pg = (((pv >> 8) & 0xFF) * a) >> 8;
4263                                let pb = ((pv & 0xFF) * a) >> 8;
4264                                let cr = (c >> 16) & 0xFF;
4265                                let cg = (c >> 8) & 0xFF;
4266                                let cb = c & 0xFF;
4267                                let outp = (cr.max(pr) << 16) | (cg.max(pg) << 8) | cb.max(pb);
4268                                *dst = outp;
4269                                *prev = outp;
4270                            }
4271                        }
4272                        if let Some(win) = g.window.as_mut() {
4273                            let _b = std::time::Instant::now();
4274                            win.update_with_buffer(&g.buffer, w, h)
4275                                .map_err(|e| EvalErr::from(format!("present error: {e}")))?;
4276                            ling_phase_add(phase::BLIT, _b.elapsed().as_nanos());
4277                        }
4278                    }
4279                    // Read mouse AFTER update_with_buffer so events are processed.
4280                    let mouse_pos = {
4281                        let gfx = self.gfx.borrow();
4282                        gfx.window
4283                            .as_ref()
4284                            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
4285                    };
4286                    let mut gfx = self.gfx.borrow_mut();
4287                    if gfx.mouse_captured {
4288                        let w = gfx.width as f32;
4289                        let h = gfx.height as f32;
4290                        if let Some((mx, my)) = mouse_pos {
4291                            if gfx.last_mx.is_nan() {
4292                                gfx.mouse_dx = 0.0;
4293                                gfx.mouse_dy = 0.0;
4294                                gfx.last_mx = mx;
4295                                gfx.last_my = my;
4296                            } else {
4297                                gfx.mouse_dx = mx - gfx.last_mx;
4298                                gfx.mouse_dy = my - gfx.last_my;
4299                                // Wrap the cursor at every edge (L/R/U/D) → infinite look
4300                                // on both axes, and the cursor is NOT trapped (alt-tab works).
4301                                let margin = 6.0;
4302                                let (mut nx, mut ny, mut warp) = (mx, my, false);
4303                                if mx < margin {
4304                                    nx = w - margin - 2.0;
4305                                    warp = true;
4306                                } else if mx > w - margin {
4307                                    nx = margin + 2.0;
4308                                    warp = true;
4309                                }
4310                                if my < margin {
4311                                    ny = h - margin - 2.0;
4312                                    warp = true;
4313                                } else if my > h - margin {
4314                                    ny = margin + 2.0;
4315                                    warp = true;
4316                                }
4317                                if warp {
4318                                    #[cfg(windows)]
4319                                    unsafe {
4320                                        #[repr(C)]
4321                                        struct RECT {
4322                                            left: i32,
4323                                            top: i32,
4324                                            right: i32,
4325                                            bottom: i32,
4326                                        }
4327                                        extern "system" {
4328                                            fn GetForegroundWindow() -> isize;
4329                                            fn GetWindowRect(hwnd: isize, lpRect: *mut RECT)
4330                                                -> i32;
4331                                            fn SetCursorPos(x: i32, y: i32) -> i32;
4332                                        }
4333                                        let hwnd = GetForegroundWindow();
4334                                        let mut rect =
4335                                            RECT { left: 0, top: 0, right: 0, bottom: 0 };
4336                                        if GetWindowRect(hwnd, &mut rect) != 0 {
4337                                            SetCursorPos(
4338                                                rect.left + nx as i32,
4339                                                rect.top + ny as i32,
4340                                            );
4341                                        }
4342                                    }
4343                                    gfx.last_mx = nx;
4344                                    gfx.last_my = ny;
4345                                } else {
4346                                    gfx.last_mx = mx;
4347                                    gfx.last_my = my;
4348                                }
4349                            }
4350                        } else {
4351                            gfx.mouse_dx = 0.0;
4352                            gfx.mouse_dy = 0.0;
4353                        }
4354                    } else if let Some((mx, my)) = mouse_pos {
4355                        if gfx.last_mx.is_nan() {
4356                            gfx.mouse_dx = 0.0;
4357                            gfx.mouse_dy = 0.0;
4358                        } else {
4359                            gfx.mouse_dx = mx - gfx.last_mx;
4360                            gfx.mouse_dy = my - gfx.last_my;
4361                        }
4362                        gfx.last_mx = mx;
4363                        gfx.last_my = my;
4364                    } else {
4365                        gfx.mouse_dx = 0.0;
4366                        gfx.mouse_dy = 0.0;
4367                    }
4368
4369                    // Alt-tab support: minifb has no WM_KILLFOCUS handler on Windows,
4370                    // so a key/button released while another window was focused can
4371                    // still read as "down" for one stale frame right after the user
4372                    // alt-tabs back. Detect the unfocused→focused transition and
4373                    // swallow raw input for a short grace window afterward instead of
4374                    // letting a phantom held key jerk the camera. See key_down /
4375                    // mouse_down* below (they early-out on gfx.input_suppressed()).
4376                    let is_active = gfx.window.as_mut().map(|w| w.is_active()).unwrap_or(true);
4377                    if is_active && !gfx.was_active {
4378                        gfx.focus_grace_frames = 5;
4379                        // Regained focus: restore HWND_TOPMOST so the borderless-
4380                        // fullscreen window covers the taskbar again.
4381                        #[cfg(windows)]
4382                        if gfx.topmost_window {
4383                            if let Some(w) = gfx.window.as_ref() {
4384                                set_window_topmost(w.get_window_handle() as isize, true);
4385                            }
4386                        }
4387                    } else if !is_active && gfx.was_active {
4388                        // Lost focus (alt-tab): drop topmost so the game stops
4389                        // covering whatever window the user just switched to —
4390                        // otherwise a topmost borderless window visually "wins"
4391                        // even though it's no longer focused, making alt-tab
4392                        // look broken.
4393                        #[cfg(windows)]
4394                        if gfx.topmost_window {
4395                            if let Some(w) = gfx.window.as_ref() {
4396                                set_window_topmost(w.get_window_handle() as isize, false);
4397                            }
4398                        }
4399                    }
4400                    gfx.was_active = is_active;
4401                    if gfx.focus_grace_frames > 0 {
4402                        gfx.focus_grace_frames -= 1;
4403                    }
4404                }
4405                #[cfg(target_arch = "wasm32")]
4406                {
4407                    {
4408                        // Software-render everything (3-D depth queue + 2-D vtex/ui that
4409                        // already wrote into the buffer) into the framebuffer, exactly
4410                        // like native, then upload that buffer to the canvas in one blit.
4411                        let mut gfx = self.gfx.borrow_mut();
4412                        let w = gfx.width;
4413                        let h = gfx.height;
4414                        if gfx.buffer.len() != w * h {
4415                            gfx.buffer.resize(w * h, 0);
4416                        }
4417                        if !gfx.depth_queue.is_empty() {
4418                            let dt = gfx.depth_test;
4419                            let reset_z = gfx.zbuf_needs_clear;
4420                            let aa = gfx.antialias;
4421                            let queue = std::mem::take(&mut gfx.depth_queue);
4422                            {
4423                                let g = &mut *gfx;
4424                                let z = if dt { Some(&mut g.depth_buf) } else { None };
4425                                queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
4426                            }
4427                            gfx.zbuf_needs_clear = false;
4428                        }
4429                        if !gfx.post_done {
4430                            gfx.toon_post_process();
4431                        }
4432                        gfx.post_done = false;
4433                        crate::gfx::webgl::blit_rgb(&gfx.buffer, w, h);
4434                    }
4435                    self.wasm_pace_frame();
4436                }
4437                // Update the click-edge latch for interactive UI widgets —
4438                // using the PRE-update_with_buffer snapshot captured at the
4439                // top of this function (see the comment there for why).
4440                #[cfg(not(target_arch = "wasm32"))]
4441                {
4442                    self.mouse_was_down = pre_update_mouse_down;
4443                }
4444                // Increment frame counter
4445                self.frame_num += 1;
4446                return Ok(Value::Unit);
4447            },
4448
4449            // ── เปิดหน้าต่างเต็มจอ(title) — true native-res fullscreen window ──
4450            "เปิดหน้าต่างเต็มจอ"
4451            | "open_fullscreen"
4452            | "fullscreen"
4453            | "全屏"
4454            | "全画面"
4455            | "전체화면" | "باز_کردن_تمام‌صفحه" | "افتح_ملء_الشاشة" | "פתח_מסך_מלא" | "فل_سکرین_کھولو" => {
4456                // In WASM the canvas defines the viewport; use its current size
4457                // as the default so the projection matches what's actually visible.
4458                #[cfg(target_arch = "wasm32")]
4459                let (default_w, default_h) = {
4460                    let (cw, ch) = crate::gfx::webgl::canvas_size();
4461                    (cw as f64, ch as f64)
4462                };
4463                // On native: query the actual primary monitor resolution.
4464                #[cfg(all(not(target_arch = "wasm32"), windows))]
4465                let (default_w, default_h) = unsafe {
4466                    extern "system" {
4467                        fn GetSystemMetrics(nIndex: i32) -> i32;
4468                    }
4469                    (GetSystemMetrics(0) as f64, GetSystemMetrics(1) as f64)
4470                };
4471                #[cfg(all(not(target_arch = "wasm32"), not(windows)))]
4472                let (default_w, default_h) = native_screen_size();
4473
4474                let w = args
4475                    .get(1)
4476                    .map(|v| self.to_number(v).unwrap_or(default_w) as usize)
4477                    .unwrap_or(default_w as usize);
4478                let h = args
4479                    .get(2)
4480                    .map(|v| self.to_number(v).unwrap_or(default_h) as usize)
4481                    .unwrap_or(default_h as usize);
4482                #[cfg(not(target_arch = "wasm32"))]
4483                {
4484                    let title = args
4485                        .first()
4486                        .map(|v| v.to_string())
4487                        .unwrap_or_else(|| "Ling".into());
4488                    let mut gfx = self.gfx.borrow_mut();
4489                    let mut win = minifb::Window::new(
4490                        &title,
4491                        w,
4492                        h,
4493                        minifb::WindowOptions {
4494                            borderless: true,
4495                            title: false,
4496                            resize: false,
4497                            topmost: true,
4498                            scale: minifb::Scale::X1,
4499                            ..Default::default()
4500                        },
4501                    )
4502                    .map_err(|e| EvalErr::from(format!("cannot open fullscreen: {e}")))?;
4503                    apply_frame_pacing(&mut win, gfx.vsync);
4504                    // Grab the native handle *before* moving the window into gfx.
4505                    #[cfg(windows)]
4506                    let hwnd = win.get_window_handle() as isize;
4507                    gfx.buffer = vec![0u32; w * h];
4508                    gfx.width = w;
4509                    gfx.height = h;
4510                    gfx.window = Some(win);
4511                    gfx.topmost_window = true;
4512                    #[cfg(windows)]
4513                    {
4514                        gfx.hwnd = hwnd;
4515                    }
4516                    gfx.sync_projection();
4517                    // Strip all chrome and cover the full screen, above the taskbar.
4518                    #[cfg(windows)]
4519                    make_borderless_fullscreen(hwnd, w as i32, h as i32);
4520                    #[cfg(windows)]
4521                    force_window_focus(hwnd);
4522                }
4523                #[cfg(target_arch = "wasm32")]
4524                {
4525                    let mut gfx = self.gfx.borrow_mut();
4526                    gfx.width = w;
4527                    gfx.height = h;
4528                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
4529                    gfx.sync_projection();
4530                    crate::gfx::webgl::resize(w as u32, h as u32);
4531                }
4532                return Ok(Value::Unit);
4533            },
4534
4535            // ── ความกว้าง() / ความสูง() — current framebuffer size ──
4536            "get_width" | "ความกว้าง" | "宽" | "幅取得" | "너비" | "عرض" | "العرض" | "רוחב" | "چوڑائی" | "obtenir_largeur" | "breite_abrufen" | "получить_ширину" => {
4537                return Ok(Value::Number(self.gfx.borrow().width as f64));
4538            },
4539            "get_height" | "ความสูง" | "高" | "高取得" | "높이" | "ارتفاع" | "الارتفاع" | "גובה" | "اونچائی" | "obtenir_hauteur" | "höhe_abrufen" | "получить_высоту" => {
4540                return Ok(Value::Number(self.gfx.borrow().height as f64));
4541            },
4542
4543            // ── monitor detection: physical display, not the framebuffer ──────
4544            // monitor_width() → primary-monitor pixel width
4545            "monitor_width" | "screen_width" | "屏宽" | "画面幅" | "화면너비" | "ความกว้างจอ" | "عرض_مانیتور" | "عرض_الشاشة" | "רוחב_צג" | "مانیٹر_چوڑائی" | "largeur_moniteur" | "monitor_breite" | "ширина_монитора" =>
4546            {
4547                return Ok(Value::Number(monitor_info().0 as f64));
4548            },
4549            // monitor_height() → primary-monitor pixel height
4550            "monitor_height" | "screen_height" | "屏高" | "画面高" | "화면높이" | "ความสูงจอ" | "ارتفاع_مانیتور" | "ارتفاع_الشاشة" | "גובה_צג" | "مانیٹر_اونچائی" | "hauteur_moniteur" | "monitor_höhe" | "высота_монитора" =>
4551            {
4552                return Ok(Value::Number(monitor_info().1 as f64));
4553            },
4554            // monitor_refresh() → refresh rate in Hz (a.k.a. the monitor framerate)
4555            "monitor_refresh"
4556            | "monitor_hz"
4557            | "monitor_fps"
4558            | "refresh_rate"
4559            | "刷新率"
4560            | "リフレッシュレート"
4561            | "주사율"
4562            | "อัตรารีเฟรช" | "نرخ_بروزرسانی_مانیتور" | "معدل_تحديث_الشاشة" | "קצב_רענון_צג" | "مانیٹر_ریفریش_ریٹ" | "fréquence_moniteur" | "monitor_bildwiederholrate" | "частота_монитора" => {
4563                return Ok(Value::Number(monitor_info().2 as f64));
4564            },
4565            // monitor_info() → [width, height, refresh_hz]
4566            "monitor_info" | "screen_info" | "屏幕信息" | "画面情報" | "화면정보" | "ข้อมูลจอ" | "اطلاعات_مانیتور" | "معلومات_الشاشة" | "מידע_צג" | "مانیٹر_معلومات" | "info_moniteur" | "bildschirminfo" | "инфо_монитора" =>
4567            {
4568                let (w, h, hz) = monitor_info();
4569                return Ok(Value::List(Rc::new(vec![
4570                    Value::Number(w as f64),
4571                    Value::Number(h as f64),
4572                    Value::Number(hz as f64),
4573                ])));
4574            },
4575            // set_fps(n) → cap the render loop at n frames per second
4576            "set_fps"
4577            | "set_target_fps"
4578            | "target_fps"
4579            | "设帧率"
4580            | "フレームレート設定"
4581            | "프레임설정"
4582            | "ตั้งเฟรมเรต" | "تنظیم_نرخ_فریم" | "عيّن_معدل_الإطارات" | "קבע_קצב_פריימים" | "ایف_پی_ایس_مقرر_کرو" | "définir_fps" | "fps_setzen" | "задать_fps" => {
4583                #[cfg(not(target_arch = "wasm32"))]
4584                {
4585                    let fps = self.arg_num(&args, 0, 60.0)?.max(1.0) as usize;
4586                    let mut gfx = self.gfx.borrow_mut();
4587                    if let Some(win) = gfx.window.as_mut() {
4588                        win.set_target_fps(fps);
4589                    }
4590                }
4591                #[cfg(target_arch = "wasm32")]
4592                {
4593                    self.wasm_target_fps = self.arg_num(&args, 0, 60.0)?.max(1.0);
4594                    self.wasm_next_present_ms = 0.0;
4595                }
4596                return Ok(Value::Unit);
4597            },
4598
4599            // set_vsync(on) → pace the window to the monitor's refresh rate.
4600            // Frame-rate pacing (minifb has no swap-interval), not tear-free
4601            // vsync; `LING_FPS_CAP` and an explicit `set_fps` call still win.
4602            "set_vsync" | "vsync" | "垂直同步" | "垂直同期" | "수직동기" | "ตั้งวีซิงก์" | "تنظیم_وی‌سینک" | "عيّن_تزامن_رأسي" | "קבע_וי_סינק" | "وی_سینک_مقرر_کرو" | "définir_vsync" | "vsync_setzen" | "задать_vsync" =>
4603            {
4604                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
4605                #[cfg(not(target_arch = "wasm32"))]
4606                {
4607                    let mut gfx = self.gfx.borrow_mut();
4608                    gfx.vsync = on;
4609                    if let Some(win) = gfx.window.as_mut() {
4610                        apply_frame_pacing(win, on);
4611                    }
4612                }
4613                #[cfg(target_arch = "wasm32")]
4614                {
4615                    self.wasm_target_fps = if on { monitor_info().2 as f64 } else { 240.0 };
4616                    self.wasm_next_present_ms = 0.0;
4617                }
4618                return Ok(Value::Unit);
4619            },
4620
4621            // ── หน้าต่างเปิดอยู่() → bool — is the window still open? ──
4622            "หน้าต่างเปิดอยู่"
4623            | "window_is_open"
4624            | "gfx_is_open"
4625            | "is_open"
4626            | "窗开"
4627            | "開いている"
4628            | "창열림" | "پنجره_باز_است" | "النافذة_مفتوحة" | "החלון_פתוח" | "ونڈو_کھلی_ہے" => {
4629                #[cfg(not(target_arch = "wasm32"))]
4630                {
4631                    let gfx = self.gfx.borrow();
4632                    if gfx.want_quit {
4633                        return Ok(Value::Bool(false));
4634                    }
4635                    // Escape-to-quit needs the same GetAsyncKeyState fallback
4636                    // as key_down/key_pressed/text_poll (see those) — raw
4637                    // w.is_key_down(Escape) is WM_KEYDOWN-based and silently
4638                    // never fires if this topmost window didn't actually win
4639                    // real Win32 keyboard focus.
4640                    #[cfg(windows)]
4641                    let escape_down = if gfx.topmost_window {
4642                        window_is_foreground(gfx.hwnd) && os_key_down(0x1B) // VK_ESCAPE
4643                    } else {
4644                        gfx.window
4645                            .as_ref()
4646                            .map(|w| w.is_key_down(minifb::Key::Escape))
4647                            .unwrap_or(false)
4648                    };
4649                    #[cfg(not(windows))]
4650                    let escape_down = gfx
4651                        .window
4652                        .as_ref()
4653                        .map(|w| w.is_key_down(minifb::Key::Escape))
4654                        .unwrap_or(false);
4655                    let open = gfx.window.as_ref().map(|w| w.is_open()).unwrap_or(false)
4656                        && !escape_down;
4657                    return Ok(Value::Bool(open));
4658                }
4659                #[cfg(target_arch = "wasm32")]
4660                return Ok(Value::Bool(true));
4661            },
4662
4663            // quit() — close the window the same way Escape does, for a
4664            // script-drawn UI element (an exit button) to call.
4665            "quit" | "exit_game" | "close_window" => {
4666                #[cfg(not(target_arch = "wasm32"))]
4667                {
4668                    self.gfx.borrow_mut().want_quit = true;
4669                }
4670                return Ok(Value::Unit);
4671            },
4672
4673            // ── key_down(name) → bool — is a key held? ──
4674            "key_down" | "กดค้าง" | "按键" | "キー押す" | "키누름" | "کلید_فشرده" | "المفتاح_مضغوط" | "מקש_לחוץ" | "بٹن_دبا_ہوا" | "touche_enfoncée" | "taste_gedrückt" | "клавиша_нажата" => {
4675                #[cfg(not(target_arch = "wasm32"))]
4676                {
4677                    let name = self.arg_str(&args, 0, "");
4678                    let mut gfx = self.gfx.borrow_mut();
4679                    // The borderless-fullscreen/topmost window can be
4680                    // visually in front without ever winning real Win32
4681                    // keyboard focus (Windows' foreground-lock) — minifb's
4682                    // is_key_down is populated from WM_KEYDOWN, which then
4683                    // never arrives. GetAsyncKeyState reads the OS key-state
4684                    // table directly and doesn't need focus, so use it
4685                    // whenever this is that window (see force_window_focus).
4686                    #[cfg(windows)]
4687                    if gfx.topmost_window {
4688                        if !window_is_foreground(gfx.hwnd) {
4689                            return Ok(Value::Bool(false));
4690                        }
4691                        return Ok(Value::Bool(
4692                            str_to_vk(&name).map(os_key_down).unwrap_or(false),
4693                        ));
4694                    }
4695                    if gfx.input_suppressed() {
4696                        return Ok(Value::Bool(false));
4697                    }
4698                    let down = gfx
4699                        .window
4700                        .as_ref()
4701                        .and_then(|w| str_to_minifb_key(&name).map(|k| w.is_key_down(k)))
4702                        .unwrap_or(false);
4703                    return Ok(Value::Bool(down));
4704                }
4705                #[cfg(target_arch = "wasm32")]
4706                {
4707                    let name = self.arg_str(&args, 0, "");
4708                    return Ok(Value::Bool(crate::gfx::wasm_is_key_down(&name)));
4709                }
4710            },
4711
4712            // ── key_pressed(name) → bool — was a key pressed this frame? ──
4713            "key_pressed" | "กดปุ่ม" | "键按" | "キー押した" | "키눌림" | "فشردن_کلید" | "ضغط_المفتاح" | "לחיצת_מקש" | "بٹن_دبانا" | "touche_appuyée" | "taste_getippt" | "клавиша_нажатие" => {
4714                #[cfg(not(target_arch = "wasm32"))]
4715                {
4716                    let name = self.arg_str(&args, 0, "");
4717                    let pressed = {
4718                        let mut gfx = self.gfx.borrow_mut();
4719                        #[cfg(windows)]
4720                        let topmost = gfx.topmost_window;
4721                        #[cfg(not(windows))]
4722                        let topmost = false;
4723                        if topmost {
4724                            #[cfg(windows)]
4725                            {
4726                                if !window_is_foreground(gfx.hwnd) {
4727                                    false
4728                                } else {
4729                                    match str_to_vk(&name) {
4730                                        Some(vk) => {
4731                                            let idx = (vk as usize) & 0xFF;
4732                                            let down = os_key_down(vk);
4733                                            let was = gfx.raw_keys_prev[idx];
4734                                            gfx.raw_keys_prev[idx] = down;
4735                                            down && !was
4736                                        },
4737                                        None => false,
4738                                    }
4739                                }
4740                            }
4741                            #[cfg(not(windows))]
4742                            {
4743                                false
4744                            }
4745                        } else if gfx.input_suppressed() {
4746                            false
4747                        } else {
4748                            gfx.window
4749                                .as_ref()
4750                                .and_then(|w| {
4751                                    str_to_minifb_key(&name)
4752                                        .map(|k| w.is_key_pressed(k, minifb::KeyRepeat::No))
4753                                })
4754                                .unwrap_or(false)
4755                        }
4756                    };
4757                    // gamepad Start behaves like Enter everywhere
4758                    let pressed =
4759                        pressed || ((name == "enter" || name == "return") && gamepad::start_edge());
4760                    return Ok(Value::Bool(pressed));
4761                }
4762                #[cfg(target_arch = "wasm32")]
4763                {
4764                    let name = self.arg_str(&args, 0, "");
4765                    let pressed = crate::gfx::wasm_is_key_pressed(&name);
4766                    return Ok(Value::Bool(pressed));
4767                }
4768            },
4769
4770            // ── mouse_dx() / mouse_dy() → f64 — delta since last frame ──
4771            "mouse_dx" | "เมาส์X" | "鼠ΔX" | "マウスΔX" | "마우스ΔX" | "دلتا_ماوس_ایکس" | "فارق_الفأرة_س" | "דלתא_עכבר_X" | "ماؤس_ڈیلٹا_ایکس" | "souris_dx" | "maus_dx" | "мышь_dx" => {
4772                #[cfg(not(target_arch = "wasm32"))]
4773                return Ok(Value::Number(self.gfx.borrow().mouse_dx as f64));
4774                #[cfg(target_arch = "wasm32")]
4775                return Ok(Value::Number(crate::gfx::wasm_mouse_dx() as f64));
4776            },
4777            // ── mouse_scroll() → f64 — vertical scroll-wheel delta this frame ──
4778            #[cfg(not(target_arch = "wasm32"))]
4779            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" | "غلتک_ماوس" | "عجلة_الفأرة" | "גלגלת_עכבר" | "ماؤس_اسکرول" =>
4780            {
4781                let gfx = self.gfx.borrow();
4782                let s = gfx
4783                    .window
4784                    .as_ref()
4785                    .and_then(|w| w.get_scroll_wheel())
4786                    .map(|(_, y)| y as f64)
4787                    .unwrap_or(0.0);
4788                return Ok(Value::Number(s));
4789            },
4790            #[cfg(target_arch = "wasm32")]
4791            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" | "غلتک_ماوس" | "عجلة_الفأرة" | "גלגלת_עכבר" | "ماؤس_اسکرول" =>
4792            {
4793                return Ok(Value::Number(0.0));
4794            },
4795            "mouse_dy" | "เมาส์Y" | "鼠ΔY" | "マウスΔY" | "마우스ΔY" | "دلتا_ماوس_ایگرگ" | "فارق_الفأرة_ص" | "דלתא_עכבר_Y" | "ماؤس_ڈیلٹا_وائی" | "souris_dy" | "maus_dy" | "мышь_dy" => {
4796                #[cfg(not(target_arch = "wasm32"))]
4797                return Ok(Value::Number(self.gfx.borrow().mouse_dy as f64));
4798                #[cfg(target_arch = "wasm32")]
4799                return Ok(Value::Number(crate::gfx::wasm_mouse_dy() as f64));
4800            },
4801
4802            // ── Gamepad / joystick input (ling-input "Sensorium" + gilrs) ──
4803            // pad_poll() → number — advance input one frame; returns # connected pads.
4804            "pad_poll" | "手柄轮询" | "パッド更新" | "패드폴링" | "อัปเดตแพด" | "بررسی_دسته" | "استطلع_اليد" | "בדוק_בקר" | "گیم_پیڈ_پول" | "interroger_manette" | "gamepad_abfragen" | "опросить_геймпад" =>
4805            {
4806                #[cfg(not(target_arch = "wasm32"))]
4807                return Ok(Value::Number(self.pad_poll() as f64));
4808                #[cfg(target_arch = "wasm32")]
4809                return Ok(Value::Number(input_web::poll() as f64));
4810            },
4811            // pad_count() → number — connected gamepads.
4812            "pad_count" | "手柄数" | "パッド数" | "패드수" | "จำนวนแพด" | "تعداد_دسته" | "عدد_أيدي_التحكم" | "מספר_בקרים" | "گیم_پیڈ_تعداد" | "nombre_manettes" | "gamepad_anzahl" | "число_геймпадов" =>
4813            {
4814                #[cfg(not(target_arch = "wasm32"))]
4815                {
4816                    let inp = self.input.borrow();
4817                    let n = inp.as_ref().map_or(0, |s| s.sensorium.devices.count());
4818                    return Ok(Value::Number(n as f64));
4819                }
4820                #[cfg(target_arch = "wasm32")]
4821                return Ok(Value::Number(input_web::count() as f64));
4822            },
4823            // pad_connected(i) → bool.
4824            "pad_connected" | "手柄连接" | "パッド接続" | "패드연결" | "แพดเชื่อม" | "دسته_متصل" | "يد_التحكم_متصلة" | "בקר_מחובר" | "گیم_پیڈ_منسلک" | "manette_connectée" | "gamepad_verbunden" | "геймпад_подключён" =>
4825            {
4826                #[cfg(not(target_arch = "wasm32"))]
4827                {
4828                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4829                    let inp = self.input.borrow();
4830                    let c = inp
4831                        .as_ref()
4832                        .is_some_and(|s| s.sensorium.devices.for_player(i as u8).is_some());
4833                    return Ok(Value::Bool(c));
4834                }
4835                #[cfg(target_arch = "wasm32")]
4836                {
4837                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4838                    return Ok(Value::Bool(input_web::is_connected(i)));
4839                }
4840            },
4841            // pad_button(i, name) → bool — is the button held?
4842            "pad_button" | "手柄按键" | "パッドボタン" | "패드버튼" | "ปุ่มแพด" | "دکمه_دسته" | "زر_اليد" | "כפתור_בקר" | "گیم_پیڈ_بٹن" | "bouton_manette" | "gamepad_taste" | "кнопка_геймпада" =>
4843            {
4844                #[cfg(not(target_arch = "wasm32"))]
4845                {
4846                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4847                    let name = self.arg_str(&args, 1, "");
4848                    let down = parse_pad_button(&name)
4849                        .is_some_and(|b| self.with_pad(i, false, |p| p.is_down(b)));
4850                    return Ok(Value::Bool(down));
4851                }
4852                #[cfg(target_arch = "wasm32")]
4853                {
4854                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4855                    let name = self.arg_str(&args, 1, "");
4856                    return Ok(Value::Bool(input_web::button_down(i, &name)));
4857                }
4858            },
4859            // pad_pressed(i, name) → bool — pressed this frame?
4860            // On WASM we only have the current snapshot, so treat as button_down.
4861            "pad_pressed" | "手柄按下" | "パッド押下" | "패드눌림" | "แพดกด" | "دکمه_دسته_فشرده" | "زر_اليد_مضغوط" | "כפתור_בקר_לחוץ" | "گیم_پیڈ_دبایا" | "manette_appuyée" | "gamepad_gedrückt" | "геймпад_нажат" =>
4862            {
4863                #[cfg(not(target_arch = "wasm32"))]
4864                {
4865                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4866                    let name = self.arg_str(&args, 1, "");
4867                    let p = parse_pad_button(&name)
4868                        .is_some_and(|b| self.with_pad(i, false, |g| g.just_pressed(b)));
4869                    return Ok(Value::Bool(p));
4870                }
4871                #[cfg(target_arch = "wasm32")]
4872                {
4873                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4874                    let name = self.arg_str(&args, 1, "");
4875                    return Ok(Value::Bool(input_web::button_down(i, &name)));
4876                }
4877            },
4878            // pad_lx(i)/pad_ly(i)/pad_rx(i)/pad_ry(i) → number — stick axes (−1..=1).
4879            "pad_lx" | "手柄左X" | "パッド左X" | "패드왼X" | "แพดซ้ายX" | "آنالوگ_چپ_ایکس" | "عصا_اليسرى_س" | "ג'ויסטיק_שמאל_X" | "بائیں_اسٹک_ایکس" | "manette_axe_gauche_x" | "gamepad_lx" | "геймпад_ось_лево_x" => {
4880                #[cfg(not(target_arch = "wasm32"))]
4881                {
4882                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4883                    return Ok(Value::Number(
4884                        self.with_pad(i, 0.0, |p| p.left_stick.x as f64),
4885                    ));
4886                }
4887                #[cfg(target_arch = "wasm32")]
4888                {
4889                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4890                    return Ok(Value::Number(input_web::axis_lx(i) as f64));
4891                }
4892            },
4893            "pad_ly" | "手柄左Y" | "パッド左Y" | "패드왼Y" | "แพดซ้ายY" | "آنالوگ_چپ_ایگرگ" | "عصا_اليسرى_ص" | "ג'ויסטיק_שמאל_Y" | "بائیں_اسٹک_وائی" | "manette_axe_gauche_y" | "gamepad_ly" | "геймпад_ось_лево_y" => {
4894                #[cfg(not(target_arch = "wasm32"))]
4895                {
4896                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4897                    return Ok(Value::Number(
4898                        self.with_pad(i, 0.0, |p| p.left_stick.y as f64),
4899                    ));
4900                }
4901                #[cfg(target_arch = "wasm32")]
4902                {
4903                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4904                    return Ok(Value::Number(input_web::axis_ly(i) as f64));
4905                }
4906            },
4907            "pad_rx" | "手柄右X" | "パッド右X" | "패드오X" | "แพดขวาX" | "آنالوگ_راست_ایکس" | "عصا_اليمنى_س" | "ג'ויסטיק_ימין_X" | "دائیں_اسٹک_ایکس" | "manette_axe_droit_x" | "gamepad_rx" | "геймпад_ось_право_x" => {
4908                #[cfg(not(target_arch = "wasm32"))]
4909                {
4910                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4911                    return Ok(Value::Number(
4912                        self.with_pad(i, 0.0, |p| p.right_stick.x as f64),
4913                    ));
4914                }
4915                #[cfg(target_arch = "wasm32")]
4916                {
4917                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4918                    return Ok(Value::Number(input_web::axis_rx(i) as f64));
4919                }
4920            },
4921            "pad_ry" | "手柄右Y" | "パッド右Y" | "패드오Y" | "แพดขวาY" | "آنالوگ_راست_ایگرگ" | "عصا_اليمنى_ص" | "ג'ויסטיק_ימין_Y" | "دائیں_اسٹک_وائی" | "manette_axe_droit_y" | "gamepad_ry" | "геймпад_ось_право_y" => {
4922                #[cfg(not(target_arch = "wasm32"))]
4923                {
4924                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4925                    return Ok(Value::Number(
4926                        self.with_pad(i, 0.0, |p| p.right_stick.y as f64),
4927                    ));
4928                }
4929                #[cfg(target_arch = "wasm32")]
4930                {
4931                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4932                    return Ok(Value::Number(input_web::axis_ry(i) as f64));
4933                }
4934            },
4935            // pad_lt(i)/pad_rt(i) → number — analog triggers (0..=1).
4936            "pad_lt" | "手柄左扳机" | "パッド左トリガー" | "패드왼트리거" | "ไกแพดซ้าย" | "ماشه_چپ" | "زناد_اليسار" | "הדק_שמאל" | "بائیں_ٹریگر" | "manette_gâchette_gauche" | "gamepad_lt" | "геймпад_триггер_лево" =>
4937            {
4938                #[cfg(not(target_arch = "wasm32"))]
4939                {
4940                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4941                    return Ok(Value::Number(
4942                        self.with_pad(i, 0.0, |p| p.left_trigger as f64),
4943                    ));
4944                }
4945                #[cfg(target_arch = "wasm32")]
4946                {
4947                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4948                    return Ok(Value::Number(input_web::trigger_lt(i) as f64));
4949                }
4950            },
4951            "pad_rt" | "手柄右扳机" | "パッド右トリガー" | "패드오트리거" | "ไกแพดขวา" | "ماشه_راست" | "زناد_اليمين" | "הדק_ימין" | "دائیں_ٹریگر" | "manette_gâchette_droite" | "gamepad_rt" | "геймпад_триггер_право" =>
4952            {
4953                #[cfg(not(target_arch = "wasm32"))]
4954                {
4955                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4956                    return Ok(Value::Number(
4957                        self.with_pad(i, 0.0, |p| p.right_trigger as f64),
4958                    ));
4959                }
4960                #[cfg(target_arch = "wasm32")]
4961                {
4962                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4963                    return Ok(Value::Number(input_web::trigger_rt(i) as f64));
4964                }
4965            },
4966            // pad_rumble(i, lo, hi) → unit — set rumble motor amplitudes (0..=1).
4967            "pad_rumble" | "手柄震动" | "パッド振動" | "패드진동" | "แพดสั่น" | "لرزش_دسته" | "اهتزاز_اليد" | "רטט_בקר" | "گیم_پیڈ_تھرتھراہٹ" | "vibration_manette" | "gamepad_vibration" | "вибрация_геймпада" =>
4968            {
4969                #[cfg(not(target_arch = "wasm32"))]
4970                {
4971                    use ling_input::backend::InputBackend;
4972                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4973                    let lo = self.arg_num(&args, 1, 0.0)? as f32;
4974                    let hi = self.arg_num(&args, 2, lo as f64)? as f32;
4975                    let mut inp = self.input.borrow_mut();
4976                    if let Some(s) = inp.as_mut() {
4977                        if let Some(dev) = s.sensorium.devices.for_player(i as u8).map(|d| d.id) {
4978                            s.backend.set_rumble(
4979                                dev,
4980                                ling_input::Rumble { low: lo, high: hi, ..Default::default() },
4981                            );
4982                        }
4983                    }
4984                    return Ok(Value::Unit);
4985                }
4986                #[cfg(target_arch = "wasm32")]
4987                return Ok(Value::Unit);
4988            },
4989
4990            // ── set_camera_pos(x, y, z) — move camera to world position ──
4991            "set_camera_pos" | "ตั้งตำแหน่งกล้อง" | "镜坐标" | "カメラ座標" | "카메라좌표" | "تنظیم_موقعیت_دوربین" | "عيّن_موضع_الكاميرا" | "קבע_מיקום_מצלמה" | "کیمرہ_مقام_مقرر_کرو" | "définir_position_caméra" | "kameraposition_setzen" | "задать_позицию_камеры" =>
4992            {
4993                let x = self.arg_num(&args, 0, 0.0)? as f32;
4994                let y = self.arg_num(&args, 1, 0.0)? as f32;
4995                let z = self.arg_num(&args, 2, 0.0)? as f32;
4996                {
4997                    let mut gfx = self.gfx.borrow_mut();
4998                    gfx.camera.tx = x;
4999                    gfx.camera.ty = y;
5000                    gfx.camera.tz = z;
5001                }
5002                #[cfg(not(target_arch = "wasm32"))]
5003                if let Some(audio) = &self.audio {
5004                    audio.set_listener_pos(x, y, z);
5005                }
5006                return Ok(Value::Unit);
5007            },
5008
5009            // ── move_camera(dx, dy, dz) — translate camera by delta ──
5010            "move_camera" => {
5011                let dx = self.arg_num(&args, 0, 0.0)? as f32;
5012                let dy = self.arg_num(&args, 1, 0.0)? as f32;
5013                let dz = self.arg_num(&args, 2, 0.0)? as f32;
5014                let mut gfx = self.gfx.borrow_mut();
5015                gfx.camera.tx += dx;
5016                gfx.camera.ty += dy;
5017                gfx.camera.tz += dz;
5018                return Ok(Value::Unit);
5019            },
5020
5021            // ── set_zdist(d) — set perspective z-offset (field-of-view taper) ──
5022            "set_zdist" | "ตั้งระยะห่าง" | "镜距" | "Z距離設定" | "Z거리설정" | "تنظیم_فاصله_عمق" | "عيّن_مسافة_العمق" | "קבע_מרחק_עומק" | "گہرائی_فاصلہ_مقرر_کرو" | "définir_distance_z" | "z_abstand_setzen" | "задать_дистанцию_z" =>
5023            {
5024                let d = self.arg_num(&args, 0, 5.0)? as f32;
5025                self.gfx.borrow_mut().camera.zdist = d;
5026                return Ok(Value::Unit);
5027            },
5028
5029            // ── capture_mouse() — hide cursor and warp to centre each frame ──
5030            "capture_mouse" | "จับเมาส์" | "捕鼠" | "マウス捕捉" | "마우스잡기" | "ضبط_ماوس" | "امسك_الفأرة" | "לכוד_עכבר" | "ماؤس_پکڑو" | "capturer_souris" | "maus_erfassen" | "захватить_мышь" =>
5031            {
5032                #[cfg(not(target_arch = "wasm32"))]
5033                {
5034                    let mut gfx = self.gfx.borrow_mut();
5035                    gfx.mouse_captured = true;
5036                    gfx.last_mx = f32::NAN;
5037                    if let Some(win) = gfx.window.as_mut() {
5038                        win.set_cursor_visibility(false);
5039                    }
5040                }
5041                return Ok(Value::Unit);
5042            },
5043
5044            // ── release_mouse() — restore cursor and remove clip region ──
5045            "release_mouse" => {
5046                #[cfg(not(target_arch = "wasm32"))]
5047                {
5048                    let mut gfx = self.gfx.borrow_mut();
5049                    gfx.mouse_captured = false;
5050                    gfx.last_mx = f32::NAN;
5051                    if let Some(win) = gfx.window.as_mut() {
5052                        win.set_cursor_visibility(true);
5053                    }
5054                    #[cfg(windows)]
5055                    unsafe {
5056                        // Null releases the clip; reuse the RECT-typed declaration above.
5057                        extern "system" {
5058                            fn ClipCursor(lpRect: *const std::ffi::c_void) -> i32;
5059                        }
5060                        ClipCursor(std::ptr::null());
5061                    }
5062                }
5063                return Ok(Value::Unit);
5064            },
5065
5066            // ── cursor_hide() / cursor_show() — just the OS cursor's visibility,
5067            // no warp-to-centre or clip region (unlike capture_mouse/release_mouse,
5068            // which are for FPS-style look-around). For point-and-click play where
5069            // the cursor still needs to move freely and mouse_x()/mouse_y() still
5070            // need to track real position, just hide the system pointer glyph.
5071            "cursor_hide" => {
5072                #[cfg(not(target_arch = "wasm32"))]
5073                if let Some(win) = self.gfx.borrow_mut().window.as_mut() {
5074                    win.set_cursor_visibility(false);
5075                }
5076                return Ok(Value::Unit);
5077            },
5078            "cursor_show" => {
5079                #[cfg(not(target_arch = "wasm32"))]
5080                if let Some(win) = self.gfx.borrow_mut().window.as_mut() {
5081                    win.set_cursor_visibility(true);
5082                }
5083                return Ok(Value::Unit);
5084            },
5085
5086            // ══════════════════════════════════════════════════════════════════
5087            // 3-D / 4-D DRAWING — camera, lights, depth-sorted geometry
5088            // ══════════════════════════════════════════════════════════════════
5089
5090            // ── set_camera(cry, sry, crx, srx) — store precomputed camera trig ──
5091            // Call once per frame after computing cos/sin of your rotation angles.
5092            "set_camera" | "ตั้งกล้อง" | "设镜" | "设置摄像机" | "カメラ設定" | "카메라설정" | "تنظیم_دوربین" | "عيّن_الكاميرا" | "קבע_מצלמה" | "کیمرہ_مقرر_کرو" | "définir_caméra" | "kamera_setzen" | "задать_камеру" =>
5093            {
5094                let cry = self.arg_num(&args, 0, 1.0)? as f32;
5095                let sry = self.arg_num(&args, 1, 0.0)? as f32;
5096                let crx = self.arg_num(&args, 2, 1.0)? as f32;
5097                let srx = self.arg_num(&args, 3, 0.0)? as f32;
5098                let mut gfx = self.gfx.borrow_mut();
5099                gfx.camera.cry = cry;
5100                gfx.camera.sry = sry;
5101                gfx.camera.crx = crx;
5102                gfx.camera.srx = srx;
5103                return Ok(Value::Unit);
5104            },
5105
5106            // ── set_projection(cx, cy, focal, zdist) — override projection params ──
5107            // Automatically set when the window opens; override only if needed.
5108            "set_projection" | "ตั้งโปรเจกชัน" | "投影" | "投影設定" | "투영설정" | "تنظیم_فرافکنی" | "عيّن_الإسقاط" | "קבע_הטלה" | "پروجیکشن_مقرر_کرو" | "définir_projection" | "projektion_setzen" | "задать_проекцию" =>
5109            {
5110                let cx = self.arg_num(&args, 0, 960.0)? as f32;
5111                let cy = self.arg_num(&args, 1, 540.0)? as f32;
5112                let focal = self.arg_num(&args, 2, 1080.0)? as f32;
5113                let zdist = self.arg_num(&args, 3, 5.0)? as f32;
5114                let mut gfx = self.gfx.borrow_mut();
5115                gfx.camera.cx = cx;
5116                gfx.camera.cy = cy;
5117                gfx.camera.focal = focal;
5118                gfx.camera.zdist = zdist;
5119                return Ok(Value::Unit);
5120            },
5121
5122            // ── mesh_load(path) → handle · loads a glb/gltf (skeleton + skin + animation) ──
5123            "gltf_load" => {
5124                let path = self.arg_str(&args, 0, "");
5125                match ling_physics::gltf::GltfModel::load(&path) {
5126                    Ok(m) => {
5127                        self.gltf_models.borrow_mut().push(m);
5128                        let h = self.gltf_models.borrow().len() - 1;
5129                        return Ok(Value::Number(h as f64));
5130                    }
5131                    Err(e) => {
5132                        eprintln!("mesh_load failed ({path}): {e}");
5133                        return Ok(Value::Number(-1.0));
5134                    }
5135                }
5136            },
5137            // mesh_anim_count(handle) → number of animation clips
5138            "gltf_anim_count" => {
5139                let h = self.arg_num(&args, 0, -1.0)? as i64;
5140                let n = self
5141                    .gltf_models
5142                    .borrow()
5143                    .get(h as usize)
5144                    .map(|m| m.animations.len())
5145                    .unwrap_or(0);
5146                return Ok(Value::Number(n as f64));
5147            },
5148            // mesh_anim_name(handle, i) → clip name
5149            "gltf_anim_name" => {
5150                let h = self.arg_num(&args, 0, -1.0)? as i64;
5151                let i = self.arg_num(&args, 1, 0.0)? as usize;
5152                let s = self
5153                    .gltf_models
5154                    .borrow()
5155                    .get(h as usize)
5156                    .and_then(|m| m.animations.get(i))
5157                    .map(|a| a.name.clone())
5158                    .unwrap_or_default();
5159                return Ok(Value::Str(s));
5160            },
5161            // mesh_anim_dur(handle, i) → clip duration (seconds)
5162            "gltf_anim_dur" => {
5163                let h = self.arg_num(&args, 0, -1.0)? as i64;
5164                let i = self.arg_num(&args, 1, 0.0)? as usize;
5165                let d = self
5166                    .gltf_models
5167                    .borrow()
5168                    .get(h as usize)
5169                    .and_then(|m| m.animations.get(i))
5170                    .map(|a| a.duration)
5171                    .unwrap_or(0.0);
5172                return Ok(Value::Number(d as f64));
5173            },
5174            // mesh_tris(handle) → total triangle count (perf sanity check)
5175            "gltf_tris" => {
5176                let h = self.arg_num(&args, 0, -1.0)? as i64;
5177                let n: usize = self
5178                    .gltf_models
5179                    .borrow()
5180                    .get(h as usize)
5181                    .map(|m| m.meshes.iter().map(|mm| mm.indices.len()).sum::<usize>() / 3)
5182                    .unwrap_or(0);
5183                return Ok(Value::Number(n as f64));
5184            },
5185
5186            // gltf_joint_count(handle) → number of skin joints (bones)
5187            "gltf_joint_count" => {
5188                let h = self.arg_num(&args, 0, -1.0)? as i64;
5189                let n = self
5190                    .gltf_models
5191                    .borrow()
5192                    .get(h as usize)
5193                    .and_then(|m| m.skins.first())
5194                    .map(|s| s.joints.len())
5195                    .unwrap_or(0);
5196                return Ok(Value::Number(n as f64));
5197            },
5198            // gltf_joint_name(handle, j) → bone name (its node's name)
5199            "gltf_joint_name" => {
5200                let h = self.arg_num(&args, 0, -1.0)? as i64;
5201                let j = self.arg_num(&args, 1, 0.0)? as usize;
5202                let models = self.gltf_models.borrow();
5203                let s = models
5204                    .get(h as usize)
5205                    .and_then(|m| {
5206                        m.skins
5207                            .first()
5208                            .and_then(|sk| sk.joints.get(j))
5209                            .and_then(|jt| m.nodes.get(jt.node_idx))
5210                            .map(|n| n.name.clone())
5211                    })
5212                    .unwrap_or_default();
5213                return Ok(Value::Str(s));
5214            },
5215
5216            // ── gltf_draw(handle, ox,oy,oz, scale, yaw) — filled render of a loaded model ──
5217            //   glTF is Y-up / -Z-forward; the engine is Y-down, so we flip Y and Z, then
5218            //   yaw about Y, scale, translate. Per-part colour by mesh name. Lit + depth-queued
5219            //   exactly like draw_mesh, so it shares the camera + z-buffer.
5220            "gltf_draw" => {
5221                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5222                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5223                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5224                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5225                let scale = self.arg_num(&args, 4, 1.0)? as f32;
5226                let yaw = self.arg_num(&args, 5, 0.0)? as f32;
5227                let (sy, cyy) = yaw.sin_cos();
5228                let models = self.gltf_models.borrow();
5229                let model = match models.get(hh as usize) {
5230                    Some(m) => m,
5231                    None => return Ok(Value::Unit),
5232                };
5233                let mut gfx = self.gfx.borrow_mut();
5234                let cp = {
5235                    let c = &gfx.camera;
5236                    ling_gpu::CameraParams {
5237                        cry: c.cry, sry: c.sry, crx: c.crx, srx: c.srx,
5238                        cx: c.cx, cy: c.cy, focal: c.focal, zdist: c.zdist,
5239                        tx: c.tx, ty: c.ty, tz: c.tz,
5240                    }
5241                };
5242                let near = -gfx.camera.zdist + 0.02;
5243                let ambient = gfx.ambient;
5244                for mesh in &model.meshes {
5245                    let nlow = mesh.name.to_lowercase();
5246                    let base: u32 = if nlow.contains("hair") {
5247                        0x7a4a28
5248                    } else if nlow.contains("cloth") || nlow.contains("top") {
5249                        0x4a86e0
5250                    } else if nlow.contains("wing") {
5251                        0xe6ecf5
5252                    } else if nlow.contains("star") {
5253                        0xffd24d
5254                    } else {
5255                        0xf2d6b8
5256                    };
5257                    let nv = mesh.verts.len();
5258                    if nv == 0 {
5259                        continue;
5260                    }
5261                    let mut world = vec![0.0f32; nv * 3];
5262                    for (i, v) in mesh.verts.iter().enumerate() {
5263                        let gx = v.pos.x * scale;
5264                        let gy = -v.pos.y * scale;
5265                        let gz = -v.pos.z * scale;
5266                        let rx = gx * cyy + gz * sy;
5267                        let rz = -gx * sy + gz * cyy;
5268                        world[i * 3] = ox + rx;
5269                        world[i * 3 + 1] = oy + gy;
5270                        world[i * 3 + 2] = oz + rz;
5271                    }
5272                    let mut proj = vec![0.0f32; nv * 3];
5273                    ling_gpu::backend().project_points(&world, &cp, &mut proj);
5274                    let idx = &mesh.indices;
5275                    let nt = idx.len() / 3;
5276                    for t in 0..nt {
5277                        let ia = idx[t * 3] as usize;
5278                        let ib = idx[t * 3 + 1] as usize;
5279                        let ic = idx[t * 3 + 2] as usize;
5280                        if ia >= nv || ib >= nv || ic >= nv {
5281                            continue;
5282                        }
5283                        let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5284                        if (da + db + dc) / 3.0 <= near {
5285                            continue;
5286                        }
5287                        let col = {
5288                            let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5289                            let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5290                            let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5291                            let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5292                            let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5293                            let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5294                            let centroid =
5295                                [(ax + bx + px) / 3.0, (ay + by + py) / 3.0, (az + bz + pz) / 3.0];
5296                            if gfx.flat_shade {
5297                                base
5298                            } else {
5299                                crate::gfx::light::compute_lit_color(
5300                                    base, normal, centroid, &gfx.lights, ambient,
5301                                )
5302                            }
5303                        };
5304                        let depth = (da + db + dc) / 3.0;
5305                        let col = gfx.fog_apply(col, depth);
5306                        gfx.depth_queue.push_triangle_zv(
5307                            col,
5308                            proj[ia * 3], proj[ia * 3 + 1], da,
5309                            proj[ib * 3], proj[ib * 3 + 1], db,
5310                            proj[ic * 3], proj[ic * 3 + 1], dc,
5311                        );
5312                    }
5313                }
5314                return Ok(Value::Unit);
5315            },
5316
5317            // ── gltf_autorig(handle) → synthesize a humanoid skeleton + skin weights ──
5318            "gltf_autorig" => {
5319                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5320                let mut models = self.gltf_models.borrow_mut();
5321                if let Some(m) = models.get_mut(hh as usize) {
5322                    return Ok(Value::Number(m.autorig() as f64));
5323                }
5324                return Ok(Value::Number(0.0));
5325            },
5326
5327            // ── gltf_pose_draw(handle, ox,oy,oz, scale, yaw, poseList) ──
5328            //   Like gltf_draw, but linear-blend-skins the mesh by `poseList` first.
5329            //   poseList = flat XYZ-euler radians, 3 per bone (12 bones → 36 values).
5330            "gltf_pose_draw" => {
5331                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5332                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5333                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5334                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5335                let scale = self.arg_num(&args, 4, 1.0)? as f32;
5336                let yaw = self.arg_num(&args, 5, 0.0)? as f32;
5337                let euler: Vec<f32> = match args.get(6) {
5338                    Some(Value::List(v)) => {
5339                        v.iter().map(|x| self.to_number(x).unwrap_or(0.0) as f32).collect()
5340                    },
5341                    _ => Vec::new(),
5342                };
5343                let (sy, cyy) = yaw.sin_cos();
5344                let models = self.gltf_models.borrow();
5345                let model = match models.get(hh as usize) {
5346                    Some(m) => m,
5347                    None => return Ok(Value::Unit),
5348                };
5349                let skinned = model.skin_local(&euler);
5350                let mut gfx = self.gfx.borrow_mut();
5351                let cp = {
5352                    let c = &gfx.camera;
5353                    ling_gpu::CameraParams {
5354                        cry: c.cry, sry: c.sry, crx: c.crx, srx: c.srx,
5355                        cx: c.cx, cy: c.cy, focal: c.focal, zdist: c.zdist,
5356                        tx: c.tx, ty: c.ty, tz: c.tz,
5357                    }
5358                };
5359                let near = -gfx.camera.zdist + 0.02;
5360                let ambient = gfx.ambient;
5361                for (mi, mesh) in model.meshes.iter().enumerate() {
5362                    let nlow = mesh.name.to_lowercase();
5363                    let base: u32 = if nlow.contains("hair") {
5364                        0x7a4a28
5365                    } else if nlow.contains("cloth") || nlow.contains("top") {
5366                        0x4a86e0
5367                    } else if nlow.contains("wing") {
5368                        0xe6ecf5
5369                    } else if nlow.contains("star") {
5370                        0xffd24d
5371                    } else {
5372                        0xf2d6b8
5373                    };
5374                    let sk = match skinned.get(mi) {
5375                        Some(s) => s,
5376                        None => continue,
5377                    };
5378                    let nv = sk.len();
5379                    if nv == 0 {
5380                        continue;
5381                    }
5382                    let mut world = vec![0.0f32; nv * 3];
5383                    for i in 0..nv {
5384                        let gx = sk[i][0] * scale;
5385                        let gy = -sk[i][1] * scale;
5386                        let gz = -sk[i][2] * scale;
5387                        let rx = gx * cyy + gz * sy;
5388                        let rz = -gx * sy + gz * cyy;
5389                        world[i * 3] = ox + rx;
5390                        world[i * 3 + 1] = oy + gy;
5391                        world[i * 3 + 2] = oz + rz;
5392                    }
5393                    let mut proj = vec![0.0f32; nv * 3];
5394                    ling_gpu::backend().project_points(&world, &cp, &mut proj);
5395                    let idx = &mesh.indices;
5396                    let nt = idx.len() / 3;
5397                    for t in 0..nt {
5398                        let ia = idx[t * 3] as usize;
5399                        let ib = idx[t * 3 + 1] as usize;
5400                        let ic = idx[t * 3 + 2] as usize;
5401                        if ia >= nv || ib >= nv || ic >= nv {
5402                            continue;
5403                        }
5404                        let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5405                        if (da + db + dc) / 3.0 <= near {
5406                            continue;
5407                        }
5408                        let col = {
5409                            let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5410                            let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5411                            let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5412                            let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5413                            let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5414                            let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5415                            let centroid =
5416                                [(ax + bx + px) / 3.0, (ay + by + py) / 3.0, (az + bz + pz) / 3.0];
5417                            if gfx.flat_shade {
5418                                base
5419                            } else {
5420                                crate::gfx::light::compute_lit_color(
5421                                    base, normal, centroid, &gfx.lights, ambient,
5422                                )
5423                            }
5424                        };
5425                        let depth = (da + db + dc) / 3.0;
5426                        let col = gfx.fog_apply(col, depth);
5427                        gfx.depth_queue.push_triangle_zv(
5428                            col,
5429                            proj[ia * 3], proj[ia * 3 + 1], da,
5430                            proj[ib * 3], proj[ib * 3 + 1], db,
5431                            proj[ic * 3], proj[ic * 3 + 1], dc,
5432                        );
5433                    }
5434                }
5435                return Ok(Value::Unit);
5436            },
5437
5438            // ── draw_mesh(pos, idx, ox, oy, oz, scale, mode) ──
5439            //   Native batched triangle mesh. pos = flat [x,y,z,…], idx = flat tri indices.
5440            //   mode 0 = lit with current pen colour; 1 = per-face hue cycle.
5441            //   Vertices are batch-projected via ling-gpu (CPU fallback, or CUDA when the
5442            //   `cuda` feature is on); the per-triangle loop runs natively (not in the
5443            //   interpreter) so dense meshes (imported glTF, grids) stay fast.
5444            "draw_mesh" | "วาดเมช" | "رسم_مش" | "ارسم_شبكة" | "צייר_רשת" | "میش_کھینچو" | "dessiner_maillage" | "netz_zeichnen" | "рисовать_меш" => {
5445                let pos = match args.first() {
5446                    Some(Value::List(v)) => v,
5447                    _ => return Ok(Value::Unit),
5448                };
5449                let idx = match args.get(1) {
5450                    Some(Value::List(v)) => v,
5451                    _ => return Ok(Value::Unit),
5452                };
5453                let ox = self.arg_num(&args, 2, 0.0)? as f32;
5454                let oy = self.arg_num(&args, 3, 0.0)? as f32;
5455                let oz = self.arg_num(&args, 4, 0.0)? as f32;
5456                let scale = self.arg_num(&args, 5, 1.0)? as f32;
5457                let mode = self.arg_num(&args, 6, 0.0)? as i64;
5458                let nv = pos.len() / 3;
5459                if nv == 0 {
5460                    return Ok(Value::Unit);
5461                }
5462                let mut world = vec![0.0f32; nv * 3];
5463                for i in 0..nv {
5464                    world[i * 3] = ox + self.to_number(&pos[i * 3]).unwrap_or(0.0) as f32 * scale;
5465                    world[i * 3 + 1] =
5466                        oy + self.to_number(&pos[i * 3 + 1]).unwrap_or(0.0) as f32 * scale;
5467                    world[i * 3 + 2] =
5468                        oz + self.to_number(&pos[i * 3 + 2]).unwrap_or(0.0) as f32 * scale;
5469                }
5470                let mut gfx = self.gfx.borrow_mut();
5471                let cp = {
5472                    let c = &gfx.camera;
5473                    ling_gpu::CameraParams {
5474                        cry: c.cry,
5475                        sry: c.sry,
5476                        crx: c.crx,
5477                        srx: c.srx,
5478                        cx: c.cx,
5479                        cy: c.cy,
5480                        focal: c.focal,
5481                        zdist: c.zdist,
5482                        tx: c.tx,
5483                        ty: c.ty,
5484                        tz: c.tz,
5485                    }
5486                };
5487                let near = -gfx.camera.zdist + 0.02;
5488                let base = gfx.color;
5489                let ambient = gfx.ambient;
5490                let mut proj = vec![0.0f32; nv * 3]; // (sx, sy, depth) per vertex
5491                ling_gpu::backend().project_points(&world, &cp, &mut proj);
5492                let nt = idx.len() / 3;
5493                for t in 0..nt {
5494                    let ia = self.to_number(&idx[t * 3]).unwrap_or(0.0) as usize;
5495                    let ib = self.to_number(&idx[t * 3 + 1]).unwrap_or(0.0) as usize;
5496                    let ic = self.to_number(&idx[t * 3 + 2]).unwrap_or(0.0) as usize;
5497                    if ia >= nv || ib >= nv || ic >= nv {
5498                        continue;
5499                    }
5500                    let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5501                    if (da + db + dc) / 3.0 <= near {
5502                        continue;
5503                    } // near-plane cull (centroid)
5504                    let col = if mode == 1 {
5505                        let h = t as f32 * 0.6;
5506                        let r = ((h.sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5507                        let g = (((h + 2.094).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5508                        let b = (((h + 4.189).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5509                        (r << 16) | (g << 8) | b
5510                    } else {
5511                        let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5512                        let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5513                        let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5514                        let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5515                        let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5516                        let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5517                        let centroid = [
5518                            (ax + bx + px) / 3.0,
5519                            (ay + by + py) / 3.0,
5520                            (az + bz + pz) / 3.0,
5521                        ];
5522                        if gfx.flat_shade {
5523                            base
5524                        } else {
5525                            crate::gfx::light::compute_lit_color(
5526                                base,
5527                                normal,
5528                                centroid,
5529                                &gfx.lights,
5530                                ambient,
5531                            )
5532                        }
5533                    };
5534                    let depth = (da + db + dc) / 3.0;
5535                    let col = gfx.fog_apply(col, depth);
5536                    // True per-vertex depth so the z-buffer resolves mesh
5537                    // self-occlusion (when depth_test is off, the flush ignores
5538                    // z and uses the screen x/y exactly as before).
5539                    gfx.depth_queue.push_triangle_zv(
5540                        col,
5541                        proj[ia * 3],
5542                        proj[ia * 3 + 1],
5543                        da,
5544                        proj[ib * 3],
5545                        proj[ib * 3 + 1],
5546                        db,
5547                        proj[ic * 3],
5548                        proj[ic * 3 + 1],
5549                        dc,
5550                    );
5551                }
5552                return Ok(Value::Unit);
5553            },
5554
5555            // ── add_light(x, y, z, r, g, b, intensity, radius) ──
5556            // Adds a point light in world space.  r/g/b in [0..1].
5557            // radius == 0 → no distance falloff.
5558            "add_light" | "เพิ่มแสง" | "加灯" | "ライト追加" | "조명추가" | "افزودن_نور" | "أضف_ضوء" | "הוסף_אור" | "روشنی_شامل_کرو" | "ajouter_lumière" | "licht_hinzufügen" | "добавить_свет" =>
5559            {
5560                let x = self.arg_num(&args, 0, 0.0)? as f32;
5561                let y = self.arg_num(&args, 1, -3.0)? as f32;
5562                let z = self.arg_num(&args, 2, 3.0)? as f32;
5563                let mut r = self.arg_num(&args, 3, 1.0)? as f32;
5564                let mut g = self.arg_num(&args, 4, 1.0)? as f32;
5565                let mut b = self.arg_num(&args, 5, 1.0)? as f32;
5566                // Forgive 0-255 colour values: if any channel is clearly > 1,
5567                // treat the triple as 0-255 and normalise. Keeps 0-1 callers exact.
5568                if r > 1.5 || g > 1.5 || b > 1.5 {
5569                    r /= 255.0;
5570                    g /= 255.0;
5571                    b /= 255.0;
5572                }
5573                let intensity = self.arg_num(&args, 6, 1.0)? as f32;
5574                let radius = self.arg_num(&args, 7, 0.0)? as f32;
5575                self.gfx
5576                    .borrow_mut()
5577                    .lights
5578                    .push(Light { x, y, z, r, g, b, intensity, radius });
5579                return Ok(Value::Unit);
5580            },
5581
5582            // ── clear_lights() — remove all lights ──
5583            "clear_lights" | "ล้างแสง" | "清灯" | "ライト消去" | "조명초기화" | "پاک‌کردن_نورها" | "امسح_الأضواء" | "נקה_אורות" | "روشنیاں_صاف_کرو" | "effacer_lumières" | "lichter_löschen" | "очистить_свет" =>
5584            {
5585                self.gfx.borrow_mut().lights.clear();
5586                return Ok(Value::Unit);
5587            },
5588
5589            // ── set_material(key, value) — configure LingMaterial field ──
5590            // Activates the material BSDF for subsequent polygon/triangle draws.
5591            // Keys (string): "albedo" "roughness" "metallic" "emission"
5592            //   "emission_strength" "specular" "specular_tint" "subsurface"
5593            //   "subsurface_color" "clearcoat" "clearcoat_roughness"
5594            //   "transmission" "ior" "iridescence" "sheen" "anisotropy"
5595            //   "anisotropy_angle" "toon_bands" "shadow_softness"
5596            //   "outline_px" "outline_color" "highlight_color"
5597            // Value: number (or packed 0xRRGGBB for colour fields)
5598            "set_material" | "ตั้งวัสดุ" | "设置材质" | "マテリアル設定" | "재질설정" | "تنظیم_متریال" | "عيّن_المادة" | "קבע_חומר" | "میٹریل_مقرر_کرو" =>
5599            {
5600                let key = self.arg_str(&args, 0, "");
5601                let val = self.arg_num(&args, 1, 0.0)?;
5602                let mut gfx = self.gfx.borrow_mut();
5603                let mat = gfx
5604                    .material
5605                    .get_or_insert_with(crate::gfx::LingMaterial::default);
5606                match key.as_str() {
5607                    "albedo" => mat.albedo = val as u32,
5608                    "roughness" => mat.roughness = val as f32,
5609                    "metallic" => mat.metallic = val as f32,
5610                    "emission" => mat.emission = val as u32,
5611                    "emission_strength" => mat.emission_strength = val as f32,
5612                    "specular" => mat.specular = val as f32,
5613                    "specular_tint" => mat.specular_tint = val as f32,
5614                    "subsurface" => mat.subsurface = val as f32,
5615                    "subsurface_color" => mat.subsurface_color = val as u32,
5616                    "clearcoat" => mat.clearcoat = val as f32,
5617                    "clearcoat_roughness" => mat.clearcoat_roughness = val as f32,
5618                    "transmission" => mat.transmission = val as f32,
5619                    "ior" => mat.ior = val as f32,
5620                    "iridescence" => mat.iridescence = val as f32,
5621                    "sheen" => mat.sheen = val as f32,
5622                    "anisotropy" => mat.anisotropy = val as f32,
5623                    "anisotropy_angle" => mat.anisotropy_angle = val as f32,
5624                    "toon_bands" => mat.toon_bands = val as u32,
5625                    "shadow_softness" => mat.shadow_softness = val as f32,
5626                    "outline_px" => mat.outline_px = val as f32,
5627                    "outline_color" => mat.outline_color = val as u32,
5628                    "highlight_color" => mat.highlight_color = val as u32,
5629                    _ => {},
5630                }
5631                return Ok(Value::Unit);
5632            },
5633
5634            // ── reset_material() — disable material override ──
5635            // After this call, draws use the legacy compute_lit_color_linear path.
5636            "reset_material" | "รีเซ็ตวัสดุ" | "重置材质" | "マテリアルリセット" | "재질초기화" | "بازنشانی_متریال" | "أعد_ضبط_المادة" | "אפס_חומר" | "میٹریل_ری_سیٹ" =>
5637            {
5638                self.gfx.borrow_mut().material = None;
5639                return Ok(Value::Unit);
5640            },
5641
5642            // ── toon_outlines(thickness, color, threshold) ──
5643            // Enable vector-smooth ink outlines on depth discontinuities.
5644            //   thickness  — ink-line half-width in pixels (0 = off, 1.5 = anime default)
5645            //   color      — 0xRRGGBB ink colour (default black = 0)
5646            //   threshold  — depth delta that triggers an edge (0.05 recommended)
5647            "toon_outlines"
5648            | "ตั้งเส้นขอบการ์ตูน"
5649            | "卡通轮廓"
5650            | "トゥーンアウトライン"
5651            | "툰아웃라인" | "خطوط_کارتونی" | "حدود_كرتونية" | "קווי_מתאר_מצוירים" | "ٹون_آؤٹ_لائنز" => {
5652                let px = self.arg_num(&args, 0, 0.0)? as f32;
5653                let color = self.arg_num(&args, 1, 0.0)? as u32;
5654                let thresh = self.arg_num(&args, 2, 0.05)? as f32;
5655                let mut gfx = self.gfx.borrow_mut();
5656                gfx.toon.outline_px = px;
5657                gfx.toon.outline_color = color;
5658                gfx.toon.outline_thresh = thresh;
5659                return Ok(Value::Unit);
5660            },
5661
5662            // ── tone_stop(t, value) ──
5663            // Add a stop to the tone ramp.
5664            //   t      — input luminance position [0..1]
5665            //   value  — output brightness [0..1]
5666            // Stops are automatically sorted; call tone_ramp_reset() first to clear.
5667            "tone_stop" | "ตั้งจุดโทน" | "色调停止" | "トーンストップ" | "톤스톱" | "نقطه_توقف_تن_رنگ" | "نقطة_توقف_اللون" | "נקודת_עצירת_גוון" | "ٹون_اسٹاپ" =>
5668            {
5669                let t = self.arg_num(&args, 0, 0.0)? as f32;
5670                let val = self.arg_num(&args, 1, 1.0)? as f32;
5671                let mut gfx = self.gfx.borrow_mut();
5672                gfx.toon.ramp.stops.push(crate::gfx::toon::ToneStop {
5673                    t: t.clamp(0.0, 1.0),
5674                    value: val.clamp(0.0, 1.0),
5675                });
5676                gfx.toon
5677                    .ramp
5678                    .stops
5679                    .sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
5680                return Ok(Value::Unit);
5681            },
5682
5683            // ── tone_smooth(enabled) ──
5684            // 0 = hard cel snap between stops (default); 1 = smooth gradient lerp.
5685            "tone_smooth" | "ตั้งโทนนุ่ม" | "色调平滑" | "トーンスムーズ" | "톤스무스" | "تن_رنگ_نرم" | "تدرج_لون_ناعم" | "גוון_חלק" | "ٹون_ہموار" =>
5686            {
5687                let v = self.arg_num(&args, 0, 0.0)? as f32;
5688                self.gfx.borrow_mut().toon.ramp.smooth = v > 0.5;
5689                return Ok(Value::Unit);
5690            },
5691
5692            // ── tone_bezier(y1, y2) ──
5693            // Apply a cubic Bézier remap to the input luminance before stop lookup.
5694            //   y1, y2 — control-point y-values (identity: y1=0.333 y2=0.667)
5695            //   0 args or tone_bezier(0, 0)  → ease-in (shadow-heavy)
5696            //   tone_bezier(1, 1)            → ease-out (highlight-heavy)
5697            //   tone_bezier(0.1, 0.9)        → S-curve (smooth both ends)
5698            //   tone_bezier_off()            → disable (back to linear)
5699            "tone_bezier" | "ตั้งโทนเบซิเยร์" | "色调贝塞尔" | "トーンベジェ" | "톤베지어" | "تن_رنگ_بزیه" | "تدرج_لون_بيزيه" | "גוון_בזייה" | "بیزیئر_ٹون" =>
5700            {
5701                let y1 = self.arg_num(&args, 0, 1.0 / 3.0)? as f32;
5702                let y2 = self.arg_num(&args, 1, 2.0 / 3.0)? as f32;
5703                self.gfx.borrow_mut().toon.ramp.bezier = Some([y1, y2]);
5704                return Ok(Value::Unit);
5705            },
5706
5707            // ── tone_bezier_off() — disable Bézier remap ──
5708            "tone_bezier_off"
5709            | "ปิดโทนเบซิเยร์"
5710            | "关闭色调贝塞尔"
5711            | "トーンベジェオフ"
5712            | "톤베지어끄기" | "خاموش‌کردن_بزیه" | "إيقاف_تدرج_بيزيه" | "כבה_גוון_בזייה" | "بیزیئر_ٹون_بند" => {
5713                self.gfx.borrow_mut().toon.ramp.bezier = None;
5714                return Ok(Value::Unit);
5715            },
5716
5717            // ── tone_ramp_reset() — restore default 3-band cel ramp ──
5718            "tone_ramp_reset"
5719            | "รีเซ็ตการไล่โทน"
5720            | "重置色调渐变"
5721            | "トーンランプリセット"
5722            | "톤램프리셋" | "بازنشانی_شیب_تن_رنگ" | "أعد_ضبط_تدرج_اللون" | "אפס_שיפוע_גוון" | "ٹون_ریمپ_ری_سیٹ" => {
5723                self.gfx.borrow_mut().toon.ramp = crate::gfx::toon::ToneRamp::default();
5724                return Ok(Value::Unit);
5725            },
5726
5727            // ── tone_ramp_clear() — clear all stops (build your own ramp) ──
5728            "tone_ramp_clear"
5729            | "ล้างการไล่โทน"
5730            | "清除色调渐变"
5731            | "トーンランプクリア"
5732            | "톤램프클리어" | "پاک‌کردن_شیب_تن_رنگ" | "امسح_تدرج_اللون" | "נקה_שיפוע_גוון" | "ٹون_ریمپ_صاف" => {
5733                self.gfx.borrow_mut().toon.ramp.stops.clear();
5734                return Ok(Value::Unit);
5735            },
5736
5737            // ── tone_soft(soft, sheen) — band-edge softness + highlight sheen ──
5738            //   soft  [0..1] — fraction of each band gap that blends smoothly
5739            //                  across the boundary (0 = crisp cel, ~0.3 = soft
5740            //                  Wind Waker shadow edges). Default 0.32.
5741            //   sheen [0..1] — bright pixels keep their smooth gradient instead
5742            //                  of being quantised (clean specular/rim sheen
5743            //                  rather than scratchy banded highlights). 0.65.
5744            "tone_soft" | "โทนขอบนุ่ม" | "色调柔边" | "トーンソフト" | "톤소프트" | "تن_رنگ_لبه‌نرم" | "تدرج_ناعم_الحواف" | "גוון_קצה_רך" | "نرم_کنارہ_ٹون" | "tonalité_douce" | "weicher_ton" | "мягкий_тон" => {
5745                let s = self.arg_num(&args, 0, 0.32)? as f32;
5746                let sh = self.arg_num(&args, 1, 0.65)? as f32;
5747                let mut gfx = self.gfx.borrow_mut();
5748                gfx.toon.ramp.soft = s.clamp(0.0, 1.0);
5749                gfx.toon.ramp.sheen = sh.clamp(0.0, 1.0);
5750                return Ok(Value::Unit);
5751            },
5752
5753            // ── set_ssao(strength, radius_px, zrange) — ambient occlusion ──
5754            // Depth-buffer contact shading: soft darkening in corners/under
5755            // objects, computed half-res + smoothed (no grain). Needs
5756            // set_depth_test(1). strength 0 disables. Defaults (0.35, 6, 12).
5757            "set_ssao" | "ตั้งเงาสัมผัส" | "环境光遮蔽" | "アンビエントオクルージョン"
5758            | "앰비언트오클루전" | "تنظیم_انسداد_محیطی" | "عيّن_تظليل_محيطي" | "קבע_הצללה_סביבתית" | "ایس_ایس_اے_او_مقرر_کرو" | "définir_ssao" | "ssao_setzen" | "задать_ssao" => {
5759                let s = self.arg_num(&args, 0, 0.35)? as f32;
5760                let r = self.arg_num(&args, 1, 6.0)? as f32;
5761                let z = self.arg_num(&args, 2, 12.0)? as f32;
5762                let mut gfx = self.gfx.borrow_mut();
5763                gfx.toon.ao_strength = s.clamp(0.0, 1.0);
5764                gfx.toon.ao_radius = r.max(1.0);
5765                gfx.toon.ao_range = z.max(0.01);
5766                return Ok(Value::Unit);
5767            },
5768
5769            // ── set_fxaa(on) — FXAA-lite screen-space edge anti-aliasing ──
5770            // Softens polygon stair-steps and ink-line jaggies over the whole
5771            // frame; flat fills are untouched. Applied last in the present
5772            // post-chain. (set_antialias smooths wireframe STROKES; this pass
5773            // smooths the composited IMAGE.)
5774            "set_fxaa" | "ลบรอยหยัก" | "屏幕抗锯齿" | "画面アンチエイリアス" | "화면안티앨리어싱" | "تنظیم_ضدلبه‌دندانه_سریع" | "عيّن_مضاد_التسنن_السريع" | "קבע_החלקת_מסך" | "ایف_ایکس_اے_اے_مقرر_کرو" | "définir_fxaa" | "fxaa_setzen" | "задать_fxaa" => {
5775                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
5776                self.gfx.borrow_mut().toon.fxaa = on;
5777                return Ok(Value::Unit);
5778            },
5779
5780            // ── set_bloom(strength, threshold) — soft HDR-style glow ──
5781            // Bright pixels (rim sheen, emissive, additive FX) bleed a soft
5782            // quarter-res glow — the "HDR material" feel for toon/vector art.
5783            // strength 0 disables; threshold = luminance cutoff [0..1].
5784            "set_bloom" | "ตั้งบลูม" | "泛光" | "ブルーム" | "블룸" | "تنظیم_درخشش" | "عيّن_التوهج" | "קבע_זוהר" | "بلوم_مقرر_کرو" | "définir_bloom" | "bloom_setzen" | "задать_блум" => {
5785                let s = self.arg_num(&args, 0, 0.45)? as f32;
5786                let t = self.arg_num(&args, 1, 0.74)? as f32;
5787                let mut gfx = self.gfx.borrow_mut();
5788                gfx.toon.bloom_strength = s.max(0.0);
5789                gfx.toon.bloom_thresh = t.clamp(0.0, 0.99);
5790                return Ok(Value::Unit);
5791            },
5792
5793            // ── shadow_smooth(softness) [compat] → tone_smooth + tone_bezier ──
5794            // Deprecated: use tone_smooth + tone_bezier instead.
5795            "shadow_smooth" | "ตั้งเงานุ่ม" | "柔化阴影" | "影ソフト" | "그림자부드럽게" | "سایه_نرم" | "ظل_ناعم" | "צל_חלק" | "نرم_سایہ" =>
5796            {
5797                let s = self.arg_num(&args, 0, 0.0)? as f32;
5798                let mut gfx = self.gfx.borrow_mut();
5799                gfx.toon.ramp.smooth = s > 0.05;
5800                if s > 0.05 {
5801                    let y1 = (0.333 + s * 0.2).clamp(0.0, 1.0);
5802                    let y2 = (0.667 - s * 0.2).clamp(0.0, 1.0);
5803                    gfx.toon.ramp.bezier = Some([y1, y2]);
5804                } else {
5805                    gfx.toon.ramp.bezier = None;
5806                }
5807                return Ok(Value::Unit);
5808            },
5809
5810            // ── toon_highlight [compat] — no-op, use tone_stop instead ──
5811            "toon_highlight"
5812            | "ตั้งไฮไลท์การ์ตูน"
5813            | "卡通高光"
5814            | "トゥーンハイライト"
5815            | "툰하이라이트" | "هایلایت_کارتونی" | "إبراز_كرتوني" | "הדגשה_מצוירת" | "ٹون_ہائی_لائٹ" => {
5816                // Remap as a lit-band brightness boost: adds a stop near the highlight threshold.
5817                let _strength = self.arg_num(&args, 0, 0.0)? as f32;
5818                let _thresh = self.arg_num(&args, 2, 0.78)? as f32;
5819                // No-op: configure via tone_stop() for precise control.
5820                return Ok(Value::Unit);
5821            },
5822
5823            // ── set_ambient(v) — ambient light level [0..1] ──
5824            "set_ambient" | "ตั้งแสงรอบข้าง" | "环境光" | "環境光設定" | "환경광설정" | "تنظیم_نور_محیطی" | "عيّن_الإضاءة_المحيطة" | "קבע_תאורה_סביבתית" | "ماحولیاتی_روشنی_مقرر_کرو" | "définir_ambiante" | "umgebungslicht_setzen" | "задать_фон" =>
5825            {
5826                let v = self.arg_num(&args, 0, 0.15)? as f32;
5827                self.gfx.borrow_mut().ambient = v;
5828                return Ok(Value::Unit);
5829            },
5830
5831            // ── set_fog(r,g,b, start, end) — distance fog toward (r,g,b).
5832            //    triangles/lines fade from `start`..`end` camera depth. end<=0 = off.
5833            "set_fog" | "ตั้งหมอก" | "雾" | "霧設定" | "안개설정" | "تنظیم_مه" | "عيّن_الضباب" | "קבע_ערפל" | "دھند_مقرر_کرو" => {
5834                let r = self.arg_num(&args, 0, 0.0)?.clamp(0.0, 255.0) as u32;
5835                let g = self.arg_num(&args, 1, 0.0)?.clamp(0.0, 255.0) as u32;
5836                let b = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 255.0) as u32;
5837                let start = self.arg_num(&args, 3, 0.0)? as f32;
5838                let end = self.arg_num(&args, 4, 0.0)? as f32;
5839                let mut gfx = self.gfx.borrow_mut();
5840                gfx.fog_color = (r << 16) | (g << 8) | b;
5841                gfx.fog_start = start;
5842                gfx.fog_end = end;
5843                return Ok(Value::Unit);
5844            },
5845
5846            // ── วาดสามเหลี่ยม3มิติ(ax,ay,az, bx,by,bz, cx,cy,cz) ──
5847            // Computes lighting from world-space normal + active lights (cel shading),
5848            // projects via the stored camera, and pushes to the depth queue.
5849            "วาดสามเหลี่ยม3มิติ" | "draw_triangle_3d" | "triangle3d" | "رسم_مثلث_سه‌بعدی" | "ارسم_مثلثا_ثلاثي_الأبعاد" | "צייר_משולש_תלת_ממדי" | "تھری_ڈی_مثلث_کھینچو" =>
5850            {
5851                let ax = self.arg_num(&args, 0, 0.0)? as f32;
5852                let ay = self.arg_num(&args, 1, 0.0)? as f32;
5853                let az = self.arg_num(&args, 2, 0.0)? as f32;
5854                let bx = self.arg_num(&args, 3, 0.0)? as f32;
5855                let by = self.arg_num(&args, 4, 0.0)? as f32;
5856                let bz = self.arg_num(&args, 5, 0.0)? as f32;
5857                let cx = self.arg_num(&args, 6, 0.0)? as f32;
5858                let cy = self.arg_num(&args, 7, 0.0)? as f32;
5859                let cz = self.arg_num(&args, 8, 0.0)? as f32;
5860
5861                let mut gfx = self.gfx.borrow_mut();
5862
5863                // Mesh capture: record raw local coords + pen colour, skip submit.
5864                if gfx.mesh_capture.is_some() {
5865                    let col = gfx.color;
5866                    gfx.mesh_capture
5867                        .as_mut()
5868                        .unwrap()
5869                        .push(([ax, ay, az, bx, by, bz, cx, cy, cz], col));
5870                    return Ok(Value::Unit);
5871                }
5872
5873                gfx.submit_triangle(ax, ay, az, bx, by, bz, cx, cy, cz);
5874                return Ok(Value::Unit);
5875            },
5876
5877            // ── เริ่มอบเมช() — begin capturing 3-D triangles into a display list ──
5878            "เริ่มอบเมช" | "mesh_bake_begin" | "شروع_پخت_مش" | "ابدأ_خبز_الشبكة" | "התחל_אפיית_רשת" | "میش_بیک_شروع" => {
5879                self.gfx.borrow_mut().mesh_capture = Some(Vec::new());
5880                return Ok(Value::Unit);
5881            },
5882
5883            // ── เมชแคชรับ(key) — keyed display-list cache lookup (-1 = miss) ──
5884            "เมชแคชรับ" | "mesh_cache_get" | "دریافت_کش_مش" | "اجلب_مخبأ_الشبكة" | "קבל_מטמון_רשת" | "میش_کیش_حاصل_کرو" => {
5885                let key = self.arg_num(&args, 0, 0.0)? as i64;
5886                let h = self.gfx.borrow().mesh_cache.get(&key).copied();
5887                return Ok(Value::Number(h.map(|x| x as f64).unwrap_or(-1.0)));
5888            },
5889
5890            // ── เมชแคชตั้ง(key, handle) — store a baked mesh under key (bounded) ──
5891            "เมชแคชตั้ง" | "mesh_cache_put" | "ذخیره_در_کش_مش" | "ضع_في_مخبأ_الشبكة" | "שמור_במטמון_רשת" | "میش_کیش_رکھو" => {
5892                let key = self.arg_num(&args, 0, 0.0)? as i64;
5893                let h = self.arg_num(&args, 1, 0.0)? as usize;
5894                let mut gfx = self.gfx.borrow_mut();
5895                const CAP: usize = 256;
5896                if gfx.mesh_cache.len() >= CAP {
5897                    let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
5898                    gfx.mesh_cache.clear();
5899                    for id in evict {
5900                        if id < gfx.meshes.len() {
5901                            gfx.meshes[id].clear();
5902                            gfx.mesh_free.push(id);
5903                        }
5904                    }
5905                }
5906                gfx.mesh_cache.insert(key, h);
5907                return Ok(Value::Unit);
5908            },
5909
5910            // ── เมชแคชล้าง() — drop the keyed cache (e.g. on level change) ──
5911            "เมชแคชล้าง" | "mesh_cache_clear" | "پاک‌کردن_کش_مش" | "امسح_مخبأ_الشبكة" | "נקה_מטמון_רשת" | "میش_کیش_صاف" => {
5912                let mut gfx = self.gfx.borrow_mut();
5913                let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
5914                gfx.mesh_cache.clear();
5915                for id in evict {
5916                    if id < gfx.meshes.len() {
5917                        gfx.meshes[id].clear();
5918                        gfx.mesh_free.push(id);
5919                    }
5920                }
5921                return Ok(Value::Unit);
5922            },
5923
5924            // ── จบอบเมช() — bake captured triangles, return mesh handle ──
5925            "จบอบเมช" | "mesh_bake_end" | "پایان_پخت_مش" | "أنهِ_خبز_الشبكة" | "סיים_אפיית_רשת" | "میش_بیک_ختم" => {
5926                let mut gfx = self.gfx.borrow_mut();
5927                let tris = gfx.mesh_capture.take().unwrap_or_default();
5928                let id = gfx.mesh_register(tris);
5929                return Ok(Value::Number(id as f64));
5930            },
5931
5932            // ── วาดอบเมช[สี](id, ox,oy,oz, rx,ry,rz, ux,uy,uz, s) — draw a baked mesh ──
5933            //   วาดอบเมช: current pen colour (tinted glyphs)
5934            //   วาดอบเมชสี: per-triangle baked colour (multi-colour models)
5935            "วาดอบเมช" | "mesh_bake_draw" | "วาดอบเมชสี" | "mesh_bake_draw_col" | "رسم_مش_پخته" | "ارسم_شبكة_مخبوزة" | "צייר_רשת_אפויה" | "بیکڈ_میش_کھینچو" =>
5936            {
5937                let baked_col = matches!(name, "วาดอบเมชสี" | "mesh_bake_draw_col");
5938                let id = self.arg_num(&args, 0, 0.0)? as usize;
5939                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5940                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5941                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5942                let rx = self.arg_num(&args, 4, 1.0)? as f32;
5943                let ry = self.arg_num(&args, 5, 0.0)? as f32;
5944                let rz = self.arg_num(&args, 6, 0.0)? as f32;
5945                let ux = self.arg_num(&args, 7, 0.0)? as f32;
5946                let uy = self.arg_num(&args, 8, 1.0)? as f32;
5947                let uz = self.arg_num(&args, 9, 0.0)? as f32;
5948                let s = self.arg_num(&args, 10, 1.0)? as f32;
5949                self.gfx
5950                    .borrow_mut()
5951                    .mesh_draw(id, ox, oy, oz, rx, ry, rz, ux, uy, uz, s, baked_col);
5952                return Ok(Value::Unit);
5953            },
5954
5955            // ── draw_quad_3d / draw_pent_3d / draw_hex_3d / draw_polygon_3d ──
5956            // Fan-triangulate convex n-gons.  Lighting, near-plane clip, fog, and
5957            // Gouraud shading all mirror draw_triangle_3d exactly.
5958            "draw_quad_3d"
5959            | "quad3d"
5960            | "วาดสี่เหลี่ยม3มิติ"
5961            | "draw_pent_3d"
5962            | "pent3d"
5963            | "วาดห้าเหลี่ยม3มิติ"
5964            | "draw_hex_3d"
5965            | "hex3d"
5966            | "วาดหกเหลี่ยม3มิติ"
5967            | "draw_polygon_3d"
5968            | "polygon3d"
5969            | "วาดรูปหลายเหลี่ยม3มิติ" | "رسم_چهارضلعی_سه‌بعدی" | "ارسم_رباعيا_ثلاثي_الأبعاد" | "צייר_מרובע_תלת_ממדי" | "تھری_ڈی_چوکور_کھینچو" => {
5970                // Collect (wx, wy, wz) triples from args or list
5971                let mut wxs: [f32; 8] = [0.0; 8];
5972                let mut wys: [f32; 8] = [0.0; 8];
5973                let mut wzs: [f32; 8] = [0.0; 8];
5974                let n_verts;
5975
5976                if args.len() == 1 {
5977                    // draw_polygon_3d([x0,y0,z0, x1,y1,z1, ...])
5978                    let list = match &args[0] {
5979                        Value::List(l) => l.clone(),
5980                        _ => {
5981                            return Err(EvalErr::from("draw_polygon_3d: expected list".to_string()))
5982                        },
5983                    };
5984                    let coords: Vec<f32> = list
5985                        .iter()
5986                        .map(|v| match v {
5987                            Value::Number(n) => *n as f32,
5988                            _ => 0.0,
5989                        })
5990                        .collect();
5991                    n_verts = (coords.len() / 3).min(8);
5992                    for i in 0..n_verts {
5993                        wxs[i] = coords[i * 3];
5994                        wys[i] = coords[i * 3 + 1];
5995                        wzs[i] = coords[i * 3 + 2];
5996                    }
5997                } else {
5998                    // draw_quad/pent/hex_3d(x0,y0,z0, x1,y1,z1, ...)
5999                    n_verts = (args.len() / 3).min(8);
6000                    for i in 0..n_verts {
6001                        wxs[i] = self.arg_num(&args, i * 3, 0.0)? as f32;
6002                        wys[i] = self.arg_num(&args, i * 3 + 1, 0.0)? as f32;
6003                        wzs[i] = self.arg_num(&args, i * 3 + 2, 0.0)? as f32;
6004                    }
6005                }
6006                if n_verts < 3 {
6007                    return Ok(Value::Unit);
6008                }
6009
6010                let mut gfx = self.gfx.borrow_mut();
6011
6012                // Mesh capture: fan-triangulate and record raw local coords +
6013                // pen colour, exactly like วาดสามเหลี่ยม3มิติ. Quads used to
6014                // fall through here and bake EMPTY display lists — the 3-D
6015                // glyph fonts (letter pickups) are built from draw_quad_3d,
6016                // which made every baked glyph invisible.
6017                if gfx.mesh_capture.is_some() {
6018                    let col = gfx.color;
6019                    let cap = gfx.mesh_capture.as_mut().unwrap();
6020                    for i in 1..n_verts - 1 {
6021                        cap.push((
6022                            [
6023                                wxs[0], wys[0], wzs[0],
6024                                wxs[i], wys[i], wzs[i],
6025                                wxs[i + 1], wys[i + 1], wzs[i + 1],
6026                            ],
6027                            col,
6028                        ));
6029                    }
6030                    return Ok(Value::Unit);
6031                }
6032
6033                // Face normal from first triangle of the fan
6034                let normal = crate::gfx::poly::face_normal(
6035                    wxs[0], wys[0], wzs[0], wxs[1], wys[1], wzs[1], wxs[2], wys[2], wzs[2],
6036                );
6037
6038                // Per-vertex lit colours
6039                let mut wcs: [u32; 8] = [0; 8];
6040                if gfx.flat_shade {
6041                    let c = gfx.color;
6042                    for wc in wcs.iter_mut().take(n_verts) {
6043                        *wc = c;
6044                    }
6045                } else if let Some(ref mat) = gfx.material.clone() {
6046                    let cam = [gfx.camera.cx, gfx.camera.cy, gfx.camera.zdist];
6047                    let lights: Vec<_> = gfx.lights.clone();
6048                    let ambient = gfx.ambient;
6049                    for i in 0..n_verts {
6050                        let v = [wxs[i], wys[i], wzs[i]];
6051                        let vd = [cam[0] - v[0], cam[1] - v[1], cam[2] - v[2]];
6052                        wcs[i] = crate::gfx::material::shade(mat, normal, vd, v, &lights, ambient);
6053                    }
6054                } else {
6055                    let base = gfx.color;
6056                    let lights: Vec<_> = gfx.lights.clone();
6057                    let ambient = gfx.ambient;
6058                    for i in 0..n_verts {
6059                        wcs[i] = crate::gfx::light::compute_lit_color_linear(
6060                            base,
6061                            normal,
6062                            [wxs[i], wys[i], wzs[i]],
6063                            &lights,
6064                            ambient,
6065                        );
6066                    }
6067                }
6068
6069                // Near-plane clip (Sutherland-Hodgman per vertex)
6070                let near = -gfx.camera.zdist + 0.05;
6071                let mut clip_in: [(f32, f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6072                    [(0.0, 0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6073                for i in 0..n_verts {
6074                    let d = gfx.camera.depth(wxs[i], wys[i], wzs[i]);
6075                    clip_in[i] = (wxs[i], wys[i], wzs[i], d, wcs[i]);
6076                }
6077                let mut clip_out: [(f32, f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6078                    [(0.0, 0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6079                let pn = crate::gfx::poly::clip_near(&clip_in, n_verts, near, &mut clip_out);
6080                if pn < 3 {
6081                    return Ok(Value::Unit);
6082                }
6083
6084                // Project + fog
6085                let mut proj: [(f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6086                    [(0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6087                for i in 0..pn {
6088                    let (sx, sy, sz) =
6089                        gfx.camera
6090                            .project(clip_out[i].0, clip_out[i].1, clip_out[i].2);
6091                    let fc = gfx.fog_apply(clip_out[i].4, sz);
6092                    proj[i] = (sx, sy, sz, fc);
6093                }
6094
6095                // Fan-triangulate and push
6096                let unlit = gfx.flat_shade;
6097                crate::gfx::poly::fan_emit_proj(
6098                    &proj,
6099                    pn,
6100                    |x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2| {
6101                        gfx.depth_queue.push_triangle_g_zv(
6102                            x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, 3, unlit,
6103                        );
6104                    },
6105                );
6106                return Ok(Value::Unit);
6107            },
6108
6109            // ── วาดเส้น3มิติ(ax,ay,az, bx,by,bz) ──
6110            // Projects two world-space points via the stored camera and pushes
6111            // a line to the depth queue.
6112            "วาดเส้น3มิติ" | "draw_line_3d" | "line3d" | "画3D线" | "3D線描く" | "3D선그리기" | "رسم_خط_سه‌بعدی" | "ارسم_خطا_ثلاثي_الأبعاد" | "צייר_קו_תלת_ממדי" | "تھری_ڈی_لکیر_کھینچو" =>
6113            {
6114                let ax = self.arg_num(&args, 0, 0.0)? as f32;
6115                let ay = self.arg_num(&args, 1, 0.0)? as f32;
6116                let az = self.arg_num(&args, 2, 0.0)? as f32;
6117                let bx = self.arg_num(&args, 3, 0.0)? as f32;
6118                let by = self.arg_num(&args, 4, 0.0)? as f32;
6119                let bz = self.arg_num(&args, 5, 0.0)? as f32;
6120
6121                let mut gfx = self.gfx.borrow_mut();
6122                let color = gfx.color;
6123                // Near-plane clip in 3-D before perspective divide
6124                let near = -gfx.camera.zdist + 0.05;
6125                let mut lax = ax;
6126                let mut lay = ay;
6127                let mut laz = az;
6128                let mut lbx = bx;
6129                let mut lby = by;
6130                let mut lbz = bz;
6131                let da_raw = gfx.camera.depth(lax, lay, laz);
6132                let db_raw = gfx.camera.depth(lbx, lby, lbz);
6133                if da_raw <= near && db_raw <= near {
6134                    return Ok(Value::Unit);
6135                }
6136                if da_raw <= near {
6137                    let t = (near - da_raw) / (db_raw - da_raw);
6138                    lax += t * (lbx - lax);
6139                    lay += t * (lby - lay);
6140                    laz += t * (lbz - laz);
6141                } else if db_raw <= near {
6142                    let t = (near - da_raw) / (db_raw - da_raw);
6143                    lbx = lax + t * (lbx - lax);
6144                    lby = lay + t * (lby - lay);
6145                    lbz = laz + t * (lbz - laz);
6146                }
6147                // Shared-edge dedup: skip if this world-space edge was already queued.
6148                if !gfx.edge_set.try_insert(lax, lay, laz, lbx, lby, lbz) {
6149                    return Ok(Value::Unit);
6150                }
6151                let (sax, say, da) = gfx.camera.project(lax, lay, laz);
6152                let (sbx, sby, db) = gfx.camera.project(lbx, lby, lbz);
6153                let depth = (da + db) / 2.0;
6154                let color = gfx.fog_apply(color, depth);
6155                gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
6156                return Ok(Value::Unit);
6157            },
6158
6159            // orb_shell(cx,cy,cz, radius, rot_y, rot_x, density, r,g,b)
6160            //   A single trippy, grayscale, depth-faded vector pattern wound around
6161            //   a sphere — two families of interleaved spherical spirals (a guilloché
6162            //   weave), NOT a lat/long cage. Each segment's brightness follows its
6163            //   facing (front bright, back dim), so it reads as a translucent
6164            //   grayscale "texture" with alpha rather than a hard wireframe; the
6165            //   inner marble shows through. `rot_y`/`rot_x` roll the texture around
6166            //   the orb; `density` = spirals per winding direction. r,g,b tint it
6167            //   (pass a gray like 230,230,230 for pure grayscale).
6168            #[cfg(not(target_arch = "wasm32"))]
6169            "orb_shell" | "球壳" | "オーブ殻" | "오브껍질" | "เปลือกทรงกลม" | "پوسته_کروی" | "قشرة_كروية" | "קליפת_כדור" | "کروی_خول" | "coque_orbe" | "orb_hülle" | "оболочка_сферы" =>
6170            {
6171                let cx = self.arg_num(&args, 0, 0.)? as f32;
6172                let cy = self.arg_num(&args, 1, 0.)? as f32;
6173                let cz = self.arg_num(&args, 2, 0.)? as f32;
6174                let radius = self.arg_num(&args, 3, 1.0)? as f32;
6175                let ry = self.arg_num(&args, 4, 0.)? as f32;
6176                let rx = self.arg_num(&args, 5, 0.)? as f32;
6177                let density = (self.arg_num(&args, 6, 10.)? as i32).clamp(1, 48);
6178                let tr = (self.arg_num(&args, 7, 230.)? as f32).clamp(0., 255.);
6179                let tg = (self.arg_num(&args, 8, 230.)? as f32).clamp(0., 255.);
6180                let tb = (self.arg_num(&args, 9, 235.)? as f32).clamp(0., 255.);
6181                let (cyr, syr) = (ry.cos(), ry.sin());
6182                let (cxr, sxr) = (rx.cos(), rx.sin());
6183                let tau = std::f32::consts::TAU;
6184                let pi = std::f32::consts::PI;
6185                let turns = 6.0_f32; // how many times each spiral wraps pole→pole
6186                let nseg = 96; // segments per spiral (smoothness)
6187                let inv_r = if radius.abs() > 1e-5 {
6188                    1.0 / radius
6189                } else {
6190                    0.0
6191                };
6192                // a point along a spiral (param u 0..1, start angle theta0, winding dir),
6193                // spun by ry/rx — returns (world point, facing 0..1 where 1 = toward camera)
6194                let pt = |u: f32, theta0: f32, dir: f32| -> ([f32; 3], f32) {
6195                    let phi = pi * u; // 0..pi  (north → south)
6196                    let th = dir * turns * tau * u + theta0;
6197                    let (mut x, y, mut z) = (
6198                        phi.sin() * th.cos() * radius,
6199                        phi.cos() * radius,
6200                        phi.sin() * th.sin() * radius,
6201                    );
6202                    let x1 = x * cyr + z * syr; // yaw about Y
6203                    let z1 = -x * syr + z * cyr;
6204                    x = x1;
6205                    z = z1;
6206                    let y2 = y * cxr - z * sxr; // pitch about X
6207                    let z2 = y * sxr + z * cxr;
6208                    // facing: camera sits at -zdist looking +z, so smaller z2 = nearer = brighter
6209                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.0, 1.0);
6210                    ([cx + x, cy + y2, cz + z2], facing)
6211                };
6212                let mut gfx = self.gfx.borrow_mut();
6213                let near = -gfx.camera.zdist + 0.05;
6214                // draw one segment (near-clipped) in a grayscale tint scaled by `lum`
6215                let seg = |gfx: &mut crate::gfx::GfxState, a: [f32; 3], b: [f32; 3], lum: f32| {
6216                    let (mut lax, mut lay, mut laz) = (a[0], a[1], a[2]);
6217                    let (mut lbx, mut lby, mut lbz) = (b[0], b[1], b[2]);
6218                    let da = gfx.camera.depth(lax, lay, laz);
6219                    let db = gfx.camera.depth(lbx, lby, lbz);
6220                    if da <= near && db <= near {
6221                        return;
6222                    }
6223                    if da <= near {
6224                        let t = (near - da) / (db - da);
6225                        lax += t * (lbx - lax);
6226                        lay += t * (lby - lay);
6227                        laz += t * (lbz - laz);
6228                    } else if db <= near {
6229                        let t = (near - da) / (db - da);
6230                        lbx = lax + t * (lbx - lax);
6231                        lby = lay + t * (lby - lay);
6232                        lbz = laz + t * (lbz - laz);
6233                    }
6234                    let (sax, say, da2) = gfx.camera.project(lax, lay, laz);
6235                    let (sbx, sby, db2) = gfx.camera.project(lbx, lby, lbz);
6236                    // grayscale-alpha: front-facing bright, back faded toward black
6237                    let l = (0.12 + 0.88 * lum).clamp(0.0, 1.0);
6238                    let cr = (tr * l) as u32;
6239                    let cg = (tg * l) as u32;
6240                    let cb = (tb * l) as u32;
6241                    let color = (cr << 16) | (cg << 8) | cb;
6242                    gfx.depth_queue
6243                        .push_line((da2 + db2) * 0.5, color, sax, say, sbx, sby);
6244                };
6245                // two opposite winding directions → a soft guilloché weave (not a cage)
6246                for &dir in &[1.0_f32, -1.0_f32] {
6247                    for s in 0..density {
6248                        let theta0 = s as f32 * tau / density as f32;
6249                        let mut prev = pt(0.0, theta0, dir);
6250                        for k in 1..=nseg {
6251                            let cur = pt(k as f32 / nseg as f32, theta0, dir);
6252                            seg(&mut gfx, prev.0, cur.0, (prev.1 + cur.1) * 0.5);
6253                            prev = cur;
6254                        }
6255                    }
6256                }
6257                return Ok(Value::Unit);
6258            },
6259
6260            // orb_particles(cx,cy,cz, radius, count, t, r,g,b)
6261            //   Fills the VOLUME of a sphere with `count` swirling vector points —
6262            //   like motes suspended inside a snow-globe orb. Points are distributed
6263            //   uniformly through the ball, slowly tumble as a cloud + wobble
6264            //   individually over time `t`, and are depth-shaded (near = bright,
6265            //   far = dim) so the cloud has real volume. Additive, so it layers under
6266            //   a shell / over a liquid marble.
6267            #[cfg(not(target_arch = "wasm32"))]
6268            "orb_particles" | "球内粒子" | "オーブ粒子" | "오브입자" | "อนุภาคทรงกลม" | "ذرات_کروی" | "جسيمات_كروية" | "חלקיקי_כדור" | "کروی_ذرات" | "particules_orbe" | "orb_partikel" | "частицы_сферы" =>
6269            {
6270                let cx = self.arg_num(&args, 0, 0.)? as f32;
6271                let cy = self.arg_num(&args, 1, 0.)? as f32;
6272                let cz = self.arg_num(&args, 2, 0.)? as f32;
6273                let radius = self.arg_num(&args, 3, 1.0)? as f32;
6274                let count = (self.arg_num(&args, 4, 160.)? as i32).clamp(1, 4000);
6275                let t = self.arg_num(&args, 5, 0.)? as f32;
6276                let tr = (self.arg_num(&args, 6, 255.)? as f32).clamp(0., 255.);
6277                let tg = (self.arg_num(&args, 7, 255.)? as f32).clamp(0., 255.);
6278                let tb = (self.arg_num(&args, 8, 255.)? as f32).clamp(0., 255.);
6279                let inv_r = if radius.abs() > 1e-5 {
6280                    1.0 / radius
6281                } else {
6282                    0.0
6283                };
6284                // cheap deterministic hash → [0,1)
6285                let h = |mut x: u32| -> f32 {
6286                    x = x.wrapping_mul(747796405).wrapping_add(2891336453);
6287                    x = ((x >> ((x >> 28).wrapping_add(4))) ^ x).wrapping_mul(277803737);
6288                    (((x >> 22) ^ x) & 0xFFFFFF) as f32 / 16_777_216.0
6289                };
6290                let tau = std::f32::consts::TAU;
6291                // slow tumble of the whole cloud
6292                let (cyr, syr) = ((t * 0.5).cos(), (t * 0.5).sin());
6293                let (cxr, sxr) = ((t * 0.23).cos(), (t * 0.23).sin());
6294                let mut gfx = self.gfx.borrow_mut();
6295                let near = -gfx.camera.zdist + 0.05;
6296                let (sw, sh) = (gfx.width as i32, gfx.height as i32);
6297                for i in 0..count {
6298                    let i = i as u32;
6299                    // uniform-in-volume: r = cbrt(u) * radius; direction from two hashes
6300                    let u = h(i.wrapping_mul(3) + 1);
6301                    let rr = u.cbrt() * radius * (0.85 + 0.15 * (t * 1.3 + i as f32).sin()); // gentle pulse
6302                    let th = h(i.wrapping_mul(3) + 2) * tau + t * (0.3 + 0.5 * h(i * 7 + 5)); // per-mote orbit
6303                    let ph = (h(i.wrapping_mul(3) + 3) * 2.0 - 1.0).acos(); // uniform cos(phi)
6304                    let (mut x, y, mut z) = (
6305                        rr * ph.sin() * th.cos(),
6306                        rr * ph.cos(),
6307                        rr * ph.sin() * th.sin(),
6308                    );
6309                    // tumble the cloud (yaw then pitch)
6310                    let x1 = x * cyr + z * syr;
6311                    let z1 = -x * syr + z * cyr;
6312                    x = x1;
6313                    z = z1;
6314                    let y2 = y * cxr - z * sxr;
6315                    let z2 = y * sxr + z * cxr;
6316                    let (wx, wy, wz) = (cx + x, cy + y2, cz + z2);
6317                    if gfx.camera.depth(wx, wy, wz) <= near {
6318                        continue;
6319                    }
6320                    let (sx, sy, dep) = gfx.camera.project(wx, wy, wz);
6321                    let sxi = sx as i32;
6322                    let syi = sy as i32;
6323                    if sxi < 0 || syi < 0 || sxi >= sw || syi >= sh {
6324                        continue;
6325                    }
6326                    // depth-shade: nearer (smaller z2) = brighter
6327                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.15, 1.0);
6328                    let l = facing;
6329                    let cr = (tr * l) as u32;
6330                    let cg = (tg * l) as u32;
6331                    let cb = (tb * l) as u32;
6332                    let color = (cr << 16) | (cg << 8) | cb;
6333                    // a 1–2px dot (bigger when near) as a short segment in the depth queue
6334                    let len = if facing > 0.7 { 1.0 } else { 0.0 };
6335                    gfx.depth_queue.push_line(dep, color, sx, sy, sx + len, sy);
6336                }
6337                return Ok(Value::Unit);
6338            },
6339
6340            // project_3d(x,y,z) -> [screen_x, screen_y, depth]; behind the camera
6341            // returns a sentinel ([-99999,-99999, depth]) so scripts can skip it.
6342            // Lets scripts place 2-D overlays (e.g. filled teardrop flames) onto 3-D points.
6343            "project_3d" | "投影3D" | "3D投影" | "3D투영" | "ฉาย3มิติ" | "فرافکنی_سه‌بعدی" | "إسقاط_ثلاثي_الأبعاد" | "הטלה_תלת_ממדית" | "تھری_ڈی_پروجیکشن" | "projeter_3d" | "projizieren_3d" | "проекция_3d" => {
6344                let x = self.arg_num(&args, 0, 0.0)? as f32;
6345                let y = self.arg_num(&args, 1, 0.0)? as f32;
6346                let z = self.arg_num(&args, 2, 0.0)? as f32;
6347                let gfx = self.gfx.borrow();
6348                let near = -gfx.camera.zdist + 0.05;
6349                let d = gfx.camera.depth(x, y, z);
6350                if d <= near {
6351                    return Ok(Value::List(Rc::new(vec![
6352                        Value::Number(-99999.0),
6353                        Value::Number(-99999.0),
6354                        Value::Number(d as f64),
6355                    ])));
6356                }
6357                let (sx, sy, depth) = gfx.camera.project(x, y, z);
6358                return Ok(Value::List(Rc::new(vec![
6359                    Value::Number(sx as f64),
6360                    Value::Number(sy as f64),
6361                    Value::Number(depth as f64),
6362                ])));
6363            },
6364
6365            // mouse_ray() -> [ox,oy,oz, dx,dy,dz] — world-space ray from the eye
6366            // through the actual mouse cursor pixel, exact inverse of project_3d's
6367            // pipeline (translate → Y-rotate → X-rotate → perspective divide by
6368            // rz+zdist). Scripts previously marched a ray along the CENTRE-SCREEN
6369            // forward vector regardless of where the cursor was — accurate only by
6370            // coincidence when the cursor happened to sit near the crosshair.
6371            #[cfg(not(target_arch = "wasm32"))]
6372            "mouse_ray" => {
6373                let gfx = self.gfx.borrow();
6374                let (mx, my) = gfx
6375                    .window
6376                    .as_ref()
6377                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
6378                    .unwrap_or((gfx.camera.cx, gfx.camera.cy));
6379                let cam = &gfx.camera;
6380                // Eye-relative pinhole direction for this pixel, in rotation-space
6381                // (before undoing the Y-then-X rotation project() applied).
6382                let dcx = (mx - cam.cx) / cam.focal;
6383                let dcy = (my - cam.cy) / cam.focal;
6384                // Undo the X-rotation, then the Y-rotation (reverse of project()'s
6385                // forward order), on the direction vector (dcx, dcy, 1.0).
6386                let a_x = dcx;
6387                let a_y = cam.crx * dcy + cam.srx * 1.0;
6388                let a_z = 0.0 - cam.srx * dcy + cam.crx * 1.0;
6389                let dir_x = cam.cry * a_x + cam.sry * a_z;
6390                let dir_y = a_y;
6391                let dir_z = 0.0 - cam.sry * a_x + cam.cry * a_z;
6392                let dlen = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
6393                    .sqrt()
6394                    .max(1e-6);
6395                let (dir_x, dir_y, dir_z) = (dir_x / dlen, dir_y / dlen, dir_z / dlen);
6396                // Origin: the camera's rotation pivot (tx,ty,tz) — i.e. wherever
6397                // the script last put it with set_camera_pos, NOT the "true"
6398                // pinhole eye zdist further back. The pivot is what scripts
6399                // already keep clear of the ground (a ground-collision pull-in
6400                // loop is standard practice for orbit cameras); the true eye
6401                // would need its own separate ground clearance since zdist is
6402                // often large relative to a close-in camera distance, and
6403                // starting a ray underground makes it hit "ground" instantly
6404                // regardless of aim. The zdist offset only matters for the
6405                // near-field parallax, which a click-to-move ray (aimed at
6406                // terrain many units out) doesn't need.
6407                let ox = cam.tx;
6408                let oy = cam.ty;
6409                let oz = cam.tz;
6410                return Ok(Value::List(Rc::new(vec![
6411                    Value::Number(ox as f64),
6412                    Value::Number(oy as f64),
6413                    Value::Number(oz as f64),
6414                    Value::Number(dir_x as f64),
6415                    Value::Number(dir_y as f64),
6416                    Value::Number(dir_z as f64),
6417                ])));
6418            },
6419            #[cfg(target_arch = "wasm32")]
6420            "mouse_ray" => {
6421                let gfx = self.gfx.borrow();
6422                let mx = crate::gfx::wasm_mouse_x();
6423                let my = crate::gfx::wasm_mouse_y();
6424                let cam = &gfx.camera;
6425                let dcx = (mx - cam.cx) / cam.focal;
6426                let dcy = (my - cam.cy) / cam.focal;
6427                let a_x = dcx;
6428                let a_y = cam.crx * dcy + cam.srx * 1.0;
6429                let a_z = 0.0 - cam.srx * dcy + cam.crx * 1.0;
6430                let dir_x = cam.cry * a_x + cam.sry * a_z;
6431                let dir_y = a_y;
6432                let dir_z = 0.0 - cam.sry * a_x + cam.cry * a_z;
6433                let dlen = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
6434                    .sqrt()
6435                    .max(1e-6);
6436                let (dir_x, dir_y, dir_z) = (dir_x / dlen, dir_y / dlen, dir_z / dlen);
6437                let ox = cam.tx;
6438                let oy = cam.ty;
6439                let oz = cam.tz;
6440                return Ok(Value::List(Rc::new(vec![
6441                    Value::Number(ox as f64),
6442                    Value::Number(oy as f64),
6443                    Value::Number(oz as f64),
6444                    Value::Number(dir_x as f64),
6445                    Value::Number(dir_y as f64),
6446                    Value::Number(dir_z as f64),
6447                ])));
6448            },
6449            // draw_poly([x0,y0,x1,y1,…]) — filled 2-D polygon in the current colour,
6450            // honouring the blend mode (additive → translucent glow). Auto-closes.
6451            #[cfg(not(target_arch = "wasm32"))]
6452            "draw_poly" | "填充多边形" | "ポリゴン塗り" | "다각형채우기" | "เติมรูปหลายเหลี่ยม" | "رسم_چندضلعی" | "ارسم_مضلع" | "צייר_מצולע" | "کثیر_الاضلاع_کھینچو" | "dessiner_polygone" | "polygon_zeichnen" | "рисовать_полигон" =>
6453            {
6454                let mut pts: Vec<[f32; 2]> = Vec::new();
6455                if let Some(Value::List(v)) = args.first() {
6456                    let mut i = 0;
6457                    while i + 1 < v.len() {
6458                        let x = self.to_number(&v[i]).unwrap_or(0.0) as f32;
6459                        let y = self.to_number(&v[i + 1]).unwrap_or(0.0) as f32;
6460                        pts.push([x, y]);
6461                        i += 2;
6462                    }
6463                }
6464                if pts.len() >= 3 {
6465                    if pts[0] != pts[pts.len() - 1] {
6466                        let p0 = pts[0];
6467                        pts.push(p0);
6468                    } // close
6469                    let mut gfx = self.gfx.borrow_mut();
6470                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
6471                    crate::gfx::raster::fill_contours_aa(
6472                        &mut gfx.buffer,
6473                        w,
6474                        h,
6475                        color,
6476                        add,
6477                        std::slice::from_ref(&pts),
6478                    );
6479                }
6480                return Ok(Value::Unit);
6481            },
6482
6483            // ══════════════════════════════════════════════════════════════════
6484            // VECTOR TEXTURE BUILTINS  (src/gfx/vtex.rs)
6485            // All patterns are depth-biased so they appear on top of surfaces.
6486            // Plane defined by: centre (cx,cy,cz) + U tangent + V tangent.
6487            // Last two args always: fr (frame f32), hue (phase offset f32).
6488            // ══════════════════════════════════════════════════════════════════
6489
6490            // vtex_grid(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cw,ch, fr,hue)
6491            "vtex_grid" | "ลายตาราง" | "纹格" | "格子模様" | "격자무늬" | "الگوی_شبکه" | "نقش_شبكة" | "דוגמת_רשת" | "نقش_جالی" | "motif_grille" | "muster_gitter" | "узор_сетка" =>
6492            {
6493                let cx = self.arg_num(&args, 0, 0.)? as f32;
6494                let cy = self.arg_num(&args, 1, 0.)? as f32;
6495                let cz = self.arg_num(&args, 2, 0.)? as f32;
6496                let ux = self.arg_num(&args, 3, 1.)? as f32;
6497                let uy = self.arg_num(&args, 4, 0.)? as f32;
6498                let uz = self.arg_num(&args, 5, 0.)? as f32;
6499                let vx = self.arg_num(&args, 6, 0.)? as f32;
6500                let vy = self.arg_num(&args, 7, 0.)? as f32;
6501                let vz = self.arg_num(&args, 8, 1.)? as f32;
6502                let cols = self.arg_num(&args, 9, 10.)? as usize;
6503                let rows = self.arg_num(&args, 10, 10.)? as usize;
6504                let cw = self.arg_num(&args, 11, 1.)? as f32;
6505                let ch = self.arg_num(&args, 12, 1.)? as f32;
6506                let fr = self.arg_num(&args, 13, 0.)? as f32;
6507                let hue = self.arg_num(&args, 14, 0.)? as f32;
6508                let mut gfx = self.gfx.borrow_mut();
6509                let cam = gfx.camera.clone();
6510                crate::gfx::vtex::draw_grid(
6511                    &mut gfx.depth_queue,
6512                    &cam,
6513                    cx,
6514                    cy,
6515                    cz,
6516                    ux,
6517                    uy,
6518                    uz,
6519                    vx,
6520                    vy,
6521                    vz,
6522                    cols,
6523                    rows,
6524                    cw,
6525                    ch,
6526                    fr,
6527                    hue,
6528                );
6529                return Ok(Value::Unit);
6530            },
6531
6532            // vtex_rings(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_rings,n_sides, max_r,twist, fr,hue)
6533            "vtex_rings" | "ลายวงซ้อน" | "纹环" | "同心円" | "동심원" | "الگوی_حلقه" | "نقش_حلقات" | "דוגמת_טבעות" | "نقش_حلقے" | "motif_anneaux" | "muster_ringe" | "узор_кольца" => {
6534                let cx = self.arg_num(&args, 0, 0.)? as f32;
6535                let cy = self.arg_num(&args, 1, 0.)? as f32;
6536                let cz = self.arg_num(&args, 2, 0.)? as f32;
6537                let ux = self.arg_num(&args, 3, 1.)? as f32;
6538                let uy = self.arg_num(&args, 4, 0.)? as f32;
6539                let uz = self.arg_num(&args, 5, 0.)? as f32;
6540                let vx = self.arg_num(&args, 6, 0.)? as f32;
6541                let vy = self.arg_num(&args, 7, 0.)? as f32;
6542                let vz = self.arg_num(&args, 8, 1.)? as f32;
6543                let nr = self.arg_num(&args, 9, 6.)? as usize;
6544                let ns = self.arg_num(&args, 10, 6.)? as usize;
6545                let mr = self.arg_num(&args, 11, 3.)? as f32;
6546                let tw = self.arg_num(&args, 12, 0.)? as f32;
6547                let fr = self.arg_num(&args, 13, 0.)? as f32;
6548                let hue = self.arg_num(&args, 14, 0.)? as f32;
6549                let mut gfx = self.gfx.borrow_mut();
6550                let cam = gfx.camera.clone();
6551                crate::gfx::vtex::draw_rings(
6552                    &mut gfx.depth_queue,
6553                    &cam,
6554                    cx,
6555                    cy,
6556                    cz,
6557                    ux,
6558                    uy,
6559                    uz,
6560                    vx,
6561                    vy,
6562                    vz,
6563                    nr,
6564                    ns,
6565                    mr,
6566                    tw,
6567                    fr,
6568                    hue,
6569                );
6570                return Ok(Value::Unit);
6571            },
6572
6573            // vtex_star(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_pts,r_out,r_in, rot_speed, fr,hue)
6574            "vtex_star" | "ลายดาว" | "纹星" | "星模様" | "별무늬" | "الگوی_ستاره" | "نقش_نجمة" | "דוגמת_כוכב" | "نقش_ستارہ" | "motif_étoile" | "muster_stern" | "узор_звезда" => {
6575                let cx = self.arg_num(&args, 0, 0.)? as f32;
6576                let cy = self.arg_num(&args, 1, 0.)? as f32;
6577                let cz = self.arg_num(&args, 2, 0.)? as f32;
6578                let ux = self.arg_num(&args, 3, 1.)? as f32;
6579                let uy = self.arg_num(&args, 4, 0.)? as f32;
6580                let uz = self.arg_num(&args, 5, 0.)? as f32;
6581                let vx = self.arg_num(&args, 6, 0.)? as f32;
6582                let vy = self.arg_num(&args, 7, 0.)? as f32;
6583                let vz = self.arg_num(&args, 8, 1.)? as f32;
6584                let np = self.arg_num(&args, 9, 6.)? as usize;
6585                let ro = self.arg_num(&args, 10, 2.)? as f32;
6586                let ri = self.arg_num(&args, 11, 1.)? as f32;
6587                let rs = self.arg_num(&args, 12, 0.01)? as f32;
6588                let fr = self.arg_num(&args, 13, 0.)? as f32;
6589                let hue = self.arg_num(&args, 14, 0.)? as f32;
6590                let mut gfx = self.gfx.borrow_mut();
6591                let cam = gfx.camera.clone();
6592                crate::gfx::vtex::draw_star(
6593                    &mut gfx.depth_queue,
6594                    &cam,
6595                    cx,
6596                    cy,
6597                    cz,
6598                    ux,
6599                    uy,
6600                    uz,
6601                    vx,
6602                    vy,
6603                    vz,
6604                    np,
6605                    ro,
6606                    ri,
6607                    rs,
6608                    fr,
6609                    hue,
6610                );
6611                return Ok(Value::Unit);
6612            },
6613
6614            // vtex_spiral(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_turns,max_r,steps, fr,hue)
6615            "vtex_spiral" | "ลายเกลียว" | "纹螺" | "螺旋" | "나선" | "الگوی_مارپیچ" | "نقش_حلزوني" | "דוגמת_ספירלה" | "نقش_سرپیچ" | "motif_spirale" | "muster_spirale" | "узор_спираль" => {
6616                let cx = self.arg_num(&args, 0, 0.)? as f32;
6617                let cy = self.arg_num(&args, 1, 0.)? as f32;
6618                let cz = self.arg_num(&args, 2, 0.)? as f32;
6619                let ux = self.arg_num(&args, 3, 1.)? as f32;
6620                let uy = self.arg_num(&args, 4, 0.)? as f32;
6621                let uz = self.arg_num(&args, 5, 0.)? as f32;
6622                let vx = self.arg_num(&args, 6, 0.)? as f32;
6623                let vy = self.arg_num(&args, 7, 0.)? as f32;
6624                let vz = self.arg_num(&args, 8, 1.)? as f32;
6625                let nt = self.arg_num(&args, 9, 3.)? as f32;
6626                let mr = self.arg_num(&args, 10, 3.)? as f32;
6627                let st = self.arg_num(&args, 11, 120.)? as usize;
6628                let fr = self.arg_num(&args, 12, 0.)? as f32;
6629                let hue = self.arg_num(&args, 13, 0.)? as f32;
6630                let mut gfx = self.gfx.borrow_mut();
6631                let cam = gfx.camera.clone();
6632                crate::gfx::vtex::draw_spiral(
6633                    &mut gfx.depth_queue,
6634                    &cam,
6635                    cx,
6636                    cy,
6637                    cz,
6638                    ux,
6639                    uy,
6640                    uz,
6641                    vx,
6642                    vy,
6643                    vz,
6644                    nt,
6645                    mr,
6646                    st,
6647                    fr,
6648                    hue,
6649                );
6650                return Ok(Value::Unit);
6651            },
6652
6653            // vtex_flower(cx,cy,cz, ux,uy,uz, vx,vy,vz, radius,n_sides, fr,hue)
6654            "vtex_flower" | "ลายดอก" | "纹花" | "花模様" | "꽃무늬" | "الگوی_گل" | "نقش_زهرة" | "דוגמת_פרח" | "نقش_پھول" | "motif_fleur" | "muster_blume" | "узор_цветок" => {
6655                let cx = self.arg_num(&args, 0, 0.)? as f32;
6656                let cy = self.arg_num(&args, 1, 0.)? as f32;
6657                let cz = self.arg_num(&args, 2, 0.)? as f32;
6658                let ux = self.arg_num(&args, 3, 1.)? as f32;
6659                let uy = self.arg_num(&args, 4, 0.)? as f32;
6660                let uz = self.arg_num(&args, 5, 0.)? as f32;
6661                let vx = self.arg_num(&args, 6, 0.)? as f32;
6662                let vy = self.arg_num(&args, 7, 0.)? as f32;
6663                let vz = self.arg_num(&args, 8, 1.)? as f32;
6664                let r = self.arg_num(&args, 9, 1.)? as f32;
6665                let ns = self.arg_num(&args, 10, 24.)? as usize;
6666                let fr = self.arg_num(&args, 11, 0.)? as f32;
6667                let hue = self.arg_num(&args, 12, 0.)? as f32;
6668                let mut gfx = self.gfx.borrow_mut();
6669                let cam = gfx.camera.clone();
6670                crate::gfx::vtex::draw_flower(
6671                    &mut gfx.depth_queue,
6672                    &cam,
6673                    cx,
6674                    cy,
6675                    cz,
6676                    ux,
6677                    uy,
6678                    uz,
6679                    vx,
6680                    vy,
6681                    vz,
6682                    r,
6683                    ns,
6684                    fr,
6685                    hue,
6686                );
6687                return Ok(Value::Unit);
6688            },
6689
6690            // vtex_letter_rain(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_cols,n_vis, col_w,row_h, speed, fr,hue)
6691            "vtex_letter_rain" | "ลายอักษรไหล" | "纹字雨" | "文字雨" | "글자비" | "الگوی_باران_حروف" | "نقش_مطر_الحروف" | "דוגמת_גשם_אותיות" | "نقش_حروف_بارش" | "motif_pluie_lettres" | "muster_buchstabenregen" | "узор_дождь_букв" =>
6692            {
6693                let cx = self.arg_num(&args, 0, 0.)? as f32;
6694                let cy = self.arg_num(&args, 1, 0.)? as f32;
6695                let cz = self.arg_num(&args, 2, 0.)? as f32;
6696                let ux = self.arg_num(&args, 3, 1.)? as f32;
6697                let uy = self.arg_num(&args, 4, 0.)? as f32;
6698                let uz = self.arg_num(&args, 5, 0.)? as f32;
6699                let vx = self.arg_num(&args, 6, 0.)? as f32;
6700                let vy = self.arg_num(&args, 7, 0.)? as f32;
6701                let vz = self.arg_num(&args, 8, 1.)? as f32;
6702                let nc = self.arg_num(&args, 9, 16.)? as usize;
6703                let nv = self.arg_num(&args, 10, 14.)? as usize;
6704                let cw = self.arg_num(&args, 11, 0.65)? as f32;
6705                let rh = self.arg_num(&args, 12, 0.60)? as f32;
6706                let sp = self.arg_num(&args, 13, 0.025)? as f32;
6707                let fr = self.arg_num(&args, 14, 0.)? as f32;
6708                let hue = self.arg_num(&args, 15, 0.)? as f32;
6709                let mut gfx = self.gfx.borrow_mut();
6710                let cam = gfx.camera.clone();
6711                crate::gfx::vtex::draw_letter_rain(
6712                    &mut gfx.depth_queue,
6713                    &cam,
6714                    cx,
6715                    cy,
6716                    cz,
6717                    ux,
6718                    uy,
6719                    uz,
6720                    vx,
6721                    vy,
6722                    vz,
6723                    nc,
6724                    nv,
6725                    cw,
6726                    rh,
6727                    sp,
6728                    fr,
6729                    hue,
6730                );
6731                return Ok(Value::Unit);
6732            },
6733
6734            // vtex_hyperbolic_uv(cx,cy,cz, ux,uy,uz, vx,vy,vz, max_r,n_circles,n_rays, fr,hue)
6735            "vtex_hyperbolic_uv" | "ลายไฮเพอร์โบลิก" | "纹曲面" | "双曲線" | "쌍곡선" | "الگوی_هذلولی" | "نقش_زائدي" | "דוגמת_היפרבולית" | "نقش_ہائپربولک" | "motif_uv_hyperbolique" | "muster_hyperbolische_uv" | "узор_гиперболический_uv" =>
6736            {
6737                let cx = self.arg_num(&args, 0, 0.)? as f32;
6738                let cy = self.arg_num(&args, 1, 0.)? as f32;
6739                let cz = self.arg_num(&args, 2, 0.)? as f32;
6740                let ux = self.arg_num(&args, 3, 1.)? as f32;
6741                let uy = self.arg_num(&args, 4, 0.)? as f32;
6742                let uz = self.arg_num(&args, 5, 0.)? as f32;
6743                let vx = self.arg_num(&args, 6, 0.)? as f32;
6744                let vy = self.arg_num(&args, 7, 0.)? as f32;
6745                let vz = self.arg_num(&args, 8, 1.)? as f32;
6746                let mr = self.arg_num(&args, 9, 5.)? as f32;
6747                let nc = self.arg_num(&args, 10, 12.)? as usize;
6748                let nr = self.arg_num(&args, 11, 18.)? as usize;
6749                let fr = self.arg_num(&args, 12, 0.)? as f32;
6750                let hue = self.arg_num(&args, 13, 0.)? as f32;
6751                let mut gfx = self.gfx.borrow_mut();
6752                let cam = gfx.camera.clone();
6753                crate::gfx::vtex::draw_hyperbolic_uv(
6754                    &mut gfx.depth_queue,
6755                    &cam,
6756                    cx,
6757                    cy,
6758                    cz,
6759                    ux,
6760                    uy,
6761                    uz,
6762                    vx,
6763                    vy,
6764                    vz,
6765                    mr,
6766                    nc,
6767                    nr,
6768                    fr,
6769                    hue,
6770                );
6771                return Ok(Value::Unit);
6772            },
6773
6774            // vtex_halftone(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell_w,cell_h, density, fr,hue)
6775            "vtex_halftone" | "ลายจุด" | "纹半调" | "網点模様" | "망점" | "الگوی_نیم‌تن" | "نقش_نصفي" | "דוגמת_חצי_גוון" | "نقش_ہاف_ٹون" | "motif_demi_ton" | "muster_halbton" | "узор_растр" => {
6776                let cx = self.arg_num(&args, 0, 0.)? as f32;
6777                let cy = self.arg_num(&args, 1, 0.)? as f32;
6778                let cz = self.arg_num(&args, 2, 0.)? as f32;
6779                let ux = self.arg_num(&args, 3, 1.)? as f32;
6780                let uy = self.arg_num(&args, 4, 0.)? as f32;
6781                let uz = self.arg_num(&args, 5, 0.)? as f32;
6782                let vx = self.arg_num(&args, 6, 0.)? as f32;
6783                let vy = self.arg_num(&args, 7, 0.)? as f32;
6784                let vz = self.arg_num(&args, 8, 1.)? as f32;
6785                let cols = self.arg_num(&args, 9, 16.)? as usize;
6786                let rows = self.arg_num(&args, 10, 12.)? as usize;
6787                let cw = self.arg_num(&args, 11, 0.5)? as f32;
6788                let ch = self.arg_num(&args, 12, 0.5)? as f32;
6789                let dens = self.arg_num(&args, 13, 0.4)? as f32;
6790                let fr = self.arg_num(&args, 14, 0.)? as f32;
6791                let hue = self.arg_num(&args, 15, 0.)? as f32;
6792                let mut gfx = self.gfx.borrow_mut();
6793                let cam = gfx.camera.clone();
6794                crate::gfx::vtex::draw_halftone(
6795                    &mut gfx.depth_queue,
6796                    &cam,
6797                    cx,
6798                    cy,
6799                    cz,
6800                    ux,
6801                    uy,
6802                    uz,
6803                    vx,
6804                    vy,
6805                    vz,
6806                    cols,
6807                    rows,
6808                    cw,
6809                    ch,
6810                    dens,
6811                    fr,
6812                    hue,
6813                );
6814                return Ok(Value::Unit);
6815            },
6816
6817            // vtex_tessellated(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell, amplitude,freq, fr,hue)
6818            "vtex_tessellated" | "ลายตาข่าย" | "纹镶嵌" | "網目模様" | "격자망" | "الگوی_کاشی‌کاری" | "نقش_مرصوف_متكرر" | "דוגמת_ריצוף_חוזר" | "نقش_ٹائلنگ" | "motif_tesselle" | "muster_tessellation" | "узор_мозаика" =>
6819            {
6820                let cx = self.arg_num(&args, 0, 0.)? as f32;
6821                let cy = self.arg_num(&args, 1, 0.)? as f32;
6822                let cz = self.arg_num(&args, 2, 0.)? as f32;
6823                let ux = self.arg_num(&args, 3, 1.)? as f32;
6824                let uy = self.arg_num(&args, 4, 0.)? as f32;
6825                let uz = self.arg_num(&args, 5, 0.)? as f32;
6826                let vx = self.arg_num(&args, 6, 0.)? as f32;
6827                let vy = self.arg_num(&args, 7, 0.)? as f32;
6828                let vz = self.arg_num(&args, 8, 1.)? as f32;
6829                let cols = self.arg_num(&args, 9, 14.)? as usize;
6830                let rows = self.arg_num(&args, 10, 10.)? as usize;
6831                let cell = self.arg_num(&args, 11, 0.6)? as f32;
6832                let amp = self.arg_num(&args, 12, 0.25)? as f32;
6833                let freq = self.arg_num(&args, 13, 4.)? as f32;
6834                let fr = self.arg_num(&args, 14, 0.)? as f32;
6835                let hue = self.arg_num(&args, 15, 0.)? as f32;
6836                let mut gfx = self.gfx.borrow_mut();
6837                let cam = gfx.camera.clone();
6838                crate::gfx::vtex::draw_tessellated(
6839                    &mut gfx.depth_queue,
6840                    &cam,
6841                    cx,
6842                    cy,
6843                    cz,
6844                    ux,
6845                    uy,
6846                    uz,
6847                    vx,
6848                    vy,
6849                    vz,
6850                    cols,
6851                    rows,
6852                    cell,
6853                    amp,
6854                    freq,
6855                    fr,
6856                    hue,
6857                );
6858                return Ok(Value::Unit);
6859            },
6860
6861            // vtex_lotus(cx,cy,cz, ux,uy,uz, vx,vy,vz, r_inner,r_outer,n_petals, fr,hue)
6862            "vtex_lotus" | "ลายดอกบัว" | "纹莲" | "蓮模様" | "연꽃무늬" | "الگوی_لوتوس" | "نقش_لوتس" | "דוגמת_לוטוס" | "نقش_کنول" | "motif_lotus" | "muster_lotus" | "узор_лотос" =>
6863            {
6864                let cx = self.arg_num(&args, 0, 0.)? as f32;
6865                let cy = self.arg_num(&args, 1, 0.)? as f32;
6866                let cz = self.arg_num(&args, 2, 0.)? as f32;
6867                let ux = self.arg_num(&args, 3, 1.)? as f32;
6868                let uy = self.arg_num(&args, 4, 0.)? as f32;
6869                let uz = self.arg_num(&args, 5, 0.)? as f32;
6870                let vx = self.arg_num(&args, 6, 0.)? as f32;
6871                let vy = self.arg_num(&args, 7, 0.)? as f32;
6872                let vz = self.arg_num(&args, 8, 1.)? as f32;
6873                let ri = self.arg_num(&args, 9, 1.)? as f32;
6874                let ro = self.arg_num(&args, 10, 2.)? as f32;
6875                let np = self.arg_num(&args, 11, 12.)? as usize;
6876                let fr = self.arg_num(&args, 12, 0.)? as f32;
6877                let hue = self.arg_num(&args, 13, 0.)? as f32;
6878                let mut gfx = self.gfx.borrow_mut();
6879                let cam = gfx.camera.clone();
6880                crate::gfx::vtex::draw_lotus(
6881                    &mut gfx.depth_queue,
6882                    &cam,
6883                    cx,
6884                    cy,
6885                    cz,
6886                    ux,
6887                    uy,
6888                    uz,
6889                    vx,
6890                    vy,
6891                    vz,
6892                    ri,
6893                    ro,
6894                    np,
6895                    fr,
6896                    hue,
6897                );
6898                return Ok(Value::Unit);
6899            },
6900
6901            // vtex_chakra(cx,cy,cz, ux,uy,uz, vx,vy,vz, r,n_spokes, fr,hue)
6902            "vtex_chakra" | "ลายจักร" | "纹轮" | "輪模様" | "바퀴무늬" | "الگوی_چاکرا" | "نقش_تشاكرا" | "דוגמת_צ'אקרה" | "نقش_چکر" | "motif_chakra" | "muster_chakra" | "узор_чакра" => {
6903                let cx = self.arg_num(&args, 0, 0.)? as f32;
6904                let cy = self.arg_num(&args, 1, 0.)? as f32;
6905                let cz = self.arg_num(&args, 2, 0.)? as f32;
6906                let ux = self.arg_num(&args, 3, 1.)? as f32;
6907                let uy = self.arg_num(&args, 4, 0.)? as f32;
6908                let uz = self.arg_num(&args, 5, 0.)? as f32;
6909                let vx = self.arg_num(&args, 6, 0.)? as f32;
6910                let vy = self.arg_num(&args, 7, 0.)? as f32;
6911                let vz = self.arg_num(&args, 8, 1.)? as f32;
6912                let r = self.arg_num(&args, 9, 2.)? as f32;
6913                let ns = self.arg_num(&args, 10, 8.)? as usize;
6914                let fr = self.arg_num(&args, 11, 0.)? as f32;
6915                let hue = self.arg_num(&args, 12, 0.)? as f32;
6916                let mut gfx = self.gfx.borrow_mut();
6917                let cam = gfx.camera.clone();
6918                crate::gfx::vtex::draw_chakra(
6919                    &mut gfx.depth_queue,
6920                    &cam,
6921                    cx,
6922                    cy,
6923                    cz,
6924                    ux,
6925                    uy,
6926                    uz,
6927                    vx,
6928                    vy,
6929                    vz,
6930                    r,
6931                    ns,
6932                    fr,
6933                    hue,
6934                );
6935                return Ok(Value::Unit);
6936            },
6937
6938            // vtex_yantra(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_layers,max_r, fr,hue)
6939            "vtex_yantra" | "ลายยันต์" | "纹咒" | "護符模様" | "부적무늬" | "الگوی_یانترا" | "نقش_يانترا" | "דוגמת_יאנטרה" | "نقش_ینترا" | "motif_yantra" | "muster_yantra" | "узор_янтра" =>
6940            {
6941                let cx = self.arg_num(&args, 0, 0.)? as f32;
6942                let cy = self.arg_num(&args, 1, 0.)? as f32;
6943                let cz = self.arg_num(&args, 2, 0.)? as f32;
6944                let ux = self.arg_num(&args, 3, 1.)? as f32;
6945                let uy = self.arg_num(&args, 4, 0.)? as f32;
6946                let uz = self.arg_num(&args, 5, 0.)? as f32;
6947                let vx = self.arg_num(&args, 6, 0.)? as f32;
6948                let vy = self.arg_num(&args, 7, 0.)? as f32;
6949                let vz = self.arg_num(&args, 8, 1.)? as f32;
6950                let nl = self.arg_num(&args, 9, 4.)? as usize;
6951                let mr = self.arg_num(&args, 10, 3.)? as f32;
6952                let fr = self.arg_num(&args, 11, 0.)? as f32;
6953                let hue = self.arg_num(&args, 12, 0.)? as f32;
6954                let mut gfx = self.gfx.borrow_mut();
6955                let cam = gfx.camera.clone();
6956                crate::gfx::vtex::draw_yantra(
6957                    &mut gfx.depth_queue,
6958                    &cam,
6959                    cx,
6960                    cy,
6961                    cz,
6962                    ux,
6963                    uy,
6964                    uz,
6965                    vx,
6966                    vy,
6967                    vz,
6968                    nl,
6969                    mr,
6970                    fr,
6971                    hue,
6972                );
6973                return Ok(Value::Unit);
6974            },
6975
6976            // vtex_spiked_cog(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_teeth,r_body,r_spike,r_hub,n_spokes, fr,hue)
6977            "vtex_spiked_cog" | "ฟันเฟืองหนาม" | "纹棘轮" | "歯車模様" | "톱니바퀴" | "الگوی_چرخ‌دنده_خاردار" | "نقش_ترس_شائك" | "דוגמת_גלגל_קוצני" | "نقش_خاردار_گیئر" | "motif_engrenage_pointes" | "muster_stachelzahnrad" | "узор_шестерня_шипы" =>
6978            {
6979                let cx = self.arg_num(&args, 0, 0.)? as f32;
6980                let cy = self.arg_num(&args, 1, 0.)? as f32;
6981                let cz = self.arg_num(&args, 2, 0.)? as f32;
6982                let ux = self.arg_num(&args, 3, 1.)? as f32;
6983                let uy = self.arg_num(&args, 4, 0.)? as f32;
6984                let uz = self.arg_num(&args, 5, 0.)? as f32;
6985                let vx = self.arg_num(&args, 6, 0.)? as f32;
6986                let vy = self.arg_num(&args, 7, 0.)? as f32;
6987                let vz = self.arg_num(&args, 8, 1.)? as f32;
6988                let nt = self.arg_num(&args, 9, 12.)? as usize;
6989                let rb = self.arg_num(&args, 10, 1.)? as f32;
6990                let rs = self.arg_num(&args, 11, 1.3)? as f32;
6991                let rh = self.arg_num(&args, 12, 0.2)? as f32;
6992                let ns = self.arg_num(&args, 13, 6.)? as usize;
6993                let fr = self.arg_num(&args, 14, 0.)? as f32;
6994                let hue = self.arg_num(&args, 15, 0.)? as f32;
6995                let mut gfx = self.gfx.borrow_mut();
6996                let cam = gfx.camera.clone();
6997                crate::gfx::vtex::draw_spiked_cog(
6998                    &mut gfx.depth_queue,
6999                    &cam,
7000                    cx,
7001                    cy,
7002                    cz,
7003                    ux,
7004                    uy,
7005                    uz,
7006                    vx,
7007                    vy,
7008                    vz,
7009                    nt,
7010                    rb,
7011                    rs,
7012                    rh,
7013                    ns,
7014                    fr,
7015                    hue,
7016                );
7017                return Ok(Value::Unit);
7018            },
7019
7020            // vtex_torii(cx,cy,cz, ux,uy,uz, vx,vy,vz, width,height, fr,hue)
7021            "vtex_torii" | "ประตูโทริอิ" | "纹鸟居" | "鳥居" | "도리이" | "الگوی_توری_ژاپنی" | "نقش_توري" | "דוגמת_טוריי" | "نقش_توری_گیٹ" | "motif_torii" | "muster_torii" | "узор_тории" =>
7022            {
7023                let cx = self.arg_num(&args, 0, 0.)? as f32;
7024                let cy = self.arg_num(&args, 1, 0.)? as f32;
7025                let cz = self.arg_num(&args, 2, 0.)? as f32;
7026                let ux = self.arg_num(&args, 3, 1.)? as f32;
7027                let uy = self.arg_num(&args, 4, 0.)? as f32;
7028                let uz = self.arg_num(&args, 5, 0.)? as f32;
7029                let vx = self.arg_num(&args, 6, 0.)? as f32;
7030                let vy = self.arg_num(&args, 7, 0.)? as f32;
7031                let vz = self.arg_num(&args, 8, 1.)? as f32;
7032                let w = self.arg_num(&args, 9, 4.)? as f32;
7033                let h = self.arg_num(&args, 10, 5.)? as f32;
7034                let fr = self.arg_num(&args, 11, 0.)? as f32;
7035                let hue = self.arg_num(&args, 12, 0.)? as f32;
7036                let mut gfx = self.gfx.borrow_mut();
7037                let cam = gfx.camera.clone();
7038                crate::gfx::vtex::draw_torii(
7039                    &mut gfx.depth_queue,
7040                    &cam,
7041                    cx,
7042                    cy,
7043                    cz,
7044                    ux,
7045                    uy,
7046                    uz,
7047                    vx,
7048                    vy,
7049                    vz,
7050                    w,
7051                    h,
7052                    fr,
7053                    hue,
7054                );
7055                return Ok(Value::Unit);
7056            },
7057
7058            // vtex_pagoda(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_tiers,base_w,tier_h,taper,eave_out, fr,hue)
7059            "vtex_pagoda" | "เจดีย์" | "纹塔" | "塔" | "탑" | "الگوی_پاگودا" | "نقش_باغودا" | "דוגמת_פגודה" | "نقش_پگوڈا" | "motif_pagode" | "muster_pagode" | "узор_пагода" => {
7060                let cx = self.arg_num(&args, 0, 0.)? as f32;
7061                let cy = self.arg_num(&args, 1, 0.)? as f32;
7062                let cz = self.arg_num(&args, 2, 0.)? as f32;
7063                let ux = self.arg_num(&args, 3, 1.)? as f32;
7064                let uy = self.arg_num(&args, 4, 0.)? as f32;
7065                let uz = self.arg_num(&args, 5, 0.)? as f32;
7066                let vx = self.arg_num(&args, 6, 0.)? as f32;
7067                let vy = self.arg_num(&args, 7, 0.)? as f32;
7068                let vz = self.arg_num(&args, 8, 1.)? as f32;
7069                let nt = self.arg_num(&args, 9, 5.)? as usize;
7070                let bw = self.arg_num(&args, 10, 2.)? as f32;
7071                let th = self.arg_num(&args, 11, 1.)? as f32;
7072                let tp = self.arg_num(&args, 12, 0.72)? as f32;
7073                let eo = self.arg_num(&args, 13, 0.28)? as f32;
7074                let fr = self.arg_num(&args, 14, 0.)? as f32;
7075                let hue = self.arg_num(&args, 15, 0.)? as f32;
7076                let mut gfx = self.gfx.borrow_mut();
7077                let cam = gfx.camera.clone();
7078                crate::gfx::vtex::draw_pagoda(
7079                    &mut gfx.depth_queue,
7080                    &cam,
7081                    cx,
7082                    cy,
7083                    cz,
7084                    ux,
7085                    uy,
7086                    uz,
7087                    vx,
7088                    vy,
7089                    vz,
7090                    nt,
7091                    bw,
7092                    th,
7093                    tp,
7094                    eo,
7095                    fr,
7096                    hue,
7097                );
7098                return Ok(Value::Unit);
7099            },
7100
7101            // ══════════════════════════════════════════════════════════════════
7102            // AUDIO BUILTINS
7103            // ══════════════════════════════════════════════════════════════════
7104
7105            // audio_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth)
7106            #[cfg(not(target_arch = "wasm32"))]
7107            "audio_tone"
7108            | "เสียงโทน"
7109            | "音调"
7110            | "音調"
7111            | "음조"
7112            | "空间音"
7113            | "空間音"
7114            | "공간음" | "تن_صدا" | "نغمة" | "צליל" | "آواز_کا_سر" | "tonalité_audio" | "audioton" | "звук_тон" => {
7115                let idx = self.arg_num(&args, 0, 0.0)? as usize;
7116                let x = self.arg_num(&args, 1, 0.0)? as f32;
7117                let y = self.arg_num(&args, 2, 0.0)? as f32;
7118                let z = self.arg_num(&args, 3, 0.0)? as f32;
7119                let w = self.arg_num(&args, 4, 1.0)? as f32;
7120                let freq = self.arg_num(&args, 5, 220.0)? as f32;
7121                let amp = self.arg_num(&args, 6, 0.15)? as f32;
7122                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
7123                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
7124                if let Some(audio) = &self.audio {
7125                    audio.set_tone(
7126                        idx,
7127                        ToneParams { x, y, z, w, freq, amp, lfo_rate, lfo_depth },
7128                    );
7129                }
7130                return Ok(Value::Unit);
7131            },
7132
7133            #[cfg(not(target_arch = "wasm32"))]
7134            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" | "شنونده_صدا" | "مستمع_الصوت" | "מאזין_קול" | "آواز_سننے_والا" | "auditeur_audio" | "audiohörer" | "звук_слушатель" =>
7135            {
7136                let cry = self.arg_num(&args, 0, 1.0)? as f32;
7137                let sry = self.arg_num(&args, 1, 0.0)? as f32;
7138                let crx = self.arg_num(&args, 2, 1.0)? as f32;
7139                let srx = self.arg_num(&args, 3, 0.0)? as f32;
7140                if let Some(audio) = &self.audio {
7141                    audio.set_listener(cry, sry, crx, srx);
7142                }
7143                return Ok(Value::Unit);
7144            },
7145
7146            #[cfg(not(target_arch = "wasm32"))]
7147            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" | "موسیقی_پس‌زمینه" | "موسيقى_خلفية" | "מוזיקת_רקע" | "پس_منظر_موسیقی" | "musique_fond" | "hintergrundmusik" | "фоновая_музыка" =>
7148            {
7149                let path = match args.first() {
7150                    Some(Value::Str(s)) => s.clone(),
7151                    _ => return Ok(Value::Unit),
7152                };
7153                let vol = self.arg_num(&args, 1, 0.5)? as f32;
7154                if let Some(audio) = &self.audio {
7155                    audio.load_bgm(&path, vol);
7156                }
7157                return Ok(Value::Unit);
7158            },
7159
7160            #[cfg(not(target_arch = "wasm32"))]
7161            "audio_bgm_volume"
7162            | "ระดับเสียงพื้นหลัง"
7163            | "ระดับเพลงประกอบ"
7164            | "背景乐音量"
7165            | "BGM音量"
7166            | "배경음악음량" | "بلندی_موسیقی_پس‌زمینه" | "مستوى_موسيقى_الخلفية" | "עוצמת_מוזיקת_רקע" | "پس_منظر_موسیقی_شدت" | "volume_musique_fond" | "hintergrundmusiklautstärke" | "громкость_фоновой_музыки" => {
7167                let vol = self.arg_num(&args, 0, 0.5)? as f32;
7168                if let Some(audio) = &self.audio {
7169                    audio.set_bgm_volume(vol);
7170                }
7171                return Ok(Value::Unit);
7172            },
7173
7174            #[cfg(not(target_arch = "wasm32"))]
7175            "audio_volume" | "ระดับเสียง" | "音量" | "음량" | "بلندی_صدا" | "مستوى_الصوت" | "עוצמת_קול" | "آواز_کی_شدت" | "volume_audio" | "audiolautstärke" | "звук_громкость" => {
7176                let vol = self.arg_num(&args, 0, 0.7)? as f32;
7177                if let Some(audio) = &self.audio {
7178                    audio.set_master_volume(vol);
7179                }
7180                return Ok(Value::Unit);
7181            },
7182
7183            // WASM audio builtins — delegate to Web Audio API
7184            #[cfg(target_arch = "wasm32")]
7185            "audio_tone"
7186            | "เสียงโทน"
7187            | "音调"
7188            | "音調"
7189            | "음조"
7190            | "空间音"
7191            | "空間音"
7192            | "공간음" | "تن_صدا" | "نغمة" | "צליל" | "آواز_کا_سر" | "tonalité_audio" | "audioton" | "звук_тон" => {
7193                let idx = self.arg_num(&args, 0, 0.0)? as usize;
7194                let x = self.arg_num(&args, 1, 0.0)? as f32;
7195                let y = self.arg_num(&args, 2, 0.0)? as f32;
7196                let z = self.arg_num(&args, 3, 0.0)? as f32;
7197                let w = self.arg_num(&args, 4, 1.0)? as f32;
7198                let freq = self.arg_num(&args, 5, 220.0)? as f32;
7199                let amp = self.arg_num(&args, 6, 0.15)? as f32;
7200                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
7201                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
7202                crate::gfx::audio_web::set_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth);
7203                return Ok(Value::Unit);
7204            },
7205
7206            #[cfg(target_arch = "wasm32")]
7207            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" | "شنونده_صدا" | "مستمع_الصوت" | "מאזין_קול" | "آواز_سننے_والا" | "auditeur_audio" | "audiohörer" | "звук_слушатель" =>
7208            {
7209                let cry = self.arg_num(&args, 0, 1.0)? as f32;
7210                let sry = self.arg_num(&args, 1, 0.0)? as f32;
7211                let crx = self.arg_num(&args, 2, 1.0)? as f32;
7212                let srx = self.arg_num(&args, 3, 0.0)? as f32;
7213                crate::gfx::audio_web::set_listener(cry, sry, crx, srx);
7214                return Ok(Value::Unit);
7215            },
7216
7217            #[cfg(target_arch = "wasm32")]
7218            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" | "موسیقی_پس‌زمینه" | "موسيقى_خلفية" | "מוזיקת_רקע" | "پس_منظر_موسیقی" | "musique_fond" | "hintergrundmusik" | "фоновая_музыка" =>
7219            {
7220                let path = self.arg_str(&args, 0, "");
7221                let vol = self.arg_num(&args, 1, 0.5)? as f32;
7222                crate::gfx::audio_web::load_bgm(&path, vol);
7223                return Ok(Value::Unit);
7224            },
7225
7226            #[cfg(target_arch = "wasm32")]
7227            "audio_bgm_volume"
7228            | "ระดับเสียงพื้นหลัง"
7229            | "ระดับเพลงประกอบ"
7230            | "背景乐音量"
7231            | "BGM音量"
7232            | "배경음악음량" | "بلندی_موسیقی_پس‌زمینه" | "مستوى_موسيقى_الخلفية" | "עוצמת_מוזיקת_רקע" | "پس_منظر_موسیقی_شدت" | "volume_musique_fond" | "hintergrundmusiklautstärke" | "громкость_фоновой_музыки" => {
7233                let vol = self.arg_num(&args, 0, 0.5)? as f32;
7234                crate::gfx::audio_web::set_bgm_volume(vol);
7235                return Ok(Value::Unit);
7236            },
7237
7238            #[cfg(target_arch = "wasm32")]
7239            "audio_volume" | "ระดับเสียง" | "音量" | "음량" | "بلندی_صدا" | "مستوى_الصوت" | "עוצמת_קול" | "آواز_کی_شدت" | "volume_audio" | "audiolautstärke" | "звук_громкость" => {
7240                let vol = self.arg_num(&args, 0, 0.7)? as f32;
7241                crate::gfx::audio_web::set_master_volume(vol);
7242                return Ok(Value::Unit);
7243            },
7244
7245            // ── WASM sample load / play / stop / FX (Web Audio pool) ─────────
7246            #[cfg(target_arch = "wasm32")]
7247            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" | "بارگذاری_نمونه_صدا" | "تحميل_عينة_صوتية" | "טעינת_דגימת_קול" | "آواز_نمونہ_لوڈ" | "charger_échantillon" | "sample_laden" | "загрузить_семпл" =>
7248            {
7249                let path = self.arg_str(&args, 0, "");
7250                let resolved = self.wasm_resolve_source_path(&path);
7251                match wasm_fetch_bytes(&resolved)
7252                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
7253                {
7254                    Ok(t) => {
7255                        let id = crate::gfx::audio_web::add_sample(&t.stereo, t.channels, t.rate);
7256                        return Ok(Value::Number(id as f64));
7257                    },
7258                    Err(e) => {
7259                        eprintln!("audio_sample_load failed ({path}): {e}");
7260                        return Ok(Value::Number(-1.0));
7261                    },
7262                }
7263            },
7264            #[cfg(target_arch = "wasm32")]
7265            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" | "پخش_نمونه_صدا" | "تشغيل_عينة_صوتية" | "נגינת_דגימת_קול" | "آواز_نمونہ_چلاؤ" | "jouer_échantillon" | "sample_abspielen" | "играть_семпл" =>
7266            {
7267                let id = self.arg_num(&args, 0, 0.0)? as usize;
7268                let x = self.arg_num(&args, 1, 0.0)? as f32;
7269                let y = self.arg_num(&args, 2, 0.0)? as f32;
7270                let z = self.arg_num(&args, 3, 0.0)? as f32;
7271                // arg 4 is w (4th spatial dim) — ignored for 3-D panner
7272                let vol = self.arg_num(&args, 5, 1.0)? as f32;
7273                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
7274                crate::gfx::audio_web::play_sample(id, x, y, z, vol, looping);
7275                return Ok(Value::Number(0.0));
7276            },
7277            #[cfg(target_arch = "wasm32")]
7278            "audio_sample_stop"
7279            | "停止采样"
7280            | "サンプル停止"
7281            | "샘플정지"
7282            | "หยุดตัวอย่างเสียง"
7283            | "audio_fx_reverb"
7284            | "混响"
7285            | "リバーブ"
7286            | "리버브"
7287            | "เสียงก้อง"
7288            | "audio_fx_delay"
7289            | "回声"
7290            | "ディレイ効果"
7291            | "딜레이"
7292            | "เสียงสะท้อน"
7293            | "audio_fx_lowpass"
7294            | "低通滤波"
7295            | "ローパス"
7296            | "저역통과"
7297            | "กรองความถี่ต่ำ" | "توقف_نمونه_صدا" | "إيقاف_عينة_صوتية" | "עצירת_דגימת_קול" | "آواز_نمونہ_روکو" | "arrêter_échantillon" | "sample_stoppen" | "остановить_семпл" => {
7298                return Ok(Value::Unit);
7299            },
7300
7301            // ── รอหน้าต่าง() — block until window closed / Escape ──
7302            "รอหน้าต่าง" | "wait_window" | "gfx_wait" | "انتظار_پنجره" | "انتظر_النافذة" | "המתן_לחלון" | "ونڈو_انتظار" => {
7303                #[cfg(not(target_arch = "wasm32"))]
7304                loop {
7305                    let still_open = {
7306                        let gfx = self.gfx.borrow();
7307                        gfx.window
7308                            .as_ref()
7309                            .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
7310                            .unwrap_or(false)
7311                    };
7312                    if !still_open {
7313                        break;
7314                    }
7315                    let (buf, w, h) = {
7316                        let gfx = self.gfx.borrow();
7317                        (gfx.buffer.clone(), gfx.width, gfx.height)
7318                    };
7319                    let mut gfx = self.gfx.borrow_mut();
7320                    if let Some(win) = gfx.window.as_mut() {
7321                        if win.update_with_buffer(&buf, w, h).is_err() {
7322                            break;
7323                        }
7324                    }
7325                }
7326                return Ok(Value::Unit);
7327            },
7328
7329            // ── File I/O ──────────────────────────────────────────────────────
7330            "read_file" | "อ่านไฟล์" | "خواندن_فایل" | "اقرأ_الملف" | "קרא_קובץ" | "فائل_پڑھو" => {
7331                #[cfg(target_arch = "wasm32")]
7332                return Ok(Value::Str(String::new()));
7333                #[cfg(not(target_arch = "wasm32"))]
7334                {
7335                    let path = self.arg_str(&args, 0, "").replace('\\', "/");
7336                    return std::fs::read_to_string(&path)
7337                        .map(Value::Str)
7338                        .map_err(|e| EvalErr::from(format!("read_file '{path}': {e}")));
7339                }
7340            },
7341            // ── networking (TCP, 2-peer co-op) ───────────────────────────────
7342            #[cfg(not(target_arch = "wasm32"))]
7343            "net_host" | "เน็ตโฮสต์" | "میزبانی_شبکه" | "استضف_الشبكة" | "ארח_רשת" | "نیٹ_ہوسٹ" => {
7344                let port = self.arg_num(&args, 0, 7777.0)? as u16;
7345                net::host(port);
7346                return Ok(Value::Unit);
7347            },
7348            #[cfg(not(target_arch = "wasm32"))]
7349            "net_join" | "เน็ตจอย" | "پیوستن_شبکه" | "انضم_للشبكة" | "הצטרף_לרשת" | "نیٹ_شمولیت" => {
7350                let ip = self.arg_str(&args, 0, "127.0.0.1");
7351                let port = self.arg_num(&args, 1, 7777.0)? as u16;
7352                net::join(&ip, port);
7353                return Ok(Value::Unit);
7354            },
7355            #[cfg(not(target_arch = "wasm32"))]
7356            "net_send" | "เน็ตส่ง" | "ارسال_شبکه" | "أرسل_عبر_الشبكة" | "שלח_ברשת" | "نیٹ_بھیجو" => {
7357                let s = self.arg_str(&args, 0, "");
7358                net::send(&s);
7359                return Ok(Value::Unit);
7360            },
7361            #[cfg(not(target_arch = "wasm32"))]
7362            "net_recv" | "เน็ตรับ" | "دریافت_شبکه" | "استقبل_من_الشبكة" | "קבל_מרשת" | "نیٹ_وصول" => {
7363                return Ok(Value::Str(net::recv()));
7364            },
7365            #[cfg(not(target_arch = "wasm32"))]
7366            "net_status" | "เน็ตสถานะ" | "وضعیت_شبکه" | "حالة_الشبكة" | "סטטוס_רשת" | "نیٹ_حالت" => {
7367                return Ok(Value::Number(net::status() as f64));
7368            },
7369            #[cfg(not(target_arch = "wasm32"))]
7370            "net_recv_from" => {
7371                return Ok(Value::Str(net::recv_from()));
7372            },
7373            #[cfg(not(target_arch = "wasm32"))]
7374            "net_send_to" => {
7375                let id = self.arg_num(&args, 0, 0.0)? as u64;
7376                let s = self.arg_str(&args, 1, "");
7377                net::send_to(id, &s);
7378                return Ok(Value::Unit);
7379            },
7380            #[cfg(not(target_arch = "wasm32"))]
7381            "net_close" | "연결종료" => {
7382                net::close();
7383                return Ok(Value::Unit);
7384            },
7385            // ── LAN lobby discovery (UDP broadcast) ──
7386            #[cfg(not(target_arch = "wasm32"))]
7387            "net_announce" | "เน็ตประกาศ" | "اعلام_شبکه" | "أعلن_في_الشبكة" | "הכרז_ברשת" | "نیٹ_اعلان" => {
7388                let port = self.arg_num(&args, 0, 7778.0)? as u16;
7389                let info = self.arg_str(&args, 1, "");
7390                net::announce(port, &info);
7391                return Ok(Value::Unit);
7392            },
7393            #[cfg(not(target_arch = "wasm32"))]
7394            "net_announce_stop" | "เน็ตหยุดประกาศ" | "توقف_اعلام" | "أوقف_الإعلان" | "עצור_הכרזה" | "اعلان_روکو" => {
7395                net::announce_stop();
7396                return Ok(Value::Unit);
7397            },
7398            #[cfg(not(target_arch = "wasm32"))]
7399            "net_discover" | "เน็ตค้นหา" | "کشف_شبکه" | "اكتشف_الشبكة" | "גלה_רשת" | "نیٹ_دریافت" => {
7400                let port = self.arg_num(&args, 0, 7778.0)? as u16;
7401                return Ok(Value::Str(net::discover(port)));
7402            },
7403            #[cfg(not(target_arch = "wasm32"))]
7404            "net_test" | "เน็ตทดสอบ" | "آزمون_شبکه" | "اختبر_الشبكة" | "בדוק_רשת" | "نیٹ_ٹیسٹ" => {
7405                let port = self.arg_num(&args, 0, 7777.0)? as u16;
7406                return Ok(Value::Str(net::test_bind(port)));
7407            },
7408            // ── HTTP server (interpreter <-> async bridge, see runtime::web) ──
7409            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7410            "http_route" | "เว็บเส้นทาง" | "مسیر_HTTP" | "مسار_HTTP" | "נתיב_HTTP" | "HTTP_روٹ" => {
7411                let method = self.arg_str(&args, 0, "GET").to_uppercase();
7412                let path = self.arg_str(&args, 1, "/");
7413                let handler = args.get(2).cloned().unwrap_or(Value::Unit);
7414                self.http_routes.push((method, path, handler));
7415                return Ok(Value::Unit);
7416            },
7417            // Registers a directory to be served as raw bytes at `prefix` (fonts,
7418            // images, generated zips/PDFs) — bypasses the String-only Request/
7419            // Response bridge entirely, so binary files come through intact.
7420            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7421            "http_static" | "เว็บสแตติก" | "فایل_ایستای_HTTP" | "ملفات_HTTP_ثابتة" | "קבצים_סטטיים_HTTP" | "HTTP_مستقل_فائل" => {
7422                let prefix = self.arg_str(&args, 0, "/static");
7423                let dir = self.arg_str(&args, 1, "static");
7424                self.http_static_dirs.push((prefix, dir));
7425                return Ok(Value::Unit);
7426            },
7427            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7428            "http_serve" | "เว็บเสิร์ฟ" | "سرویس_HTTP" | "قدّم_HTTP" | "הגש_HTTP" | "HTTP_سرو" => {
7429                let host = self.arg_str(&args, 0, "127.0.0.1");
7430                let port = self.arg_num(&args, 1, 8080.0)? as u16;
7431                let routes = std::mem::take(&mut self.http_routes);
7432                let static_dirs = std::mem::take(&mut self.http_static_dirs);
7433                // No premature "listening" print here: ling_http::serve_http
7434                // (called from spawn_server's background thread) now prints
7435                // its own banner, but only after the socket is actually
7436                // bound — a more honest signal than printing right after
7437                // requesting the background thread be spawned.
7438                let rx = web::spawn_server(host.clone(), port, static_dirs);
7439                for pending in rx {
7440                    let matched = routes
7441                        .iter()
7442                        .find(|(m, p, _)| m == &pending.method && p == &pending.path);
7443                    let response = match matched {
7444                        Some((_, _, handler)) => {
7445                            let req_value = Value::Struct {
7446                                name: "Request".to_string(),
7447                                fields: vec![
7448                                    ("method".to_string(), Value::Str(pending.method.clone())),
7449                                    ("path".to_string(), Value::Str(pending.path.clone())),
7450                                    ("query".to_string(), Value::Str(pending.query.clone())),
7451                                    ("body".to_string(), Value::Str(pending.body.clone())),
7452                                    ("cookie".to_string(), Value::Str(pending.cookie.clone())),
7453                                    (
7454                                        "authorization".to_string(),
7455                                        Value::Str(pending.authorization.clone()),
7456                                    ),
7457                                    (
7458                                        "client_ip".to_string(),
7459                                        Value::Str(pending.client_ip.clone()),
7460                                    ),
7461                                ],
7462                            };
7463                            match self.call_value(handler.clone(), vec![req_value]) {
7464                                Ok(v) => web::value_to_response(&v),
7465                                Err(e) => web::HttpResponse {
7466                                    status: 500,
7467                                    content_type: "text/plain; charset=utf-8".to_string(),
7468                                    body: format!("handler error: {e:?}"),
7469                                    set_cookie: None,
7470                                    location: None,
7471                                },
7472                            }
7473                        },
7474                        None => web::HttpResponse {
7475                            status: 404,
7476                            content_type: "text/plain; charset=utf-8".to_string(),
7477                            body: "not found".to_string(),
7478                            set_cookie: None,
7479                            location: None,
7480                        },
7481                    };
7482                    let _ = pending.respond_to.send(response);
7483                }
7484                return Ok(Value::Unit);
7485            },
7486            // Fires a POST request on a background async runtime and returns a job
7487            // id immediately — for slow external calls (e.g. local Stable Diffusion
7488            // generation) that must not block http_serve's single-threaded loop.
7489            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7490            "http_post_async" | "เว็บโพสต์ไม่บล็อก" | "ارسال_ناهمگام_HTTP" | "أرسل_HTTP_غير_متزامن" | "שלח_HTTP_אסינכרוני" | "HTTP_غیر_ہمزمان_بھیجو" => {
7491                let url = self.arg_str(&args, 0, "");
7492                let body = self.arg_str(&args, 1, "");
7493                let content_type = self.arg_str(&args, 2, "application/json");
7494                let id = self.async_jobs.start_post(url, content_type, body);
7495                return Ok(Value::Str(id));
7496            },
7497            // Non-blocking poll: "" while the job named by http_post_async (or
7498            // sdai_generate_start) is still running, the result once it
7499            // completes — same job table, same builtin polls both.
7500            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7501            "http_job_poll" | "เว็บงานสำรวจ" | "بررسی_وظیفه_HTTP" | "استطلع_مهمة_HTTP" | "בדוק_משימת_HTTP" | "HTTP_کام_پول" => {
7502                let id = self.arg_str(&args, 0, "");
7503                return Ok(Value::Str(self.async_jobs.poll(&id).unwrap_or_default()));
7504            },
7505            // Starts an AUTOMATIC1111-compatible txt2img generation in the
7506            // background against `base_url` (e.g. "http://127.0.0.1:1342").
7507            // Poll with http_job_poll: the result is the plain base64 PNG
7508            // once ready, or a string starting with "ERROR:" on failure —
7509            // the JSON response itself is parsed in Rust (see
7510            // AsyncJobs::start_sdai_txt2img), since `.ling` has no JSON parser.
7511            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7512            "sdai_generate_start" | "เอสดีเอไอเริ่มสร้าง" | "شروع_تولید_هوش" | "ابدأ_توليد_الذكاء" | "התחל_יצירת_בינה" | "اے_آئی_تخلیق_شروع" => {
7513                let base_url = self.arg_str(&args, 0, "http://127.0.0.1:1342");
7514                let prompt = self.arg_str(&args, 1, "");
7515                let width = self.arg_num(&args, 2, 512.0)? as u32;
7516                let height = self.arg_num(&args, 3, 512.0)? as u32;
7517                let id = self.async_jobs.start_sdai_txt2img(base_url, prompt, width, height);
7518                return Ok(Value::Str(id));
7519            },
7520            // ── query_param("q=a&page=2", "q", "") → "a" (URL-decoded) ──
7521            "query_param" | "พารามิเตอร์" | "پارامتر_پرسوجو" | "معامل_الاستعلام" | "פרמטר_שאילתה" | "کوئری_پیرامیٹر" => {
7522                let qs = self.arg_str(&args, 0, "");
7523                let name = self.arg_str(&args, 1, "");
7524                let default = self.arg_str(&args, 2, "");
7525                let mut found = default;
7526                for pair in qs.split('&') {
7527                    let mut it = pair.splitn(2, '=');
7528                    if it.next().unwrap_or("") == name {
7529                        found = url_decode(it.next().unwrap_or(""));
7530                        break;
7531                    }
7532                }
7533                return Ok(Value::Str(found));
7534            },
7535            // ── cookie_get("sid=abc; x=1", "sid", "") → "abc" ──
7536            "cookie_get" | "รับคุกกี้" | "دریافت_کوکی" | "اجلب_الكعكة" | "קבל_עוגייה" | "کوکی_حاصل_کرو" => {
7537                let header = self.arg_str(&args, 0, "");
7538                let name = self.arg_str(&args, 1, "");
7539                let default = self.arg_str(&args, 2, "");
7540                let mut found = default;
7541                for pair in header.split(';') {
7542                    let p = pair.trim();
7543                    let mut it = p.splitn(2, '=');
7544                    if it.next().unwrap_or("") == name {
7545                        found = it.next().unwrap_or("").to_string();
7546                        break;
7547                    }
7548                }
7549                return Ok(Value::Str(found));
7550            },
7551            // ── html_escape(s) — & < > " ' → entities, for echoing user input ──
7552            "html_escape" | "กันเอชทีเอ็มแอล" | "فرار_HTML" | "أفلت_HTML" | "בריחת_HTML" | "HTML_ایسکیپ" => {
7553                let s = self.arg_str(&args, 0, "");
7554                return Ok(Value::Str(
7555                    s.replace('&', "&amp;")
7556                        .replace('<', "&lt;")
7557                        .replace('>', "&gt;")
7558                        .replace('"', "&quot;")
7559                        .replace('\'', "&#39;"),
7560                ));
7561            },
7562            // json_escape(s) — escape a string for embedding inside a JSON
7563            // string literal (", \, and control chars). Needed because the
7564            // registry builds JSON API responses by concatenation; without
7565            // this a value containing " or \ breaks or injects into the JSON.
7566            "json_escape" | "หนีเจสัน" | "فرار_JSON" | "أفلت_JSON" | "בריחת_JSON" | "JSON_ایسکیپ" => {
7567                let s = self.arg_str(&args, 0, "");
7568                let mut out = String::with_capacity(s.len() + 8);
7569                for c in s.chars() {
7570                    match c {
7571                        '"' => out.push_str("\\\""),
7572                        '\\' => out.push_str("\\\\"),
7573                        '\n' => out.push_str("\\n"),
7574                        '\r' => out.push_str("\\r"),
7575                        '\t' => out.push_str("\\t"),
7576                        c if (c as u32) < 0x20 => {
7577                            out.push_str(&format!("\\u{:04x}", c as u32))
7578                        },
7579                        c => out.push(c),
7580                    }
7581                }
7582                return Ok(Value::Str(out));
7583            },
7584            // ── CLI arguments: cli_arg("port", "8080") reads `--port 6688` ──
7585            "cli_arg" | "อาร์กิวเมนต์" | "آرگومان_خط‌فرمان" | "معامل_سطر_الأوامر" | "ארגומנט_שורת_פקודה" | "سی_ایل_آئی_دلیل" => {
7586                let name = self.arg_str(&args, 0, "");
7587                let default = self.arg_str(&args, 1, "");
7588                let flag = format!("--{name}");
7589                let argv: Vec<String> = std::env::args().collect();
7590                let found = argv
7591                    .iter()
7592                    .position(|a| a == &flag)
7593                    .and_then(|i| argv.get(i + 1))
7594                    .cloned()
7595                    .unwrap_or(default);
7596                return Ok(Value::Str(found));
7597            },
7598            // ── SQLite (rusqlite, synchronous — matches the interpreter) ──
7599            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7600            "db_open" | "ฐานข้อมูลเปิด" | "باز_کردن_پایگاه_داده" | "افتح_قاعدة_البيانات" | "פתח_מסד_נתונים" | "ڈیٹا_بیس_کھولو" => {
7601                let path = self.arg_str(&args, 0, "app.db");
7602                let conn = ling_http::rusqlite::Connection::open(&path)
7603                    .map_err(|e| EvalErr::from(format!("db_open '{path}': {e}")))?;
7604                let _ = conn.execute_batch("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;");
7605                self.db = Some(conn);
7606                return Ok(Value::Unit);
7607            },
7608            // db_exec(sql, ...params) → rows affected. Params bind positionally
7609            // (?1, ?2, ...): numbers as REAL, bools as 0/1, everything else TEXT.
7610            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7611            "db_exec" | "ฐานข้อมูลรัน" | "اجرای_پایگاه_داده" | "نفّذ_في_قاعدة_البيانات" | "בצע_במסד_נתונים" | "ڈیٹا_بیس_عمل" => {
7612                let sql = self.arg_str(&args, 0, "");
7613                let params = values_to_sql_params(&args[1.min(args.len())..]);
7614                let conn = self
7615                    .db
7616                    .as_ref()
7617                    .ok_or_else(|| EvalErr::from("db_exec: call db_open first".to_string()))?;
7618                let n = conn
7619                    .execute(
7620                        &sql,
7621                        ling_http::rusqlite::params_from_iter(params.iter()),
7622                    )
7623                    .map_err(|e| EvalErr::from(format!("db_exec: {e}\n  sql: {sql}")))?;
7624                return Ok(Value::Number(n as f64));
7625            },
7626            // db_query(sql, ...params) → List of Row structs (row.column_name).
7627            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7628            "db_query" | "ฐานข้อมูลถาม" | "پرسوجوی_پایگاه_داده" | "استعلم_قاعدة_البيانات" | "שאילתת_מסד_נתונים" | "ڈیٹا_بیس_سوال" => {
7629                let sql = self.arg_str(&args, 0, "");
7630                let params = values_to_sql_params(&args[1.min(args.len())..]);
7631                let conn = self
7632                    .db
7633                    .as_ref()
7634                    .ok_or_else(|| EvalErr::from("db_query: call db_open first".to_string()))?;
7635                let mut stmt = conn
7636                    .prepare(&sql)
7637                    .map_err(|e| EvalErr::from(format!("db_query: {e}\n  sql: {sql}")))?;
7638                let col_names: Vec<String> =
7639                    stmt.column_names().iter().map(|s| s.to_string()).collect();
7640                let mut rows = stmt
7641                    .query(ling_http::rusqlite::params_from_iter(params.iter()))
7642                    .map_err(|e| EvalErr::from(format!("db_query: {e}")))?;
7643                let mut out = Vec::new();
7644                while let Some(row) = rows
7645                    .next()
7646                    .map_err(|e| EvalErr::from(format!("db_query row: {e}")))?
7647                {
7648                    let mut fields = Vec::with_capacity(col_names.len());
7649                    for (i, col) in col_names.iter().enumerate() {
7650                        use ling_http::rusqlite::types::ValueRef;
7651                        let v = match row.get_ref(i) {
7652                            Ok(ValueRef::Null) => Value::Str(String::new()),
7653                            Ok(ValueRef::Integer(n)) => Value::Number(n as f64),
7654                            Ok(ValueRef::Real(n)) => Value::Number(n),
7655                            Ok(ValueRef::Text(t)) => {
7656                                Value::Str(String::from_utf8_lossy(t).into_owned())
7657                            },
7658                            Ok(ValueRef::Blob(b)) =>
7659
7660                            {
7661                                use base64::Engine as _;
7662                                Value::Str(base64::engine::general_purpose::STANDARD.encode(b))
7663                            },
7664                            Err(_) => Value::Str(String::new()),
7665                        };
7666                        fields.push((col.clone(), v));
7667                    }
7668                    out.push(Value::Struct { name: "Row".to_string(), fields });
7669                }
7670                return Ok(Value::List(Rc::new(out)));
7671            },
7672            // ── gamepad (gilrs) ──
7673            #[cfg(not(target_arch = "wasm32"))]
7674            "gamepad_poll" | "จอยโพล" | "بررسی_دسته_بازی" | "استطلع_يد_اللعب" | "בדוק_בקר_משחק" | "گیم_پیڈ_پول_کرو" => {
7675                gamepad::poll();
7676                return Ok(Value::Unit);
7677            },
7678            #[cfg(not(target_arch = "wasm32"))]
7679            "gamepad_button" | "จอยปุ่ม" | "دکمه_دسته_بازی" | "زر_يد_اللعب" | "כפתור_בקר_משחק" | "گیم_پیڈ_بٹن_دبایا" => {
7680                let name = self.arg_str(&args, 0, "");
7681                return Ok(Value::Number(if gamepad::button(&name) {
7682                    1.0
7683                } else {
7684                    0.0
7685                }));
7686            },
7687            #[cfg(not(target_arch = "wasm32"))]
7688            "gamepad_axis" | "จอยแกน" | "محور_دسته_بازی" | "محور_يد_اللعب" | "ציר_בקר_משחק" | "گیم_پیڈ_محور" => {
7689                let name = self.arg_str(&args, 0, "");
7690                return Ok(Value::Number(gamepad::axis(&name) as f64));
7691            },
7692            #[cfg(not(target_arch = "wasm32"))]
7693            "gamepad_rumble" | "จอยสั่น" | "لرزش_دسته_بازی" | "اهتزاز_يد_اللعب" | "רטט_בקר_משחק" | "گیم_پیڈ_لرزش" => {
7694                let low = self.arg_num(&args, 0, 0.0)? as f32;
7695                let high = self.arg_num(&args, 1, 0.0)? as f32;
7696                let ms = self.arg_num(&args, 2, 200.0)? as u32;
7697                gamepad::rumble(low, high, ms);
7698                return Ok(Value::Unit);
7699            },
7700            #[cfg(not(target_arch = "wasm32"))]
7701            "gamepad_list" | "จอยรายการ" | "فهرست_دسته‌های_بازی" | "قائمة_أيدي_اللعب" | "רשימת_בקרי_משחק" | "گیم_پیڈ_فہرست" => {
7702                return Ok(Value::Str(gamepad::list()));
7703            },
7704            #[cfg(not(target_arch = "wasm32"))]
7705            "gamepad_any" | "จอยใดๆ" | "هر_دسته_بازی" | "أي_يد_لعب" | "בקר_כלשהו" | "کوئی_بھی_گیم_پیڈ" => {
7706                return Ok(Value::Number(if gamepad::any_button() { 1.0 } else { 0.0 }));
7707            },
7708            // wasm32: gamepad not available — return safe no-op values
7709            #[cfg(target_arch = "wasm32")]
7710            "gamepad_poll" | "จอยโพล" | "gamepad_rumble" | "จอยสั่น" | "بررسی_دسته_بازی" | "استطلع_يد_اللعب" | "בדוק_בקר_משחק" | "گیم_پیڈ_پول_کرو" => {
7711                return Ok(Value::Unit);
7712            },
7713            #[cfg(target_arch = "wasm32")]
7714            "gamepad_button" | "จอยปุ่ม" | "gamepad_axis" | "จอยแกน" | "gamepad_any" | "จอยใดๆ" | "دکمه_دسته_بازی" | "زر_يد_اللعب" | "כפתור_בקר_משחק" | "گیم_پیڈ_بٹن" =>
7715            {
7716                return Ok(Value::Number(0.0));
7717            },
7718            #[cfg(target_arch = "wasm32")]
7719            "gamepad_list" | "จอยรายการ" | "فهرست_دسته‌های_بازی" | "قائمة_أيدي_اللعب" | "רשימת_בקרי_משחק" | "گیم_پیڈ_فہرست" => {
7720                return Ok(Value::Str(String::new()));
7721            },
7722
7723            // ── game AI: neural networks ─────────────────────────────────────
7724            // nn_new(inputs[, seed]) → handle
7725            #[cfg(not(target_arch = "wasm32"))]
7726            "nn_new" | "建神经网" | "ニューラル作成" | "신경망생성" | "สร้างโครงข่าย" | "شبکه_جدید" | "شبكة_جديدة" | "רשת_חדשה" | "نئی_نیورل_نیٹ" | "nouveau_réseau" | "neues_netz" | "новая_сеть" =>
7727            {
7728                let n_in = self.arg_num(&args, 0, 1.0)?.max(0.0) as usize;
7729                let seed = self.arg_num(&args, 1, 1.0)? as u64;
7730                return Ok(Value::Number(ai::nn_new(n_in, seed) as f64));
7731            },
7732            // nn_dense(handle, units[, activation]) — append a layer
7733            #[cfg(not(target_arch = "wasm32"))]
7734            "nn_dense" | "密集层" | "密層追加" | "밀집층" | "ชั้นหนาแน่น" | "لایه_متراکم" | "طبقة_كثيفة" | "שכבה_צפופה" | "ڈینس_لیئر" | "réseau_dense" | "netz_dicht" | "плотная_сеть" =>
7735            {
7736                let id = self.arg_num(&args, 0, -1.0)? as i64;
7737                let units = self.arg_num(&args, 1, 1.0)?.max(1.0) as usize;
7738                let act = self.arg_str(&args, 2, "relu");
7739                ai::nn_dense(id, units, &act);
7740                return Ok(Value::Unit);
7741            },
7742            // nn_forward(handle, [inputs]) → [outputs]
7743            #[cfg(not(target_arch = "wasm32"))]
7744            "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" | "پیش‌روی_شبکه" | "تمرير_أمامي" | "העברה_קדימה" | "فارورڈ_پاس" | "propager_réseau" | "netz_vorwärts" | "прямой_проход_сети" =>
7745            {
7746                let id = self.arg_num(&args, 0, -1.0)? as i64;
7747                let input = self.arg_list_f32(&args, 1);
7748                let out = ai::nn_forward(id, &input);
7749                return Ok(Value::List(Rc::new(
7750                    out.into_iter().map(|v| Value::Number(v as f64)).collect(),
7751                )));
7752            },
7753            // nn_train(handle, [inputs], [targets][, lr]) → loss
7754            #[cfg(not(target_arch = "wasm32"))]
7755            "nn_train" | "训练网" | "ニューラル学習" | "신경망학습" | "ฝึกโครงข่าย" | "آموزش_شبکه" | "درّب_الشبكة" | "אמן_רשת" | "نیٹ_ٹریننگ" | "entraîner_réseau" | "netz_trainieren" | "обучить_сеть" =>
7756            {
7757                let id = self.arg_num(&args, 0, -1.0)? as i64;
7758                let input = self.arg_list_f32(&args, 1);
7759                let target = self.arg_list_f32(&args, 2);
7760                let lr = self.arg_num(&args, 3, 0.01)? as f32;
7761                return Ok(Value::Number(ai::nn_train(id, &input, &target, lr) as f64));
7762            },
7763            // nn_save(handle, path) → bool
7764            #[cfg(not(target_arch = "wasm32"))]
7765            "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" | "ذخیره_شبکه" | "احفظ_الشبكة" | "שמור_רשת" | "نیٹ_محفوظ_کرو" | "sauvegarder_réseau" | "netz_speichern" | "сохранить_сеть" =>
7766            {
7767                let id = self.arg_num(&args, 0, -1.0)? as i64;
7768                let path = self.arg_str(&args, 1, "model.lnn");
7769                return Ok(Value::Bool(ai::nn_save(id, &path)));
7770            },
7771            // nn_load(path) → handle (-1 on failure)
7772            #[cfg(not(target_arch = "wasm32"))]
7773            "nn_load" | "载入网" | "網読込" | "신경망불러오기" | "โหลดโครงข่าย" | "بارگذاری_شبکه" | "حمّل_الشبكة" | "טען_רשת" | "نیٹ_لوڈ" | "charger_réseau" | "netz_laden" | "загрузить_сеть" =>
7774            {
7775                let path = self.arg_str(&args, 0, "model.lnn");
7776                return Ok(Value::Number(ai::nn_load(&path) as f64));
7777            },
7778
7779            // ── game AI: behavior trees ──────────────────────────────────────
7780            // bt_build(dsl_string) → handle
7781            #[cfg(not(target_arch = "wasm32"))]
7782            "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" | "ساخت_درخت_رفتار" | "ابنِ_شجرة_السلوك" | "בנה_עץ_התנהגות" | "بی_ٹی_تعمیر" | "construire_arbre_comportement" | "verhaltensbaum_bauen" | "построить_дерево_поведения" =>
7783            {
7784                let spec = self.arg_str(&args, 0, "");
7785                return Ok(Value::Number(ai::bt_build(&spec) as f64));
7786            },
7787            // bt_set(handle, key, value) — set a blackboard fact
7788            #[cfg(not(target_arch = "wasm32"))]
7789            "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" | "تنظیم_واقعیت" | "عيّن_حقيقة" | "קבע_עובדה" | "بی_ٹی_سیٹ" | "définir_arbre_comportement" | "verhaltensbaum_setzen" | "задать_дерево_поведения" =>
7790            {
7791                let id = self.arg_num(&args, 0, -1.0)? as i64;
7792                let key = self.arg_str(&args, 1, "");
7793                let val = self.arg_num(&args, 2, 0.0)? as f32;
7794                ai::bt_set(id, &key, val);
7795                return Ok(Value::Unit);
7796            },
7797            // bt_tick(handle) → chosen action name ("" if none)
7798            #[cfg(not(target_arch = "wasm32"))]
7799            "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" | "تیک_درخت_رفتار" | "نبضة_شجرة_السلوك" | "טיק_עץ_התנהגות" | "بی_ٹی_ٹک" | "tick_arbre_comportement" | "verhaltensbaum_tick" | "тик_дерева_поведения" =>
7800            {
7801                let id = self.arg_num(&args, 0, -1.0)? as i64;
7802                return Ok(Value::Str(ai::bt_tick(id)));
7803            },
7804            // bt_status(handle) → 0 fail / 1 success / 2 running
7805            #[cfg(not(target_arch = "wasm32"))]
7806            "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" | "وضعیت_درخت_رفتار" | "حالة_شجرة_السلوك" | "סטטוס_עץ_התנהגות" | "بی_ٹی_حالت" | "statut_arbre_comportement" | "verhaltensbaum_status" | "статус_дерева_поведения" =>
7807            {
7808                let id = self.arg_num(&args, 0, -1.0)? as i64;
7809                return Ok(Value::Number(ai::bt_status(id) as f64));
7810            },
7811
7812            // ── game AI: miniature dialog LLM ────────────────────────────────
7813            // dialog_new([ctx, embed, hidden, seed]) → handle
7814            #[cfg(not(target_arch = "wasm32"))]
7815            "dialog_new" | "建对话模型" | "対話モデル作成" | "대화모델생성" | "สร้างโมเดลสนทนา" | "مدل_گفتگوی_جدید" | "نموذج_حوار_جديد" | "מודל_דיאלוג_חדש" | "نیا_مکالمہ_ماڈل" | "nouveau_dialogue" | "neuer_dialog" | "новый_диалог" =>
7816            {
7817                let ctx = self.arg_num(&args, 0, 3.0)?.max(1.0) as usize;
7818                let embed = self.arg_num(&args, 1, 32.0)?.max(1.0) as usize;
7819                let hidden = self.arg_num(&args, 2, 64.0)?.max(1.0) as usize;
7820                let seed = self.arg_num(&args, 3, 1.0)? as u64;
7821                return Ok(Value::Number(
7822                    ai::dialog_new(ctx, embed, hidden, seed) as f64
7823                ));
7824            },
7825            // dialog_learn(handle, text) — add one utterance to the corpus
7826            #[cfg(not(target_arch = "wasm32"))]
7827            "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" | "یادگیری_گفتگو" | "تعلّم_الحوار" | "למד_דיאלוג" | "مکالمہ_سیکھو" | "apprendre_dialogue" | "dialog_lernen" | "обучить_диалог" =>
7828            {
7829                let id = self.arg_num(&args, 0, -1.0)? as i64;
7830                let text = self.arg_str(&args, 1, "");
7831                ai::dialog_learn(id, &text);
7832                return Ok(Value::Unit);
7833            },
7834            // dialog_load(handle, path) → lines added (-1 on error)
7835            #[cfg(not(target_arch = "wasm32"))]
7836            "dialog_load" | "对话载入" | "対話読込" | "대화불러오기" | "โหลดชุดสนทนา" | "بارگذاری_مجموعه_گفتگو" | "حمّل_مجموعة_الحوار" | "טען_מערך_דיאלוג" | "مکالمہ_مجموعہ_لوڈ" | "charger_dialogue" | "dialog_laden" | "загрузить_диалог" =>
7837            {
7838                let id = self.arg_num(&args, 0, -1.0)? as i64;
7839                let path = self.arg_str(&args, 1, "");
7840                return Ok(Value::Number(ai::dialog_load(id, &path) as f64));
7841            },
7842            // dialog_train(handle[, epochs, lr]) → loss
7843            #[cfg(not(target_arch = "wasm32"))]
7844            "dialog_train" | "对话训练" | "対話訓練" | "대화훈련" | "ฝึกสนทนา" | "آموزش_گفتگو" | "درّب_الحوار" | "אמן_דיאלוג" | "مکالمہ_ٹریننگ" | "entraîner_dialogue" | "dialog_trainieren" | "тренировать_диалог" =>
7845            {
7846                let id = self.arg_num(&args, 0, -1.0)? as i64;
7847                let epochs = self.arg_num(&args, 1, 20.0)?.max(1.0) as usize;
7848                let lr = self.arg_num(&args, 2, 0.1)? as f32;
7849                return Ok(Value::Number(ai::dialog_train(id, epochs, lr) as f64));
7850            },
7851            // dialog_say(handle, prompt[, max_tokens, temperature]) → reply text
7852            #[cfg(not(target_arch = "wasm32"))]
7853            "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" | "بگو" | "قل" | "אמור" | "کہو" | "dire_dialogue" | "dialog_sagen" | "сказать_диалог" =>
7854            {
7855                let id = self.arg_num(&args, 0, -1.0)? as i64;
7856                let prompt = self.arg_str(&args, 1, "");
7857                let max = self.arg_num(&args, 2, 24.0)?.max(1.0) as usize;
7858                let temp = self.arg_num(&args, 3, 0.8)? as f32;
7859                return Ok(Value::Str(ai::dialog_say(id, &prompt, max, temp)));
7860            },
7861            // dialog_save(handle, path) → bool
7862            #[cfg(not(target_arch = "wasm32"))]
7863            "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" | "ذخیره_مدل_گفتگو" | "احفظ_نموذج_الحوار" | "שמור_מודל_דיאלוג" | "مکالمہ_ماڈل_محفوظ" | "sauvegarder_dialogue" | "dialog_speichern" | "сохранить_диалог" =>
7864            {
7865                let id = self.arg_num(&args, 0, -1.0)? as i64;
7866                let path = self.arg_str(&args, 1, "model.llm");
7867                return Ok(Value::Bool(ai::dialog_save(id, &path)));
7868            },
7869            // dialog_load_model(path) → handle (-1 on failure)
7870            #[cfg(not(target_arch = "wasm32"))]
7871            "dialog_load_model"
7872            | "对话载模"
7873            | "対話モデル読込"
7874            | "대화모델불러오기"
7875            | "โหลดโมเดลสนทนา" | "بارگذاری_مدل_گفتگو" | "حمّل_نموذج_الحوار" | "טען_מודל_דיאלוג" | "مکالمہ_ماڈل_لوڈ" | "charger_modèle_dialogue" | "dialog_modell_laden" | "загрузить_модель_диалога" => {
7876                let path = self.arg_str(&args, 0, "model.llm");
7877                return Ok(Value::Number(ai::dialog_load_model(&path) as f64));
7878            },
7879
7880            // ── in-process chat-completion (chat.ling-lang.org, Milestone 2) ──
7881            // Real transformer inference (candle-transformers, GGUF, CUDA) —
7882            // distinct from the miniature dialog_* LM above. Generation runs
7883            // on a background thread; these builtins only ever start/poll a
7884            // job, never block http_serve's single-threaded dispatch loop.
7885            // llm_load(gguf_path, tokenizer_path[, device_index]) → handle (-1 on failure)
7886            #[cfg(all(not(target_arch = "wasm32"), feature = "llm"))]
7887            "llm_load" => {
7888                let gguf_path = self.arg_str(&args, 0, "");
7889                let tokenizer_path = self.arg_str(&args, 1, "");
7890                let device_index = self.arg_num(&args, 2, 0.0)? as i64;
7891                return Ok(Value::Number(
7892                    llm::llm_load(&gguf_path, &tokenizer_path, device_index) as f64
7893                ));
7894            },
7895            // llm_generate_start(handle, system_prompt, history[, max_tokens, temperature, top_p]) → job_id ("" on failure)
7896            #[cfg(all(not(target_arch = "wasm32"), feature = "llm"))]
7897            "llm_generate_start" => {
7898                let handle = self.arg_num(&args, 0, -1.0)? as i64;
7899                let system_prompt = self.arg_str(&args, 1, "");
7900                let history = self.arg_str(&args, 2, "");
7901                let max_tokens = self.arg_num(&args, 3, 512.0)? as i64;
7902                let temperature = self.arg_num(&args, 4, 0.7)?;
7903                let top_p = self.arg_num(&args, 5, 0.9)?;
7904                return Ok(Value::Str(llm::llm_generate_start(
7905                    handle, &system_prompt, &history, max_tokens, temperature, top_p,
7906                )));
7907            },
7908            // llm_generate_poll(job_id) → text generated so far
7909            #[cfg(all(not(target_arch = "wasm32"), feature = "llm"))]
7910            "llm_generate_poll" => {
7911                let job_id = self.arg_str(&args, 0, "");
7912                return Ok(Value::Str(llm::llm_generate_poll(&job_id)));
7913            },
7914            // llm_generate_done(job_id) → bool
7915            #[cfg(all(not(target_arch = "wasm32"), feature = "llm"))]
7916            "llm_generate_done" => {
7917                let job_id = self.arg_str(&args, 0, "");
7918                return Ok(Value::Bool(llm::llm_generate_done(&job_id)));
7919            },
7920            // llm_job_status(job_id) → "pending" | "tool_call" | "done"
7921            #[cfg(all(not(target_arch = "wasm32"), feature = "llm"))]
7922            "llm_job_status" => {
7923                let job_id = self.arg_str(&args, 0, "");
7924                return Ok(Value::Str(llm::llm_job_status(&job_id)));
7925            },
7926            // llm_job_tool_name(job_id) → "" until Milestone 5 lands
7927            #[cfg(all(not(target_arch = "wasm32"), feature = "llm"))]
7928            "llm_job_tool_name" => {
7929                let job_id = self.arg_str(&args, 0, "");
7930                return Ok(Value::Str(llm::llm_job_tool_name(&job_id)));
7931            },
7932            // llm_job_provide_tool_result(job_id, result_text) → bool (always false until Milestone 5)
7933            #[cfg(all(not(target_arch = "wasm32"), feature = "llm"))]
7934            "llm_job_provide_tool_result" => {
7935                let job_id = self.arg_str(&args, 0, "");
7936                let result_text = self.arg_str(&args, 1, "");
7937                return Ok(Value::Bool(llm::llm_job_provide_tool_result(&job_id, &result_text)));
7938            },
7939            // llm_job_cancel(job_id)
7940            #[cfg(all(not(target_arch = "wasm32"), feature = "llm"))]
7941            "llm_job_cancel" => {
7942                let job_id = self.arg_str(&args, 0, "");
7943                llm::llm_job_cancel(&job_id);
7944                return Ok(Value::Unit);
7945            },
7946
7947            // ── Google OAuth sign-in (chat.ling-lang.org) ──────────────────
7948            // Exchanging a code for a token and fetching the profile are both
7949            // outbound HTTPS calls, so this runs on a background thread and
7950            // is polled the same non-blocking way as http_post_async, above.
7951            // oauth_google_start(code, client_id, client_secret, redirect_uri) → job_id
7952            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7953            "oauth_google_start" => {
7954                let code = self.arg_str(&args, 0, "");
7955                let client_id = self.arg_str(&args, 1, "");
7956                let client_secret = self.arg_str(&args, 2, "");
7957                let redirect_uri = self.arg_str(&args, 3, "");
7958                let id = self.oauth_jobs.start_google_login(code, client_id, client_secret, redirect_uri);
7959                return Ok(Value::Str(id));
7960            },
7961            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7962            "oauth_google_done" => {
7963                let id = self.arg_str(&args, 0, "");
7964                return Ok(Value::Bool(self.oauth_jobs.done(&id)));
7965            },
7966            // "" while pending or on success; the error message on failure.
7967            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7968            "oauth_google_error" => {
7969                let id = self.arg_str(&args, 0, "");
7970                return Ok(Value::Str(self.oauth_jobs.error(&id)));
7971            },
7972            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7973            "oauth_google_sub" => {
7974                let id = self.arg_str(&args, 0, "");
7975                return Ok(Value::Str(self.oauth_jobs.sub(&id)));
7976            },
7977            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7978            "oauth_google_email" => {
7979                let id = self.arg_str(&args, 0, "");
7980                return Ok(Value::Str(self.oauth_jobs.email(&id)));
7981            },
7982            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7983            "oauth_google_name" => {
7984                let id = self.arg_str(&args, 0, "");
7985                return Ok(Value::Str(self.oauth_jobs.name(&id)));
7986            },
7987
7988            // ── image understanding (chat.ling-lang.org attach-an-image) ──
7989            // Moondream2 (quantized). Same background-job pattern as llm_*.
7990            // vision_load(gguf_path, tokenizer_path[, device_index]) → handle (-1 on failure)
7991            #[cfg(all(not(target_arch = "wasm32"), feature = "vision"))]
7992            "vision_load" => {
7993                let gguf_path = self.arg_str(&args, 0, "");
7994                let tokenizer_path = self.arg_str(&args, 1, "");
7995                let device_index = self.arg_num(&args, 2, 0.0)? as i64;
7996                return Ok(Value::Number(
7997                    vision::vision_load(&gguf_path, &tokenizer_path, device_index) as f64
7998                ));
7999            },
8000            // vision_analyze_start(handle, image_base64, question[, max_tokens, temperature, top_p]) → job_id ("" on failure)
8001            #[cfg(all(not(target_arch = "wasm32"), feature = "vision"))]
8002            "vision_analyze_start" => {
8003                let handle = self.arg_num(&args, 0, -1.0)? as i64;
8004                let image_base64 = self.arg_str(&args, 1, "");
8005                let question = self.arg_str(&args, 2, "Describe this image.");
8006                let max_tokens = self.arg_num(&args, 3, 256.0)? as i64;
8007                let temperature = self.arg_num(&args, 4, 0.7)?;
8008                let top_p = self.arg_num(&args, 5, 0.9)?;
8009                return Ok(Value::Str(vision::vision_analyze_start(
8010                    handle, &image_base64, &question, max_tokens, temperature, top_p,
8011                )));
8012            },
8013            #[cfg(all(not(target_arch = "wasm32"), feature = "vision"))]
8014            "vision_analyze_poll" => {
8015                let job_id = self.arg_str(&args, 0, "");
8016                return Ok(Value::Str(vision::vision_analyze_poll(&job_id)));
8017            },
8018            #[cfg(all(not(target_arch = "wasm32"), feature = "vision"))]
8019            "vision_analyze_done" => {
8020                let job_id = self.arg_str(&args, 0, "");
8021                return Ok(Value::Bool(vision::vision_analyze_done(&job_id)));
8022            },
8023            // "" unless analysis failed (bad image data, decode error, etc).
8024            #[cfg(all(not(target_arch = "wasm32"), feature = "vision"))]
8025            "vision_analyze_error" => {
8026                let job_id = self.arg_str(&args, 0, "");
8027                return Ok(Value::Str(vision::vision_analyze_error(&job_id)));
8028            },
8029
8030            // Decodes `application/x-www-form-urlencoded` text: '+' -> space,
8031            // '%XX' -> byte. Needed to read plain HTML `<form>` POST bodies.
8032            "url_decode" | "网址解码" => {
8033                let s = self.arg_str(&args, 0, "");
8034                let bytes = s.as_bytes();
8035                let mut out = Vec::with_capacity(bytes.len());
8036                let mut i = 0;
8037                while i < bytes.len() {
8038                    match bytes[i] {
8039                        b'+' => {
8040                            out.push(b' ');
8041                            i += 1;
8042                        },
8043                        b'%' if i + 2 < bytes.len() => {
8044                            let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("");
8045                            match u8::from_str_radix(hex, 16) {
8046                                Ok(b) => {
8047                                    out.push(b);
8048                                    i += 3;
8049                                },
8050                                Err(_) => {
8051                                    out.push(bytes[i]);
8052                                    i += 1;
8053                                },
8054                            }
8055                        },
8056                        b => {
8057                            out.push(b);
8058                            i += 1;
8059                        },
8060                    }
8061                }
8062                return Ok(Value::Str(String::from_utf8_lossy(&out).into_owned()));
8063            },
8064            // Seconds since Unix epoch (float — sub-second precision). No ISO/date
8065            // formatting builtin exists yet; `.ling` code that wants a display
8066            // string currently just uses the raw number.
8067            "now_unix" | "现在时间" => {
8068                return Ok(Value::Number(now_secs()));
8069            },
8070            "file_exists" | "文件存在" => {
8071                #[cfg(target_arch = "wasm32")]
8072                return Ok(Value::Bool(false));
8073                #[cfg(not(target_arch = "wasm32"))]
8074                {
8075                    let path = self.arg_str(&args, 0, "").replace('\\', "/");
8076                    return Ok(Value::Bool(std::path::Path::new(&path).exists()));
8077                }
8078            },
8079            "write_file" | "เขียนไฟล์" | "نوشتن_فایل" | "اكتب_الملف" | "כתוב_קובץ" | "فائل_لکھو" => {
8080                #[cfg(target_arch = "wasm32")]
8081                return Ok(Value::Unit);
8082                #[cfg(not(target_arch = "wasm32"))]
8083                {
8084                    let path = self.arg_str(&args, 0, "").replace('\\', "/");
8085                    let content = self.arg_str(&args, 1, "");
8086                    if let Some(parent) = std::path::Path::new(&path).parent() {
8087                        if !parent.as_os_str().is_empty() {
8088                            let _ = std::fs::create_dir_all(parent);
8089                        }
8090                    }
8091                    std::fs::write(&path, content.as_bytes())
8092                        .map_err(|e| EvalErr::from(format!("write_file '{path}': {e}")))?;
8093                    return Ok(Value::Unit);
8094                }
8095            },
8096            "print_file" | "พิมพ์ไฟล์" | "چاپ_فایل" | "اطبع_الملف" | "הדפס_קובץ" | "فائل_چھاپو" => {
8097                let content = self.arg_str(&args, 0, "");
8098                print!("{content}");
8099                return Ok(Value::Unit);
8100            },
8101
8102            // ── CLI arguments ─────────────────────────────────────────────────
8103            "get_args" | "รับอาร์กิวเมนต์" | "دریافت_آرگومان‌ها" | "اجلب_المعاملات" | "קבל_ארגומנטים" | "دلائل_حاصل_کرو" => {
8104                let v: Vec<Value> = std::env::args().map(Value::Str).collect();
8105                return Ok(Value::List(Rc::new(v)));
8106            },
8107
8108            // ── Filesystem: directory walking, stat, content hashing (native) ──
8109            // These power headless batch tools (asset pipelines, indexers). Errors
8110            // degrade gracefully (empty list / 0 / "") so a walk never aborts on one
8111            // unreadable entry.
8112            #[cfg(not(target_arch = "wasm32"))]
8113            "list_dir" | "รายการไดเรกทอรี" | "فهرست_پوشه" | "اسرد_المجلد" | "רשום_תיקייה" | "فولڈر_فہرست" => {
8114                let path = self.arg_str(&args, 0, ".").replace('\\', "/");
8115                let mut paths: Vec<String> = Vec::new();
8116                if let Ok(rd) = std::fs::read_dir(&path) {
8117                    for e in rd.flatten() {
8118                        paths.push(e.path().to_string_lossy().replace('\\', "/"));
8119                    }
8120                }
8121                paths.sort();
8122                let out: Vec<Value> = paths.into_iter().map(Value::Str).collect();
8123                return Ok(Value::List(Rc::new(out)));
8124            },
8125            #[cfg(not(target_arch = "wasm32"))]
8126            "is_dir" | "เป็นไดเรกทอรี" | "آیا_پوشه_است" | "هل_مجلد" | "האם_תיקייה" | "کیا_فولڈر_ہے" => {
8127                let path = self.arg_str(&args, 0, "").replace('\\', "/");
8128                return Ok(Value::Bool(std::path::Path::new(&path).is_dir()));
8129            },
8130            #[cfg(not(target_arch = "wasm32"))]
8131            "is_file" | "เป็นไฟล์" | "آیا_فایل_است" | "هل_ملف" | "האם_קובץ" | "کیا_فائل_ہے" => {
8132                let path = self.arg_str(&args, 0, "").replace('\\', "/");
8133                return Ok(Value::Bool(std::path::Path::new(&path).is_file()));
8134            },
8135            #[cfg(not(target_arch = "wasm32"))]
8136            "path_name" | "ชื่อไฟล์" | "نام_مسیر" | "اسم_المسار" | "שם_נתיב" | "پاتھ_نام" => {
8137                let path = self.arg_str(&args, 0, "").replace('\\', "/");
8138                let name = std::path::Path::new(&path)
8139                    .file_name()
8140                    .map(|s| s.to_string_lossy().into_owned())
8141                    .unwrap_or_default();
8142                return Ok(Value::Str(name));
8143            },
8144            #[cfg(not(target_arch = "wasm32"))]
8145            "path_ext" | "นามสกุลไฟล์" | "پسوند_مسیر" | "امتداد_المسار" | "סיומת_נתיב" | "پاتھ_ایکسٹینشن" => {
8146                let path = self.arg_str(&args, 0, "").replace('\\', "/");
8147                let ext = std::path::Path::new(&path)
8148                    .extension()
8149                    .map(|s| s.to_string_lossy().to_lowercase())
8150                    .unwrap_or_default();
8151                return Ok(Value::Str(ext));
8152            },
8153            #[cfg(not(target_arch = "wasm32"))]
8154            "file_size" | "ขนาดไฟล์" | "اندازه_فایل" | "حجم_الملف" | "גודל_קובץ" | "فائل_سائز" => {
8155                let path = self.arg_str(&args, 0, "");
8156                let sz = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
8157                return Ok(Value::Number(sz as f64));
8158            },
8159            #[cfg(not(target_arch = "wasm32"))]
8160            "file_modified" | "เวลาที่แก้ไข" | "زمان_تغییر_فایل" | "وقت_تعديل_الملف" | "זמן_עדכון_קובץ" | "فائل_ترمیم_وقت" => {
8161                let path = self.arg_str(&args, 0, "");
8162                let secs = std::fs::metadata(&path)
8163                    .and_then(|m| m.modified())
8164                    .ok()
8165                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
8166                    .map(|d| d.as_secs_f64())
8167                    .unwrap_or(0.0);
8168                return Ok(Value::Number(secs));
8169            },
8170            #[cfg(not(target_arch = "wasm32"))]
8171            "file_created" | "เวลาที่สร้าง" | "زمان_ایجاد_فایل" | "وقت_إنشاء_الملف" | "זמן_יצירת_קובץ" | "فائل_تخلیق_وقت" => {
8172                let path = self.arg_str(&args, 0, "");
8173                let secs = std::fs::metadata(&path)
8174                    .ok()
8175                    .and_then(|m| m.created().or_else(|_| m.modified()).ok())
8176                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
8177                    .map(|d| d.as_secs_f64())
8178                    .unwrap_or(0.0);
8179                return Ok(Value::Number(secs));
8180            },
8181            #[cfg(not(target_arch = "wasm32"))]
8182            "make_dir" | "สร้างไดเรกทอรี" | "ساخت_پوشه" | "أنشئ_مجلدا" | "צור_תיקייה" | "فولڈر_بناؤ" => {
8183                let path = self.arg_str(&args, 0, "");
8184                return Ok(Value::Bool(std::fs::create_dir_all(&path).is_ok()));
8185            },
8186            // str_strip_prefix("Bearer x", "Bearer ") → "x" (unchanged if absent).
8187            "str_strip_prefix" | "ตัดคำนำหน้า" | "حذف_پیشوند" | "أزل_البادئة" | "הסר_קידומת" | "سابقہ_ہٹاؤ" => {
8188                let s = self.arg_str(&args, 0, "");
8189                let prefix = self.arg_str(&args, 1, "");
8190                return Ok(Value::Str(
8191                    s.strip_prefix(&prefix).unwrap_or(&s).to_string(),
8192                ));
8193            },
8194            // Classify a file by magic bytes: "gzip" | "zip" | "other" | "missing".
8195            // The build-verification gate: only real built archives may publish.
8196            #[cfg(not(target_arch = "wasm32"))]
8197            "file_magic" | "มายาไฟล์" | "امضای_فایل" | "توقيع_الملف" | "חתימת_קובץ" | "فائل_میجک" => {
8198                let path = self.arg_str(&args, 0, "");
8199                let kind = match std::fs::File::open(&path) {
8200                    Ok(mut f) => {
8201                        use std::io::Read;
8202                        let mut buf = [0u8; 4];
8203                        let n = f.read(&mut buf).unwrap_or(0);
8204                        if n >= 2 && buf[0] == 0x1f && buf[1] == 0x8b {
8205                            "gzip"
8206                        } else if n >= 4 && &buf[0..2] == b"PK" {
8207                            "zip"
8208                        } else {
8209                            "other"
8210                        }
8211                    },
8212                    Err(_) => "missing",
8213                };
8214                return Ok(Value::Str(kind.to_string()));
8215            },
8216            // Binary-safe file copy (backups): copy_file(src, dst) → bool.
8217            #[cfg(not(target_arch = "wasm32"))]
8218            "copy_file" | "คัดลอกไฟล์" | "کپی_فایل" | "انسخ_الملف" | "העתק_קובץ" | "فائل_کاپی" => {
8219                let src = self.arg_str(&args, 0, "");
8220                let dst = self.arg_str(&args, 1, "");
8221                if let Some(parent) = std::path::Path::new(&dst).parent() {
8222                    let _ = std::fs::create_dir_all(parent);
8223                }
8224                return Ok(Value::Bool(std::fs::copy(&src, &dst).is_ok()));
8225            },
8226            // ── Read a .tgz (gzip tarball): list file entries / read one file ──
8227            // Powers the GitHub-style "Code / Files" browser: tar_gz_list gives
8228            // the file tree, tar_gz_read pulls one file's text for the viewer.
8229            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8230            "tar_gz_list" | "รายการทาร์" | "فهرست_TAR_GZ" | "اسرد_TAR_GZ" | "רשום_TAR_GZ" | "TAR_GZ_فہرست" => {
8231                let path = self.arg_str(&args, 0, "");
8232                let mut names: Vec<String> = Vec::new();
8233                if let Ok(file) = std::fs::File::open(&path) {
8234                    let gz = flate2::read::GzDecoder::new(file);
8235                    let mut ar = tar::Archive::new(gz);
8236                    if let Ok(entries) = ar.entries() {
8237                        for entry in entries.flatten() {
8238                            if entry.header().entry_type().is_file() {
8239                                if let Ok(p) = entry.path() {
8240                                    names.push(
8241                                        p.to_string_lossy()
8242                                            .trim_start_matches("./")
8243                                            .replace('\\', "/"),
8244                                    );
8245                                }
8246                            }
8247                        }
8248                    }
8249                }
8250                names.sort();
8251                names.dedup();
8252                let out: Vec<Value> = names.into_iter().map(Value::Str).collect();
8253                return Ok(Value::List(Rc::new(out)));
8254            },
8255            // tar_gz_read(archive, entry) → that file's text (utf-8 lossy,
8256            // capped at 256 KiB). Only reads entries that exist in the archive,
8257            // so a caller can't traverse outside it. "" if not found/unreadable.
8258            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8259            "tar_gz_read" | "อ่านทาร์" | "خواندن_TAR_GZ" | "اقرأ_TAR_GZ" | "קרא_TAR_GZ" | "TAR_GZ_پڑھو" => {
8260                let path = self.arg_str(&args, 0, "");
8261                let want = self.arg_str(&args, 1, "");
8262                let want = want.trim_start_matches("./").replace('\\', "/");
8263                let mut content = String::new();
8264                if let Ok(file) = std::fs::File::open(&path) {
8265                    let gz = flate2::read::GzDecoder::new(file);
8266                    let mut ar = tar::Archive::new(gz);
8267                    if let Ok(entries) = ar.entries() {
8268                        for entry in entries.flatten() {
8269                            let mut entry = entry;
8270                            let name = match entry.path() {
8271                                Ok(p) => p
8272                                    .to_string_lossy()
8273                                    .trim_start_matches("./")
8274                                    .replace('\\', "/"),
8275                                Err(_) => continue,
8276                            };
8277                            if name == want {
8278                                use std::io::Read;
8279                                let mut buf = Vec::new();
8280                                let cap = 256 * 1024;
8281                                if entry.take(cap as u64 + 1).read_to_end(&mut buf).is_ok() {
8282                                    let slice = if buf.len() > cap { &buf[..cap] } else { &buf[..] };
8283                                    content = String::from_utf8_lossy(slice).into_owned();
8284                                }
8285                                break;
8286                            }
8287                        }
8288                    }
8289                }
8290                return Ok(Value::Str(content));
8291            },
8292            // ── TOTP (RFC 6238, HMAC-SHA1, 6 digits, 30s) for 2FA ──
8293            // Base32 secret compatible with Google Authenticator / Authy etc.
8294            // `base32_encode`/`totp_code`/`totp_check` only exist under this
8295            // same `feature = "web"` gate (see their definitions above) — a
8296            // build without it must skip these arms too, not just fail to
8297            // link; matches how `file_hash` etc. gate their own arms below.
8298            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8299            "totp_secret" | "โทเทนลับ" | "راز_TOTP" | "سر_TOTP" | "סוד_TOTP" | "TOTP_راز" => {
8300                let mut bytes = [0u8; 20];
8301                rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut bytes);
8302                return Ok(Value::Str(base32_encode(&bytes)));
8303            },
8304            // otpauth:// URI to paste into an authenticator app (or make a QR of).
8305            // No cfg gate needed — pure string formatting, no dependency on
8306            // the web-only TOTP helpers.
8307            "totp_uri" | "โทเทนยูอาร์ไอ" | "آدرس_TOTP" | "رابط_TOTP" | "כתובת_TOTP" | "TOTP_یو_آر_آئی" => {
8308                let secret = self.arg_str(&args, 0, "");
8309                let account = self.arg_str(&args, 1, "user");
8310                let issuer = self.arg_str(&args, 2, "lingfu");
8311                return Ok(Value::Str(format!(
8312                    "otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}&algorithm=SHA1&digits=6&period=30"
8313                )));
8314            },
8315            // Verify a 6-digit code against the secret, allowing ±1 time step.
8316            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8317            "totp_verify" | "โทเทนตรวจ" | "تایید_TOTP" | "تحقق_TOTP" | "אמת_TOTP" | "TOTP_تصدیق" => {
8318                let secret = self.arg_str(&args, 0, "");
8319                let code = self.arg_str(&args, 1, "");
8320                let ok = totp_check(&secret, code.trim());
8321                return Ok(Value::Bool(ok));
8322            },
8323            // The current valid code, for tests/tools.
8324            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8325            "totp_now" | "โทเทนตอนนี้" | "TOTP_اکنون" | "TOTP_الآن" | "TOTP_עכשיו" | "TOTP_ابھی" => {
8326                let secret = self.arg_str(&args, 0, "");
8327                let step = (crate::runtime::now_secs() as u64) / 30;
8328                return Ok(Value::Str(
8329                    totp_code(&secret, step).unwrap_or_default(),
8330                ));
8331            },
8332            // BLAKE3 hex of a file's bytes (binary-safe content fingerprint).
8333            #[cfg(not(target_arch = "wasm32"))]
8334            "file_hash" | "แฮชไฟล์" | "درهم_فایل" | "بصمة_الملف" | "גיבוב_קובץ" | "فائل_ہیش" => {
8335                let path = self.arg_str(&args, 0, "");
8336                match std::fs::read(&path) {
8337                    Ok(bytes) => {
8338                        return Ok(Value::Str(hex_encode(&ling_crypto::Blake3::hash(&bytes))))
8339                    },
8340                    Err(_) => return Ok(Value::Str(String::new())),
8341                }
8342            },
8343            // BLAKE3 hex of an arbitrary string (deterministic id/colour/role seed).
8344            "hash_hex" | "แฮชสตริง" | "درهم_هگزادسیمال" | "بصمة_سداسية" | "גיבוב_הקסדצימלי" | "ہیکس_ہیش" => {
8345                let s = self.arg_str(&args, 0, "");
8346                return Ok(Value::Str(hex_encode(&ling_crypto::Blake3::hash(
8347                    s.as_bytes(),
8348                ))));
8349            },
8350            // Read an environment variable, falling back to a default.
8351            #[cfg(not(target_arch = "wasm32"))]
8352            "env_get" | "รับตัวแปรแวดล้อม" | "دریافت_متغیر_محیطی" | "اجلب_متغير_البيئة" | "קבל_משתנה_סביבה" | "ماحولیاتی_متغیر_حاصل_کرو" => {
8353                let name = self.arg_str(&args, 0, "");
8354                let dflt = self.arg_str(&args, 1, "");
8355                return Ok(Value::Str(std::env::var(&name).unwrap_or(dflt)));
8356            },
8357
8358            // ── String utilities ──────────────────────────────────────────────
8359            // Parses a string to a number (0 on failure — degrades gracefully,
8360            // like the other filesystem/parsing builtins in this file).
8361            "to_number" | "转数字" => {
8362                let s = self.arg_str(&args, 0, "");
8363                return Ok(Value::Number(s.trim().parse().unwrap_or(0.0)));
8364            },
8365            // Plain SHA-256 hex — matches the browser's native SubtleCrypto
8366            // digest("SHA-256", ...), which is what proof-of-work mining uses
8367            // client-side (Web Crypto has no Blake3/SHA-3, so this is the one
8368            // hash both sides can compute natively and fast).
8369            "sha256_hex" | "SHA256哈希" => {
8370                use sha2::Digest;
8371                let s = self.arg_str(&args, 0, "");
8372                let mut h = sha2::Sha256::new();
8373                h.update(s.as_bytes());
8374                return Ok(Value::Str(hex_encode(&h.finalize())));
8375            },
8376            // Parses a hex string (no "0x" prefix) to a number — `to_number`
8377            // uses Rust's plain f64 parser, which doesn't understand hex.
8378            "hex_to_number" | "十六进制转数字" => {
8379                let s = self.arg_str(&args, 0, "");
8380                let v = u64::from_str_radix(s.trim(), 16).unwrap_or(0);
8381                return Ok(Value::Number(v as f64));
8382            },
8383            "split" | "str_split" | "แยก" | "جداسازی" | "قسّم" | "פצל" | "تقسیم_کرو" => {
8384                let s = self.arg_str(&args, 0, "");
8385                let sep = self.arg_str(&args, 1, "\n");
8386                let sep = if sep.is_empty() { "\n".into() } else { sep };
8387                let parts: Vec<Value> = s
8388                    .split(sep.as_str())
8389                    .map(|p| Value::Str(p.to_string()))
8390                    .collect();
8391                return Ok(Value::List(Rc::new(parts)));
8392            },
8393            "trim" | "str_trim" | "ตัดช่องว่าง" | "حذف_فاصله" | "اقتطع_الفراغات" | "חתוך_רווחים" | "خالی_جگہ_کاٹو" => {
8394                let s = self.arg_str(&args, 0, "");
8395                return Ok(Value::Str(s.trim().to_string()));
8396            },
8397            "starts_with" | "str_starts_with" | "เริ่มด้วย" | "شروع_می‌شود_با" | "يبدأ_بـ" | "מתחיל_ב" | "شروع_ہوتا_ہے" => {
8398                let s = self.arg_str(&args, 0, "");
8399                let prefix = self.arg_str(&args, 1, "");
8400                return Ok(Value::Bool(s.starts_with(prefix.as_str())));
8401            },
8402            "ends_with" | "str_ends_with" | "ลงท้ายด้วย" | "پایان_می‌یابد_با" | "ينتهي_بـ" | "מסתיים_ב" | "ختم_ہوتا_ہے" => {
8403                let s = self.arg_str(&args, 0, "");
8404                let suffix = self.arg_str(&args, 1, "");
8405                return Ok(Value::Bool(s.ends_with(suffix.as_str())));
8406            },
8407            "str_replace" | "แทนสตริง" | "جایگزینی_رشته" | "استبدل_النص" | "החלף_מחרוזת" | "اسٹرنگ_تبدیل" => {
8408                let s = self.arg_str(&args, 0, "");
8409                let from = self.arg_str(&args, 1, "");
8410                let to = self.arg_str(&args, 2, "");
8411                return Ok(Value::Str(s.replace(from.as_str(), to.as_str())));
8412            },
8413            "str_find" | "หาในสตริง" | "جستجوی_رشته" | "ابحث_في_النص" | "חפש_מחרוזת" | "اسٹرنگ_تلاش" => {
8414                let s = self.arg_str(&args, 0, "");
8415                let needle = self.arg_str(&args, 1, "");
8416                // Return char index (not byte index) for consistency with substr
8417                let pos = s
8418                    .find(needle.as_str())
8419                    .map(|byte_i| s[..byte_i].chars().count() as f64)
8420                    .unwrap_or(-1.0);
8421                return Ok(Value::Number(pos));
8422            },
8423            "substr" | "str_slice" | "ส่วนสตริง" | "زیررشته" | "جزء_النص" | "תת_מחרוזת" | "ذیلی_اسٹرنگ" => {
8424                let s = self.arg_str(&args, 0, "");
8425                let start = self.arg_num(&args, 1, 0.0)? as usize;
8426                let len = args
8427                    .get(2)
8428                    .map(|v| self.to_number(v).unwrap_or(999999.0) as usize)
8429                    .unwrap_or_else(|| s.chars().count().saturating_sub(start));
8430                let chars: Vec<char> = s.chars().collect();
8431                let end = (start + len).min(chars.len());
8432                let slice: String = chars.get(start..end).unwrap_or(&[]).iter().collect();
8433                return Ok(Value::Str(slice));
8434            },
8435            "to_str" | "str" | "num_str" | "แปลงสตริง" | "تبدیل_به_رشته" | "حوّل_لنص" | "המר_למחרוזת" | "اسٹرنگ_میں_بدلو" => {
8436                let v = args.into_iter().next().unwrap_or(Value::Unit);
8437                return Ok(Value::Str(v.to_string()));
8438            },
8439            "str_repeat" | "ทำซ้ำสตริง" | "تکرار_رشته" | "كرّر_النص" | "חזור_על_מחרוזת" | "اسٹرنگ_دہراؤ" => {
8440                let s = self.arg_str(&args, 0, "");
8441                let n = self.arg_num(&args, 1, 1.0)? as usize;
8442                return Ok(Value::Str(s.repeat(n)));
8443            },
8444            "str_upper" => {
8445                let s = self.arg_str(&args, 0, "");
8446                return Ok(Value::Str(s.to_uppercase()));
8447            },
8448            "str_lower" => {
8449                let s = self.arg_str(&args, 0, "");
8450                return Ok(Value::Str(s.to_lowercase()));
8451            },
8452            "str_len" | "len" | "ความยาว" | "长度" | "長さ" | "길이" | "طول_رشته" | "طول_النص" | "אורך_מחרוזת" | "اسٹرنگ_لمبائی" => {
8453                match args.first() {
8454                    Some(Value::Str(s)) => return Ok(Value::Number(s.chars().count() as f64)),
8455                    Some(Value::List(v)) => return Ok(Value::Number(v.len() as f64)),
8456                    _ => return Ok(Value::Number(0.0)),
8457                }
8458            },
8459
8460            // ── FNV-1a hash (deterministic, normalized 0.0–1.0) ──────────────
8461            "hash_str" | "แฮช" | "درهم_رشته" | "بصمة_نص" | "גיבוב_מחרוזת" | "اسٹرنگ_ہیش" => {
8462                let s = self.arg_str(&args, 0, "");
8463                let mut h: u64 = 14695981039346656037_u64;
8464                for b in s.bytes() {
8465                    h ^= b as u64;
8466                    h = h.wrapping_mul(1099511628211);
8467                }
8468                return Ok(Value::Number((h & 0xFFFFFF) as f64 / 16777215.0));
8469            },
8470            "hash_int" | "แฮชจำนวน" | "درهم_عدد" | "بصمة_عدد" | "גיבוב_מספר" | "نمبر_ہیش" => {
8471                let s = self.arg_str(&args, 0, "");
8472                let n = self.arg_num(&args, 1, 100.0)? as u64;
8473                let mut h: u64 = 14695981039346656037_u64;
8474                for b in s.bytes() {
8475                    h ^= b as u64;
8476                    h = h.wrapping_mul(1099511628211);
8477                }
8478                return Ok(Value::Number((h % n.max(1)) as f64));
8479            },
8480
8481            // ── List utilities ────────────────────────────────────────────────
8482            "list_new" | "รายการใหม่" | "新建列表" | "新規リスト" | "새목록" | "فهرست_جدید" | "قائمة_جديدة" | "רשימה_חדשה" | "نئی_فہرست" | "nouvelle_liste" | "neue_liste" | "новый_список" =>
8483            {
8484                return Ok(Value::List(Rc::new(Vec::new())));
8485            },
8486            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가" | "افزودن_به_فهرست" | "أضف_للقائمة" | "הוסף_לרשימה" | "فہرست_میں_شامل_کرو" | "ajouter_liste" | "liste_anhängen" | "добавить_в_список" =>
8487            {
8488                let lst = args
8489                    .first()
8490                    .cloned()
8491                    .unwrap_or(Value::List(Rc::new(vec![])));
8492                let val = args.get(1).cloned().unwrap_or(Value::Unit);
8493                if let Value::List(mut v) = lst {
8494                    Rc::make_mut(&mut v).push(val);
8495                    return Ok(Value::List(v));
8496                }
8497                return Ok(Value::List(Rc::new(vec![val])));
8498            },
8499            "list_get" | "รับรายการ" | "取元素" | "要素取得" | "요소가져오기" | "دریافت_از_فهرست" | "اجلب_من_القائمة" | "קבל_מרשימה" | "فہرست_سے_حاصل_کرو" | "obtenir_liste" | "liste_abrufen" | "получить_из_списка" =>
8500            {
8501                // Borrow the list; clone only the element (was cloning the whole list).
8502                let i = self.arg_num(&args, 1, 0.0)? as usize;
8503                if let Some(Value::List(v)) = args.first() {
8504                    return Ok(v.get(i).cloned().unwrap_or(Value::Str(String::new())));
8505                }
8506                return Ok(Value::Str(String::new()));
8507            },
8508            // list_max(numbers, default) / list_min(numbers, default) — `default`
8509            // is returned for an empty list (there's no numeric identity element
8510            // to fall back to otherwise).
8511            "list_max" | "列表最大值" => {
8512                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8513                let default = self.arg_num(&args, 1, 0.0)?;
8514                if let Value::List(v) = lst {
8515                    let mut best = default;
8516                    let mut any = false;
8517                    for item in v.iter() {
8518                        if let Value::Number(n) = item {
8519                            if !any || *n > best {
8520                                best = *n;
8521                                any = true;
8522                            }
8523                        }
8524                    }
8525                    return Ok(Value::Number(best));
8526                }
8527                return Ok(Value::Number(default));
8528            },
8529            "list_min" | "列表最小值" => {
8530                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8531                let default = self.arg_num(&args, 1, 0.0)?;
8532                if let Value::List(v) = lst {
8533                    let mut best = default;
8534                    let mut any = false;
8535                    for item in v.iter() {
8536                        if let Value::Number(n) = item {
8537                            if !any || *n < best {
8538                                best = *n;
8539                                any = true;
8540                            }
8541                        }
8542                    }
8543                    return Ok(Value::Number(best));
8544                }
8545                return Ok(Value::Number(default));
8546            },
8547            // list_set(lst, idx, val) → new list with index replaced. Engine builtin
8548            // (O(n) one copy) to replace the O(n²) ling `ตั้งรายการ` that looped
8549            // list_push + list_get (each of which copied the whole list).
8550            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정" | "تنظیم_عنصر_فهرست" | "عيّن_عنصر_القائمة" | "קבע_איבר_רשימה" | "فہرست_سیٹ" =>
8551            {
8552                let idx = self.arg_num(&args, 1, 0.0)? as usize;
8553                let mut ai = args.into_iter();
8554                let lst = ai.next().unwrap_or(Value::List(Rc::new(vec![])));
8555                ai.next(); // skip idx
8556                let val = ai.next().unwrap_or(Value::Unit);
8557                if let Value::List(mut v) = lst {
8558                    if idx < v.len() {
8559                        Rc::make_mut(&mut v)[idx] = val;
8560                    }
8561                    return Ok(Value::List(v));
8562                }
8563                return Ok(Value::List(Rc::new(vec![])));
8564            },
8565            "list_join" | "join" | "รวมรายการ" | "连接" | "連結" | "연결" | "پیوستن_فهرست" | "اربط_القائمة" | "חבר_רשימה" | "فہرست_جوڑو" =>
8566            {
8567                let lst = args
8568                    .first()
8569                    .cloned()
8570                    .unwrap_or(Value::List(Rc::new(vec![])));
8571                let sep = args.get(1).map(|v| v.to_string()).unwrap_or_default();
8572                if let Value::List(v) = lst {
8573                    return Ok(Value::Str(
8574                        v.iter()
8575                            .map(|x| x.to_string())
8576                            .collect::<Vec<_>>()
8577                            .join(&sep),
8578                    ));
8579                }
8580                return Ok(Value::Str(String::new()));
8581            },
8582            // list_map/list_filter/list_find — take a closure. Necessary as real
8583            // builtins (not expressible in `.ling` itself): a bare-identifier call
8584            // `f(x)` where `f` is a local variable always resolves through
8585            // `call_named`, which only looks at top-level `fn` definitions by
8586            // design ("call-site locals are intentionally NOT visible to fns") —
8587            // so a closure held in a variable/parameter can't be invoked from
8588            // `.ling` source directly. These call it from the Rust side instead,
8589            // the same way `http_serve` already invokes route-handler closures.
8590            "list_map" | "映射列表" => {
8591                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8592                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8593                if let Value::List(v) = lst {
8594                    let mut out = Vec::with_capacity(v.len());
8595                    for item in v.iter() {
8596                        out.push(self.call_value(f.clone(), vec![item.clone()])?);
8597                    }
8598                    return Ok(Value::List(Rc::new(out)));
8599                }
8600                return Ok(Value::List(Rc::new(vec![])));
8601            },
8602            "list_filter" | "过滤列表" => {
8603                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8604                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8605                if let Value::List(v) = lst {
8606                    let mut out = Vec::new();
8607                    for item in v.iter() {
8608                        if matches!(self.call_value(f.clone(), vec![item.clone()])?, Value::Bool(true)) {
8609                            out.push(item.clone());
8610                        }
8611                    }
8612                    return Ok(Value::List(Rc::new(out)));
8613                }
8614                return Ok(Value::List(Rc::new(vec![])));
8615            },
8616            // First element for which `f` returns true, or Unit if none match.
8617            "list_find" | "查找列表" => {
8618                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8619                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8620                if let Value::List(v) = lst {
8621                    for item in v.iter() {
8622                        if matches!(self.call_value(f.clone(), vec![item.clone()])?, Value::Bool(true)) {
8623                            return Ok(item.clone());
8624                        }
8625                    }
8626                }
8627                return Ok(Value::Unit);
8628            },
8629            // blob_f32("<deflate+base64>") / blob_i32(...) — decode an embedded,
8630            // losslessly-compressed numeric blob into a list. Produced by
8631            // `ling convert`; lets converted assets carry geometry/PCM/etc. compactly.
8632            #[cfg(not(target_arch = "wasm32"))]
8633            "blob_f32" | "blob_i32" => {
8634                let s = self.arg_str(&args, 0, "");
8635                let is_i32 = name == "blob_i32";
8636                match decode_blob(&s) {
8637                    Ok(bytes) => {
8638                        let mut out = Vec::with_capacity(bytes.len() / 4);
8639                        for ch in bytes.chunks_exact(4) {
8640                            let arr = [ch[0], ch[1], ch[2], ch[3]];
8641                            let n = if is_i32 {
8642                                i32::from_le_bytes(arr) as f64
8643                            } else {
8644                                f32::from_le_bytes(arr) as f64
8645                            };
8646                            out.push(Value::Number(n));
8647                        }
8648                        return Ok(Value::List(Rc::new(out)));
8649                    },
8650                    Err(e) => {
8651                        eprintln!("blob decode failed: {e}");
8652                        return Ok(Value::List(Rc::new(vec![])));
8653                    },
8654                }
8655            },
8656
8657            // ══════════════════════════════════════════════════════════════════
8658            // SVG EXPORT  (svg_begin / svg_rect / svg_circle / svg_line /
8659            //              svg_polyline / svg_text / svg_end / hsl_color)
8660            // Chinese aliases: 开始SVG 结束SVG SVG矩形 SVG圆形 SVG线段 SVG折线 SVG文本 HSL颜色
8661            // Thai aliases:    เริ่มSVG จบSVG SVGสี่เหลี่ยม SVGวงกลม SVGเส้น SVGเส้นหัก SVGข้อความ สีHSL
8662            // ══════════════════════════════════════════════════════════════════
8663            "svg_begin" | "开始SVG" | "เริ่มSVG" | "شروع_SVG" | "ابدأ_SVG" | "התחל_SVG" | "SVG_شروع" | "commencer_svg" | "svg_beginnen" | "начать_svg" => {
8664                let path = self.arg_str(&args, 0, "output.svg");
8665                let width = self.arg_num(&args, 1, 800.0)?;
8666                let height = self.arg_num(&args, 2, 600.0)?;
8667                *self.svg.borrow_mut() = Some(SvgWriter::new(path, width, height));
8668                return Ok(Value::Unit);
8669            },
8670
8671            "svg_rect" | "SVG矩形" | "SVGสี่เหลี่ยม" | "مستطیل_SVG" | "مستطيل_SVG" | "מלבן_SVG" | "SVG_مستطیل" | "rectangle_svg" | "svg_rechteck" | "прямоугольник_svg" => {
8672                let x = self.arg_num(&args, 0, 0.0)?;
8673                let y = self.arg_num(&args, 1, 0.0)?;
8674                let w = self.arg_num(&args, 2, 10.0)?;
8675                let h = self.arg_num(&args, 3, 10.0)?;
8676                let fill = self.arg_str(&args, 4, "#ffffff");
8677                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8678                    svg.elements.push(format!(
8679                        "<rect x=\"{x:.1}\" y=\"{y:.1}\" width=\"{w:.1}\" \
8680                         height=\"{h:.1}\" fill=\"{fill}\"/>"
8681                    ));
8682                }
8683                return Ok(Value::Unit);
8684            },
8685
8686            "svg_circle" | "SVG圆形" | "SVGวงกลม" | "دایره_SVG" | "دائرة_SVG" | "עיגול_SVG" | "SVG_دائرہ" | "cercle_svg" | "svg_kreis" | "круг_svg" => {
8687                let cx = self.arg_num(&args, 0, 0.0)?;
8688                let cy = self.arg_num(&args, 1, 0.0)?;
8689                let r = self.arg_num(&args, 2, 5.0)?;
8690                let fill = self.arg_str(&args, 3, "#ffffff");
8691                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8692                    svg.elements.push(format!(
8693                        "<circle cx=\"{cx:.1}\" cy=\"{cy:.1}\" r=\"{r:.1}\" fill=\"{fill}\"/>"
8694                    ));
8695                }
8696                return Ok(Value::Unit);
8697            },
8698
8699            "svg_line" | "SVG线段" | "SVGเส้น" | "خط_SVG" | "קו_SVG" | "SVG_لکیر" | "ligne_svg" | "svg_linie" | "линия_svg" => {
8700                let x1 = self.arg_num(&args, 0, 0.0)?;
8701                let y1 = self.arg_num(&args, 1, 0.0)?;
8702                let x2 = self.arg_num(&args, 2, 0.0)?;
8703                let y2 = self.arg_num(&args, 3, 0.0)?;
8704                let stroke = self.arg_str(&args, 4, "#ffffff");
8705                let sw = self.arg_num(&args, 5, 1.0)?;
8706                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8707                    svg.elements.push(format!(
8708                        "<line x1=\"{x1:.1}\" y1=\"{y1:.1}\" x2=\"{x2:.1}\" y2=\"{y2:.1}\" \
8709                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
8710                    ));
8711                }
8712                return Ok(Value::Unit);
8713            },
8714
8715            "svg_polyline" | "SVG折线" | "SVGเส้นหัก" | "چندخطی_SVG" | "خط_متعدد_SVG" | "קו_שבור_SVG" | "SVG_پولی_لائن" | "polyligne_svg" | "svg_polylinie" | "ломаная_svg" => {
8716                let pts = self.arg_str(&args, 0, "");
8717                let stroke = self.arg_str(&args, 1, "#ffffff");
8718                let sw = self.arg_num(&args, 2, 1.0)?;
8719                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8720                    svg.elements.push(format!(
8721                        "<polyline points=\"{pts}\" fill=\"none\" \
8722                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
8723                    ));
8724                }
8725                return Ok(Value::Unit);
8726            },
8727
8728            "svg_text" | "SVG文本" | "SVGข้อความ" | "متن_SVG" | "نص_SVG" | "טקסט_SVG" | "SVG_متن" | "texte_svg" | "текст_svg" => {
8729                let x = self.arg_num(&args, 0, 0.0)?;
8730                let y = self.arg_num(&args, 1, 0.0)?;
8731                let text = self.arg_str(&args, 2, "");
8732                let fill = self.arg_str(&args, 3, "#ffffff");
8733                let size = self.arg_num(&args, 4, 12.0)?;
8734                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8735                    let safe = text
8736                        .replace('&', "&amp;")
8737                        .replace('<', "&lt;")
8738                        .replace('>', "&gt;");
8739                    svg.elements.push(format!(
8740                        "<text x=\"{x:.1}\" y=\"{y:.1}\" fill=\"{fill}\" \
8741                         font-family=\"monospace\" font-size=\"{size:.0}\">{safe}</text>"
8742                    ));
8743                }
8744                return Ok(Value::Unit);
8745            },
8746
8747            "svg_end" | "结束SVG" | "จบSVG" | "پایان_SVG" | "أنهِ_SVG" | "סיים_SVG" | "SVG_ختم" | "terminer_svg" | "svg_beenden" | "закончить_svg" => {
8748                {
8749                    let borrow = self.svg.borrow();
8750                    if let Some(svg) = borrow.as_ref() {
8751                        svg.save()
8752                            .map_err(|e| EvalErr::from(format!("svg_end: {e}")))?;
8753                    }
8754                }
8755                *self.svg.borrow_mut() = None;
8756                return Ok(Value::Unit);
8757            },
8758
8759            "hsl_color" | "HSL颜色" | "สีHSL" | "رنگ_HSL" | "لون_HSL" | "צבע_HSL" | "HSL_رنگ" | "couleur_hsl" | "hsl_farbe" | "цвет_hsl" => {
8760                let h = self.arg_num(&args, 0, 0.0)?;
8761                let s = self.arg_num(&args, 1, 70.0)?;
8762                let l = self.arg_num(&args, 2, 50.0)?;
8763                return Ok(Value::Str(hsl_to_hex(h, s, l)));
8764            },
8765
8766            // ══════════════════════════════════════════════════════════════════
8767            // FFT / AUDIO ANALYSIS BUILTINS  (native only)
8768            // ══════════════════════════════════════════════════════════════════
8769
8770            // fft_push(samples_list) — feed raw audio samples and run FFT
8771            #[cfg(not(target_arch = "wasm32"))]
8772            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" | "ورودی_FFT" | "أدخل_FFT" | "הזנת_FFT" | "FFT_ان_پٹ" | "fft_entrée" | "fft_eingabe" | "fft_вход" =>
8773            {
8774                if let Some(Value::List(v)) = args.first() {
8775                    let samples: Vec<f32> = v
8776                        .iter()
8777                        .filter_map(|x| {
8778                            if let Value::Number(n) = x {
8779                                Some(*n as f32)
8780                            } else {
8781                                None
8782                            }
8783                        })
8784                        .collect();
8785                    self.fft.borrow_mut().push_samples(&samples);
8786                }
8787                return Ok(Value::Unit);
8788            },
8789
8790            // fft_bands(n) → list of n log-spaced magnitude bands (0..1)
8791            #[cfg(not(target_arch = "wasm32"))]
8792            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" | "باندهای_FFT" | "نطاقات_FFT" | "פסי_FFT" | "FFT_بینڈز" | "fft_bandes" | "fft_bänder" | "fft_полосы" =>
8793            {
8794                let n = self.arg_num(&args, 0, 32.0)? as usize;
8795                let bands = self.fft.borrow().freq_bands(n);
8796                *self.fft_bands_cache.borrow_mut() = bands.clone();
8797                return Ok(Value::List(Rc::new(
8798                    bands.into_iter().map(|v| Value::Number(v as f64)).collect(),
8799                )));
8800            },
8801
8802            // fft_beat() → bool
8803            #[cfg(not(target_arch = "wasm32"))]
8804            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" | "ضرب_FFT" | "نبضة_FFT" | "פעימת_FFT" | "FFT_دھڑکن" | "fft_battement" | "fft_takt" | "fft_удар" =>
8805            {
8806                return Ok(Value::Bool(self.fft.borrow().is_beat()));
8807            },
8808
8809            // fft_beat_ratio() → f64  (1.0 = at threshold, >1 = strong beat)
8810            #[cfg(not(target_arch = "wasm32"))]
8811            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" | "نسبت_ضرب_FFT" | "نسبة_نبضة_FFT" | "יחס_פעימת_FFT" | "FFT_بیٹ_تناسب" =>
8812            {
8813                return Ok(Value::Number(self.fft.borrow().beat_ratio() as f64));
8814            },
8815
8816            // fft_rms() → f64
8817            #[cfg(not(target_arch = "wasm32"))]
8818            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" | "RMS_صدا" | "جذر_متوسط_مربع_FFT" | "RMS_של_FFT" | "FFT_RMS" => {
8819                return Ok(Value::Number(self.fft.borrow().rms() as f64));
8820            },
8821
8822            // fft_dominant_freq() → f64  in Hz
8823            #[cfg(not(target_arch = "wasm32"))]
8824            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" | "فرکانس_غالب" | "التردد_السائد" | "תדר_דומיננטי" | "غالب_فریکوئنسی" | "fft_fréquence_dominante" | "fft_dominante_frequenz" | "fft_доминирующая_частота" =>
8825            {
8826                return Ok(Value::Number(self.fft.borrow().dominant_freq() as f64));
8827            },
8828
8829            // ── wasm32 stubs: fft builtins are no-ops on web ───────────────
8830            #[cfg(target_arch = "wasm32")]
8831            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" | "ورودی_FFT" | "أدخل_FFT" | "הזנת_FFT" | "FFT_ان_پٹ" | "fft_entrée" | "fft_eingabe" | "fft_вход" =>
8832            {
8833                return Ok(Value::Unit);
8834            },
8835            #[cfg(target_arch = "wasm32")]
8836            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" | "باندهای_FFT" | "نطاقات_FFT" | "פסי_FFT" | "FFT_بینڈز" | "fft_bandes" | "fft_bänder" | "fft_полосы" =>
8837            {
8838                let n = self.arg_num(&args, 0, 32.0)? as usize;
8839                return Ok(Value::List(vec![Value::Number(0.0); n].into()));
8840            },
8841            #[cfg(target_arch = "wasm32")]
8842            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" | "ضرب_FFT" | "نبضة_FFT" | "פעימת_FFT" | "FFT_دھڑکن" | "fft_battement" | "fft_takt" | "fft_удар" =>
8843            {
8844                return Ok(Value::Bool(false));
8845            },
8846            #[cfg(target_arch = "wasm32")]
8847            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" | "نسبت_ضرب_FFT" | "نسبة_نبضة_FFT" | "יחס_פעימת_FFT" | "FFT_بیٹ_تناسب" =>
8848            {
8849                return Ok(Value::Number(1.0));
8850            },
8851            #[cfg(target_arch = "wasm32")]
8852            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" | "RMS_صدا" | "جذر_متوسط_مربع_FFT" | "RMS_של_FFT" | "FFT_RMS" => {
8853                return Ok(Value::Number(0.0));
8854            },
8855            #[cfg(target_arch = "wasm32")]
8856            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" | "فرکانس_غالب" | "التردد_السائد" | "תדר_דומיננטי" | "غالب_فریکوئنسی" | "fft_fréquence_dominante" | "fft_dominante_frequenz" | "fft_доминирующая_частота" =>
8857            {
8858                return Ok(Value::Number(0.0));
8859            },
8860
8861            // ══════════════════════════════════════════════════════════════════
8862            // PROCEDURAL TEXTURE BLIT BUILTINS  (screen-space)
8863            // All: name(dst_x, dst_y, width, height, ...params, palette)
8864            // palette: "rainbow" | "fire" | "ocean" | "psychedelic" | "neon" | "forest"
8865            // ══════════════════════════════════════════════════════════════════
8866
8867            // tex_checkerboard(x, y, w, h, tiles, r1,g1,b1, r2,g2,b2)
8868            "tex_checkerboard" | "ลายตารางหมากรุก" | "بافت_شطرنجی" | "نسيج_رقعة_الشطرنج" | "מרקם_שחמט" | "شطرنج_ٹیکسچر" => {
8869                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8870                let tiles = self.arg_num(&args, 4, 8.0)? as u32;
8871                let (r1, g1, b1) = (
8872                    self.arg_num(&args, 5, 255.)? as u32,
8873                    self.arg_num(&args, 6, 255.)? as u32,
8874                    self.arg_num(&args, 7, 255.)? as u32,
8875                );
8876                let (r2, g2, b2) = (
8877                    self.arg_num(&args, 8, 0.)? as u32,
8878                    self.arg_num(&args, 9, 0.)? as u32,
8879                    self.arg_num(&args, 10, 0.)? as u32,
8880                );
8881                let c1 = (r1 << 16) | (g1 << 8) | b1;
8882                let c2 = (r2 << 16) | (g2 << 8) | b2;
8883                let mut gfx = self.gfx.borrow_mut();
8884                let (bw, bh) = (gfx.width, gfx.height);
8885                for row in 0..th {
8886                    for col in 0..tw {
8887                        let cx = col as u32 * tiles / tw as u32;
8888                        let cy = row as u32 * tiles / th as u32;
8889                        let (dx, dy) = (tx + col, ty + row);
8890                        if dx < bw && dy < bh {
8891                            gfx.buffer[dy * bw + dx] = if (cx + cy) % 2 == 0 { c1 } else { c2 };
8892                        }
8893                    }
8894                }
8895                return Ok(Value::Unit);
8896            },
8897
8898            // tex_gradient(x, y, w, h, angle_deg, r1,g1,b1, r2,g2,b2)
8899            "tex_gradient" | "ลายไล่สี" | "بافت_گرادیان" | "نسيج_متدرج" | "מרקם_גרדיאנט" | "گریڈینٹ_ٹیکسچر" => {
8900                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8901                let angle = self.arg_num(&args, 4, 0.0)? as f32;
8902                let (r1, g1, b1) = (
8903                    self.arg_num(&args, 5, 0.)? as f32 / 255.,
8904                    self.arg_num(&args, 6, 0.)? as f32 / 255.,
8905                    self.arg_num(&args, 7, 0.)? as f32 / 255.,
8906                );
8907                let (r2, g2, b2) = (
8908                    self.arg_num(&args, 8, 255.)? as f32 / 255.,
8909                    self.arg_num(&args, 9, 255.)? as f32 / 255.,
8910                    self.arg_num(&args, 10, 255.)? as f32 / 255.,
8911                );
8912                let (ca, sa) = (angle.to_radians().cos(), angle.to_radians().sin());
8913                let mut gfx = self.gfx.borrow_mut();
8914                let (bw, bh) = (gfx.width, gfx.height);
8915                for row in 0..th {
8916                    for col in 0..tw {
8917                        let nx = col as f32 / tw as f32 - 0.5;
8918                        let ny = row as f32 / th as f32 - 0.5;
8919                        let t = ((nx * ca + ny * sa + 0.707) / 1.414).clamp(0., 1.);
8920                        let (dx, dy) = (tx + col, ty + row);
8921                        if dx < bw && dy < bh {
8922                            gfx.buffer[dy * bw + dx] =
8923                                tex_rgb(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t);
8924                        }
8925                    }
8926                }
8927                return Ok(Value::Unit);
8928            },
8929
8930            // tex_noise(x, y, w, h, scale, octaves, seed, palette)
8931            "tex_noise" | "ลายนอยส์" | "بافت_نویز" | "نسيج_ضجيج" | "מרקם_רעש" | "نوائز_ٹیکسچر" => {
8932                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8933                let scale = self.arg_num(&args, 4, 4.0)? as f32;
8934                let octaves = self.arg_num(&args, 5, 4.0)? as u32;
8935                let seed = self.arg_num(&args, 6, 0.0)? as u32;
8936                let palette = self.arg_str(&args, 7, "rainbow");
8937                let mut gfx = self.gfx.borrow_mut();
8938                let (bw, bh) = (gfx.width, gfx.height);
8939                for row in 0..th {
8940                    for col in 0..tw {
8941                        let v = tex_fbm(
8942                            col as f32 * scale / tw as f32,
8943                            row as f32 * scale / th as f32,
8944                            octaves,
8945                            seed,
8946                        );
8947                        let [r, g, b] = tex_palette(&palette, v);
8948                        let (dx, dy) = (tx + col, ty + row);
8949                        if dx < bw && dy < bh {
8950                            gfx.buffer[dy * bw + dx] = tex_rgb(r, g, b);
8951                        }
8952                    }
8953                }
8954                return Ok(Value::Unit);
8955            },
8956
8957            // tex_freq_map(x, y, w, h, time, speed, palette)
8958            // Uses bands written by the last fft_bands() call.
8959            "tex_freq_map" | "ลายความถี่" | "نقشه_فرکانس_بافت" | "خريطة_تردد_النسيج" | "מפת_תדר_מרקם" | "فریکوئنسی_میپ_ٹیکسچر" => {
8960                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8961                let time = self.arg_num(&args, 4, 0.0)? as f32;
8962                let speed = self.arg_num(&args, 5, 0.3)? as f32;
8963                let palette = self.arg_str(&args, 6, "rainbow");
8964                let bands: Vec<f32> = {
8965                    let c = self.fft_bands_cache.borrow();
8966                    if c.is_empty() {
8967                        vec![0.0; 32]
8968                    } else {
8969                        c.clone()
8970                    }
8971                };
8972                let n = bands.len().max(1);
8973                let mut gfx = self.gfx.borrow_mut();
8974                let (bw, bh) = (gfx.width, gfx.height);
8975                for row in 0..th {
8976                    for col in 0..tw {
8977                        let band_idx = (col * n / tw.max(1)).min(n - 1);
8978                        let mag = bands[band_idx].clamp(0., 1.);
8979                        let fill_y = (mag * th as f32) as usize;
8980                        if row >= th.saturating_sub(fill_y) {
8981                            let t = (col as f32 / tw as f32 + time * speed) % 1.0;
8982                            let [r, g, b] = tex_palette(&palette, t);
8983                            let bright = mag * (1.0 - row as f32 / th as f32 * 0.5);
8984                            let (dx, dy) = (tx + col, ty + row);
8985                            if dx < bw && dy < bh {
8986                                gfx.buffer[dy * bw + dx] =
8987                                    tex_rgb(r * bright, g * bright, b * bright);
8988                            }
8989                        }
8990                    }
8991                }
8992                return Ok(Value::Unit);
8993            },
8994
8995            // tex_spiral(x, y, w, h, freq, bands, time, palette)
8996            "tex_spiral" | "ลายเกลียวหมุน" | "بافت_مارپیچ" | "نسيج_حلزوني" | "מרקם_ספירלה" | "سرپیچ_ٹیکسچر" => {
8997                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8998                let freq = self.arg_num(&args, 4, 5.0)? as f32;
8999                let n_bands = self.arg_num(&args, 5, 8.0)? as f32;
9000                let time = self.arg_num(&args, 6, 0.0)? as f32;
9001                let palette = self.arg_str(&args, 7, "rainbow");
9002                let mut gfx = self.gfx.borrow_mut();
9003                let (bw, bh) = (gfx.width, gfx.height);
9004                for row in 0..th {
9005                    for col in 0..tw {
9006                        let nx = col as f32 / tw as f32 - 0.5;
9007                        let ny = row as f32 / th as f32 - 0.5;
9008                        let r = (nx * nx + ny * ny).sqrt();
9009                        let theta = ny.atan2(nx);
9010                        let t = ((r * freq - theta / std::f32::consts::TAU + time * 0.5) * n_bands
9011                            % 1.0)
9012                            .abs();
9013                        let [cr, cg, cb] = tex_palette(&palette, t);
9014                        let (dx, dy) = (tx + col, ty + row);
9015                        if dx < bw && dy < bh {
9016                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
9017                        }
9018                    }
9019                }
9020                return Ok(Value::Unit);
9021            },
9022
9023            // tex_ripple(x, y, w, h, freq, cx, cy, time, palette)
9024            "tex_ripple" | "ลายระลอก" | "بافت_موج" | "نسيج_تموج" | "מרקם_אדווה" | "ریپل_ٹیکسچر" => {
9025                let (tx, ty, tw, th) = self.tex_rect(&args)?;
9026                let freq = self.arg_num(&args, 4, 10.0)? as f32;
9027                let rcx = self.arg_num(&args, 5, 0.5)? as f32;
9028                let rcy = self.arg_num(&args, 6, 0.5)? as f32;
9029                let time = self.arg_num(&args, 7, 0.0)? as f32;
9030                let palette = self.arg_str(&args, 8, "ocean");
9031                let mut gfx = self.gfx.borrow_mut();
9032                let (bw, bh) = (gfx.width, gfx.height);
9033                for row in 0..th {
9034                    for col in 0..tw {
9035                        let nx = col as f32 / tw as f32 - rcx;
9036                        let ny = row as f32 / th as f32 - rcy;
9037                        let r = (nx * nx + ny * ny).sqrt();
9038                        let t = ((r * freq - time) % 1.0).abs();
9039                        let [cr, cg, cb] = tex_palette(&palette, t);
9040                        let (dx, dy) = (tx + col, ty + row);
9041                        if dx < bw && dy < bh {
9042                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
9043                        }
9044                    }
9045                }
9046                return Ok(Value::Unit);
9047            },
9048
9049            // tex_mandelbrot(x, y, w, h, zoom, cx, cy, max_iter, palette)
9050            "tex_mandelbrot" | "ลายแมนเดลบรอต" | "بافت_ماندلبرو" | "نسيج_مانديلبروت" | "מרקם_מנדלברוט" | "مینڈل_بروٹ_ٹیکسچر" => {
9051                let (tx, ty, tw, th) = self.tex_rect(&args)?;
9052                let zoom = self.arg_num(&args, 4, 1.0)?;
9053                let mcx = self.arg_num(&args, 5, -0.5)?;
9054                let mcy = self.arg_num(&args, 6, 0.0)?;
9055                let max_iter = self.arg_num(&args, 7, 64.0)? as u32;
9056                let palette = self.arg_str(&args, 8, "psychedelic");
9057                let mut gfx = self.gfx.borrow_mut();
9058                let (bw, bh) = (gfx.width, gfx.height);
9059                for row in 0..th {
9060                    for col in 0..tw {
9061                        let zx0 = (col as f64 / tw as f64 - 0.5) / zoom + mcx;
9062                        let zy0 = (row as f64 / th as f64 - 0.5) / zoom + mcy;
9063                        let mut x = 0.0f64;
9064                        let mut y = 0.0f64;
9065                        let mut i = 0u32;
9066                        while i < max_iter && x * x + y * y < 4.0 {
9067                            let t = x * x - y * y + zx0;
9068                            y = 2.0 * x * y + zy0;
9069                            x = t;
9070                            i += 1;
9071                        }
9072                        let t = if i == max_iter {
9073                            0.0f32
9074                        } else {
9075                            (i as f32
9076                                - (x as f32 * x as f32 + y as f32 * y as f32).ln().ln()
9077                                    / 2.0f32.ln())
9078                                / max_iter as f32
9079                        };
9080                        let [cr, cg, cb] = tex_palette(&palette, t.clamp(0., 1.));
9081                        let (dx, dy) = (tx + col, ty + row);
9082                        if dx < bw && dy < bh {
9083                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
9084                        }
9085                    }
9086                }
9087                return Ok(Value::Unit);
9088            },
9089
9090            // tex_julia(x, y, w, h, c_re, c_im, max_iter, palette)
9091            "tex_julia" | "ลายจูเลีย" | "بافت_ژولیا" | "نسيج_جوليا" | "מרקם_ג'וליה" | "جولیا_ٹیکسچر" => {
9092                let (tx, ty, tw, th) = self.tex_rect(&args)?;
9093                let c_re = self.arg_num(&args, 4, -0.7)?;
9094                let c_im = self.arg_num(&args, 5, 0.27)?;
9095                let max_iter = self.arg_num(&args, 6, 64.0)? as u32;
9096                let palette = self.arg_str(&args, 7, "neon");
9097                let mut gfx = self.gfx.borrow_mut();
9098                let (bw, bh) = (gfx.width, gfx.height);
9099                for row in 0..th {
9100                    for col in 0..tw {
9101                        let mut zx = (col as f64 / tw as f64 - 0.5) * 3.5;
9102                        let mut zy = (row as f64 / th as f64 - 0.5) * 3.5;
9103                        let mut i = 0u32;
9104                        while i < max_iter && zx * zx + zy * zy < 4.0 {
9105                            let t = zx * zx - zy * zy + c_re;
9106                            zy = 2.0 * zx * zy + c_im;
9107                            zx = t;
9108                            i += 1;
9109                        }
9110                        let t = i as f32 / max_iter as f32;
9111                        let [cr, cg, cb] = tex_palette(&palette, t);
9112                        let (dx, dy) = (tx + col, ty + row);
9113                        if dx < bw && dy < bh {
9114                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
9115                        }
9116                    }
9117                }
9118                return Ok(Value::Unit);
9119            },
9120
9121            // tex_voronoi(x, y, w, h, cells, seed, palette)
9122            "tex_voronoi" | "ลายโวโรนอย" | "بافت_ورونوی" | "نسيج_فورونوي" | "מרקם_וורונוי" | "ورونوئی_ٹیکسچر" => {
9123                let (tx, ty, tw, th) = self.tex_rect(&args)?;
9124                let cells = self.arg_num(&args, 4, 16.0)? as u32;
9125                let seed = self.arg_num(&args, 5, 42.0)? as u32;
9126                let palette = self.arg_str(&args, 6, "rainbow");
9127                let pts: Vec<[f32; 2]> = (0..cells)
9128                    .map(|i| {
9129                        [
9130                            tex_hash(i as i32, 0, seed),
9131                            tex_hash(i as i32, 1, seed + 999),
9132                        ]
9133                    })
9134                    .collect();
9135                let mut gfx = self.gfx.borrow_mut();
9136                let (bw, bh) = (gfx.width, gfx.height);
9137                for row in 0..th {
9138                    for col in 0..tw {
9139                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
9140                        let (min_d, nearest) = pts.iter().enumerate().fold(
9141                            (f32::MAX, 0usize),
9142                            |(d, idx), (i, &[cx, cy])| {
9143                                let dd = (fx - cx).powi(2) + (fy - cy).powi(2);
9144                                if dd < d {
9145                                    (dd, i)
9146                                } else {
9147                                    (d, idx)
9148                                }
9149                            },
9150                        );
9151                        let t = (nearest as f32 / cells as f32 + min_d * 4.0) % 1.0;
9152                        let [cr, cg, cb] = tex_palette(&palette, t);
9153                        let (dx, dy) = (tx + col, ty + row);
9154                        if dx < bw && dy < bh {
9155                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
9156                        }
9157                    }
9158                }
9159                return Ok(Value::Unit);
9160            },
9161
9162            // tex_halftone(x, y, w, h, dot_size, time, palette)
9163            "tex_halftone" | "ลายฮาล์ฟโทน" | "بافت_نیم‌تن" | "نسيج_نصفي" | "מרקם_חצי_גוון" | "ہاف_ٹون_ٹیکسچر" => {
9164                let (tx, ty, tw, th) = self.tex_rect(&args)?;
9165                let dot_size = self.arg_num(&args, 4, 0.05)? as f32;
9166                let time = self.arg_num(&args, 5, 0.0)? as f32;
9167                let palette = self.arg_str(&args, 6, "rainbow");
9168                let mut gfx = self.gfx.borrow_mut();
9169                let (bw, bh) = (gfx.width, gfx.height);
9170                for row in 0..th {
9171                    for col in 0..tw {
9172                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
9173                        let gx = (fx / dot_size).floor();
9174                        let gy = (fy / dot_size).floor();
9175                        let lx = (fx / dot_size - gx - 0.5) * 2.0;
9176                        let ly = (fy / dot_size - gy - 0.5) * 2.0;
9177                        let r = (lx * lx + ly * ly).sqrt();
9178                        let t = (gx / (1.0 / dot_size) + time * 0.1) % 1.0;
9179                        let a = if r < 0.7 {
9180                            ((0.7 - r) / 0.7).clamp(0., 1.)
9181                        } else {
9182                            0.0
9183                        };
9184                        if a > 0.0 {
9185                            let [cr, cg, cb] = tex_palette(&palette, t);
9186                            let (dx, dy) = (tx + col, ty + row);
9187                            if dx < bw && dy < bh {
9188                                gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
9189                            }
9190                        }
9191                    }
9192                }
9193                return Ok(Value::Unit);
9194            },
9195
9196            // ══════════════════════════════════════════════════════════════════
9197            // RENDER / LIGHTING MODES  (holographic cel shading)
9198            // ══════════════════════════════════════════════════════════════════
9199            // set_shade_mode(m) — 0 flat · 1 cel · 2 holo (default)
9200            "set_shade_mode" | "设置着色" | "シェード設定" | "셰이드모드" | "ตั้งการแรเงา" | "تنظیم_حالت_سایه‌پردازی" | "عيّن_نمط_التظليل" | "קבע_מצב_הצללה" | "شیڈ_موڈ_مقرر_کرو" | "définir_mode_ombrage" | "schattierungsmodus_setzen" | "задать_режим_затенения" =>
9201            {
9202                let m = self.arg_num(&args, 0, 2.0)? as u8;
9203                self.gfx.borrow_mut().shade_mode = m;
9204                return Ok(Value::Unit);
9205            },
9206            // set_cel_bands(n) — number of posterisation bands (>=2)
9207            "set_cel_bands" | "设置色阶" | "セル段数" | "셀밴드" | "ตั้งระดับสี" | "تنظیم_باندهای_سل" | "عيّن_نطاقات_التظليل" | "קבע_רצועות_הצללה" | "سیل_بینڈز_مقرر_کرو" | "définir_bandes_cel" | "cel_bänder_setzen" | "задать_полосы_cel" =>
9208            {
9209                let n = (self.arg_num(&args, 0, 4.0)? as u32).max(2);
9210                self.gfx.borrow_mut().shade.bands = n;
9211                return Ok(Value::Unit);
9212            },
9213            // set_shadow_color(r,g,b) — coloured-shadow tint, 0-255
9214            "set_shadow_color" | "设置阴影色" | "影の色" | "그림자색" | "ตั้งสีเงา" | "تنظیم_رنگ_سایه" | "عيّن_لون_الظل" | "קבע_צבע_צל" | "سایہ_رنگ_مقرر_کرو" | "définir_couleur_ombre" | "schattenfarbe_setzen" | "задать_цвет_тени" =>
9215            {
9216                let r = self.arg_num(&args, 0, 26.)? as f32 / 255.0;
9217                let g = self.arg_num(&args, 1, 33.)? as f32 / 255.0;
9218                let b = self.arg_num(&args, 2, 77.)? as f32 / 255.0;
9219                self.gfx.borrow_mut().shade.shadow = [r, g, b];
9220                return Ok(Value::Unit);
9221            },
9222            // set_rim(strength, r,g,b) — holographic fresnel edge glow
9223            // ══════════════════════════════════════════════════════════════════
9224            // CRYPTOGRAPHY (ling-crypto) — geo suite, hybrid PQ KEM, holographic
9225            // Bytes cross the language boundary as lowercase hex strings.
9226            // ══════════════════════════════════════════════════════════════════
9227            #[cfg(not(target_arch = "wasm32"))]
9228            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" | "درهم_رمزنگاری" | "بصمة_تشفير" | "גיבוב_הצפנה" | "خفیہ_ہیش" | "hachage_crypto" | "krypto_hash" | "крипто_хеш" =>
9229            {
9230                let s = self.arg_str(&args, 0, "");
9231                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
9232                    s.as_bytes(),
9233                ))));
9234            },
9235            #[cfg(target_arch = "wasm32")]
9236            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" | "درهم_رمزنگاری" | "بصمة_تشفير" | "גיבוב_הצפנה" | "خفیہ_ہیش" | "hachage_crypto" | "krypto_hash" | "крипто_хеш" =>
9237            {
9238                let s = self.arg_str(&args, 0, "");
9239                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
9240                    s.as_bytes(),
9241                ))));
9242            },
9243            // 3-D torus-knot fingerprint of any text/key → flat [x,y,z, x,y,z, …]
9244            #[cfg(not(target_arch = "wasm32"))]
9245            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" | "نقاط_گره" | "نقاط_العقدة" | "נקודות_קשר" | "گرہ_پوائنٹس" | "points_nœud" | "knotenpunkte" | "точки_узла" => {
9246                let s = self.arg_str(&args, 0, "");
9247                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
9248                let mut out = Vec::with_capacity(shape.points.len() * 3);
9249                for p in &shape.points {
9250                    out.push(Value::Number(p[0] as f64));
9251                    out.push(Value::Number(p[1] as f64));
9252                    out.push(Value::Number(p[2] as f64));
9253                }
9254                return Ok(Value::List(Rc::new(out)));
9255            },
9256            #[cfg(target_arch = "wasm32")]
9257            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" | "نقاط_گره" | "نقاط_العقدة" | "נקודות_קשר" | "گرہ_پوائنٹس" | "points_nœud" | "knotenpunkte" | "точки_узла" => {
9258                let s = self.arg_str(&args, 0, "");
9259                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
9260                let mut out = Vec::with_capacity(shape.points.len() * 3);
9261                for p in &shape.points {
9262                    out.push(Value::Number(p[0] as f64));
9263                    out.push(Value::Number(p[1] as f64));
9264                    out.push(Value::Number(p[2] as f64));
9265                }
9266                return Ok(Value::List(out.into()));
9267            },
9268            #[cfg(not(target_arch = "wasm32"))]
9269            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" | "برچسب_گره" | "تسمية_العقدة" | "תווית_קשר" | "گرہ_لیبل" | "étiquette_nœud" | "knotenbezeichnung" | "метка_узла" =>
9270            {
9271                let s = self.arg_str(&args, 0, "");
9272                return Ok(Value::Str(
9273                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
9274                ));
9275            },
9276            #[cfg(target_arch = "wasm32")]
9277            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" | "برچسب_گره" | "تسمية_العقدة" | "תווית_קשר" | "گرہ_لیبل" | "étiquette_nœud" | "knotenbezeichnung" | "метка_узла" =>
9278            {
9279                let s = self.arg_str(&args, 0, "");
9280                return Ok(Value::Str(
9281                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
9282                ));
9283            },
9284            // KEM keypair (hybrid X25519+ML-KEM-768) → integer handle
9285            #[cfg(not(target_arch = "wasm32"))]
9286            "knot_keygen" | "hybrid_keygen" | "สร้างกุญแจปม" | "生成密钥" | "鍵生成" | "키생성" | "تولید_کلید_گره" | "توليد_مفتاح_العقدة" | "יצירת_מפתח_קשר" | "گرہ_کلید_تخلیق" | "génération_clé_nœud" | "knotenschlüsselerzeugung" | "генерация_ключа_узла" =>
9287            {
9288                self.crypto_ids.push(ling_crypto::KnotIdentity::generate());
9289                return Ok(Value::Number((self.crypto_ids.len() - 1) as f64));
9290            },
9291            #[cfg(not(target_arch = "wasm32"))]
9292            "knot_public" | "hybrid_public" | "กุญแจสาธารณะปม" | "公钥" | "公開鍵" | "공개키" | "کلید_عمومی_گره" | "مفتاح_العقدة_العام" | "מפתח_ציבורי_קשר" | "گرہ_عوامی_کلید" | "clé_publique_nœud" | "knoten_öffentlicher_schlüssel" | "публичный_ключ_узла" =>
9293            {
9294                let h = self.arg_num(&args, 0, 0.0)? as usize;
9295                let pk = self
9296                    .crypto_ids
9297                    .get(h)
9298                    .map(|id| hex_encode(id.public_key()))
9299                    .unwrap_or_default();
9300                return Ok(Value::Str(pk));
9301            },
9302            // encapsulate(pubkey_hex) → [ciphertext_hex, shared_secret_hex]
9303            #[cfg(not(target_arch = "wasm32"))]
9304            "knot_encapsulate"
9305            | "hybrid_encapsulate"
9306            | "ห่อกุญแจปม"
9307            | "封装密钥"
9308            | "カプセル化"
9309            | "캡슐화" | "کپسوله‌سازی_گره" | "تغليف_مفتاح_العقدة" | "עטיפת_קשר" | "گرہ_احاطہ" | "encapsuler_nœud" | "knoten_kapseln" | "инкапсулировать_узел" => {
9310                let pk = hex_decode(&self.arg_str(&args, 0, ""));
9311                match ling_crypto::geo::knot_encapsulate(&pk) {
9312                    Ok((ct, ss)) => {
9313                        return Ok(Value::List(Rc::new(vec![
9314                            Value::Str(hex_encode(&ct)),
9315                            Value::Str(hex_encode(&ss)),
9316                        ])))
9317                    },
9318                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9319                }
9320            },
9321            // decapsulate(handle, ciphertext_hex) → shared_secret_hex
9322            #[cfg(not(target_arch = "wasm32"))]
9323            "knot_decapsulate"
9324            | "hybrid_decapsulate"
9325            | "แกะกุญแจปม"
9326            | "解封装密钥"
9327            | "カプセル解除"
9328            | "캡슐해제" | "بازکردن_کپسوله_گره" | "فك_تغليف_مفتاح_العقدة" | "פתיחת_עטיפת_קשר" | "گرہ_احاطہ_کھولو" | "décapsuler_nœud" | "knoten_entkapseln" | "декапсулировать_узел" => {
9329                let h = self.arg_num(&args, 0, 0.0)? as usize;
9330                let ct = hex_decode(&self.arg_str(&args, 1, ""));
9331                let ss = self
9332                    .crypto_ids
9333                    .get(h)
9334                    .and_then(|id| id.decapsulate(&ct).ok())
9335                    .map(|s| hex_encode(&s))
9336                    .unwrap_or_default();
9337                return Ok(Value::Str(ss));
9338            },
9339            // Authenticated encryption (XChaCha20-Poly1305) — seal(key_hex, text) → ct_hex
9340            #[cfg(not(target_arch = "wasm32"))]
9341            "crypto_seal" | "ผนึก" | "封印" | "封印する" | "봉인" | "مهر_رمزنگاری" | "ختم_تشفير" | "חתימת_הצפנה" | "خفیہ_مہر" | "sceller_crypto" | "krypto_versiegeln" | "запечатать_крипто" => {
9342                let key = hex_to_32(&self.arg_str(&args, 0, ""));
9343                let pt = self.arg_str(&args, 1, "");
9344                match ling_crypto::geo::holo_seal(key, pt.as_bytes()) {
9345                    Ok(ct) => return Ok(Value::Str(hex_encode(&ct))),
9346                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9347                }
9348            },
9349            #[cfg(not(target_arch = "wasm32"))]
9350            "crypto_open" | "เปิดผนึก" | "解封" | "封印解除" | "봉인해제" | "بازکردن_مهر" | "فتح_الختم" | "פתיחת_חתימה" | "مہر_کھولو" | "ouvrir_crypto" | "krypto_öffnen" | "открыть_крипто" =>
9351            {
9352                let key = hex_to_32(&self.arg_str(&args, 0, ""));
9353                let ct = hex_decode(&self.arg_str(&args, 1, ""));
9354                match ling_crypto::geo::holo_open(key, &ct) {
9355                    Ok(pt) => return Ok(Value::Str(String::from_utf8_lossy(&pt).into_owned())),
9356                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9357                }
9358            },
9359            // Holographic all-or-nothing transform — 4-D fragment coords [a,b,c,d, …]
9360            #[cfg(not(target_arch = "wasm32"))]
9361            "holo_points" | "จุดโฮโลแกรม" | "全息点" | "ホログラム点" | "홀로그램점" | "نقاط_هولوگرام" | "نقاط_الهولوغرام" | "נקודות_הולוגרמה" | "ہولوگرام_پوائنٹس" | "points_holo" | "holo_punkte" | "точки_голо" =>
9362            {
9363                let s = self.arg_str(&args, 0, "");
9364                let frags = ling_crypto::geo::scatter(s.as_bytes());
9365                let mut out = Vec::with_capacity(frags.len() * 4);
9366                for f in &frags {
9367                    for c in f.coord {
9368                        out.push(Value::Number(c as f64));
9369                    }
9370                }
9371                return Ok(Value::List(Rc::new(out)));
9372            },
9373            #[cfg(not(target_arch = "wasm32"))]
9374            "holo_fragment_count"
9375            | "จำนวนชิ้นโฮโลแกรม"
9376            | "全息碎片数"
9377            | "ホログラム断片数"
9378            | "홀로그램조각수" | "تعداد_قطعات_هولوگرام" | "عدد_شظايا_الهولوغرام" | "מספר_שברי_הולוגרמה" | "ہولوگرام_ٹکڑے_تعداد" | "nombre_fragments_holo" | "holo_fragmentanzahl" | "число_фрагментов_голо" => {
9379                let s = self.arg_str(&args, 0, "");
9380                return Ok(Value::Number(
9381                    ling_crypto::geo::scatter(s.as_bytes()).len() as f64
9382                ));
9383            },
9384            // SHAKE-256 XOF, squeezed to an arbitrary output length in bytes
9385            // (`shake_hex(s, 128)` = a 1024-bit seal digest).
9386            #[cfg(not(target_arch = "wasm32"))]
9387            "shake_hex" | "SHAKE哈希" => {
9388                let s = self.arg_str(&args, 0, "");
9389                let len = self.arg_num(&args, 1, 32.0)?.max(0.0) as usize;
9390                return Ok(Value::Str(hex_encode(&ling_crypto::Shake256::hash(s.as_bytes(), len))));
9391            },
9392            // Ed25519 signing keypair (issuer identity) → integer handle.
9393            #[cfg(not(target_arch = "wasm32"))]
9394            "ed25519_keygen"
9395            | "생성서명키"
9396            | "สร้างกุญแจลายเซ็น"
9397            | "生成签名密钥"
9398            | "署名鍵生成"
9399            | "تولید_کلید_امضا"
9400            | "توليد_مفتاح_التوقيع"
9401            | "יצירת_מפתח_חתימה"
9402            | "دستخط_کلید_تخلیق"
9403            | "génération_clé_signature"
9404            | "signaturschlüsselerzeugung"
9405            | "генерация_ключа_подписи" => {
9406                self.ed25519_ids.push(ling_crypto::Ed25519Keypair::generate());
9407                return Ok(Value::Number((self.ed25519_ids.len() - 1) as f64));
9408            },
9409            // Deterministic keypair from a 32-byte hex seed — the same seed
9410            // always yields the same keypair, so a program can persist just the
9411            // seed (e.g. a bank's issuer identity) and rederive identical keys
9412            // across restarts instead of every run minting a fresh, unrelated one.
9413            #[cfg(not(target_arch = "wasm32"))]
9414            "ed25519_keygen_from_seed"
9415            | "씨앗에서생성서명키"
9416            | "สร้างกุญแจลายเซ็นจากเมล็ด"
9417            | "从种子生成签名密钥"
9418            | "シードから署名鍵生成"
9419            | "تولید_کلید_امضا_از_دانه"
9420            | "توليد_مفتاح_التوقيع_من_البذرة"
9421            | "יצירת_מפתח_חתימה_מזרע"
9422            | "بیج_سے_دستخط_کلید_تخلیق"
9423            | "génération_clé_signature_depuis_graine"
9424            | "signaturschlüsselerzeugung_aus_saat"
9425            | "генерация_ключа_подписи_из_семени" => {
9426                let seed = hex_to_32(&self.arg_str(&args, 0, ""));
9427                self.ed25519_ids.push(ling_crypto::Ed25519Keypair::from_seed(seed));
9428                return Ok(Value::Number((self.ed25519_ids.len() - 1) as f64));
9429            },
9430            #[cfg(not(target_arch = "wasm32"))]
9431            "ed25519_public"
9432            | "서명공개키"
9433            | "กุญแจสาธารณะลายเซ็น"
9434            | "签名公钥"
9435            | "署名公開鍵"
9436            | "کلید_عمومی_امضا"
9437            | "مفتاح_التوقيع_العام"
9438            | "מפתח_חתימה_ציבורי"
9439            | "دستخط_عوامی_کلید"
9440            | "clé_publique_signature"
9441            | "signatur_öffentlicher_schlüssel"
9442            | "публичный_ключ_подписи" => {
9443                let h = self.arg_num(&args, 0, 0.0)? as usize;
9444                let pk = self
9445                    .ed25519_ids
9446                    .get(h)
9447                    .map(|kp| hex_encode(&kp.public_key()))
9448                    .unwrap_or_default();
9449                return Ok(Value::Str(pk));
9450            },
9451            // ed25519_sign(handle, message) → signature hex (64 bytes)
9452            #[cfg(not(target_arch = "wasm32"))]
9453            "ed25519_sign"
9454            | "서명하다"
9455            | "เซ็นชื่อ"
9456            | "签名"
9457            | "署名する"
9458            | "امضا_کردن"
9459            | "توقيع"
9460            | "לחתום"
9461            | "دستخط_کریں"
9462            | "signer"
9463            | "signieren"
9464            | "подписать" => {
9465                let h = self.arg_num(&args, 0, 0.0)? as usize;
9466                let msg = self.arg_str(&args, 1, "");
9467                let sig = self
9468                    .ed25519_ids
9469                    .get(h)
9470                    .map(|kp| hex_encode(&kp.sign(msg.as_bytes())))
9471                    .unwrap_or_default();
9472                return Ok(Value::Str(sig));
9473            },
9474            // ed25519_verify(pubkey_hex, message, signature_hex) → bool
9475            #[cfg(not(target_arch = "wasm32"))]
9476            "ed25519_verify"
9477            | "서명확인"
9478            | "ยืนยันลายเซ็น"
9479            | "验证签名"
9480            | "署名検証"
9481            | "تایید_امضا"
9482            | "التحقق_من_التوقيع"
9483            | "אימות_חתימה"
9484            | "دستخط_تصدیق"
9485            | "vérifier_signature"
9486            | "signatur_verifizieren"
9487            | "проверить_подпись" => {
9488                let pk_hex = self.arg_str(&args, 0, "");
9489                let msg = self.arg_str(&args, 1, "");
9490                let sig_hex = self.arg_str(&args, 2, "");
9491                let pk_bytes = hex_decode(&pk_hex);
9492                let sig_bytes = hex_decode(&sig_hex);
9493                let ok = (|| {
9494                    let pk: [u8; 32] = pk_bytes.try_into().ok()?;
9495                    let sig: [u8; 64] = sig_bytes.try_into().ok()?;
9496                    Some(ling_crypto::Ed25519Keypair::verify(&pk, msg.as_bytes(), &sig).is_ok())
9497                })()
9498                .unwrap_or(false);
9499                return Ok(Value::Bool(ok));
9500            },
9501            // ML-DSA-65 (FIPS 204, post-quantum) signing keypair → integer
9502            // handle. Meant to be composed with ed25519_* by application code
9503            // into a hybrid signature (verify passes only if both check out),
9504            // not as a replacement for Ed25519 on its own. Aliases below mirror
9505            // the ed25519_* ones with each language's word for "quantum" folded
9506            // in, the same way knot_keygen/hybrid_keygen distinguish themselves.
9507            #[cfg(not(target_arch = "wasm32"))]
9508            "mldsa_keygen"
9509            | "양자서명키생성"
9510            | "สร้างกุญแจลายเซ็นควอนตัม"
9511            | "生成量子签名密钥"
9512            | "量子署名鍵生成"
9513            | "تولید_کلید_امضای_کوانتومی"
9514            | "توليد_مفتاح_التوقيع_الكمي"
9515            | "יצירת_מפתח_חתימה_קוונטי"
9516            | "کوانٹم_دستخط_کلید_تخلیق"
9517            | "génération_clé_signature_quantique"
9518            | "quantensignaturschlüsselerzeugung"
9519            | "генерация_ключа_квантовой_подписи" => {
9520                self.mldsa_ids.push(ling_crypto::MlDsa65Keypair::generate());
9521                return Ok(Value::Number((self.mldsa_ids.len() - 1) as f64));
9522            },
9523            // Deterministic keypair from a 32-byte hex seed — same rederive-
9524            // from-seed contract as ed25519_keygen_from_seed.
9525            #[cfg(not(target_arch = "wasm32"))]
9526            "mldsa_keygen_from_seed"
9527            | "씨앗에서양자서명키생성"
9528            | "สร้างกุญแจลายเซ็นควอนตัมจากเมล็ด"
9529            | "从种子生成量子签名密钥"
9530            | "シードから量子署名鍵生成"
9531            | "تولید_کلید_امضای_کوانتومی_از_دانه"
9532            | "توليد_مفتاح_التوقيع_الكمي_من_البذرة"
9533            | "יצירת_מפתח_חתימה_קוונטי_מזרע"
9534            | "بیج_سے_کوانٹم_دستخط_کلید_تخلیق"
9535            | "génération_clé_signature_quantique_depuis_graine"
9536            | "quantensignaturschlüsselerzeugung_aus_saat"
9537            | "генерация_ключа_квантовой_подписи_из_семени" => {
9538                let seed = hex_to_32(&self.arg_str(&args, 0, ""));
9539                self.mldsa_ids.push(ling_crypto::MlDsa65Keypair::from_seed(seed));
9540                return Ok(Value::Number((self.mldsa_ids.len() - 1) as f64));
9541            },
9542            #[cfg(not(target_arch = "wasm32"))]
9543            "mldsa_public"
9544            | "양자서명공개키"
9545            | "กุญแจสาธารณะลายเซ็นควอนตัม"
9546            | "量子签名公钥"
9547            | "量子署名公開鍵"
9548            | "کلید_عمومی_امضای_کوانتومی"
9549            | "مفتاح_التوقيع_الكمي_العام"
9550            | "מפתח_חתימה_קוונטי_ציבורי"
9551            | "کوانٹم_دستخط_عوامی_کلید"
9552            | "clé_publique_signature_quantique"
9553            | "quantensignatur_öffentlicher_schlüssel"
9554            | "публичный_ключ_квантовой_подписи" => {
9555                let h = self.arg_num(&args, 0, 0.0)? as usize;
9556                let pk = self
9557                    .mldsa_ids
9558                    .get(h)
9559                    .map(|kp| hex_encode(&kp.public_key()))
9560                    .unwrap_or_default();
9561                return Ok(Value::Str(pk));
9562            },
9563            // mldsa_sign(handle, message) → signature hex (~3309 bytes)
9564            #[cfg(not(target_arch = "wasm32"))]
9565            "mldsa_sign"
9566            | "양자서명하다"
9567            | "เซ็นชื่อควอนตัม"
9568            | "量子签名"
9569            | "量子署名する"
9570            | "امضای_کوانتومی_کردن"
9571            | "توقيع_كمي"
9572            | "לחתום_קוונטית"
9573            | "کوانٹم_دستخط_کریں"
9574            | "signer_quantique"
9575            | "quantensignieren"
9576            | "подписать_квантово" => {
9577                let h = self.arg_num(&args, 0, 0.0)? as usize;
9578                let msg = self.arg_str(&args, 1, "");
9579                let sig = self
9580                    .mldsa_ids
9581                    .get(h)
9582                    .map(|kp| hex_encode(&kp.sign(msg.as_bytes())))
9583                    .unwrap_or_default();
9584                return Ok(Value::Str(sig));
9585            },
9586            // mldsa_verify(pubkey_hex, message, signature_hex) → bool
9587            #[cfg(not(target_arch = "wasm32"))]
9588            "mldsa_verify"
9589            | "양자서명확인"
9590            | "ยืนยันลายเซ็นควอนตัม"
9591            | "验证量子签名"
9592            | "量子署名検証"
9593            | "تایید_امضای_کوانتومی"
9594            | "التحقق_من_التوقيع_الكمي"
9595            | "אימות_חתימה_קוונטית"
9596            | "کوانٹم_دستخط_تصدیق"
9597            | "vérifier_signature_quantique"
9598            | "quantensignatur_verifizieren"
9599            | "проверить_квантовую_подпись" => {
9600                let pk_hex = self.arg_str(&args, 0, "");
9601                let msg = self.arg_str(&args, 1, "");
9602                let sig_hex = self.arg_str(&args, 2, "");
9603                let pk_bytes = hex_decode(&pk_hex);
9604                let sig_bytes = hex_decode(&sig_hex);
9605                let ok = ling_crypto::MlDsa65Keypair::verify(&pk_bytes, msg.as_bytes(), &sig_bytes)
9606                    .is_ok();
9607                return Ok(Value::Bool(ok));
9608            },
9609            // Argon2id password hashing — password_hash(pw) → PHC string,
9610            // password_verify(pw, phc_string) → bool.
9611            #[cfg(not(target_arch = "wasm32"))]
9612            "password_hash" | "비밀번호해시" => {
9613                let pw = self.arg_str(&args, 0, "");
9614                let hash = ling_crypto::Argon2idParams::default()
9615                    .hash_password(pw.as_bytes())
9616                    .unwrap_or_default();
9617                return Ok(Value::Str(hash));
9618            },
9619            #[cfg(not(target_arch = "wasm32"))]
9620            "password_verify" | "비밀번호확인" => {
9621                let pw = self.arg_str(&args, 0, "");
9622                let hash = self.arg_str(&args, 1, "");
9623                let ok = ling_crypto::Argon2idParams::verify_password(pw.as_bytes(), &hash).is_ok();
9624                return Ok(Value::Bool(ok));
9625            },
9626            // OS-CSPRNG random bytes as hex — session ids / nonces (not the
9627            // xorshift `rand` builtin, which is for game logic, not security).
9628            #[cfg(not(target_arch = "wasm32"))]
9629            "random_hex" | "무작위16진수" => {
9630                use rand::RngCore;
9631                let n = self.arg_num(&args, 0, 16.0)?.max(0.0) as usize;
9632                let mut buf = vec![0u8; n];
9633                rand::rngs::OsRng.fill_bytes(&mut buf);
9634                return Ok(Value::Str(hex_encode(&buf)));
9635            },
9636            // base64_encode(s) — text -> base64 (matches what canvas.toDataURL()
9637            // already produces client-side, so PNG uploads never need a binary
9638            // request body).
9639            #[cfg(not(target_arch = "wasm32"))]
9640            "base64_encode" | "base64인코딩" => {
9641                use base64::Engine as _;
9642                let s = self.arg_str(&args, 0, "");
9643                return Ok(Value::Str(
9644                    base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
9645                ));
9646            },
9647            // base64_decode_to_file(b64, path) — writes decoded bytes straight to
9648            // disk; returns true on success. The only way binary data (an
9649            // uploaded/rendered PNG) reaches the filesystem from `.ling` source.
9650            #[cfg(not(target_arch = "wasm32"))]
9651            "base64_decode_to_file" | "base64파일로저장" => {
9652                use base64::Engine as _;
9653                let b64 = self.arg_str(&args, 0, "");
9654                let path = self.arg_str(&args, 1, "");
9655                let b64 = b64
9656                    .split(',')
9657                    .next_back()
9658                    .unwrap_or(&b64); // tolerate a "data:image/png;base64,..." prefix
9659                let ok = base64::engine::general_purpose::STANDARD
9660                    .decode(b64)
9661                    .ok()
9662                    .and_then(|bytes| std::fs::write(&path, bytes).ok())
9663                    .is_some();
9664                return Ok(Value::Bool(ok));
9665            },
9666            // qr_svg(text) — a scannable QR code as an inline <svg>...</svg>
9667            // string (e.g. for an otpauth:// 2FA enrollment URI). Kept as SVG
9668            // rather than a rasterized image so it fits the same "everything
9669            // stays vector" theme as the banknote seal art.
9670            #[cfg(not(target_arch = "wasm32"))]
9671            "qr_svg" | "QR코드" => {
9672                let text = self.arg_str(&args, 0, "");
9673                let svg = qrcode::QrCode::new(text.as_bytes())
9674                    .map(|code| {
9675                        code.render::<qrcode::render::svg::Color>()
9676                            .min_dimensions(240, 240)
9677                            .dark_color(qrcode::render::svg::Color("#1a0f3d"))
9678                            .light_color(qrcode::render::svg::Color("#ffffff"))
9679                            .build()
9680                    })
9681                    .unwrap_or_default();
9682                return Ok(Value::Str(svg));
9683            },
9684            // zip_files(paths_list, out_path) — bundles files into a zip archive
9685            // (used by the "render" step to package a note's PNG/SVG/PDF).
9686            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
9687            "zip_files" | "압축파일" => {
9688                let paths = match args.first() {
9689                    Some(Value::List(l)) => l.iter().map(|v| v.to_string()).collect::<Vec<_>>(),
9690                    _ => Vec::new(),
9691                };
9692                let out_path = self.arg_str(&args, 1, "out.zip");
9693                let ok = (|| -> std::io::Result<()> {
9694                    let file = std::fs::File::create(&out_path)?;
9695                    let mut writer = zip::ZipWriter::new(file);
9696                    let options: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default()
9697                        .compression_method(zip::CompressionMethod::Deflated);
9698                    for p in &paths {
9699                        let name = std::path::Path::new(p)
9700                            .file_name()
9701                            .map(|n| n.to_string_lossy().into_owned())
9702                            .unwrap_or_else(|| p.clone());
9703                        let bytes = std::fs::read(p)?;
9704                        writer.start_file(name, options)?;
9705                        std::io::Write::write_all(&mut writer, &bytes)?;
9706                    }
9707                    writer.finish()?;
9708                    Ok(())
9709                })()
9710                .is_ok();
9711                return Ok(Value::Bool(ok));
9712            },
9713            // pdf_from_images(png_paths_list, out_path) — one page per image,
9714            // sized to its pixel dimensions. No PDF crate dependency: `image`
9715            // (decode) and `flate2` (deflate the page's raw RGB stream) are
9716            // already unconditional deps, so this hand-writes the handful of
9717            // PDF objects (Catalog/Pages/Page/Contents/Image XObject) directly.
9718            // `build_pdf_from_images` itself only exists under this same gate.
9719            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
9720            "pdf_from_images" | "PDF来自图片" => {
9721                let paths = match args.first() {
9722                    Some(Value::List(l)) => l.iter().map(|v| v.to_string()).collect::<Vec<_>>(),
9723                    _ => Vec::new(),
9724                };
9725                let out_path = self.arg_str(&args, 1, "out.pdf");
9726                let ok = build_pdf_from_images(&paths, &out_path).is_ok();
9727                return Ok(Value::Bool(ok));
9728            },
9729
9730            // ══════════════════════════════════════════════════════════════════
9731            // ling-ui — animation easings + holographic vector widgets + text I/O
9732            // ══════════════════════════════════════════════════════════════════
9733            "ease" => {
9734                let name = self.arg_str(&args, 0, "ease");
9735                let t = self.arg_num(&args, 1, 0.0)? as f32;
9736                return Ok(Value::Number(
9737                    ling_ui::Easing::from_name(&name).apply(t) as f64
9738                ));
9739            },
9740
9741            // ══════════════════════════════════════════════════════════════════
9742            // Anima — unified animation drivers (ling-animation). Organic 灵 +
9743            // mechanical 机 scalar drivers, callable per frame from a script.
9744            // ══════════════════════════════════════════════════════════════════
9745            "tween" | "补间" | "補間" | "트윈" | "แทรกค่า" | "میان‌فریم" | "تدرج_حركي" | "טווין" | "ٹوئین" | "твин" => {
9746                let a = self.arg_num(&args, 0, 0.0)?;
9747                let b = self.arg_num(&args, 1, 0.0)?;
9748                let t = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 1.0);
9749                return Ok(Value::Number(a + (b - a) * t));
9750            },
9751            "tween_ease" | "缓动补间" | "緩和補間" | "이징트윈" | "แทรกนุ่ม" | "میان‌فریم_نرم" | "تدرج_ناعم_حركي" | "טווין_חלק" | "ٹوئین_ایز" | "tween_lisse" | "tween_glättung" | "твин_плавность" =>
9752            {
9753                let a = self.arg_num(&args, 0, 0.0)? as f32;
9754                let b = self.arg_num(&args, 1, 0.0)? as f32;
9755                let t = self.arg_num(&args, 2, 0.0)? as f32;
9756                let kind = self.arg_str(&args, 3, "linear");
9757                let e = ling_animation::EaseFunction::from_name(&kind);
9758                return Ok(Value::Number(
9759                    ling_animation::ease::tween_ease(&a, &b, t, e) as f64,
9760                ));
9761            },
9762            // ── Organic 灵 ──
9763            "breathe" | "呼吸" | "호흡" | "หายใจ" | "تنفس" | "נשימה" | "سانس" | "respirer" | "atmen" | "дышать" => {
9764                let t = self.arg_num(&args, 0, 0.0)? as f32;
9765                let rate = self.arg_num(&args, 1, 1.0)? as f32;
9766                let depth = self.arg_num(&args, 2, 0.1)? as f32;
9767                return Ok(Value::Number(
9768                    ling_animation::scalar::breathe(t, rate, depth) as f64,
9769                ));
9770            },
9771            "wobble" | "摆动" | "揺れ" | "흔들림" | "โยก" | "نوسان" | "تذبذب" | "תנודה" | "لرزش" | "osciller" | "wackeln" | "покачивание" => {
9772                let t = self.arg_num(&args, 0, 0.0)? as f32;
9773                let freq = self.arg_num(&args, 1, 1.0)? as f32;
9774                let amp = self.arg_num(&args, 2, 1.0)? as f32;
9775                let phase = self.arg_num(&args, 3, 0.0)? as f32;
9776                return Ok(Value::Number(
9777                    ling_animation::scalar::wobble(t, freq, amp, phase) as f64,
9778                ));
9779            },
9780            "gait_phase" | "步相" | "歩相" | "걸음위상" | "เฟสก้าว" | "فاز_گام" | "طور_المشية" | "שלב_הליכה" | "چال_مرحلہ" | "phase_démarche" | "gangphase" | "фаза_походки" => {
9781                let t = self.arg_num(&args, 0, 0.0)? as f32;
9782                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9783                return Ok(Value::Number(
9784                    ling_animation::scalar::gait_phase(t, speed) as f64
9785                ));
9786            },
9787            "gait_swing" | "步摆" | "歩振り" | "걸음흔들" | "ก้าวแกว่ง" | "نوسان_گام" | "أرجحة_المشية" | "נדנוד_הליכה" | "چال_جھولا" | "balancement_démarche" | "gangschwung" | "мах_походки" =>
9788            {
9789                let t = self.arg_num(&args, 0, 0.0)? as f32;
9790                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9791                let stride = self.arg_num(&args, 2, 1.0)? as f32;
9792                return Ok(Value::Number(
9793                    ling_animation::scalar::gait_swing(t, speed, stride) as f64,
9794                ));
9795            },
9796            "gait_lift" | "抬脚" | "足上げ" | "발들기" | "ยกเท้า" | "بلندشدن_گام" | "رفع_المشية" | "הרמת_הליכה" | "چال_اٹھاؤ" | "levée_démarche" | "ganghub" | "подъём_походки" => {
9797                let t = self.arg_num(&args, 0, 0.0)? as f32;
9798                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9799                let height = self.arg_num(&args, 2, 1.0)? as f32;
9800                return Ok(Value::Number(
9801                    ling_animation::scalar::gait_lift(t, speed, height) as f64,
9802                ));
9803            },
9804            "spring_to" | "弹向" | "バネ寄せ" | "스프링이동" | "สปริงไป" | "فنر_به‌سوی" | "نابض_إلى" | "קפיץ_אל" | "اسپرنگ_تک" | "ressort_vers" | "feder_zu" | "пружина_к" =>
9805            {
9806                let pos = self.arg_num(&args, 0, 0.0)? as f32;
9807                let vel = self.arg_num(&args, 1, 0.0)? as f32;
9808                let target = self.arg_num(&args, 2, 0.0)? as f32;
9809                let stiffness = self.arg_num(&args, 3, 120.0)? as f32;
9810                let damping = self.arg_num(&args, 4, 14.0)? as f32;
9811                let dt = self.arg_num(&args, 5, 1.0 / 60.0)? as f32;
9812                let (np, nv) =
9813                    ling_animation::scalar::spring_step(pos, vel, target, stiffness, damping, dt);
9814                return Ok(Value::List(Rc::new(vec![
9815                    Value::Number(np as f64),
9816                    Value::Number(nv as f64),
9817                ])));
9818            },
9819            "ik2" | "反解" | "逆運動" | "역운동" | "ไอเค2" | "سینماتیک_معکوس2" | "حركية_عكسية2" | "קינמטיקה_הפוכה2" | "آئی_کے2" | "cinématique_inverse2" | "inverse_kinematik2" | "обратная_кинематика2" => {
9820                let l1 = self.arg_num(&args, 0, 1.0)? as f32;
9821                let l2 = self.arg_num(&args, 1, 1.0)? as f32;
9822                let tx = self.arg_num(&args, 2, 0.0)? as f32;
9823                let ty = self.arg_num(&args, 3, 0.0)? as f32;
9824                let (sh, el) = ling_animation::scalar::two_bone_ik(l1, l2, tx, ty);
9825                return Ok(Value::List(Rc::new(vec![
9826                    Value::Number(sh as f64),
9827                    Value::Number(el as f64),
9828                ])));
9829            },
9830            // ── Mechanical 机 ──
9831            "gear_couple" | "齿轮联动" | "歯車連動" | "기어연동" | "เฟืองทด" | "جفت_چرخ‌دنده" | "اقتران_التروس" | "צימוד_גלגלי_שיניים" | "گیئر_جوڑا" | "accoupler_engrenage" | "zahnrad_koppeln" | "сцепить_шестерни" =>
9832            {
9833                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9834                let ti = self.arg_num(&args, 1, 1.0)? as f32;
9835                let to = self.arg_num(&args, 2, 1.0)? as f32;
9836                return Ok(Value::Number(
9837                    ling_animation::scalar::gear(angle, ti, to) as f64
9838                ));
9839            },
9840            "gear_train" | "齿轮组" | "歯車列" | "기어열" | "ชุดเฟือง" | "مجموعه_چرخ‌دنده" | "قطار_التروس" | "שרשרת_גלגלי_שיניים" | "گیئر_ٹرین" | "train_engrenages" | "zahnradgetriebe" | "передача_шестерён" => {
9841                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9842                let teeth: Vec<f32> = match args.get(1) {
9843                    Some(Value::List(items)) => items
9844                        .iter()
9845                        .filter_map(|v| {
9846                            if let Value::Number(n) = v {
9847                                Some(*n as f32)
9848                            } else {
9849                                None
9850                            }
9851                        })
9852                        .collect(),
9853                    _ => Vec::new(),
9854                };
9855                let out = ling_animation::mechanism::gear_train(angle, &teeth);
9856                return Ok(Value::List(Rc::new(
9857                    out.into_iter().map(|a| Value::Number(a as f64)).collect(),
9858                )));
9859            },
9860            "cam_lift" | "凸轮升程" | "カム揚程" | "캠리프트" | "ยกลูกเบี้ยว" | "بلندشدن_بادامک" | "رفع_الكامة" | "הרמת_קאם" | "کیم_اٹھاؤ" | "levée_came" | "nockenhub" | "подъём_кулачка" =>
9861            {
9862                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9863                let lift = self.arg_num(&args, 1, 1.0)? as f32;
9864                return Ok(Value::Number(
9865                    ling_animation::scalar::cam_lift(angle, lift) as f64
9866                ));
9867            },
9868            "piston" | "活塞" | "ピストン" | "피스톤" | "ลูกสูบ" | "پیستون" | "مكبس" | "בוכנה" | "پسٹن" | "kolben" | "поршень" => {
9869                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9870                let crank = self.arg_num(&args, 1, 1.0)? as f32;
9871                let rod = self.arg_num(&args, 2, 2.0)? as f32;
9872                return Ok(Value::Number(
9873                    ling_animation::scalar::piston(angle, crank, rod) as f64,
9874                ));
9875            },
9876            "rack" | "齿条" | "ラック" | "랙" | "แร็ค" | "زبانه‌دنده" | "سكة_مسننة" | "מוט_שיניים" | "ریک" | "crémaillère" | "zahnstange" | "рейка" => {
9877                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9878                let radius = self.arg_num(&args, 1, 1.0)? as f32;
9879                return Ok(Value::Number(
9880                    ling_animation::scalar::rack(angle, radius) as f64
9881                ));
9882            },
9883            #[cfg(not(target_arch = "wasm32"))]
9884            "mouse_x" => {
9885                let gfx = self.gfx.borrow();
9886                let v = gfx
9887                    .window
9888                    .as_ref()
9889                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
9890                    .map(|p| p.0 as f64)
9891                    .unwrap_or(0.0);
9892                return Ok(Value::Number(v));
9893            },
9894            #[cfg(target_arch = "wasm32")]
9895            "mouse_x" => {
9896                return Ok(Value::Number(crate::gfx::wasm_mouse_x() as f64));
9897            },
9898            #[cfg(not(target_arch = "wasm32"))]
9899            "mouse_y" => {
9900                let gfx = self.gfx.borrow();
9901                let v = gfx
9902                    .window
9903                    .as_ref()
9904                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
9905                    .map(|p| p.1 as f64)
9906                    .unwrap_or(0.0);
9907                return Ok(Value::Number(v));
9908            },
9909            #[cfg(target_arch = "wasm32")]
9910            "mouse_y" => {
9911                return Ok(Value::Number(crate::gfx::wasm_mouse_y() as f64));
9912            },
9913            #[cfg(not(target_arch = "wasm32"))]
9914            "mouse_down" => {
9915                let mut gfx = self.gfx.borrow_mut();
9916                let d = !gfx.input_suppressed()
9917                    && gfx
9918                        .window
9919                        .as_ref()
9920                        .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
9921                        .unwrap_or(false);
9922                return Ok(Value::Bool(d));
9923            },
9924            #[cfg(target_arch = "wasm32")]
9925            "mouse_down" => {
9926                return Ok(Value::Bool(crate::gfx::wasm_mouse_down()));
9927            },
9928            #[cfg(not(target_arch = "wasm32"))]
9929            "mouse_down_right" | "เมาส์ขวา" | "ماوس_راست_فشرده" | "الفأرة_اليمنى_مضغوطة" | "עכבר_ימני_לחוץ" | "دایاں_ماؤس_دبا_ہوا" => {
9930                let mut gfx = self.gfx.borrow_mut();
9931                let d = !gfx.input_suppressed()
9932                    && gfx
9933                        .window
9934                        .as_ref()
9935                        .map(|w| w.get_mouse_down(minifb::MouseButton::Right))
9936                        .unwrap_or(false);
9937                return Ok(Value::Bool(d));
9938            },
9939            #[cfg(target_arch = "wasm32")]
9940            "mouse_down_right" | "เมาส์ขวา" | "ماوس_راست_فشرده" | "الفأرة_اليمنى_مضغوطة" | "עכבר_ימני_לחוץ" | "دایاں_ماؤس_دبا_ہوا" => {
9941                return Ok(Value::Bool(crate::gfx::wasm_mouse_down_right()));
9942            },
9943            #[cfg(not(target_arch = "wasm32"))]
9944            "mouse_down_middle" | "เมาส์กลาง" | "ماوس_وسط_فشرده" | "الفأرة_الوسطى_مضغوطة" | "עכבר_אמצעי_לחוץ" | "درمیانی_ماؤس_دبا_ہوا" => {
9945                let mut gfx = self.gfx.borrow_mut();
9946                let d = !gfx.input_suppressed()
9947                    && gfx
9948                        .window
9949                        .as_ref()
9950                        .map(|w| w.get_mouse_down(minifb::MouseButton::Middle))
9951                        .unwrap_or(false);
9952                return Ok(Value::Bool(d));
9953            },
9954            #[cfg(target_arch = "wasm32")]
9955            "mouse_down_middle" | "เมาส์กลาง" | "ماوس_وسط_فشرده" | "الفأرة_الوسطى_مضغوطة" | "עכבר_אמצעי_לחוץ" | "درمیانی_ماؤس_دبا_ہوا" => {
9956                return Ok(Value::Bool(crate::gfx::wasm_mouse_down_middle()));
9957            },
9958            #[cfg(not(target_arch = "wasm32"))]
9959            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" | "ناحیه_فعال" | "منطقة_ساخنة" | "אזור_חם" | "ہاٹ_زون" | "survol_ui" | "ui_hover" | "ui_наведение" =>
9960            {
9961                let x = self.arg_num(&args, 0, 0.0)? as f32;
9962                let y = self.arg_num(&args, 1, 0.0)? as f32;
9963                let w = self.arg_num(&args, 2, 0.0)? as f32;
9964                let h = self.arg_num(&args, 3, 0.0)? as f32;
9965                let gfx = self.gfx.borrow();
9966                let (mx, my) = gfx
9967                    .window
9968                    .as_ref()
9969                    .and_then(|win| win.get_mouse_pos(minifb::MouseMode::Clamp))
9970                    .unwrap_or((0.0, 0.0));
9971                return Ok(Value::Bool(ling_ui::holo::hit_rect(mx, my, x, y, w, h)));
9972            },
9973            #[cfg(target_arch = "wasm32")]
9974            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" | "ناحیه_فعال" | "منطقة_ساخنة" | "אזור_חם" | "ہاٹ_زون" | "survol_ui" | "ui_hover" | "ui_наведение" =>
9975            {
9976                return Ok(Value::Bool(false));
9977            },
9978            // ui_text(x, y, scale, "string") — holographic vector text
9979            "ui_text" | "界面文字" | "UI文字" | "UI텍스트" | "ข้อความหน้าจอ" | "متن_رابط" | "نص_الواجهة" | "טקסט_ממשק" | "یو_آئی_متن" | "texte_ui" | "ui_beschriftung" | "ui_текст" =>
9980            {
9981                let x = self.arg_num(&args, 0, 0.0)? as f32;
9982                let y = self.arg_num(&args, 1, 0.0)? as f32;
9983                let scale = self.arg_num(&args, 2, 16.0)? as f32;
9984                let s = self.arg_str(&args, 3, "");
9985                let segs = ling_ui::holo::text_lines(&s, x, y, scale * 0.62, scale, scale * 0.24);
9986                let mut gfx = self.gfx.borrow_mut();
9987                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
9988                for sg in segs {
9989                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
9990                }
9991                return Ok(Value::Unit);
9992            },
9993            // font_load("path.ttf") — load a vector font (outlines cached lazily as
9994            // cache/fonts/<stem>/<codepoint>.ling). Returns a handle, or -1 on failure.
9995            #[cfg(not(target_arch = "wasm32"))]
9996            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" | "بارگذاری_فونت" | "تحميل_الخط" | "טעינת_גופן" | "فونٹ_لوڈ" | "charger_police" | "schriftart_laden" | "загрузить_шрифт" =>
9997            {
9998                let path = self.arg_str(&args, 0, "");
9999                // Optional 2nd arg: variable-font weight (e.g. 600 for a solid, bold UI).
10000                let weight = match self.arg_num(&args, 1, 0.0)? {
10001                    w if w > 0.0 => Some(w as f32),
10002                    _ => None,
10003                };
10004                // Try the path as given, then relative to the script's directory.
10005                let mut loaded = ling_graphics::VectorFont::from_path_weight(&path, weight);
10006                if loaded.is_err() {
10007                    if let Some(dir) = &self.source_dir {
10008                        let joined = dir.join(&path);
10009                        loaded = ling_graphics::VectorFont::from_path_weight(
10010                            &joined.to_string_lossy(),
10011                            weight,
10012                        );
10013                    }
10014                }
10015                match loaded {
10016                    Ok(f) => {
10017                        let id = self.fonts.len();
10018                        self.fonts.push(f);
10019                        return Ok(Value::Number(id as f64));
10020                    },
10021                    Err(e) => {
10022                        eprintln!("font_load failed ({path}): {e}");
10023                        return Ok(Value::Number(-1.0));
10024                    },
10025                }
10026            },
10027            #[cfg(target_arch = "wasm32")]
10028            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" | "بارگذاری_فونت" | "تحميل_الخط" | "טעינת_גופן" | "فونٹ_لوڈ" | "charger_police" | "schriftart_laden" | "загрузить_шрифт" =>
10029            {
10030                // Web runtime does not load host TTF/OTF files yet.
10031                // Return -1 so scripts can fall back to ui_text.
10032                return Ok(Value::Number(-1.0));
10033            },
10034            // image_load("path.png") — decode a raster image (via the `image` crate)
10035            // for pixel sampling (image_width/image_height/image_pixel_r/g/b/a) —
10036            // used by the coin-stamp mosaic tool to read a source photo's
10037            // colour/darkness. Returns a handle, or -1 on failure.
10038            #[cfg(not(target_arch = "wasm32"))]
10039            "image_load" =>
10040            {
10041                let path = self.arg_str(&args, 0, "").replace('\\', "/");
10042                let mut loaded = image::open(&path);
10043                if loaded.is_err() {
10044                    if let Some(dir) = &self.source_dir {
10045                        let joined = dir.join(&path);
10046                        loaded = image::open(&joined);
10047                    }
10048                }
10049                match loaded {
10050                    Ok(img) => {
10051                        let id = self.images.len();
10052                        self.images.push(img.to_rgba8());
10053                        return Ok(Value::Number(id as f64));
10054                    },
10055                    Err(e) => {
10056                        eprintln!("image_load failed ({path}): {e}");
10057                        return Ok(Value::Number(-1.0));
10058                    },
10059                }
10060            },
10061            #[cfg(target_arch = "wasm32")]
10062            "image_load" =>
10063            {
10064                // Web runtime does not load host image files yet.
10065                return Ok(Value::Number(-1.0));
10066            },
10067            "image_width" =>
10068            {
10069                let id = self.arg_num(&args, 0, -1.0)? as i64;
10070                if id >= 0 && (id as usize) < self.images.len() {
10071                    return Ok(Value::Number(self.images[id as usize].width() as f64));
10072                }
10073                return Ok(Value::Number(0.0));
10074            },
10075            "image_height" =>
10076            {
10077                let id = self.arg_num(&args, 0, -1.0)? as i64;
10078                if id >= 0 && (id as usize) < self.images.len() {
10079                    return Ok(Value::Number(self.images[id as usize].height() as f64));
10080                }
10081                return Ok(Value::Number(0.0));
10082            },
10083            "image_pixel_r" | "image_pixel_g" | "image_pixel_b" | "image_pixel_a" =>
10084            {
10085                let id = self.arg_num(&args, 0, -1.0)? as i64;
10086                let px = self.arg_num(&args, 1, 0.0)? as i64;
10087                let py = self.arg_num(&args, 2, 0.0)? as i64;
10088                if id >= 0 && (id as usize) < self.images.len() {
10089                    let img = &self.images[id as usize];
10090                    if px >= 0 && py >= 0 && (px as u32) < img.width() && (py as u32) < img.height() {
10091                        let p = img.get_pixel(px as u32, py as u32);
10092                        let ch = match name {
10093                            "image_pixel_r" => p[0],
10094                            "image_pixel_g" => p[1],
10095                            "image_pixel_b" => p[2],
10096                            _ => p[3],
10097                        };
10098                        return Ok(Value::Number(ch as f64));
10099                    }
10100                }
10101                return Ok(Value::Number(0.0));
10102            },
10103            // image_new(w, h) — a new blank (fully transparent) RGBA image the
10104            // script can paint into with image_set_pixel and write out with
10105            // image_save. Lives in the same self.images table as image_load,
10106            // so image_width/image_height/image_pixel_* all work on it too.
10107            // Used by the coin-stamp tool to build cropped, physically-sized
10108            // (mm x DPI) PNG exports — something a raw window screenshot()
10109            // can't do, since it always captures the whole on-screen
10110            // framebuffer at whatever size the window happens to be.
10111            "image_new" =>
10112            {
10113                let w = self.arg_num(&args, 0, 1.0)?.max(1.0) as u32;
10114                let h = self.arg_num(&args, 1, 1.0)?.max(1.0) as u32;
10115                let id = self.images.len();
10116                self.images.push(image::RgbaImage::new(w, h));
10117                return Ok(Value::Number(id as f64));
10118            },
10119            // image_set_pixel(id, x, y, r, g, b, a) — paint one pixel of an
10120            // image created with image_new (0..255 channels; out-of-bounds is
10121            // a silent no-op, matching image_pixel_*'s own out-of-bounds
10122            // behaviour).
10123            "image_set_pixel" =>
10124            {
10125                let id = self.arg_num(&args, 0, -1.0)? as i64;
10126                let px = self.arg_num(&args, 1, 0.0)? as i64;
10127                let py = self.arg_num(&args, 2, 0.0)? as i64;
10128                let r = self.arg_num(&args, 3, 0.0)?.clamp(0.0, 255.0) as u8;
10129                let g = self.arg_num(&args, 4, 0.0)?.clamp(0.0, 255.0) as u8;
10130                let b = self.arg_num(&args, 5, 0.0)?.clamp(0.0, 255.0) as u8;
10131                let a = self.arg_num(&args, 6, 255.0)?.clamp(0.0, 255.0) as u8;
10132                if id >= 0 && (id as usize) < self.images.len() {
10133                    let img = &mut self.images[id as usize];
10134                    if px >= 0 && py >= 0 && (px as u32) < img.width() && (py as u32) < img.height() {
10135                        img.put_pixel(px as u32, py as u32, image::Rgba([r, g, b, a]));
10136                    }
10137                }
10138                return Ok(Value::Unit);
10139            },
10140            // image_save(id, "path.png") — encode an image (from image_new or
10141            // image_load) to disk, alpha preserved. Returns 1 on success, -1
10142            // on failure (bad id or write error), mirroring image_load's own
10143            // -1-on-failure convention. Path resolves the same way
10144            // write_file/copy_file's outputs do: relative to the script's own
10145            // working directory (typically the app dir the launcher cd's
10146            // into), not source_dir.
10147            #[cfg(not(target_arch = "wasm32"))]
10148            "image_save" =>
10149            {
10150                let id = self.arg_num(&args, 0, -1.0)? as i64;
10151                let path = self.arg_str(&args, 1, "");
10152                if id >= 0 && (id as usize) < self.images.len() {
10153                    if let Some(parent) = std::path::Path::new(&path).parent() {
10154                        if !parent.as_os_str().is_empty() {
10155                            let _ = std::fs::create_dir_all(parent);
10156                        }
10157                    }
10158                    if self.images[id as usize].save(&path).is_ok() {
10159                        return Ok(Value::Number(1.0));
10160                    }
10161                }
10162                return Ok(Value::Number(-1.0));
10163            },
10164            #[cfg(target_arch = "wasm32")]
10165            "image_save" =>
10166            {
10167                return Ok(Value::Number(-1.0));
10168            },
10169            // image_draw(id, x, y, w, h) — blit an image (nearest-neighbour
10170            // scaled to w x h, alpha-blended against whatever's already in
10171            // the framebuffer) into the current frame. A native pixel loop,
10172            // not a .ling-level per-pixel image_pixel_*+pixel() loop: doing
10173            // this from script for even a modest thumbnail grid re-incurs
10174            // the exact per-frame interpreted-call-volume cost that made
10175            // small mosaic tiles hang the UI (see mosaic.ling's
10176            // xform_glyph_pts_fit fix) — this is the "read the framebuffer
10177            // out" direction's counterpart to screenshot().
10178            #[cfg(not(target_arch = "wasm32"))]
10179            "image_draw" =>
10180            {
10181                let id = self.arg_num(&args, 0, -1.0)? as i64;
10182                let dx = self.arg_num(&args, 1, 0.0)? as i32;
10183                let dy = self.arg_num(&args, 2, 0.0)? as i32;
10184                let dw = self.arg_num(&args, 3, 0.0)?.max(0.0) as i32;
10185                let dh = self.arg_num(&args, 4, 0.0)?.max(0.0) as i32;
10186                if id >= 0 && (id as usize) < self.images.len() && dw > 0 && dh > 0 {
10187                    let img = &self.images[id as usize];
10188                    let sw = img.width() as i32;
10189                    let sh = img.height() as i32;
10190                    if sw > 0 && sh > 0 {
10191                        let mut gfx = self.gfx.borrow_mut();
10192                        let (fw, fh) = (gfx.width as i32, gfx.height as i32);
10193                        for py in 0..dh {
10194                            let ty = dy + py;
10195                            if ty < 0 || ty >= fh {
10196                                continue;
10197                            }
10198                            let sy = (py * sh / dh).clamp(0, sh - 1) as u32;
10199                            for px in 0..dw {
10200                                let tx = dx + px;
10201                                if tx < 0 || tx >= fw {
10202                                    continue;
10203                                }
10204                                let sx = (px * sw / dw).clamp(0, sw - 1) as u32;
10205                                let p = img.get_pixel(sx, sy);
10206                                let a = p[3] as u32;
10207                                if a == 0 {
10208                                    continue;
10209                                }
10210                                let idx = ty as usize * gfx.width + tx as usize;
10211                                if a >= 255 {
10212                                    gfx.buffer[idx] =
10213                                        ((p[0] as u32) << 16) | ((p[1] as u32) << 8) | (p[2] as u32);
10214                                } else {
10215                                    let bg = gfx.buffer[idx];
10216                                    let br = (bg >> 16) & 0xff;
10217                                    let bg_g = (bg >> 8) & 0xff;
10218                                    let bb = bg & 0xff;
10219                                    let r = (p[0] as u32 * a + br * (255 - a)) / 255;
10220                                    let g = (p[1] as u32 * a + bg_g * (255 - a)) / 255;
10221                                    let b = (p[2] as u32 * a + bb * (255 - a)) / 255;
10222                                    gfx.buffer[idx] = (r << 16) | (g << 8) | b;
10223                                }
10224                            }
10225                        }
10226                    }
10227                }
10228                return Ok(Value::Unit);
10229            },
10230            #[cfg(target_arch = "wasm32")]
10231            "image_draw" =>
10232            {
10233                return Ok(Value::Unit);
10234            },
10235            // font_text(handle, x, y, px, "string") — anti-aliased *stroked* vector outline
10236            // in the current set_color / set_blend. (x,y) is the text box top-left.
10237            #[cfg(not(target_arch = "wasm32"))]
10238            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" | "متن_فونت" | "نص_الخط" | "טקסט_גופן" | "فونٹ_متن" | "texte_police" | "schriftart_text" | "текст_шрифт" =>
10239            {
10240                let id = self.arg_num(&args, 0, 0.0)? as i64;
10241                let x = self.arg_num(&args, 1, 0.0)? as f32;
10242                let y = self.arg_num(&args, 2, 0.0)? as f32;
10243                let px = self.arg_num(&args, 3, 16.0)? as f32;
10244                let s = self.arg_str(&args, 4, "");
10245                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
10246                    let strokes = self.font_layout_2d(id as usize, x, y, px, &s);
10247                    let mut gfx = self.gfx.borrow_mut();
10248                    let (w, h, color, add, aa) =
10249                        (gfx.width, gfx.height, gfx.color, gfx.blend == 1, gfx.font_antialias);
10250                    for pl in &strokes {
10251                        for seg in pl.windows(2) {
10252                            if aa {
10253                                crate::gfx::raster::draw_line_aa(
10254                                    &mut gfx.buffer,
10255                                    w,
10256                                    h,
10257                                    color,
10258                                    add,
10259                                    seg[0][0],
10260                                    seg[0][1],
10261                                    seg[1][0],
10262                                    seg[1][1],
10263                                );
10264                            } else {
10265                                crate::gfx::raster::draw_line(
10266                                    &mut gfx.buffer,
10267                                    w,
10268                                    h,
10269                                    color,
10270                                    seg[0][0],
10271                                    seg[0][1],
10272                                    seg[1][0],
10273                                    seg[1][1],
10274                                );
10275                            }
10276                        }
10277                    }
10278                }
10279                return Ok(Value::Unit);
10280            },
10281            #[cfg(target_arch = "wasm32")]
10282            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" | "متن_فونت" | "نص_الخط" | "טקסט_גופן" | "فونٹ_متن" | "texte_police" | "schriftart_text" | "текст_шрифт" =>
10283            {
10284                return Ok(Value::Unit);
10285            },
10286            // font_text_fill(handle, x, y, px, "string") — filled vector glyphs;
10287            // anti-aliased when `set_font_antialias(1)` is on (default off = crisp).
10288            #[cfg(not(target_arch = "wasm32"))]
10289            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" | "پرکردن_متن_فونت" | "تعبئة_نص_الخط" | "מילוי_טקסט_גופן" | "فونٹ_متن_بھرو" | "remplir_texte_police" | "schriftart_text_füllen" | "заполнить_текст_шрифт" =>
10290            {
10291                let id = self.arg_num(&args, 0, 0.0)? as i64;
10292                let x = self.arg_num(&args, 1, 0.0)? as f32;
10293                let y = self.arg_num(&args, 2, 0.0)? as f32;
10294                let px = self.arg_num(&args, 3, 16.0)? as f32;
10295                let s = self.arg_str(&args, 4, "");
10296                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
10297                    // fill each glyph independently so interior holes (winding) stay correct
10298                    let glyphs = self.font_layout_2d_glyphs(id as usize, x, y, px, &s);
10299                    let mut gfx = self.gfx.borrow_mut();
10300                    let (w, h, color, add, aa) =
10301                        (gfx.width, gfx.height, gfx.color, gfx.blend == 1, gfx.font_antialias);
10302                    for contours in &glyphs {
10303                        if aa {
10304                            crate::gfx::raster::fill_contours_aa(
10305                                &mut gfx.buffer,
10306                                w,
10307                                h,
10308                                color,
10309                                add,
10310                                contours,
10311                            );
10312                        } else {
10313                            crate::gfx::raster::fill_contours(
10314                                &mut gfx.buffer,
10315                                w,
10316                                h,
10317                                color,
10318                                add,
10319                                contours,
10320                            );
10321                        }
10322                    }
10323                }
10324                return Ok(Value::Unit);
10325            },
10326            #[cfg(target_arch = "wasm32")]
10327            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" | "پرکردن_متن_فونت" | "تعبئة_نص_الخط" | "מילוי_טקסט_גופן" | "فونٹ_متن_بھرو" | "remplir_texte_police" | "schriftart_text_füllen" | "заполнить_текст_шрифт" =>
10328            {
10329                return Ok(Value::Unit);
10330            },
10331            // font_text_3d(handle, cx,cy,cz, ux,uy,uz, vx,vy,vz, size, "string")
10332            // — stroked vector text on a 3D plane: u = advance dir, v = up dir, size = world/em.
10333            //   Flows through the depth-sorted line pipeline, so it rotates with the camera (and 4D).
10334            #[cfg(not(target_arch = "wasm32"))]
10335            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" | "متن_فونت_سه‌بعدی" | "نص_خط_ثلاثي_الأبعاد" | "טקסט_גופן_תלת_ממדי" | "تھری_ڈی_فونٹ_متن" | "texte_police_3d" | "schriftart_text_3d" | "текст_шрифт_3d" =>
10336            {
10337                let id = self.arg_num(&args, 0, 0.0)? as i64;
10338                let cx = self.arg_num(&args, 1, 0.0)? as f32;
10339                let cy = self.arg_num(&args, 2, 0.0)? as f32;
10340                let cz = self.arg_num(&args, 3, 0.0)? as f32;
10341                let ux = self.arg_num(&args, 4, 1.0)? as f32;
10342                let uy = self.arg_num(&args, 5, 0.0)? as f32;
10343                let uz = self.arg_num(&args, 6, 0.0)? as f32;
10344                let vx = self.arg_num(&args, 7, 0.0)? as f32;
10345                let vy = self.arg_num(&args, 8, 1.0)? as f32;
10346                let vz = self.arg_num(&args, 9, 0.0)? as f32;
10347                let size = self.arg_num(&args, 10, 1.0)? as f32;
10348                let s = self.arg_str(&args, 11, "");
10349                // Optional arg 12: fill_rows — when > 0, each glyph interior is
10350                // filled with that many even-odd scanline spans (true filled
10351                // letterforms, not a bounding box). 0/omitted = outline only.
10352                let fill_rows = self.arg_num(&args, 12, 0.0)? as i32;
10353                if id >= 0 && (id as usize) < self.fonts.len() && size > 0.0 {
10354                    // Build world-space polylines: world = C + (pen+ex)*size*U + ey*size*V
10355                    let font = &mut self.fonts[id as usize];
10356                    let asc = font.ascent();
10357                    let mut pen = 0.0f32;
10358                    let mut lines: Vec<[f32; 6]> = Vec::new();
10359                    for ch in s.chars() {
10360                        let go = font.glyph_outline(ch, 0.01);
10361                        let map = |p: [f32; 2], pen: f32| {
10362                            let a = pen + p[0];
10363                            let b = p[1] - asc; // shift so the top of the cap sits near C
10364                            [
10365                                cx + a * size * ux + b * size * vx,
10366                                cy + a * size * uy + b * size * vy,
10367                                cz + a * size * uz + b * size * vz,
10368                            ]
10369                        };
10370                        for pl in &go.polylines {
10371                            for seg in pl.windows(2) {
10372                                let p0 = map(seg[0], pen);
10373                                let p1 = map(seg[1], pen);
10374                                lines.push([p0[0], p0[1], p0[2], p1[0], p1[1], p1[2]]);
10375                            }
10376                        }
10377                        if fill_rows > 0 {
10378                            // Even-odd scanline fill in glyph space. Contours may
10379                            // omit their closing edge, so the implicit last→first
10380                            // segment is scanned too (skipped when degenerate).
10381                            let (mut ymin, mut ymax) = (f32::MAX, f32::MIN);
10382                            for pl in &go.polylines {
10383                                for p in pl {
10384                                    ymin = ymin.min(p[1]);
10385                                    ymax = ymax.max(p[1]);
10386                                }
10387                            }
10388                            if ymax > ymin {
10389                                for r in 0..fill_rows {
10390                                    let y =
10391                                        ymin + (r as f32 + 0.5) * (ymax - ymin) / fill_rows as f32;
10392                                    let mut xs: Vec<f32> = Vec::new();
10393                                    for pl in &go.polylines {
10394                                        let n = pl.len();
10395                                        if n < 2 {
10396                                            continue;
10397                                        }
10398                                        for k in 0..n {
10399                                            let p0 = pl[k];
10400                                            let p1 = pl[(k + 1) % n];
10401                                            if k + 1 == n
10402                                                && (p1[0] - p0[0]).abs() < 1e-6
10403                                                && (p1[1] - p0[1]).abs() < 1e-6
10404                                            {
10405                                                continue; // contour already closed
10406                                            }
10407                                            let (y0, y1) = (p0[1], p1[1]);
10408                                            if (y0 <= y && y1 > y) || (y1 <= y && y0 > y) {
10409                                                let t = (y - y0) / (y1 - y0);
10410                                                xs.push(p0[0] + t * (p1[0] - p0[0]));
10411                                            }
10412                                        }
10413                                    }
10414                                    xs.sort_by(|a, b| {
10415                                        a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
10416                                    });
10417                                    let mut k = 0;
10418                                    while k + 1 < xs.len() {
10419                                        let a = map([xs[k], y], pen);
10420                                        let b = map([xs[k + 1], y], pen);
10421                                        lines.push([a[0], a[1], a[2], b[0], b[1], b[2]]);
10422                                        k += 2;
10423                                    }
10424                                }
10425                            }
10426                        }
10427                        pen += go.advance;
10428                    }
10429                    let mut gfx = self.gfx.borrow_mut();
10430                    let color = gfx.color;
10431                    let near = -gfx.camera.zdist + 0.05;
10432                    for l in &lines {
10433                        let (mut ax, mut ay, mut az) = (l[0], l[1], l[2]);
10434                        let (mut bx, mut by, mut bz) = (l[3], l[4], l[5]);
10435                        let da = gfx.camera.depth(ax, ay, az);
10436                        let db = gfx.camera.depth(bx, by, bz);
10437                        if da <= near && db <= near {
10438                            continue;
10439                        }
10440                        if da <= near {
10441                            let t = (near - da) / (db - da);
10442                            ax += t * (bx - ax);
10443                            ay += t * (by - ay);
10444                            az += t * (bz - az);
10445                        } else if db <= near {
10446                            let t = (near - da) / (db - da);
10447                            bx = ax + t * (bx - ax);
10448                            by = ay + t * (by - ay);
10449                            bz = az + t * (bz - az);
10450                        }
10451                        let (sax, say, da2) = gfx.camera.project(ax, ay, az);
10452                        let (sbx, sby, db2) = gfx.camera.project(bx, by, bz);
10453                        let depth = (da2 + db2) / 2.0;
10454                        gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
10455                    }
10456                }
10457                return Ok(Value::Unit);
10458            },
10459            #[cfg(target_arch = "wasm32")]
10460            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" | "متن_فونت_سه‌بعدی" | "نص_خط_ثلاثي_الأبعاد" | "טקסט_גופן_תלת_ממדי" | "تھری_ڈی_فونٹ_متن" | "texte_police_3d" | "schriftart_text_3d" | "текст_шрифт_3d" =>
10461            {
10462                return Ok(Value::Unit);
10463            },
10464            // font_width(handle, px, "string") — pixel width of a string in a loaded font.
10465            #[cfg(not(target_arch = "wasm32"))]
10466            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" | "عرض_فونت" | "عرض_الخط" | "רוחב_גופן" | "فونٹ_چوڑائی" | "largeur_police" | "schriftart_breite" | "ширина_шрифта" =>
10467            {
10468                let id = self.arg_num(&args, 0, 0.0)? as i64;
10469                let px = self.arg_num(&args, 1, 16.0)? as f32;
10470                let s = self.arg_str(&args, 2, "");
10471                if id >= 0 && (id as usize) < self.fonts.len() {
10472                    return Ok(Value::Number(self.fonts[id as usize].measure(&s, px) as f64));
10473                }
10474                return Ok(Value::Number(0.0));
10475            },
10476            #[cfg(target_arch = "wasm32")]
10477            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" | "عرض_فونت" | "عرض_الخط" | "רוחב_גופן" | "فونٹ_چوڑائی" | "largeur_police" | "schriftart_breite" | "ширина_шрифта" =>
10478            {
10479                return Ok(Value::Number(0.0));
10480            },
10481            // font_glyph_outline(handle, "char", tol_em) — flattened vector outline of
10482            // ONE glyph in normalized em space (x→right, y→up, baseline at 0). Returns a
10483            // list of contours; each contour is a flat list [x0,y0,x1,y1,…]. Curves are
10484            // subdivided so deviation stays under tol_em (default 0.01). Empty on failure.
10485            #[cfg(not(target_arch = "wasm32"))]
10486            "font_glyph_outline" | "font_outline" | "เส้นขอบฟอนต์" | "字体轮廓"
10487            | "フォント輪郭" | "글꼴윤곽" | "خط‌دور_نویسه_فونت" | "حدود_حرف_الخط" | "קו_מתאר_גליף" | "فونٹ_گلف_آؤٹ_لائن" => {
10488                let id = self.arg_num(&args, 0, 0.0)? as i64;
10489                let s = self.arg_str(&args, 1, "");
10490                let tol = self.arg_num(&args, 2, 0.01)? as f32;
10491                let ch = s.chars().next().unwrap_or(' ');
10492                if id >= 0 && (id as usize) < self.fonts.len() {
10493                    let go = self.fonts[id as usize].glyph_outline(ch, tol.max(1e-4));
10494                    let mut contours: Vec<Value> = Vec::with_capacity(go.polylines.len());
10495                    for pl in &go.polylines {
10496                        let mut flat: Vec<Value> = Vec::with_capacity(pl.len() * 2);
10497                        for p in pl {
10498                            flat.push(Value::Number(p[0] as f64));
10499                            flat.push(Value::Number(p[1] as f64));
10500                        }
10501                        contours.push(Value::List(Rc::new(flat)));
10502                    }
10503                    return Ok(Value::List(Rc::new(contours)));
10504                }
10505                return Ok(Value::List(Rc::new(vec![])));
10506            },
10507            #[cfg(target_arch = "wasm32")]
10508            "font_glyph_outline" | "font_outline" | "เส้นขอบฟอนต์" | "字体轮廓"
10509            | "フォント輪郭" | "글꼴윤곽" | "خط‌دور_نویسه_فونت" | "حدود_حرف_الخط" | "קו_מתאר_גליף" | "فونٹ_گلف_آؤٹ_لائن" => {
10510                return Ok(Value::List(Rc::new(vec![])));
10511            },
10512            // font_advance(handle, "char") — normalized em advance width of ONE glyph
10513            // (baseline metric, ignores side bearings). Multiply by px for pixels.
10514            #[cfg(not(target_arch = "wasm32"))]
10515            "font_advance" | "ระยะฟอนต์" | "字体步进" | "フォント送り" | "글꼴전진" | "پیشروی_فونت" | "تقدم_الخط" | "קידום_גופן" | "فونٹ_ایڈوانس" => {
10516                let id = self.arg_num(&args, 0, 0.0)? as i64;
10517                let s = self.arg_str(&args, 1, "");
10518                let ch = s.chars().next().unwrap_or(' ');
10519                if id >= 0 && (id as usize) < self.fonts.len() {
10520                    return Ok(Value::Number(self.fonts[id as usize].advance(ch) as f64));
10521                }
10522                return Ok(Value::Number(0.0));
10523            },
10524            #[cfg(target_arch = "wasm32")]
10525            "font_advance" | "ระยะฟอนต์" | "字体步进" | "フォント送り" | "글꼴전진" | "پیشروی_فونت" | "تقدم_الخط" | "קידום_גופן" | "فونٹ_ایڈوانس" => {
10526                return Ok(Value::Number(0.0));
10527            },
10528
10529            // ui_frame(x,y,w,h, bracketLen) — sci-fi corner brackets
10530            "ui_frame" | "边框" | "フレーム枠" | "프레임틀" | "กรอบ" | "قاب_رابط" | "إطار_الواجهة" | "מסגרת_ממשק" | "یو_آئی_فریم" | "cadre_ui" | "ui_rahmen" | "ui_рамка" => {
10531                let x = self.arg_num(&args, 0, 0.0)? as f32;
10532                let y = self.arg_num(&args, 1, 0.0)? as f32;
10533                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
10534                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
10535                let l = self.arg_num(&args, 4, 14.0)? as f32;
10536                let segs = ling_ui::holo::corner_brackets(x, y, w0, h0, l);
10537                let mut gfx = self.gfx.borrow_mut();
10538                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
10539                for sg in segs {
10540                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
10541                }
10542                return Ok(Value::Unit);
10543            },
10544            // ui_bevel(x,y,w,h, bevel) — beveled holographic panel outline
10545            "ui_bevel" | "斜角框" | "ベベル枠" | "베벨틀" | "กรอบเฉียง" | "لبه_شیبدار" | "حافة_مشطوفة" | "מסגרת_משופעת" | "یو_آئی_بیول" | "biseau_ui" | "ui_fase" | "ui_фаска" =>
10546            {
10547                let x = self.arg_num(&args, 0, 0.0)? as f32;
10548                let y = self.arg_num(&args, 1, 0.0)? as f32;
10549                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
10550                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
10551                let bv = self.arg_num(&args, 4, 10.0)? as f32;
10552                let segs = ling_ui::holo::beveled_rect(x, y, w0, h0, bv);
10553                let mut gfx = self.gfx.borrow_mut();
10554                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
10555                for sg in segs {
10556                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
10557                }
10558                return Ok(Value::Unit);
10559            },
10560
10561            // ══════════════════════════════════════════════════════════════════
10562            // VECTOR UI TOOLKIT  (crates/ling-ui/src/widgets.rs)
10563            // All widgets are vector + theme-coloured with an optional trailing
10564            // r,g,b override; interactive ones read the mouse and return state.
10565            // ══════════════════════════════════════════════════════════════════
10566            #[cfg(not(target_arch = "wasm32"))]
10567            "ui_theme" | "界面主题" | "UIテーマ" | "인터페이스테마" | "ธีมส่วนติดต่อ" | "پوسته_رابط" | "سمة_الواجهة" | "ערכת_נושא" | "یو_آئی_تھیم" | "thème_ui" | "ui_thema" | "ui_тема" =>
10568            {
10569                let cur = self.ui_theme;
10570                let primary = self.color_at(&args, 0, cur.primary);
10571                let accent = self.color_at(&args, 3, cur.accent);
10572                let track = self.color_at(&args, 6, cur.track);
10573                let warn = self.color_at(&args, 9, cur.warn);
10574                let text = self.color_at(&args, 12, cur.text);
10575                let bg = self.color_at(&args, 15, cur.bg);
10576                self.ui_theme = UiTheme { primary, accent, track, warn, text, bg };
10577                return Ok(Value::Unit);
10578            },
10579
10580            // ui_theme_colors() -> [pr,pg,pb, ar,ag,ab, tr,tg,tb, wr,wg,wb,
10581            // xr,xg,xb, br,bg,bb] — the live theme every ui_* widget already
10582            // draws from (primary/accent/track/warn/text/bg, each 0-255),
10583            // so script-drawn UI (e.g. a hand-rolled text field) can match it
10584            // instead of guessing its own colours.
10585            "ui_theme_colors" | "인터페이스테마색상" => {
10586                let th = self.ui_theme;
10587                let mut out = Vec::with_capacity(18);
10588                for c in [th.primary, th.accent, th.track, th.warn, th.text, th.bg] {
10589                    out.push(Value::Number(((c >> 16) & 0xFF) as f64));
10590                    out.push(Value::Number(((c >> 8) & 0xFF) as f64));
10591                    out.push(Value::Number((c & 0xFF) as f64));
10592                }
10593                return Ok(Value::List(Rc::new(out)));
10594            },
10595
10596            // ── HUD ──────────────────────────────────────────────────────────
10597            #[cfg(not(target_arch = "wasm32"))]
10598            "ui_radar" | "雷达" | "レーダー" | "레이더" | "เรดาร์" | "رادار_رابط" | "رادار_الواجهة" | "מכ״ם_ממשק" | "یو_آئی_ریڈار" | "radar_ui" | "ui_радар" => {
10599                let cx = self.arg_num(&args, 0, 0.)? as f32;
10600                let cy = self.arg_num(&args, 1, 0.)? as f32;
10601                let r = self.arg_num(&args, 2, 60.)? as f32;
10602                let sweep = self.arg_num(&args, 3, 0.)? as f32;
10603                let th = self.ui_theme;
10604                let prim = self.color_at(&args, 4, th.primary);
10605                self.draw_ui(&ling_ui::widgets::radar(
10606                    cx, cy, r, sweep, prim, th.accent, th.track,
10607                ));
10608                return Ok(Value::Unit);
10609            },
10610            #[cfg(not(target_arch = "wasm32"))]
10611            "ui_compass" | "罗盘" | "コンパス" | "나침반" | "เข็มทิศ" | "قطب‌نمای_رابط" | "بوصلة_الواجهة" | "מצפן_ממשק" | "یو_آئی_قطب_نما" | "boussole_ui" | "ui_kompass" | "ui_компас" => {
10612                let x = self.arg_num(&args, 0, 0.)? as f32;
10613                let y = self.arg_num(&args, 1, 0.)? as f32;
10614                let w0 = self.arg_num(&args, 2, 300.)? as f32;
10615                let h0 = self.arg_num(&args, 3, 24.)? as f32;
10616                let head = self.arg_num(&args, 4, 0.)? as f32;
10617                let th = self.ui_theme;
10618                let prim = self.color_at(&args, 5, th.primary);
10619                self.draw_ui(&ling_ui::widgets::compass(
10620                    x, y, w0, h0, head, prim, th.track,
10621                ));
10622                return Ok(Value::Unit);
10623            },
10624            #[cfg(not(target_arch = "wasm32"))]
10625            "ui_reticle" | "准星" | "照準" | "조준선" | "เป้าเล็ง" | "نشانه_رابط" | "علامة_تصويب" | "כוונת" | "نشانہ" | "réticule_ui" | "ui_fadenkreuz" | "ui_прицел" => {
10626                let cx = self.arg_num(&args, 0, 0.)? as f32;
10627                let cy = self.arg_num(&args, 1, 0.)? as f32;
10628                let r = self.arg_num(&args, 2, 30.)? as f32;
10629                let spread = self.arg_num(&args, 3, 0.)? as f32;
10630                let th = self.ui_theme;
10631                let prim = self.color_at(&args, 4, th.primary);
10632                self.draw_ui(&ling_ui::widgets::reticle(cx, cy, r, spread, prim));
10633                return Ok(Value::Unit);
10634            },
10635            #[cfg(not(target_arch = "wasm32"))]
10636            "ui_target" | "锁定框" | "ターゲット" | "표적" | "กรอบเป้า" | "قاب_هدف" | "إطار_الهدف" | "מסגרת_מטרה" | "ہدف_فریم" | "cible_ui" | "ui_ziel" | "ui_цель" =>
10637            {
10638                let x = self.arg_num(&args, 0, 0.)? as f32;
10639                let y = self.arg_num(&args, 1, 0.)? as f32;
10640                let w0 = self.arg_num(&args, 2, 80.)? as f32;
10641                let h0 = self.arg_num(&args, 3, 80.)? as f32;
10642                let lock = self.arg_num(&args, 4, 0.)? as f32;
10643                let th = self.ui_theme;
10644                let prim = self.color_at(&args, 5, th.primary);
10645                self.draw_ui(&ling_ui::widgets::target(
10646                    x, y, w0, h0, lock, prim, th.accent,
10647                ));
10648                return Ok(Value::Unit);
10649            },
10650            #[cfg(not(target_arch = "wasm32"))]
10651            "ui_panel" | "面板" | "パネル" | "패널" | "แผง" | "پنل_رابط" | "لوحة_الواجهة" | "לוח_ממשק" | "یو_آئی_پینل" | "panneau_ui" | "ui_feld" | "ui_панель" => {
10652                let x = self.arg_num(&args, 0, 0.)? as f32;
10653                let y = self.arg_num(&args, 1, 0.)? as f32;
10654                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10655                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10656                let bv = self.arg_num(&args, 4, 12.)? as f32;
10657                let th = self.ui_theme;
10658                let prim = self.color_at(&args, 5, th.primary);
10659                self.draw_ui(&ling_ui::widgets::panel(x, y, w0, h0, bv, prim, th.bg));
10660                return Ok(Value::Unit);
10661            },
10662            #[cfg(not(target_arch = "wasm32"))]
10663            "ui_scanlines" | "扫描线" | "走査線" | "스캔라인" | "เส้นสแกน" | "خطوط_اسکن" | "خطوط_المسح" | "קווי_סריקה" | "اسکین_لائنز" | "lignes_balayage_ui" | "ui_abtastzeilen" | "ui_линии_развёртки" =>
10664            {
10665                let x = self.arg_num(&args, 0, 0.)? as f32;
10666                let y = self.arg_num(&args, 1, 0.)? as f32;
10667                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10668                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10669                let dens = self.arg_num(&args, 4, 24.)? as usize;
10670                let th = self.ui_theme;
10671                let line = self.color_at(&args, 5, th.track);
10672                self.draw_ui(&ling_ui::widgets::scanlines(x, y, w0, h0, dens, line));
10673                return Ok(Value::Unit);
10674            },
10675
10676            // ── Meters ───────────────────────────────────────────────────────
10677            #[cfg(not(target_arch = "wasm32"))]
10678            "ui_bar" | "进度条" | "バー" | "막대" | "แถบ" | "نوار_رابط" | "شريط_الواجهة" | "סרגל_ממשק" | "یو_آئی_بار" | "barre_ui" | "ui_leiste" | "ui_полоса" => {
10679                let x = self.arg_num(&args, 0, 0.)? as f32;
10680                let y = self.arg_num(&args, 1, 0.)? as f32;
10681                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10682                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10683                let val = self.arg_num(&args, 4, 0.)? as f32;
10684                let max = self.arg_num(&args, 5, 1.)? as f32;
10685                let th = self.ui_theme;
10686                let fill = self.color_at(&args, 6, th.primary);
10687                self.draw_ui(&ling_ui::widgets::bar(
10688                    x,
10689                    y,
10690                    w0,
10691                    h0,
10692                    val / max.max(1e-6),
10693                    fill,
10694                    th.track,
10695                ));
10696                return Ok(Value::Unit);
10697            },
10698            #[cfg(not(target_arch = "wasm32"))]
10699            "ui_segbar" | "分段条" | "分割バー" | "분할막대" | "แถบแบ่ง" | "نوار_قطعه‌ای" | "شريط_مقسم" | "סרגל_מקוטע" | "سیگمنٹ_بار" | "barre_segmentée_ui" | "ui_segmentleiste" | "ui_сегментная_полоса" =>
10700            {
10701                let x = self.arg_num(&args, 0, 0.)? as f32;
10702                let y = self.arg_num(&args, 1, 0.)? as f32;
10703                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10704                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10705                let val = self.arg_num(&args, 4, 0.)? as f32;
10706                let max = self.arg_num(&args, 5, 1.)? as f32;
10707                let segs = self.arg_num(&args, 6, 10.)? as usize;
10708                let th = self.ui_theme;
10709                let fill = self.color_at(&args, 7, th.primary);
10710                self.draw_ui(&ling_ui::widgets::segbar(
10711                    x,
10712                    y,
10713                    w0,
10714                    h0,
10715                    val / max.max(1e-6),
10716                    segs,
10717                    fill,
10718                    th.track,
10719                ));
10720                return Ok(Value::Unit);
10721            },
10722            #[cfg(not(target_arch = "wasm32"))]
10723            "ui_gauge" | "仪表" | "ゲージ" | "게이지" | "มาตรวัด" | "گیج_رابط" | "مقياس_الواجهة" | "מד_ממשק" | "یو_آئی_گیج" | "jauge_ui" | "ui_anzeige" | "ui_индикатор" => {
10724                let cx = self.arg_num(&args, 0, 0.)? as f32;
10725                let cy = self.arg_num(&args, 1, 0.)? as f32;
10726                let r = self.arg_num(&args, 2, 50.)? as f32;
10727                let val = self.arg_num(&args, 3, 0.)? as f32;
10728                let max = self.arg_num(&args, 4, 1.)? as f32;
10729                let th = self.ui_theme;
10730                let needle = self.color_at(&args, 5, th.warn);
10731                self.draw_ui(&ling_ui::widgets::gauge(
10732                    cx,
10733                    cy,
10734                    r,
10735                    val / max.max(1e-6),
10736                    needle,
10737                    th.accent,
10738                    th.track,
10739                ));
10740                return Ok(Value::Unit);
10741            },
10742            #[cfg(not(target_arch = "wasm32"))]
10743            "ui_ring" | "环表" | "リングメーター" | "링미터" | "วงแหวนวัด" | "حلقه_گیج" | "حلقة_قياس" | "טבעת_מד" | "رنگ_گیج" | "anneau_ui" | "ui_кольцо" =>
10744            {
10745                let cx = self.arg_num(&args, 0, 0.)? as f32;
10746                let cy = self.arg_num(&args, 1, 0.)? as f32;
10747                let r = self.arg_num(&args, 2, 40.)? as f32;
10748                let val = self.arg_num(&args, 3, 0.)? as f32;
10749                let max = self.arg_num(&args, 4, 1.)? as f32;
10750                let th = self.ui_theme;
10751                let fill = self.color_at(&args, 5, th.primary);
10752                self.draw_ui(&ling_ui::widgets::ring(
10753                    cx,
10754                    cy,
10755                    r,
10756                    val / max.max(1e-6),
10757                    fill,
10758                    th.track,
10759                ));
10760                return Ok(Value::Unit);
10761            },
10762            #[cfg(not(target_arch = "wasm32"))]
10763            "ui_vu" | "音量条" | "VUメーター" | "음량막대" | "มาตรเสียง" | "گیج_صدا" | "مقياس_مستوى_الصوت" | "מד_עוצמה" | "وی_یو_میٹر" | "vumètre_ui" | "ui_vumeter" | "ui_вю_метр" =>
10764            {
10765                let x = self.arg_num(&args, 0, 0.)? as f32;
10766                let y = self.arg_num(&args, 1, 0.)? as f32;
10767                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10768                let h0 = self.arg_num(&args, 3, 60.)? as f32;
10769                let levels = self.arg_list_f32(&args, 4);
10770                let th = self.ui_theme;
10771                let fill = self.color_at(&args, 5, th.primary);
10772                self.draw_ui(&ling_ui::widgets::vu(x, y, w0, h0, &levels, fill, th.warn));
10773                return Ok(Value::Unit);
10774            },
10775            #[cfg(not(target_arch = "wasm32"))]
10776            "ui_spark" | "迷你图" | "スパークライン" | "스파크라인" | "กราฟจิ๋ว" | "نمودار_ریز" | "رسم_مصغر" | "גרף_זעיר" | "اسپارک_لائن" | "mini_graphe_ui" | "ui_sparkline" | "ui_мини_график" =>
10777            {
10778                let x = self.arg_num(&args, 0, 0.)? as f32;
10779                let y = self.arg_num(&args, 1, 0.)? as f32;
10780                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10781                let h0 = self.arg_num(&args, 3, 40.)? as f32;
10782                let vals = self.arg_list_f32(&args, 4);
10783                let th = self.ui_theme;
10784                let line = self.color_at(&args, 5, th.accent);
10785                self.draw_ui(&ling_ui::widgets::spark(x, y, w0, h0, &vals, line));
10786                return Ok(Value::Unit);
10787            },
10788            #[cfg(not(target_arch = "wasm32"))]
10789            "ui_battery" | "电池" | "バッテリー" | "배터리" | "แบตเตอรี่" | "نشانگر_باتری" | "مؤشر_البطارية" | "מחוון_סוללה" | "بیٹری_انڈیکیٹر" | "batterie_ui" | "ui_batterie" | "ui_батарея" =>
10790            {
10791                let x = self.arg_num(&args, 0, 0.)? as f32;
10792                let y = self.arg_num(&args, 1, 0.)? as f32;
10793                let w0 = self.arg_num(&args, 2, 50.)? as f32;
10794                let h0 = self.arg_num(&args, 3, 22.)? as f32;
10795                let val = self.arg_num(&args, 4, 1.)? as f32;
10796                let max = self.arg_num(&args, 5, 1.)? as f32;
10797                let th = self.ui_theme;
10798                let fill = self.color_at(&args, 6, th.accent);
10799                self.draw_ui(&ling_ui::widgets::battery(
10800                    x,
10801                    y,
10802                    w0,
10803                    h0,
10804                    val / max.max(1e-6),
10805                    fill,
10806                    th.track,
10807                    th.warn,
10808                ));
10809                return Ok(Value::Unit);
10810            },
10811
10812            // ── Interface controls (interactive → return state) ──────────────
10813            #[cfg(not(target_arch = "wasm32"))]
10814            "ui_button" | "按钮" | "ボタン" | "버튼" | "ปุ่ม" | "دکمه_رابط" | "زر_الواجهة" | "כפתור_ממשק" | "یو_آئی_بٹن" | "bouton_ui" | "ui_knopf" | "ui_кнопка" => {
10815                let x = self.arg_num(&args, 0, 0.)? as f32;
10816                let y = self.arg_num(&args, 1, 0.)? as f32;
10817                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10818                let h0 = self.arg_num(&args, 3, 40.)? as f32;
10819                let (mx, my, down) = self.mouse_now();
10820                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
10821                let clicked = hover && down && !self.mouse_was_down;
10822                let th = self.ui_theme;
10823                let prim = self.color_at(&args, 4, th.primary);
10824                self.draw_ui(&ling_ui::widgets::button(
10825                    x,
10826                    y,
10827                    w0,
10828                    h0,
10829                    hover,
10830                    down && hover,
10831                    prim,
10832                    th.bg,
10833                ));
10834                return Ok(Value::Number(if clicked { 1.0 } else { 0.0 }));
10835            },
10836            #[cfg(not(target_arch = "wasm32"))]
10837            "ui_toggle" | "开关" | "トグル" | "토글" | "สวิตช์" | "کلید_ضامن" | "مفتاح_تبديل" | "מתג" | "ٹوگل" | "bascule_ui" | "ui_schalter" | "ui_переключатель" => {
10838                let x = self.arg_num(&args, 0, 0.)? as f32;
10839                let y = self.arg_num(&args, 1, 0.)? as f32;
10840                let w0 = self.arg_num(&args, 2, 52.)? as f32;
10841                let h0 = self.arg_num(&args, 3, 24.)? as f32;
10842                let mut state = self.arg_num(&args, 4, 0.)? > 0.5;
10843                let (mx, my, down) = self.mouse_now();
10844                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
10845                if hover && down && !self.mouse_was_down {
10846                    state = !state;
10847                }
10848                let th = self.ui_theme;
10849                let on = self.color_at(&args, 5, th.accent);
10850                self.draw_ui(&ling_ui::widgets::toggle(x, y, w0, h0, state, on, th.track));
10851                return Ok(Value::Number(if state { 1.0 } else { 0.0 }));
10852            },
10853            #[cfg(not(target_arch = "wasm32"))]
10854            "ui_slider" | "滑块" | "スライダー" | "슬라이더" | "แถบเลื่อน" | "لغزنده" | "شريط_انزلاق" | "מחוון_החלקה" | "سلائیڈر" | "curseur_ui" | "ui_schieberegler" | "ui_ползунок" =>
10855            {
10856                let x = self.arg_num(&args, 0, 0.)? as f32;
10857                let y = self.arg_num(&args, 1, 0.)? as f32;
10858                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10859                let mut val = self.arg_num(&args, 3, 0.)? as f32;
10860                let mn = self.arg_num(&args, 4, 0.)? as f32;
10861                let mx_ = self.arg_num(&args, 5, 1.)? as f32;
10862                let (mx, my, down) = self.mouse_now();
10863                let hover = ling_ui::holo::hit_rect(mx, my, x - 8.0, y - 10.0, w0 + 16.0, 20.0);
10864                if hover && down {
10865                    let frac = ((mx - x) / w0).clamp(0.0, 1.0);
10866                    val = mn + (mx_ - mn) * frac;
10867                }
10868                let frac = ((val - mn) / (mx_ - mn).abs().max(1e-6)).clamp(0.0, 1.0);
10869                let th = self.ui_theme;
10870                let fill = self.color_at(&args, 6, th.primary);
10871                self.draw_ui(&ling_ui::widgets::slider(
10872                    x, y, w0, frac, hover, fill, th.track,
10873                ));
10874                return Ok(Value::Number(val as f64));
10875            },
10876            #[cfg(not(target_arch = "wasm32"))]
10877            "ui_checkbox" | "复选框" | "チェックボックス" | "체크박스" | "ช่องเลือก" | "جعبه_علامت" | "مربع_اختيار" | "תיבת_סימון" | "چیک_باکس" | "case_cocher_ui" | "ui_kontrollkästchen" | "ui_флажок" =>
10878            {
10879                let x = self.arg_num(&args, 0, 0.)? as f32;
10880                let y = self.arg_num(&args, 1, 0.)? as f32;
10881                let s = self.arg_num(&args, 2, 20.)? as f32;
10882                let mut checked = self.arg_num(&args, 3, 0.)? > 0.5;
10883                let (mx, my, down) = self.mouse_now();
10884                let hover = ling_ui::holo::hit_rect(mx, my, x, y, s, s);
10885                if hover && down && !self.mouse_was_down {
10886                    checked = !checked;
10887                }
10888                let th = self.ui_theme;
10889                let prim = self.color_at(&args, 4, th.primary);
10890                self.draw_ui(&ling_ui::widgets::checkbox(
10891                    x, y, s, checked, hover, prim, th.track,
10892                ));
10893                return Ok(Value::Number(if checked { 1.0 } else { 0.0 }));
10894            },
10895            #[cfg(not(target_arch = "wasm32"))]
10896            "ui_tabs" | "标签页" | "タブ" | "탭" | "แท็บ" | "برگه‌ها" | "ألسنة_الواجهة" | "לשוניות" | "ٹیبز" | "onglets_ui" | "ui_reiter" | "ui_вкладки" => {
10897                let x = self.arg_num(&args, 0, 0.)? as f32;
10898                let y = self.arg_num(&args, 1, 0.)? as f32;
10899                let w0 = self.arg_num(&args, 2, 240.)? as f32;
10900                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10901                let count = self.arg_num(&args, 4, 3.)? as usize;
10902                let mut active = self.arg_num(&args, 5, 0.)? as i32;
10903                let (mx, my, down) = self.mouse_now();
10904                let mut hover = -1;
10905                if my >= y && my <= y + h0 && mx >= x && mx <= x + w0 && count > 0 {
10906                    hover = (((mx - x) / (w0 / count as f32)) as i32)
10907                        .max(0)
10908                        .min(count as i32 - 1);
10909                    if down && !self.mouse_was_down {
10910                        active = hover;
10911                    }
10912                }
10913                let th = self.ui_theme;
10914                let prim = self.color_at(&args, 6, th.primary);
10915                self.draw_ui(&ling_ui::widgets::tabs(
10916                    x,
10917                    y,
10918                    w0,
10919                    h0,
10920                    count,
10921                    active as usize,
10922                    hover,
10923                    prim,
10924                    th.track,
10925                ));
10926                return Ok(Value::Number(active as f64));
10927            },
10928            #[cfg(not(target_arch = "wasm32"))]
10929            "ui_progress" | "进度" | "プログレス" | "진행바" | "ความคืบหน้า" | "نوار_پیشرفت" | "شريط_التقدم" | "פס_התקדמות" | "پیش_رفت_بار" | "progression_ui" | "ui_fortschritt" | "ui_прогресс" =>
10930            {
10931                let x = self.arg_num(&args, 0, 0.)? as f32;
10932                let y = self.arg_num(&args, 1, 0.)? as f32;
10933                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10934                let h0 = self.arg_num(&args, 3, 12.)? as f32;
10935                let frac = self.arg_num(&args, 4, 0.)? as f32;
10936                let th = self.ui_theme;
10937                let fill = self.color_at(&args, 5, th.accent);
10938                self.draw_ui(&ling_ui::widgets::progress(
10939                    x, y, w0, h0, frac, fill, th.track,
10940                ));
10941                return Ok(Value::Unit);
10942            },
10943            #[cfg(not(target_arch = "wasm32"))]
10944            "ui_tooltip" | "提示框" | "ツールチップ" | "툴팁" | "คำแนะนำ" | "راهنمای_شناور" | "تلميح_الواجهة" | "חלונית_עזרה" | "ٹول_ٹپ" | "infobulle_ui" | "ui_подсказка" =>
10945            {
10946                let x = self.arg_num(&args, 0, 0.)? as f32;
10947                let y = self.arg_num(&args, 1, 0.)? as f32;
10948                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10949                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10950                let th = self.ui_theme;
10951                let prim = self.color_at(&args, 4, th.primary);
10952                self.draw_ui(&ling_ui::widgets::tooltip(x, y, w0, h0, prim, th.bg));
10953                return Ok(Value::Unit);
10954            },
10955            #[cfg(not(target_arch = "wasm32"))]
10956            "ui_stepper" | "步进器" | "ステッパー" | "스테퍼" | "ตัวปรับค่า" | "پله‌گر" | "زر_خطوات" | "בורר_מדורג" | "اسٹیپر" | "pas_à_pas_ui" | "ui_schrittsteuerung" | "ui_степпер" =>
10957            {
10958                let x = self.arg_num(&args, 0, 0.)? as f32;
10959                let y = self.arg_num(&args, 1, 0.)? as f32;
10960                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10961                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10962                let mut val = self.arg_num(&args, 4, 0.)? as f32;
10963                let step = self.arg_num(&args, 5, 1.)? as f32;
10964                let (mx, my, down) = self.mouse_now();
10965                let hm = ling_ui::holo::hit_rect(mx, my, x, y, h0, h0);
10966                let hp = ling_ui::holo::hit_rect(mx, my, x + w0 - h0, y, h0, h0);
10967                if down && !self.mouse_was_down {
10968                    if hm {
10969                        val -= step;
10970                    }
10971                    if hp {
10972                        val += step;
10973                    }
10974                }
10975                let th = self.ui_theme;
10976                let prim = self.color_at(&args, 6, th.primary);
10977                self.draw_ui(&ling_ui::widgets::stepper(
10978                    x, y, w0, h0, hm, hp, prim, th.track,
10979                ));
10980                return Ok(Value::Number(val as f64));
10981            },
10982
10983            // ── Game UI ──────────────────────────────────────────────────────
10984            #[cfg(not(target_arch = "wasm32"))]
10985            "ui_healthbar" | "血条" | "体力バー" | "체력바" | "แถบพลังชีวิต" | "نوار_سلامتی" | "شريط_الصحة" | "פס_בריאות" | "ہیلتھ_بار" | "barre_vie_ui" | "ui_lebensbalken" | "ui_полоса_здоровья" =>
10986            {
10987                let x = self.arg_num(&args, 0, 0.)? as f32;
10988                let y = self.arg_num(&args, 1, 0.)? as f32;
10989                let w0 = self.arg_num(&args, 2, 180.)? as f32;
10990                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10991                let val = self.arg_num(&args, 4, 1.)? as f32;
10992                let max = self.arg_num(&args, 5, 1.)? as f32;
10993                let pulse = self.arg_num(&args, 6, 0.)? as f32;
10994                let th = self.ui_theme;
10995                let full = self.color_at(&args, 7, th.accent);
10996                self.draw_ui(&ling_ui::widgets::healthbar(
10997                    x,
10998                    y,
10999                    w0,
11000                    h0,
11001                    val / max.max(1e-6),
11002                    pulse,
11003                    full,
11004                    th.warn,
11005                    th.track,
11006                ));
11007                return Ok(Value::Unit);
11008            },
11009            #[cfg(not(target_arch = "wasm32"))]
11010            "ui_cooldown" | "冷却" | "クールダウン" | "쿨다운" | "คูลดาวน์" | "زمان_خنک‌سازی" | "مؤقت_التهدئة" | "זמן_קירור" | "کول_ڈاؤن" | "recharge_ui" | "ui_abklingzeit" | "ui_перезарядка" =>
11011            {
11012                let cx = self.arg_num(&args, 0, 0.)? as f32;
11013                let cy = self.arg_num(&args, 1, 0.)? as f32;
11014                let r = self.arg_num(&args, 2, 28.)? as f32;
11015                let frac = self.arg_num(&args, 3, 0.)? as f32;
11016                let th = self.ui_theme;
11017                let fill = self.color_at(&args, 4, th.primary);
11018                self.draw_ui(&ling_ui::widgets::cooldown(cx, cy, r, frac, fill, th.track));
11019                return Ok(Value::Unit);
11020            },
11021            #[cfg(not(target_arch = "wasm32"))]
11022            "ui_counter" | "计数器" | "カウンター" | "카운터" | "ตัวนับ" | "شمارشگر" | "عداد_الواجهة" | "מונה_ממשק" | "کاؤنٹر" | "compteur_ui" | "ui_zähler" | "ui_счётчик" => {
11023                let x = self.arg_num(&args, 0, 0.)? as f32;
11024                let y = self.arg_num(&args, 1, 0.)? as f32;
11025                let dw = self.arg_num(&args, 2, 14.)? as f32;
11026                let dh = self.arg_num(&args, 3, 24.)? as f32;
11027                let val = self.arg_num(&args, 4, 0.)? as i64;
11028                let digits = self.arg_num(&args, 5, 4.)? as usize;
11029                let th = self.ui_theme;
11030                let on = self.color_at(&args, 6, th.primary);
11031                let off = ling_ui::widgets::shade(th.track, 0.5);
11032                self.draw_ui(&ling_ui::widgets::counter(
11033                    x, y, dw, dh, val, digits, on, off,
11034                ));
11035                return Ok(Value::Unit);
11036            },
11037            #[cfg(not(target_arch = "wasm32"))]
11038            "ui_minimap" | "小地图" | "ミニマップ" | "미니맵" | "แผนที่ย่อ" | "نقشه_کوچک" | "خريطة_مصغرة" | "מפה_מוקטנת" | "منی_میپ" | "minicarte_ui" | "ui_minikarte" | "ui_миникарта" =>
11039            {
11040                let x = self.arg_num(&args, 0, 0.)? as f32;
11041                let y = self.arg_num(&args, 1, 0.)? as f32;
11042                let w0 = self.arg_num(&args, 2, 140.)? as f32;
11043                let h0 = self.arg_num(&args, 3, 140.)? as f32;
11044                let th = self.ui_theme;
11045                let prim = self.color_at(&args, 4, th.primary);
11046                self.draw_ui(&ling_ui::widgets::minimap(x, y, w0, h0, prim, th.bg));
11047                return Ok(Value::Unit);
11048            },
11049            #[cfg(not(target_arch = "wasm32"))]
11050            "ui_dpad" | "方向键" | "方向パッド" | "방향패드" | "ปุ่มทิศทาง" | "دسته_جهت‌دار" | "لوحة_الاتجاهات" | "לוח_כיוונים" | "ڈی_پیڈ" | "croix_direction_ui" | "ui_steuerkreuz" | "ui_крестовина" =>
11051            {
11052                let cx = self.arg_num(&args, 0, 0.)? as f32;
11053                let cy = self.arg_num(&args, 1, 0.)? as f32;
11054                let r = self.arg_num(&args, 2, 50.)? as f32;
11055                let (mx, my, down) = self.mouse_now();
11056                let mut dir = 0;
11057                if down {
11058                    let (dx, dy) = (mx - cx, my - cy);
11059                    if dx * dx + dy * dy <= r * r {
11060                        if dx.abs() > dy.abs() {
11061                            dir = if dx > 0.0 { 2 } else { 4 };
11062                        } else {
11063                            dir = if dy > 0.0 { 3 } else { 1 };
11064                        }
11065                    }
11066                }
11067                let th = self.ui_theme;
11068                let prim = self.color_at(&args, 3, th.primary);
11069                self.draw_ui(&ling_ui::widgets::dpad(cx, cy, r, dir, prim, th.track));
11070                return Ok(Value::Number(dir as f64));
11071            },
11072            #[cfg(not(target_arch = "wasm32"))]
11073            "ui_slotgrid" | "物品格" | "スロットグリッド" | "슬롯격자" | "ช่องไอเทม" | "شبکه_شیار" | "شبكة_الفتحات" | "רשת_חריצים" | "سلاٹ_گرڈ" | "grille_emplacements_ui" | "ui_slotraster" | "ui_сетка_слотов" =>
11074            {
11075                let x = self.arg_num(&args, 0, 0.)? as f32;
11076                let y = self.arg_num(&args, 1, 0.)? as f32;
11077                let cols = self.arg_num(&args, 2, 4.)? as usize;
11078                let rows = self.arg_num(&args, 3, 1.)? as usize;
11079                let cell = self.arg_num(&args, 4, 36.)? as f32;
11080                let sel = self.arg_num(&args, 5, -1.)? as i32;
11081                let th = self.ui_theme;
11082                let prim = self.color_at(&args, 6, th.primary);
11083                self.draw_ui(&ling_ui::widgets::slotgrid(
11084                    x, y, cols, rows, cell, sel, prim, th.track,
11085                ));
11086                return Ok(Value::Unit);
11087            },
11088            #[cfg(not(target_arch = "wasm32"))]
11089            "ui_vignette" | "暗角" | "ビネット" | "비네트" | "ขอบมืด" | "سایه‌گرد_کادر" | "تظليل_الحواف" | "הצללת_מסגרת" | "ویگنیٹ" | "vignette_ui" | "ui_виньетка" => {
11090                let intensity = self.arg_num(&args, 0, 0.5)? as f32;
11091                let (w, h) = {
11092                    let g = self.gfx.borrow();
11093                    (g.width as f32, g.height as f32)
11094                };
11095                let th = self.ui_theme;
11096                let col = self.color_at(&args, 1, th.warn);
11097                self.draw_ui(&ling_ui::widgets::vignette(w, h, intensity, col));
11098                return Ok(Value::Unit);
11099            },
11100
11101            // ── Faux-3D in 2D space ──────────────────────────────────────────
11102            #[cfg(not(target_arch = "wasm32"))]
11103            "ui_gauge3d" | "立体仪表" | "立体ゲージ" | "입체게이지" | "มาตรวัด3มิติ" | "گیج_سه‌بعدی" | "مقياس_ثلاثي_الأبعاد" | "מד_תלת_ממדי" | "تھری_ڈی_گیج" | "jauge_3d_ui" | "ui_anzeige_3d" | "ui_индикатор_3d" =>
11104            {
11105                let cx = self.arg_num(&args, 0, 0.)? as f32;
11106                let cy = self.arg_num(&args, 1, 0.)? as f32;
11107                let r = self.arg_num(&args, 2, 50.)? as f32;
11108                let val = self.arg_num(&args, 3, 0.)? as f32;
11109                let max = self.arg_num(&args, 4, 1.)? as f32;
11110                let spin = self.arg_num(&args, 5, 0.)? as f32;
11111                let th = self.ui_theme;
11112                let fill = self.color_at(&args, 6, th.primary);
11113                self.draw_ui(&ling_ui::widgets::gauge3d(
11114                    cx,
11115                    cy,
11116                    r,
11117                    val / max.max(1e-6),
11118                    spin,
11119                    fill,
11120                    th.track,
11121                ));
11122                return Ok(Value::Unit);
11123            },
11124            #[cfg(not(target_arch = "wasm32"))]
11125            "ui_panel3d" | "立体面板" | "立体パネル" | "입체패널" | "แผง3มิติ" | "پنل_سه‌بعدی" | "لوحة_ثلاثية_الأبعاد" | "לוח_תלת_ממדי" | "تھری_ڈی_پینل" | "panneau_3d_ui" | "ui_feld_3d" | "ui_панель_3d" =>
11126            {
11127                let x = self.arg_num(&args, 0, 0.)? as f32;
11128                let y = self.arg_num(&args, 1, 0.)? as f32;
11129                let w0 = self.arg_num(&args, 2, 200.)? as f32;
11130                let h0 = self.arg_num(&args, 3, 120.)? as f32;
11131                let depth = self.arg_num(&args, 4, 14.)? as f32;
11132                let th = self.ui_theme;
11133                let prim = self.color_at(&args, 5, th.primary);
11134                self.draw_ui(&ling_ui::widgets::panel3d(x, y, w0, h0, depth, prim, th.bg));
11135                return Ok(Value::Unit);
11136            },
11137            #[cfg(not(target_arch = "wasm32"))]
11138            "ui_radar3d" | "立体雷达" | "立体レーダー" | "입체레이더" | "เรดาร์3มิติ" | "رادار_سه‌بعدی" | "رادار_ثلاثي_الأبعاد" | "מכ״ם_תלת_ממדי" | "تھری_ڈی_ریڈار" | "radar_3d_ui" | "ui_radar_3d" | "ui_радар_3d" =>
11139            {
11140                let cx = self.arg_num(&args, 0, 0.)? as f32;
11141                let cy = self.arg_num(&args, 1, 0.)? as f32;
11142                let r = self.arg_num(&args, 2, 60.)? as f32;
11143                let tilt = self.arg_num(&args, 3, 0.9)? as f32;
11144                let sweep = self.arg_num(&args, 4, 0.)? as f32;
11145                let th = self.ui_theme;
11146                let prim = self.color_at(&args, 5, th.primary);
11147                self.draw_ui(&ling_ui::widgets::radar3d(
11148                    cx, cy, r, tilt, sweep, prim, th.track,
11149                ));
11150                return Ok(Value::Unit);
11151            },
11152
11153            // ── Interface sounds ─────────────────────────────────────────────
11154            #[cfg(not(target_arch = "wasm32"))]
11155            "audio_blip" | "提示音" | "ビープ音" | "효과음" | "เสียงบี๊บ" | "بوق_کوتاه" | "نغمة_قصيرة" | "ביפ" | "بلپ_آواز" | "bip_audio" | "звук_бип" =>
11156            {
11157                let freq = self.arg_num(&args, 0, 660.)? as f32;
11158                let dur = self.arg_num(&args, 1, 0.08)? as f32;
11159                let wave = Wave::from_name(&self.arg_str(&args, 2, "sine"));
11160                let amp = self.arg_num(&args, 3, 0.25)? as f32;
11161                if let Some(audio) = &self.audio {
11162                    audio.blip(freq, amp, dur, wave);
11163                }
11164                return Ok(Value::Unit);
11165            },
11166            #[cfg(not(target_arch = "wasm32"))]
11167            "ui_sound" | "界面音" | "UI音" | "인터페이스음" | "เสียงปุ่ม" | "صدای_رابط" | "صوت_الواجهة" | "צליל_ממשק" | "یو_آئی_آواز" | "son_ui" | "ui_klang" | "ui_звук" =>
11168            {
11169                let name = self.arg_str(&args, 0, "click");
11170                if let Some(audio) = &self.audio {
11171                    match name.as_str() {
11172                        "hover" => audio.blip(880.0, 0.10, 0.04, Wave::Sine),
11173                        "confirm" => {
11174                            audio.blip(660.0, 0.22, 0.07, Wave::Square);
11175                            audio.blip(990.0, 0.18, 0.10, Wave::Square);
11176                        },
11177                        "error" => {
11178                            audio.blip(180.0, 0.30, 0.16, Wave::Saw);
11179                            audio.blip(140.0, 0.30, 0.18, Wave::Saw);
11180                        },
11181                        "toggle" => audio.blip(520.0, 0.22, 0.05, Wave::Triangle),
11182                        "tick" => audio.blip(1500.0, 0.12, 0.02, Wave::Square),
11183                        _ => audio.blip(720.0, 0.26, 0.05, Wave::Square), // "click"
11184                    }
11185                }
11186                return Ok(Value::Unit);
11187            },
11188
11189            // ══════════════════════════════════════════════════════════════════
11190            // MUSIC TOOLKIT  (crates/ling-music) — decode · analysis · GM synth ·
11191            // rhythm · karaoke. Analysis/decoding need no audio device; playback
11192            // and synthesis lazily start a dedicated music engine.
11193            // ══════════════════════════════════════════════════════════════════
11194
11195            // music_load(path) -> track handle (decodes WAV/FLAC/OGG/MP3/AAC)
11196            #[cfg(not(target_arch = "wasm32"))]
11197            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" | "بارگذاری_موسیقی" | "تحميل_الموسيقى" | "טעינת_מוזיקה" | "موسیقی_لوڈ" | "charger_musique" | "musik_laden" | "загрузить_музыку" =>
11198            {
11199                let path = self.arg_str(&args, 0, "");
11200                let resolved = if std::path::Path::new(&path).exists() {
11201                    path.clone()
11202                } else if let Some(d) = &self.source_dir {
11203                    d.join(&path).to_string_lossy().into_owned()
11204                } else {
11205                    path.clone()
11206                };
11207                match ling_music::load(&resolved) {
11208                    Ok(t) => {
11209                        let id = self.tracks.len();
11210                        self.tracks.push(t);
11211                        return Ok(Value::Number(id as f64));
11212                    },
11213                    Err(e) => {
11214                        eprintln!("music_load failed ({path}): {e}");
11215                        return Ok(Value::Number(-1.0));
11216                    },
11217                }
11218            },
11219            #[cfg(not(target_arch = "wasm32"))]
11220            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" | "مدت_موسیقی" | "مدة_الموسيقى" | "משך_מוזיקה" | "موسیقی_دورانیہ" | "durée_musique" | "musik_dauer" | "длительность_музыки" =>
11221            {
11222                let id = self.arg_num(&args, 0, 0.0)? as i64;
11223                let d = self
11224                    .tracks
11225                    .get(id as usize)
11226                    .map(|t| t.duration)
11227                    .unwrap_or(0.0);
11228                return Ok(Value::Number(d as f64));
11229            },
11230            #[cfg(not(target_arch = "wasm32"))]
11231            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" | "ضربان_در_دقیقه" | "نبضات_بالدقيقة" | "פעימות_לדקה" | "بی_پی_ایم" | "bpm_musique" | "musik_bpm" | "музыка_bpm" =>
11232            {
11233                let id = self.arg_num(&args, 0, 0.0)? as i64;
11234                let b = self
11235                    .tracks
11236                    .get(id as usize)
11237                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
11238                    .unwrap_or(0.0);
11239                return Ok(Value::Number(b as f64));
11240            },
11241            #[cfg(not(target_arch = "wasm32"))]
11242            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" | "گام_موسیقی" | "مقام_الموسيقى" | "סולם_מוזיקלי" | "موسیقی_کلید" | "tonalité_musique" | "musik_tonart" | "тональность_музыки" => {
11243                let id = self.arg_num(&args, 0, 0.0)? as i64;
11244                let k = self
11245                    .tracks
11246                    .get(id as usize)
11247                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
11248                    .unwrap_or_default();
11249                return Ok(Value::Str(k));
11250            },
11251            #[cfg(not(target_arch = "wasm32"))]
11252            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" | "آغازهای_نت" | "بدايات_النغمات" | "התחלות_תווים" | "نوٹ_شروعات" | "attaques_musique" | "musik_einsätze" | "атаки_музыки" =>
11253            {
11254                let id = self.arg_num(&args, 0, 0.0)? as i64;
11255                let v = self
11256                    .tracks
11257                    .get(id as usize)
11258                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
11259                    .unwrap_or_default();
11260                return Ok(Value::List(Rc::new(
11261                    v.into_iter().map(|x| Value::Number(x as f64)).collect(),
11262                )));
11263            },
11264            #[cfg(not(target_arch = "wasm32"))]
11265            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" | "شبکه_ضرب" | "شبكة_الإيقاع" | "רשת_פעימות" | "بیٹ_گرڈ" | "grille_temps_musique" | "musik_taktraster" | "сетка_ритма_музыки" =>
11266            {
11267                let id = self.arg_num(&args, 0, 0.0)? as i64;
11268                let beats = self
11269                    .tracks
11270                    .get(id as usize)
11271                    .map(|t| {
11272                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
11273                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
11274                    })
11275                    .unwrap_or_default();
11276                return Ok(Value::List(Rc::new(
11277                    beats.into_iter().map(|x| Value::Number(x as f64)).collect(),
11278                )));
11279            },
11280
11281            // ── playback ──
11282            #[cfg(not(target_arch = "wasm32"))]
11283            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" | "پخش_موسیقی" | "شغّل_الموسيقى" | "נגן_מוזיקה" | "موسیقی_چلاؤ" | "jouer_musique" | "musik_abspielen" | "играть_музыку" =>
11284            {
11285                let id = self.arg_num(&args, 0, 0.0)? as i64;
11286                if self.ensure_music() {
11287                    let track = self
11288                        .tracks
11289                        .get(id as usize)
11290                        .map(|t| (t.stereo.clone(), t.rate));
11291                    if let (Some((st, rate)), Some(m)) = (track, &self.music) {
11292                        m.set_track(st, rate);
11293                        m.play();
11294                    } else if let Some(m) = &self.music {
11295                        m.play();
11296                    }
11297                }
11298                return Ok(Value::Unit);
11299            },
11300            #[cfg(not(target_arch = "wasm32"))]
11301            "music_pause" | "暂停音乐" | "音楽一時停止" | "음악일시정지" | "หยุดเพลงชั่วคราว" | "مکث_موسیقی" | "ألبث_الموسيقى" | "השהה_מוזיקה" | "موسیقی_روکو_مؤقت" | "pause_musique" | "musik_pausieren" | "пауза_музыки" =>
11302            {
11303                if let Some(m) = &self.music {
11304                    m.pause();
11305                }
11306                return Ok(Value::Unit);
11307            },
11308            #[cfg(not(target_arch = "wasm32"))]
11309            "music_stop" | "停止音乐" | "音楽停止" | "음악정지" | "หยุดเพลง" | "توقف_موسیقی" | "أوقف_الموسيقى" | "עצור_מוזיקה" | "موسیقی_روکو" | "arrêter_musique" | "musik_stoppen" | "остановить_музыку" =>
11310            {
11311                if let Some(m) = &self.music {
11312                    m.stop();
11313                }
11314                return Ok(Value::Unit);
11315            },
11316            #[cfg(not(target_arch = "wasm32"))]
11317            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" | "جستجوی_موسیقی" | "ابحث_في_الموسيقى" | "חפש_במוזיקה" | "موسیقی_تلاش" | "chercher_musique" | "musik_suchen" | "перемотать_музыку" =>
11318            {
11319                let sec = self.arg_num(&args, 0, 0.0)? as f32;
11320                if let Some(m) = &self.music {
11321                    m.seek(sec);
11322                }
11323                return Ok(Value::Unit);
11324            },
11325            #[cfg(not(target_arch = "wasm32"))]
11326            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" | "موقعیت_موسیقی" | "موضع_الموسيقى" | "מיקום_מוזיקה" | "موسیقی_مقام" | "position_musique" | "musik_position" | "позиция_музыки" =>
11327            {
11328                let p = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
11329                return Ok(Value::Number(p as f64));
11330            },
11331            #[cfg(not(target_arch = "wasm32"))]
11332            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" | "بلندی_موسیقی" | "مستوى_الموسيقى" | "עוצמת_מוזיקה" | "موسیقی_شدت" | "volume_musique" | "musik_lautstärke" | "громкость_музыки" =>
11333            {
11334                let v = self.arg_num(&args, 0, 0.8)? as f32;
11335                if self.ensure_music() {
11336                    if let Some(m) = &self.music {
11337                        m.set_volume(v);
11338                    }
11339                }
11340                return Ok(Value::Unit);
11341            },
11342
11343            // ── synthesis (GM-capable, patches from .ling files) ──
11344            #[cfg(not(target_arch = "wasm32"))]
11345            "music_patch" | "乐器音色" | "音色読込" | "악기패치" | "แพตช์เครื่องดนตรี" | "پچ_موسیقی" | "آلة_الموسيقى" | "תיקון_כלי_נגינה" | "میوزک_پیچ" | "patch_musique" | "musik_patch" | "патч_музыки" =>
11346            {
11347                let path = self.arg_str(&args, 0, "");
11348                let resolved = if std::path::Path::new(&path).exists() {
11349                    path.clone()
11350                } else if let Some(d) = &self.source_dir {
11351                    d.join(&path).to_string_lossy().into_owned()
11352                } else {
11353                    path.clone()
11354                };
11355                if !self.ensure_music() {
11356                    return Ok(Value::Number(-1.0));
11357                }
11358                match ling_music::patch::from_path(&resolved) {
11359                    Ok(p) => {
11360                        let id = self.music.as_ref().unwrap().add_patch(p);
11361                        return Ok(Value::Number(id as f64));
11362                    },
11363                    Err(e) => {
11364                        eprintln!("music_patch failed ({path}): {e}");
11365                        return Ok(Value::Number(-1.0));
11366                    },
11367                }
11368            },
11369            #[cfg(not(target_arch = "wasm32"))]
11370            "music_note" | "弹音符" | "音符演奏" | "음표연주" | "เล่นโน้ต" | "نواختن_نت" | "عزف_نغمة" | "נגן_תו" | "نوٹ_بجاؤ" | "note_musique" | "musik_note" | "нота_музыки" =>
11371            {
11372                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11373                let midi = self.pitch_arg(&args, 1, 60);
11374                let dur = self.arg_num(&args, 2, 0.5)? as f32;
11375                let vel = self.arg_num(&args, 3, 0.9)? as f32;
11376                if self.ensure_music() {
11377                    if let Some(m) = &self.music {
11378                        m.note(inst, midi, vel, dur);
11379                    }
11380                }
11381                return Ok(Value::Unit);
11382            },
11383            #[cfg(not(target_arch = "wasm32"))]
11384            "music_note_on" | "音符开始" | "音符オン" | "음표켜기" | "โน้ตเริ่ม" | "شروع_نت" | "بدء_النغمة" | "התחלת_תו" | "نوٹ_شروع" | "note_musique_on" | "musik_note_an" | "нота_музыки_вкл" =>
11385            {
11386                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11387                let midi = self.pitch_arg(&args, 1, 60);
11388                let vel = self.arg_num(&args, 2, 0.9)? as f32;
11389                if self.ensure_music() {
11390                    if let Some(m) = &self.music {
11391                        m.note_on(inst, midi, vel);
11392                    }
11393                }
11394                return Ok(Value::Unit);
11395            },
11396            #[cfg(not(target_arch = "wasm32"))]
11397            "music_note_off" | "音符结束" | "音符オフ" | "음표끄기" | "โน้ตจบ" | "پایان_نت" | "إيقاف_النغمة" | "סיום_תו" | "نوٹ_ختم" | "note_musique_off" | "musik_note_aus" | "нота_музыки_выкл" =>
11398            {
11399                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11400                let midi = self.pitch_arg(&args, 1, 60);
11401                if let Some(m) = &self.music {
11402                    m.note_off(inst, midi);
11403                }
11404                return Ok(Value::Unit);
11405            },
11406
11407            // ── rhythm-game judging ──
11408            #[cfg(not(target_arch = "wasm32"))]
11409            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" | "داوری_ضرب" | "حكم_الإيقاع" | "שיפוט_קצב" | "بیٹ_فیصلہ" | "juger_musique" | "musik_bewerten" | "оценить_музыку" =>
11410            {
11411                let delta_ms = self.arg_num(&args, 0, 9999.0)? as f32;
11412                return Ok(Value::Number(
11413                    ling_music::Grade::judge(delta_ms).index() as f64
11414                ));
11415            },
11416            #[cfg(not(target_arch = "wasm32"))]
11417            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" | "نام_رتبه" | "اسم_التقييم" | "שם_דירוג" | "گریڈ_نام" | "nom_grade_musique" | "musik_bewertungsname" | "имя_оценки_музыки" =>
11418            {
11419                let idx = self.arg_num(&args, 0, 4.0)? as i32;
11420                return Ok(Value::Str(
11421                    ling_music::Grade::from_index(idx).name().to_string(),
11422                ));
11423            },
11424
11425            // ── karaoke ──
11426            #[cfg(not(target_arch = "wasm32"))]
11427            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" | "بارگذاری_متن_ترانه" | "تحميل_كلمات_الأغنية" | "טעינת_מילות_שיר" | "گیت_متن_لوڈ" | "lrc_musique" | "musik_lrc" | "lrc_музыки" =>
11428            {
11429                let path = self.arg_str(&args, 0, "");
11430                let resolved = if std::path::Path::new(&path).exists() {
11431                    path.clone()
11432                } else if let Some(d) = &self.source_dir {
11433                    d.join(&path).to_string_lossy().into_owned()
11434                } else {
11435                    path.clone()
11436                };
11437                match std::fs::read_to_string(&resolved) {
11438                    Ok(text) => {
11439                        let id = self.lyrics.len();
11440                        self.lyrics.push(ling_music::Lyrics::parse(&text));
11441                        return Ok(Value::Number(id as f64));
11442                    },
11443                    Err(e) => {
11444                        eprintln!("music_lrc failed ({path}): {e}");
11445                        return Ok(Value::Number(-1.0));
11446                    },
11447                }
11448            },
11449            #[cfg(not(target_arch = "wasm32"))]
11450            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" | "متن_ترانه_فعلی" | "كلمات_الأغنية_الحالية" | "מילות_שיר_נוכחיות" | "موجودہ_گیت_متن" | "paroles_musique" | "musik_liedtext" | "текст_песни" =>
11451            {
11452                let id = self.arg_num(&args, 0, 0.0)? as i64;
11453                let t = self.arg_num(&args, 1, 0.0)? as f32;
11454                let line = self
11455                    .lyrics
11456                    .get(id as usize)
11457                    .map(|l| l.line_at(t).to_string())
11458                    .unwrap_or_default();
11459                return Ok(Value::Str(line));
11460            },
11461            #[cfg(not(target_arch = "wasm32"))]
11462            "music_mic_pitch" | "麦克风音高" | "マイク音程" | "마이크음정" | "ระดับเสียงไมค์" | "زیروبمی_میکروفون" | "طبقة_صوت_الميكروفون" | "גובה_צליל_מיקרופון" | "مائیکروفون_پچ" | "hauteur_micro_musique" | "musik_mikrofon_tonhöhe" | "высота_тона_микрофона" =>
11463            {
11464                let hz = if let Some(mic) = self.mic.as_ref() {
11465                    let s = mic.latest_samples();
11466                    let rate = mic.sample_rate();
11467                    ling_music::pitch::detect(&s, rate).unwrap_or(0.0)
11468                } else {
11469                    0.0
11470                };
11471                return Ok(Value::Number(hz as f64));
11472            },
11473            #[cfg(not(target_arch = "wasm32"))]
11474            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" | "نام_نت" | "اسم_النغمة" | "שם_תו" | "نوٹ_نام" | "nom_note_musique" | "musik_notenname" | "имя_ноты_музыки" =>
11475            {
11476                let hz = self.arg_num(&args, 0, 0.0)? as f32;
11477                return Ok(Value::Str(ling_music::note::hz_to_name(hz)));
11478            },
11479            #[cfg(not(target_arch = "wasm32"))]
11480            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" | "فرکانس_نت" | "تردد_النغمة" | "תדר_תו" | "نوٹ_ہرٹز" | "hz_musique" | "musik_hz" | "музыка_гц" =>
11481            {
11482                let midi = self.pitch_arg(&args, 0, 69);
11483                return Ok(Value::Number(
11484                    ling_music::note::midi_to_hz(midi as f32) as f64
11485                ));
11486            },
11487            #[cfg(not(target_arch = "wasm32"))]
11488            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" | "امتیاز_زیروبمی" | "درجة_طبقة_الصوت" | "ציון_גובה_צליל" | "پچ_اسکور" | "score_hauteur_musique" | "musik_tonhöhen_punktzahl" | "счёт_высоты_тона" =>
11489            {
11490                let hz = self.arg_num(&args, 0, 0.0)? as f32;
11491                let target = self.arg_num(&args, 1, 0.0)? as f32;
11492                return Ok(Value::Number(
11493                    ling_music::karaoke::pitch_score(hz, target) as f64
11494                ));
11495            },
11496
11497            // ── MIDI (inaudible note source: drive coins, cues, etc.) ──
11498            #[cfg(not(target_arch = "wasm32"))]
11499            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" | "بارگذاری_MIDI" | "تحميل_MIDI" | "טעינת_MIDI" | "MIDI_لوڈ" | "charger_midi_musique" | "musik_midi_laden" | "загрузить_midi_музыки" =>
11500            {
11501                let path = self.arg_str(&args, 0, "");
11502                let resolved = if std::path::Path::new(&path).exists() {
11503                    path.clone()
11504                } else if let Some(d) = &self.source_dir {
11505                    d.join(&path).to_string_lossy().into_owned()
11506                } else {
11507                    path.clone()
11508                };
11509                match ling_music::midi::load(&resolved) {
11510                    Ok(m) => {
11511                        let id = self.midis.len();
11512                        self.midis.push(m);
11513                        return Ok(Value::Number(id as f64));
11514                    },
11515                    Err(e) => {
11516                        eprintln!("music_midi_load failed ({path}): {e}");
11517                        return Ok(Value::Number(-1.0));
11518                    },
11519                }
11520            },
11521            #[cfg(not(target_arch = "wasm32"))]
11522            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" | "تعداد_MIDI" | "عدد_MIDI" | "מספר_MIDI" | "MIDI_تعداد" | "nombre_midi_musique" | "musik_midi_anzahl" | "число_midi_музыки" =>
11523            {
11524                let id = self.arg_num(&args, 0, 0.0)? as i64;
11525                let n = self
11526                    .midis
11527                    .get(id as usize)
11528                    .map(|m| m.notes.len())
11529                    .unwrap_or(0);
11530                return Ok(Value::Number(n as f64));
11531            },
11532            // music_midi_notes(id) -> flat [time, midi, time, midi, …]
11533            #[cfg(not(target_arch = "wasm32"))]
11534            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" | "نت‌های_MIDI" | "نغمات_MIDI" | "תווי_MIDI" | "MIDI_نوٹس" | "notes_midi_musique" | "musik_midi_noten" | "ноты_midi_музыки" =>
11535            {
11536                let id = self.arg_num(&args, 0, 0.0)? as i64;
11537                let mut out = Vec::new();
11538                if let Some(m) = self.midis.get(id as usize) {
11539                    for n in &m.notes {
11540                        out.push(Value::Number(n.time as f64));
11541                        out.push(Value::Number(n.midi as f64));
11542                    }
11543                }
11544                return Ok(Value::List(Rc::new(out)));
11545            },
11546            // music_midi_bars(id) -> flat [time, midi, dur, …] (for karaoke note bars)
11547            #[cfg(not(target_arch = "wasm32"))]
11548            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" | "میله‌های_MIDI" | "أعمدة_MIDI" | "עמודות_MIDI" | "MIDI_بارز" | "mesures_midi_musique" | "musik_midi_takte" | "такты_midi_музыки" =>
11549            {
11550                let id = self.arg_num(&args, 0, 0.0)? as i64;
11551                let mut out = Vec::new();
11552                if let Some(m) = self.midis.get(id as usize) {
11553                    for n in &m.notes {
11554                        out.push(Value::Number(n.time as f64));
11555                        out.push(Value::Number(n.midi as f64));
11556                        out.push(Value::Number(n.dur as f64));
11557                    }
11558                }
11559                return Ok(Value::List(Rc::new(out)));
11560            },
11561
11562            // music_fft(track_id, nbands) -> spectrum at the current playback position
11563            #[cfg(not(target_arch = "wasm32"))]
11564            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" | "طیف_موسیقی" | "طيف_الموسيقى" | "ספקטרום_מוזיקה" | "میوزک_اسپیکٹرم" | "fft_musique" | "musik_fft" | "fft_музыки" =>
11565            {
11566                let id = self.arg_num(&args, 0, 0.0)? as i64;
11567                let nbands = self.arg_num(&args, 1, 16.0)? as usize;
11568                let pos = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
11569                if let Some(t) = self.tracks.get(id as usize) {
11570                    let idx = (pos * t.rate as f32) as usize;
11571                    let end = (idx + 2048).min(t.mono.len());
11572                    if end > idx + 64 {
11573                        self.fft.borrow_mut().push_samples(&t.mono[idx..end]);
11574                    }
11575                }
11576                let bands = self.fft.borrow().freq_bands(nbands);
11577                return Ok(Value::List(Rc::new(
11578                    bands.into_iter().map(|x| Value::Number(x as f64)).collect(),
11579                )));
11580            },
11581
11582            // ── stop every one-shot SFX/morph/sample voice (scene cleanup) ──
11583            #[cfg(not(target_arch = "wasm32"))]
11584            "audio_stop_sfx" | "停止音效" | "効果音停止" | "효과음정지" | "หยุดเอฟเฟกต์ทั้งหมด" | "توقف_همه_جلوه‌ها" | "أوقف_كل_المؤثرات" | "עצור_כל_האפקטים" | "تمام_ایفیکٹ_روکو" =>
11585            {
11586                if let Some(a) = &self.audio {
11587                    a.stop_all_sfx();
11588                }
11589                return Ok(Value::Unit);
11590            },
11591            // ── spatial (2D/3D/4D) one-shot SFX ──
11592            #[cfg(not(target_arch = "wasm32"))]
11593            "audio_sfx" | "音效" | "空間効果音" | "공간효과음" | "เสียงเอฟเฟกต์" | "جلوه_صوتی" | "مؤثرات_صوتية" | "אפקט_קול" | "آواز_ایفیکٹ" | "effet_sonore" | "klangeffekt" | "звуковой_эффект" =>
11594            {
11595                let x = self.arg_num(&args, 0, 0.0)? as f32;
11596                let y = self.arg_num(&args, 1, 0.0)? as f32;
11597                let z = self.arg_num(&args, 2, 0.0)? as f32;
11598                let w = self.arg_num(&args, 3, 1.0)? as f32;
11599                let freq = self.arg_num(&args, 4, 440.0)? as f32;
11600                let amp = self.arg_num(&args, 5, 0.3)? as f32;
11601                let dur = self.arg_num(&args, 6, 0.15)? as f32;
11602                let wave = Wave::from_name(&self.arg_str(&args, 7, "sine"));
11603                if let Some(a) = &self.audio {
11604                    a.sfx(x, y, z, w, freq, amp, dur, wave);
11605                }
11606                return Ok(Value::Unit);
11607            },
11608            // ── YIN-YANG morph synth note: physical-model(light) ↔ FM/crush(dark) ──
11609            // โน้ตมอร์ฟ(x,y,z,w, freq, amp, dur, material, morph)
11610            //   material: 0 bowed-string · 1 plucked · 2 blown · 3 struck-metal
11611            //   morph:    0.0 light/acoustic .. 1.0 dark/digital
11612            #[cfg(not(target_arch = "wasm32"))]
11613            "morph_note" | "โน้ตมอร์ฟ" | "变形音" | "モーフ音" | "모프음" | "نت_مورف" | "نغمة_متحولة" | "תו_מורף" | "مورف_نوٹ" =>
11614            {
11615                let x = self.arg_num(&args, 0, 0.0)? as f32;
11616                let y = self.arg_num(&args, 1, 0.0)? as f32;
11617                let z = self.arg_num(&args, 2, 0.0)? as f32;
11618                let w = self.arg_num(&args, 3, 1.0)? as f32;
11619                let freq = self.arg_num(&args, 4, 220.0)? as f32;
11620                let amp = self.arg_num(&args, 5, 0.3)? as f32;
11621                let dur = self.arg_num(&args, 6, 0.6)? as f32;
11622                let material = self.arg_num(&args, 7, 0.0)?.clamp(0.0, 3.0) as u8;
11623                let morph = self.arg_num(&args, 8, 0.0)? as f32;
11624                if let Some(a) = &self.audio {
11625                    a.morph_note(x, y, z, w, freq, amp, dur, material, morph);
11626                }
11627                return Ok(Value::Unit);
11628            },
11629            // ── sample load / positional play / loop / stop ──
11630            #[cfg(not(target_arch = "wasm32"))]
11631            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" | "بارگذاری_نمونه_صدا" | "تحميل_عينة_صوتية" | "טעינת_דגימת_קול" | "آواز_نمونہ_لوڈ" | "charger_échantillon" | "sample_laden" | "загрузить_семпл" =>
11632            {
11633                let path = self.arg_str(&args, 0, "");
11634                let resolved = if std::path::Path::new(&path).exists() {
11635                    path.clone()
11636                } else if let Some(d) = &self.source_dir {
11637                    d.join(&path).to_string_lossy().into_owned()
11638                } else {
11639                    path.clone()
11640                };
11641                match ling_music::load(&resolved) {
11642                    Ok(t) => {
11643                        if let Some(a) = &self.audio {
11644                            return Ok(Value::Number(a.add_sample(t.mono, t.rate) as f64));
11645                        }
11646                        return Ok(Value::Number(-1.0));
11647                    },
11648                    Err(e) => {
11649                        eprintln!("audio_sample_load failed ({path}): {e}");
11650                        return Ok(Value::Number(-1.0));
11651                    },
11652                }
11653            },
11654            #[cfg(not(target_arch = "wasm32"))]
11655            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" | "پخش_نمونه_صدا" | "تشغيل_عينة_صوتية" | "נגינת_דגימת_קול" | "آواز_نمونہ_چلاؤ" | "jouer_échantillon" | "sample_abspielen" | "играть_семпл" =>
11656            {
11657                let id = self.arg_num(&args, 0, 0.0)? as usize;
11658                let x = self.arg_num(&args, 1, 0.0)? as f32;
11659                let y = self.arg_num(&args, 2, 0.0)? as f32;
11660                let z = self.arg_num(&args, 3, 0.0)? as f32;
11661                let w = self.arg_num(&args, 4, 1.0)? as f32;
11662                let vol = self.arg_num(&args, 5, 1.0)? as f32;
11663                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
11664                let v = self
11665                    .audio
11666                    .as_ref()
11667                    .map(|a| a.play_sample(id, x, y, z, w, vol, looping))
11668                    .unwrap_or(0);
11669                return Ok(Value::Number(v as f64));
11670            },
11671            // Flat/non-positional sample playback — no distance falloff, no
11672            // stereo pan, same level regardless of listener position. For
11673            // VO/narration/UI cues, as opposed to audio_sample_play's
11674            // world-space sound effects (which fall off with camera distance).
11675            #[cfg(not(target_arch = "wasm32"))]
11676            "audio_sample_play_flat" | "播放采样平" | "サンプル再生平" | "샘플재생평" | "เล่นตัวอย่างเสียงเรียบ" =>
11677            {
11678                let id = self.arg_num(&args, 0, 0.0)? as usize;
11679                let vol = self.arg_num(&args, 1, 1.0)? as f32;
11680                let looping = self.arg_num(&args, 2, 0.0)? > 0.5;
11681                let v = self
11682                    .audio
11683                    .as_ref()
11684                    .map(|a| a.play_sample_flat(id, vol, looping))
11685                    .unwrap_or(0);
11686                return Ok(Value::Number(v as f64));
11687            },
11688            #[cfg(not(target_arch = "wasm32"))]
11689            "audio_sample_stop" | "停止采样" | "サンプル停止" | "샘플정지" | "หยุดตัวอย่างเสียง" | "توقف_نمونه_صدا" | "إيقاف_عينة_صوتية" | "עצירת_דגימת_קול" | "آواز_نمونہ_روکو" | "arrêter_échantillon" | "sample_stoppen" | "остановить_семпл" =>
11690            {
11691                let v = self.arg_num(&args, 0, 0.0)? as u32;
11692                if let Some(a) = &self.audio {
11693                    a.stop_sample(v);
11694                }
11695                return Ok(Value::Unit);
11696            },
11697            // Duration (seconds) of an already-loaded sample. 0.0 if the id
11698            // is invalid. Lets a cutscene pace itself off the real length of
11699            // whichever language's VO clip actually got loaded, instead of a
11700            // hand-measured constant that only covers one language.
11701            #[cfg(not(target_arch = "wasm32"))]
11702            "audio_sample_duration" | "采样时长" | "サンプル長さ" | "샘플길이" | "ความยาวตัวอย่างเสียง" =>
11703            {
11704                let id = self.arg_num(&args, 0, 0.0)? as usize;
11705                let dur = self.audio.as_ref().map(|a| a.sample_duration(id)).unwrap_or(0.0);
11706                return Ok(Value::Number(dur as f64));
11707            },
11708            // ── master FX: delay / reverb / low-pass (underwater) ──
11709            #[cfg(not(target_arch = "wasm32"))]
11710            "audio_fx_delay" | "回声" | "ディレイ効果" | "딜레이" | "เสียงสะท้อน" | "افکت_تاخیر" | "صدى_تأخير" | "אפקט_עיכוב" | "تاخیر_ایفیکٹ" | "délai_audio" | "audio_verzögerung" | "звук_задержка" =>
11711            {
11712                let time = self.arg_num(&args, 0, 0.3)? as f32;
11713                let fb = self.arg_num(&args, 1, 0.3)? as f32;
11714                let mix = self.arg_num(&args, 2, 0.3)? as f32;
11715                if let Some(a) = &self.audio {
11716                    a.fx_delay(time, fb, mix);
11717                }
11718                return Ok(Value::Unit);
11719            },
11720            #[cfg(not(target_arch = "wasm32"))]
11721            "audio_fx_reverb" | "混响" | "リバーブ" | "리버브" | "เสียงก้อง" | "افکت_پژواک" | "صدى_ارتداد" | "אפקט_הדהוד" | "بازگشت_آواز_ایفیکٹ" | "réverbération_audio" | "audio_nachhall" | "звук_реверберация" =>
11722            {
11723                let mix = self.arg_num(&args, 0, 0.3)? as f32;
11724                if let Some(a) = &self.audio {
11725                    a.fx_reverb(mix);
11726                }
11727                return Ok(Value::Unit);
11728            },
11729            #[cfg(not(target_arch = "wasm32"))]
11730            "audio_fx_lowpass" | "低通滤波" | "ローパス" | "저역통과" | "กรองความถี่ต่ำ" | "فیلتر_پایین‌گذر" | "مرشح_تمرير_منخفض" | "מסנן_תדר_נמוך" | "لو_پاس_فلٹر" | "passe_bas_audio" | "audio_tiefpass" | "звук_фнч" =>
11731            {
11732                let cutoff = self.arg_num(&args, 0, 1.0)? as f32;
11733                if let Some(a) = &self.audio {
11734                    a.fx_lowpass(cutoff);
11735                }
11736                return Ok(Value::Unit);
11737            },
11738
11739            // ══════════════════════════════════════════════════════════════════
11740            // PHYSICS BUILTINS  (crates/ling-physics) — soft bodies, rigid+angular,
11741            // and a fast 2-D water/oil liquid sim mappable onto 3-D surfaces.
11742            // ══════════════════════════════════════════════════════════════════
11743
11744            // ── soft bodies (deformable bouncy balls) ──
11745            #[cfg(not(target_arch = "wasm32"))]
11746            "soft_ball" | "软球" | "ソフトボール" | "소프트볼" | "ลูกบอลนุ่ม" | "توپ_نرم" | "كرة_ناعمة" | "כדור_רך" | "نرم_گیند" | "balle_molle" | "weicher_ball" | "мягкий_шар" =>
11747            {
11748                let x = self.arg_num(&args, 0, 0.)? as f32;
11749                let y = self.arg_num(&args, 1, 0.)? as f32;
11750                let z = self.arg_num(&args, 2, 0.)? as f32;
11751                let r = self.arg_num(&args, 3, 1.0)? as f32;
11752                let b = ling_physics::soft::SoftBody::sphere(
11753                    ling_physics::Vec3::new(x, y, z),
11754                    r,
11755                    8,
11756                    12,
11757                    1.0,
11758                );
11759                let id = self.soft_bodies.len();
11760                self.soft_bodies.push(b);
11761                return Ok(Value::Number(id as f64));
11762            },
11763            #[cfg(not(target_arch = "wasm32"))]
11764            "soft_step" | "软体步进" | "ソフト更新" | "소프트스텝" | "ก้าวนุ่ม" | "گام_نرم_جسم" | "خطوة_ناعمة" | "צעד_רך" | "نرم_قدم" | "pas_mou" | "weicher_schritt" | "мягкий_шаг" =>
11765            {
11766                let id = self.arg_num(&args, 0, 0.)? as usize;
11767                let dt = self.arg_num(&args, 1, 0.016)? as f32;
11768                let gy = self.arg_num(&args, 2, 15.0)? as f32;
11769                if let Some(b) = self.soft_bodies.get_mut(id) {
11770                    b.integrate(dt, ling_physics::Vec3::new(0.0, gy, 0.0), 4);
11771                }
11772                return Ok(Value::Unit);
11773            },
11774            #[cfg(not(target_arch = "wasm32"))]
11775            "soft_bounce" | "软体落地" | "ソフト着地" | "소프트바운스" | "เด้งนุ่ม" | "جهش_نرم" | "ارتداد_ناعم" | "קפיצה_רכה" | "نرم_اچھال" | "rebond_mou" | "weicher_abprall" | "мягкий_отскок" =>
11776            {
11777                let id = self.arg_num(&args, 0, 0.)? as usize;
11778                let fy = self.arg_num(&args, 1, 0.)? as f32;
11779                let rest = self.arg_num(&args, 2, 0.5)? as f32;
11780                if let Some(b) = self.soft_bodies.get_mut(id) {
11781                    b.floor_collision(fy, rest);
11782                }
11783                return Ok(Value::Unit);
11784            },
11785            #[cfg(not(target_arch = "wasm32"))]
11786            "soft_contain" | "软体边界" | "ソフト箱" | "소프트경계" | "กล่องนุ่ม" | "محفظه_نرم" | "احتواء_ناعم" | "הכלה_רכה" | "نرم_احاطہ" | "contenir_mou" | "weiche_eindämmung" | "мягкое_сдерживание" =>
11787            {
11788                let id = self.arg_num(&args, 0, 0.)? as usize;
11789                let nx = self.arg_num(&args, 1, -5.)? as f32;
11790                let ny = self.arg_num(&args, 2, -5.)? as f32;
11791                let nz = self.arg_num(&args, 3, -5.)? as f32;
11792                let mx = self.arg_num(&args, 4, 5.)? as f32;
11793                let my = self.arg_num(&args, 5, 5.)? as f32;
11794                let mz = self.arg_num(&args, 6, 5.)? as f32;
11795                let rest = self.arg_num(&args, 7, 0.6)? as f32;
11796                if let Some(b) = self.soft_bodies.get_mut(id) {
11797                    b.contain(
11798                        ling_physics::Vec3::new(nx, ny, nz),
11799                        ling_physics::Vec3::new(mx, my, mz),
11800                        rest,
11801                    );
11802                }
11803                return Ok(Value::Unit);
11804            },
11805            #[cfg(not(target_arch = "wasm32"))]
11806            "soft_kick" | "软体踢" | "ソフト衝撃" | "소프트킥" | "เตะนุ่ม" | "ضربه_نرم" | "ركلة_ناعمة" | "בעיטה_רכה" | "نرم_ٹھوکر" | "coup_mou" | "weicher_stoß" | "мягкий_удар" =>
11807            {
11808                let id = self.arg_num(&args, 0, 0.)? as usize;
11809                let dx = self.arg_num(&args, 1, 0.)? as f32;
11810                let dy = self.arg_num(&args, 2, 0.)? as f32;
11811                let dz = self.arg_num(&args, 3, 0.)? as f32;
11812                let s = self.arg_num(&args, 4, 0.1)? as f32;
11813                if let Some(b) = self.soft_bodies.get_mut(id) {
11814                    b.kick(ling_physics::Vec3::new(dx, dy, dz), s);
11815                }
11816                return Ok(Value::Unit);
11817            },
11818            // soft_spin(id, ax, ay, az, rate) — add angular velocity about the axis
11819            // through the centroid (rate = rad/step; ≈ surface_speed / radius to roll)
11820            #[cfg(not(target_arch = "wasm32"))]
11821            "soft_spin" | "软体自旋" | "ソフト回転" | "소프트회전" | "หมุนนุ่ม" | "چرخش_نرم" | "دوران_ناعم" | "סיבוב_רך" | "نرم_گھماؤ" | "rotation_molle" | "weicher_spin" | "мягкое_вращение" =>
11822            {
11823                let id = self.arg_num(&args, 0, 0.)? as usize;
11824                let ax = self.arg_num(&args, 1, 0.)? as f32;
11825                let ay = self.arg_num(&args, 2, 0.)? as f32;
11826                let az = self.arg_num(&args, 3, 0.)? as f32;
11827                let rate = self.arg_num(&args, 4, 0.1)? as f32;
11828                if let Some(b) = self.soft_bodies.get_mut(id) {
11829                    b.spin(ling_physics::Vec3::new(ax, ay, az), rate);
11830                }
11831                return Ok(Value::Unit);
11832            },
11833            #[cfg(not(target_arch = "wasm32"))]
11834            "soft_deform" | "形变量" | "変形量" | "변형량" | "ความบิดเบี้ยว" | "تغییرشکل_نرم" | "تشوه_ناعم" | "עיוות_רך" | "نرم_بگاڑ" | "déformer_mou" | "weiches_verformen" | "мягкая_деформация" =>
11835            {
11836                let id = self.arg_num(&args, 0, 0.)? as usize;
11837                let d = self
11838                    .soft_bodies
11839                    .get(id)
11840                    .map(|b| b.deformation())
11841                    .unwrap_or(0.0);
11842                return Ok(Value::Number(d as f64));
11843            },
11844            // soft_angular_speed(id) -> magnitude of the body's angular velocity
11845            // (how fast it is tumbling/rolling), derived from its node velocities.
11846            #[cfg(not(target_arch = "wasm32"))]
11847            "soft_angular_speed"
11848            | "软体角速"
11849            | "ソフト角速度"
11850            | "소프트각속도"
11851            | "ความเร็วเชิงมุมนุ่ม" | "سرعت_زاویه‌ای_نرم" | "سرعة_زاوية_ناعمة" | "מהירות_זוויתית_רכה" | "نرم_زاویائی_رفتار" | "vitesse_angulaire_molle" | "weiche_winkelgeschwindigkeit" | "мягкая_угловая_скорость" => {
11852                let id = self.arg_num(&args, 0, 0.)? as usize;
11853                let w = self
11854                    .soft_bodies
11855                    .get(id)
11856                    .map(|b| b.angular_speed())
11857                    .unwrap_or(0.0);
11858                return Ok(Value::Number(w as f64));
11859            },
11860            #[cfg(not(target_arch = "wasm32"))]
11861            "soft_centroid" | "软体质心" | "ソフト重心" | "소프트중심" | "จุดศูนย์กลางนุ่ม" | "مرکز_جرم_نرم" | "مركز_ثقل_ناعم" | "מרכז_כובד_רך" | "نرم_مرکز_ثقل" | "centroïde_mou" | "weicher_schwerpunkt" | "мягкий_центроид" =>
11862            {
11863                let id = self.arg_num(&args, 0, 0.)? as usize;
11864                let c = self
11865                    .soft_bodies
11866                    .get(id)
11867                    .map(|b| b.centroid())
11868                    .unwrap_or(ling_physics::Vec3::ZERO);
11869                return Ok(Value::List(Rc::new(vec![
11870                    Value::Number(c.x as f64),
11871                    Value::Number(c.y as f64),
11872                    Value::Number(c.z as f64),
11873                ])));
11874            },
11875            // soft_nodes(id) -> flat [x,y,z, x,y,z, …] for rendering the deformed mesh
11876            #[cfg(not(target_arch = "wasm32"))]
11877            "soft_nodes" | "软体节点" | "ソフト節点" | "소프트노드" | "จุดนุ่ม" | "گره‌های_نرم" | "عقد_ناعمة" | "צמתי_רך" | "نرم_نوڈز" | "nœuds_mous" | "weiche_knoten" | "мягкие_узлы" =>
11878            {
11879                let id = self.arg_num(&args, 0, 0.)? as usize;
11880                let mut out = Vec::new();
11881                if let Some(b) = self.soft_bodies.get(id) {
11882                    for n in &b.nodes {
11883                        out.push(Value::Number(n.pos.x as f64));
11884                        out.push(Value::Number(n.pos.y as f64));
11885                        out.push(Value::Number(n.pos.z as f64));
11886                    }
11887                }
11888                return Ok(Value::List(Rc::new(out)));
11889            },
11890
11891            // ── rigid bodies with angular dynamics ──
11892            #[cfg(not(target_arch = "wasm32"))]
11893            "rb_add" | "刚体添加" | "剛体追加" | "강체추가" | "เพิ่มวัตถุแข็ง" | "افزودن_جسم_صلب" | "أضف_جسما_صلبا" | "הוסף_גוף_קשיח" | "سخت_جسم_شامل_کرو" | "ajouter_corps_rigide" | "starrkörper_hinzufügen" | "добавить_твёрдое_тело" =>
11894            {
11895                let x = self.arg_num(&args, 0, 0.)? as f32;
11896                let y = self.arg_num(&args, 1, 0.)? as f32;
11897                let z = self.arg_num(&args, 2, 0.)? as f32;
11898                let mass = self.arg_num(&args, 3, 1.0)? as f32;
11899                let mut b =
11900                    ling_physics::rigid::RigidBody::new(ling_physics::Vec3::new(x, y, z), mass);
11901                b.restitution = 0.6;
11902                return Ok(Value::Number(self.rigid_world.add(b) as f64));
11903            },
11904            #[cfg(not(target_arch = "wasm32"))]
11905            "rb_torque" | "扭矩" | "トルク" | "토크" | "แรงบิด" | "گشتاور" | "عزم_دوران" | "מומנט" | "ٹارک" | "couple_corps_rigide" | "starrkörper_drehmoment" | "крутящий_момент_твёрдого_тела" => {
11906                let i = self.arg_num(&args, 0, 0.)? as usize;
11907                let tx = self.arg_num(&args, 1, 0.)? as f32;
11908                let ty = self.arg_num(&args, 2, 0.)? as f32;
11909                let tz = self.arg_num(&args, 3, 0.)? as f32;
11910                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11911                    b.apply_torque(ling_physics::Vec3::new(tx, ty, tz));
11912                }
11913                return Ok(Value::Unit);
11914            },
11915            #[cfg(not(target_arch = "wasm32"))]
11916            "rb_spin" | "自旋" | "スピン" | "스핀" | "หมุน" | "چرخش_جسم_صلب" | "دوران_جسم_صلب" | "סיבוב_גוף_קשיח" | "سخت_جسم_گھماؤ" | "spin_corps_rigide" | "starrkörper_spin" | "вращение_твёрдого_тела" => {
11917                let i = self.arg_num(&args, 0, 0.)? as usize;
11918                let wx = self.arg_num(&args, 1, 0.)? as f32;
11919                let wy = self.arg_num(&args, 2, 0.)? as f32;
11920                let wz = self.arg_num(&args, 3, 0.)? as f32;
11921                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11922                    b.apply_spin(ling_physics::Vec3::new(wx, wy, wz));
11923                }
11924                return Ok(Value::Unit);
11925            },
11926            #[cfg(not(target_arch = "wasm32"))]
11927            "rb_impulse" | "刚体冲量" | "剛体インパルス" | "강체충격" | "แรงดลแข็ง" | "ضربه_جسم_صلب" | "دفعة_جسم_صلب" | "דחף_גוף_קשיח" | "سخت_جسم_دھکا" | "impulsion_corps_rigide" | "starrkörper_impuls" | "импульс_твёрдого_тела" =>
11928            {
11929                let i = self.arg_num(&args, 0, 0.)? as usize;
11930                let ix = self.arg_num(&args, 1, 0.)? as f32;
11931                let iy = self.arg_num(&args, 2, 0.)? as f32;
11932                let iz = self.arg_num(&args, 3, 0.)? as f32;
11933                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11934                    b.apply_impulse(ling_physics::Vec3::new(ix, iy, iz));
11935                }
11936                return Ok(Value::Unit);
11937            },
11938            #[cfg(not(target_arch = "wasm32"))]
11939            "rb_floor" | "刚体落地" | "剛体着地" | "강체바닥" | "พื้นแข็ง" | "کف_جسم_صلب" | "أرضية_جسم_صلب" | "רצפת_גוף_קשיח" | "سخت_جسم_فرش" | "sol_corps_rigide" | "starrkörper_boden" | "пол_твёрдого_тела" =>
11940            {
11941                let i = self.arg_num(&args, 0, 0.)? as usize;
11942                let fy = self.arg_num(&args, 1, 0.)? as f32;
11943                let rest = self.arg_num(&args, 2, 0.6)? as f32;
11944                let fric = self.arg_num(&args, 3, 0.6)? as f32;
11945                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11946                    b.bounce_floor(fy, rest, fric);
11947                }
11948                return Ok(Value::Unit);
11949            },
11950            #[cfg(not(target_arch = "wasm32"))]
11951            "rb_gravity" | "刚体重力" | "剛体重力" | "강체중력" | "แรงโน้มถ่วงแข็ง" | "گرانش_جسم_صلب" | "جاذبية_جسم_صلب" | "כבידת_גוף_קשיח" | "سخت_جسم_کشش_ثقل" | "gravité_corps_rigide" | "starrkörper_schwerkraft" | "гравитация_твёрдого_тела" =>
11952            {
11953                let gx = self.arg_num(&args, 0, 0.)? as f32;
11954                let gy = self.arg_num(&args, 1, 9.81)? as f32;
11955                let gz = self.arg_num(&args, 2, 0.)? as f32;
11956                self.rigid_world.gravity = ling_physics::Vec3::new(gx, gy, gz);
11957                return Ok(Value::Unit);
11958            },
11959            #[cfg(not(target_arch = "wasm32"))]
11960            "rb_step" | "刚体步进" | "剛体更新" | "강체스텝" | "ก้าวแข็ง" | "گام_جسم_صلب" | "خطوة_جسم_صلب" | "צעד_גוף_קשיח" | "سخت_جسم_قدم" | "pas_corps_rigide" | "starrkörper_schritt" | "шаг_твёрдого_тела" =>
11961            {
11962                let dt = self.arg_num(&args, 0, 0.016)? as f32;
11963                self.rigid_world.step(dt);
11964                return Ok(Value::Unit);
11965            },
11966            #[cfg(not(target_arch = "wasm32"))]
11967            "rb_pos" | "刚体位置" | "剛体位置" | "강체위치" | "ตำแหน่งแข็ง" | "موقعیت_جسم_صلب" | "موضع_جسم_صلب" | "מיקום_גוף_קשיח" | "سخت_جسم_مقام" | "position_corps_rigide" | "starrkörper_position" | "позиция_твёрдого_тела" =>
11968            {
11969                let i = self.arg_num(&args, 0, 0.)? as usize;
11970                let p = self
11971                    .rigid_world
11972                    .bodies
11973                    .get(i)
11974                    .map(|b| b.pos)
11975                    .unwrap_or(ling_physics::Vec3::ZERO);
11976                return Ok(Value::List(Rc::new(vec![
11977                    Value::Number(p.x as f64),
11978                    Value::Number(p.y as f64),
11979                    Value::Number(p.z as f64),
11980                ])));
11981            },
11982            #[cfg(not(target_arch = "wasm32"))]
11983            "rb_rot" | "刚体旋转" | "剛体回転" | "강체회전" | "การหมุนแข็ง" | "چرخش_وضعية_جسم_صلب" | "دوران_وضعية_جسم_صلب" | "סיבוב_זווית_גוף_קשיח" | "سخت_جسم_گردش" | "rotation_corps_rigide" | "starrkörper_rotation" | "поворот_твёрдого_тела" =>
11984            {
11985                let i = self.arg_num(&args, 0, 0.)? as usize;
11986                let q = self
11987                    .rigid_world
11988                    .bodies
11989                    .get(i)
11990                    .map(|b| b.orientation)
11991                    .unwrap_or(ling_physics::Quat::IDENTITY);
11992                return Ok(Value::List(Rc::new(vec![
11993                    Value::Number(q.x as f64),
11994                    Value::Number(q.y as f64),
11995                    Value::Number(q.z as f64),
11996                    Value::Number(q.w as f64),
11997                ])));
11998            },
11999
12000            // ── native-res mesh (.lmesh): load once, draw fast (unlit, per-tri colour) ──
12001            #[cfg(not(target_arch = "wasm32"))]
12002            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" | "بارگذاری_مش" | "حمّل_شبكة" | "טען_מש" | "میش_لوڈ" =>
12003            {
12004                let path = self.arg_str(&args, 0, "");
12005                let resolved = if std::path::Path::new(&path).exists() {
12006                    path.clone()
12007                } else if let Some(d) = &self.source_dir {
12008                    d.join(&path).to_string_lossy().into_owned()
12009                } else {
12010                    path.clone()
12011                };
12012                let bytes = match std::fs::read(&resolved) {
12013                    Ok(b) => b,
12014                    Err(e) => {
12015                        eprintln!("mesh_load failed ({path}): {e}");
12016                        return Ok(Value::Number(-1.0));
12017                    },
12018                };
12019                if bytes.len() < 16 || &bytes[0..4] != b"LMSH" {
12020                    eprintln!("mesh_load: bad header ({path})");
12021                    return Ok(Value::Number(-1.0));
12022                }
12023                let rd4 =
12024                    |o: usize| -> [u8; 4] { [bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]] };
12025                let height = f32::from_le_bytes(rd4(8));
12026                let ntri = u32::from_le_bytes(rd4(12)) as usize;
12027                let need = 16usize.saturating_add(ntri.saturating_mul(9 * 4 + 3));
12028                if bytes.len() < need {
12029                    eprintln!("mesh_load: truncated ({path})");
12030                    return Ok(Value::Number(-1.0));
12031                }
12032                let mut pos = Vec::with_capacity(ntri * 3);
12033                let mut col = Vec::with_capacity(ntri);
12034                let mut off = 16usize;
12035                for _ in 0..ntri {
12036                    for _k in 0..3 {
12037                        let x = f32::from_le_bytes(rd4(off));
12038                        let y = f32::from_le_bytes(rd4(off + 4));
12039                        let z = f32::from_le_bytes(rd4(off + 8));
12040                        off += 12;
12041                        pos.push([x, y, z]);
12042                    }
12043                    col.push([bytes[off], bytes[off + 1], bytes[off + 2]]);
12044                    off += 3;
12045                }
12046                eprintln!("mesh_load: {} ({} tris, h={:.2})", path, ntri, height);
12047                let id = self.meshes.len();
12048                self.meshes
12049                    .push(crate::gfx::shapes::ColorMesh { pos, col, height });
12050                return Ok(Value::Number(id as f64));
12051            },
12052            #[cfg(target_arch = "wasm32")]
12053            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" | "بارگذاری_مش" | "حمّل_شبكة" | "טען_רשת" | "میش_لوڈ" =>
12054            {
12055                // Native .lmesh loading is file-system based and not wired for wasm yet.
12056                // Return an invalid handle so scripts can choose a fallback path.
12057                return Ok(Value::Number(-1.0));
12058            },
12059            #[cfg(not(target_arch = "wasm32"))]
12060            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" | "رسم_مش_رنگی" | "ارسم_شبكة_ملونة" | "צייר_רשת_צבעונית" | "رنگین_میش_کھینچو" =>
12061            {
12062                // ('วาดเมช' is taken by draw_mesh — use a distinct Thai alias)
12063                let id = self.arg_num(&args, 0, 0.)? as usize;
12064                let cx = self.arg_num(&args, 1, 0.)? as f32;
12065                let cy = self.arg_num(&args, 2, 0.)? as f32;
12066                let cz = self.arg_num(&args, 3, 0.)? as f32;
12067                let sc = self.arg_num(&args, 4, 1.)? as f32;
12068                let yaw = self.arg_num(&args, 5, 0.)? as f32;
12069                let sway = self.arg_num(&args, 6, 0.)? as f32;
12070                let arm = self.arg_num(&args, 7, 0.)? as f32;
12071                let lean = self.arg_num(&args, 8, 0.)? as f32;
12072                let leg = self.arg_num(&args, 9, 0.)? as f32;
12073                let tuck = self.arg_num(&args, 10, 0.)? as f32;
12074                if id < self.meshes.len() {
12075                    let m = &self.meshes[id];
12076                    let mut gfx = self.gfx.borrow_mut();
12077                    gfx.draw_color_mesh(m, cx, cy, cz, sc, yaw, sway, arm, lean, leg, tuck);
12078                }
12079                return Ok(Value::Unit);
12080            },
12081            #[cfg(target_arch = "wasm32")]
12082            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" | "رسم_مش_رنگی" | "ارسم_شبكة_ملونة" | "צייר_רשת_צבעונית" | "رنگین_میش_کھینچو" =>
12083            {
12084                return Ok(Value::Unit);
12085            },
12086
12087            // ── liquid sim (water + oil, immiscible) ──
12088            "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" | "مایع_جدید" | "سائل_جديد" | "נוזל_חדש" | "نیا_مائع" | "nouveau_liquide" | "neue_flüssigkeit" | "новая_жидкость" =>
12089            {
12090                let w = self.arg_num(&args, 0, 64.)? as usize;
12091                let h = self.arg_num(&args, 1, 64.)? as usize;
12092                let id = self.liquids.len();
12093                self.liquids
12094                    .push(ling_physics::liquid::LiquidGrid::new(w, h));
12095                return Ok(Value::Number(id as f64));
12096            },
12097            "liquid_set_colors" | "液体颜色" | "液体配色" | "액체색상" | "สีของเหลว" | "تنظیم_رنگ_مایع" | "عيّن_ألوان_السائل" | "קבע_צבעי_נוזל" | "مائع_رنگ_مقرر_کرو" =>
12098            {
12099                let id = self.arg_num(&args, 0, 0.)? as usize;
12100                let wr = self.arg_num(&args, 1, 40.)? as f32;
12101                let wg = self.arg_num(&args, 2, 110.)? as f32;
12102                let wb = self.arg_num(&args, 3, 235.)? as f32;
12103                let or_ = self.arg_num(&args, 4, 240.)? as f32;
12104                let og = self.arg_num(&args, 5, 175.)? as f32;
12105                let ob = self.arg_num(&args, 6, 45.)? as f32;
12106                if let Some(g) = self.liquids.get_mut(id) {
12107                    g.set_colors(wr, wg, wb, or_, og, ob);
12108                }
12109                return Ok(Value::Unit);
12110            },
12111            "liquid_splat" | "液体注入" | "液体追加" | "액체분사" | "หยดของเหลว" | "پاشش_مایع" | "بقعة_سائل" | "התזת_נוזל" | "مائع_چھینٹا" | "éclaboussure_liquide" | "flüssigkeit_spritzer" | "брызги_жидкости" =>
12112            {
12113                let id = self.arg_num(&args, 0, 0.)? as usize;
12114                let x = self.arg_num(&args, 1, 0.)? as f32;
12115                let y = self.arg_num(&args, 2, 0.)? as f32;
12116                let kind = self.arg_num(&args, 3, 0.)? as i32;
12117                let amt = self.arg_num(&args, 4, 1.0)? as f32;
12118                let rad = self.arg_num(&args, 5, 4.0)? as f32;
12119                if let Some(g) = self.liquids.get_mut(id) {
12120                    g.splat(x, y, kind, amt, rad);
12121                }
12122                return Ok(Value::Unit);
12123            },
12124            "liquid_gravity" | "液体重力" | "液体重力ベクトル" | "액체중력" | "แรงโน้มถ่วงเหลว" | "گرانش_مایع" | "جاذبية_السائل" | "כבידת_נוזל" | "مائع_کشش_ثقل" | "gravité_liquide" | "flüssigkeit_schwerkraft" | "гравитация_жидкости" =>
12125            {
12126                let id = self.arg_num(&args, 0, 0.)? as usize;
12127                let gx = self.arg_num(&args, 1, 0.)? as f32;
12128                let gy = self.arg_num(&args, 2, 60.)? as f32;
12129                if let Some(g) = self.liquids.get_mut(id) {
12130                    g.set_gravity(gx, gy);
12131                }
12132                return Ok(Value::Unit);
12133            },
12134            "liquid_step" | "液体步进" | "液体更新" | "액체스텝" | "ก้าวของเหลว" | "گام_مایع" | "خطوة_السائل" | "צעד_נוזל" | "مائع_قدم" | "pas_liquide" | "flüssigkeit_schritt" | "шаг_жидкости" =>
12135            {
12136                let id = self.arg_num(&args, 0, 0.)? as usize;
12137                let dt = self.arg_num(&args, 1, 0.016)? as f32;
12138                if let Some(g) = self.liquids.get_mut(id) {
12139                    g.step(dt);
12140                }
12141                return Ok(Value::Unit);
12142            },
12143            // liquid_step_all(dt) — advance EVERY liquid grid one tick, in parallel
12144            // across instances (rayon). Independent grids share no state, so this is
12145            // an embarrassingly-parallel batch: a scene with many liquid surfaces
12146            // steps in one call that scales across cores instead of N serial
12147            // `liquid_step` calls.
12148            "liquid_step_all"
12149            | "液体全步进"
12150            | "液体全更新"
12151            | "전체액체스텝"
12152            | "ก้าวของเหลวทั้งหมด" | "گام_همه_مایعات" | "خطوة_كل_السوائل" | "צעד_כל_הנוזלים" | "تمام_مائع_قدم" => {
12153                let dt = self.arg_num(&args, 0, 0.016)? as f32;
12154                ling_physics::liquid::step_all(&mut self.liquids, dt);
12155                return Ok(Value::Unit);
12156            },
12157            // liquid_rainbow(id, on) — colour the fluid as a flowing ROYGBIV marble
12158            "liquid_rainbow" | "液体彩虹" | "液体虹" | "액체무지개" | "ของเหลวสายรุ้ง" | "مایع_رنگین‌کمان" | "سائل_قوس_قزح" | "נוזל_קשת" | "قوس_قزح_مائع" | "arc_en_ciel_liquide" | "flüssigkeit_regenbogen" | "радуга_жидкости" =>
12159            {
12160                let id = self.arg_num(&args, 0, 0.)? as usize;
12161                let on = self.arg_num(&args, 1, 1.0)? > 0.5;
12162                if let Some(g) = self.liquids.get_mut(id) {
12163                    g.rainbow = on;
12164                }
12165                return Ok(Value::Unit);
12166            },
12167            // liquid_mix(id) -> 0 (oil/water separated) .. 1 (fully intermixed)
12168            "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" | "ترکیب_مایع" | "مزج_سائل" | "ערבוב_נוזל" | "مائع_ملاؤ" | "mélanger_liquide" | "flüssigkeit_mischen" | "смешать_жидкость" =>
12169            {
12170                let id = self.arg_num(&args, 0, 0.)? as usize;
12171                let m = self.liquids.get(id).map(|g| g.mix_amount()).unwrap_or(0.0);
12172                return Ok(Value::Number(m as f64));
12173            },
12174            // liquid_draw(id, sx, sy, scale) — fast flat 2-D blit of the colour field
12175            #[cfg(not(target_arch = "wasm32"))]
12176            "liquid_draw" | "绘制液体" | "液体描画" | "액체그리기" | "วาดของเหลว" | "رسم_مایع" | "ارسم_سائلا" | "צייר_נוזל" | "مائع_کھینچو" | "dessiner_liquide" | "flüssigkeit_zeichnen" | "рисовать_жидкость" =>
12177            {
12178                let id = self.arg_num(&args, 0, 0.)? as usize;
12179                let sx = self.arg_num(&args, 1, 0.)? as i32;
12180                let sy = self.arg_num(&args, 2, 0.)? as i32;
12181                let scale = (self.arg_num(&args, 3, 4.)? as i32).max(1);
12182                if id < self.liquids.len() {
12183                    let (gw, gh) = {
12184                        let g = &self.liquids[id];
12185                        (g.w, g.h)
12186                    };
12187                    let mut gfx = self.gfx.borrow_mut();
12188                    let (w, h) = (gfx.width as i32, gfx.height as i32);
12189                    let g = &self.liquids[id];
12190                    for cy in 0..gh {
12191                        for cx in 0..gw {
12192                            let col = g.sample_rgb(cx, cy);
12193                            let bx = sx + cx as i32 * scale;
12194                            let by = sy + cy as i32 * scale;
12195                            for dy in 0..scale {
12196                                for dx in 0..scale {
12197                                    let px = bx + dx;
12198                                    let py = by + dy;
12199                                    if px >= 0 && py >= 0 && px < w && py < h {
12200                                        gfx.buffer[(py * w + px) as usize] = col;
12201                                    }
12202                                }
12203                            }
12204                        }
12205                    }
12206                }
12207                return Ok(Value::Unit);
12208            },
12209            // liquid_draw_surface(id, kind, cx,cy,cz, radius, height)
12210            //   kind: 0 plane · 1 sphere · 2 cylinder · 3 cone · 4 dome
12211            "liquid_draw_surface" | "液体贴面" | "液体曲面" | "액체곡면" | "ของเหลวบนพื้นผิว" | "رسم_سطح_مایع" | "ارسم_سطح_السائل" | "צייר_משטח_נוזל" | "مائع_سطح_کھینچو" | "dessiner_surface_liquide" | "flüssigkeit_oberfläche_zeichnen" | "рисовать_поверхность_жидкости" =>
12212            {
12213                #[cfg(not(target_arch = "wasm32"))]
12214                {
12215                    let id = self.arg_num(&args, 0, 0.)? as usize;
12216                    let kind = self.arg_num(&args, 1, 1.)? as i32;
12217                    let cx = self.arg_num(&args, 2, 0.)? as f32;
12218                    let cy = self.arg_num(&args, 3, 0.)? as f32;
12219                    let cz = self.arg_num(&args, 4, 0.)? as f32;
12220                    let radius = self.arg_num(&args, 5, 2.0)? as f32;
12221                    let height = self.arg_num(&args, 6, 3.0)? as f32;
12222                    if id < self.liquids.len() {
12223                        let (gw, gh) = {
12224                            let g = &self.liquids[id];
12225                            (g.w, g.h)
12226                        };
12227                        let mut gfx = self.gfx.borrow_mut();
12228                        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
12229                        let cam = gfx.camera.clone();
12230                        let near = -cam.zdist + 0.05;
12231                        let g = &self.liquids[id];
12232                        let tau = std::f32::consts::TAU;
12233                        let pi = std::f32::consts::PI;
12234                        // surface point for a (u,v) in [0,1] on the chosen primitive
12235                        let sp = |u: f32, v: f32| -> [f32; 3] {
12236                            if kind == 0 {
12237                                [
12238                                    cx + (u - 0.5) * 2.0 * radius,
12239                                    cy,
12240                                    cz + (v - 0.5) * 2.0 * radius,
12241                                ]
12242                            } else if kind == 2 {
12243                                let th = u * tau;
12244                                [
12245                                    cx + th.cos() * radius,
12246                                    cy + (v - 0.5) * height,
12247                                    cz + th.sin() * radius,
12248                                ]
12249                            } else if kind == 3 {
12250                                let th = u * tau;
12251                                let rr = radius * (1.0 - v);
12252                                [
12253                                    cx + th.cos() * rr,
12254                                    cy + (v - 0.5) * height,
12255                                    cz + th.sin() * rr,
12256                                ]
12257                            } else if kind == 4 {
12258                                let th = u * tau;
12259                                let ph = v * pi * 0.5;
12260                                [
12261                                    cx + ph.sin() * th.cos() * radius,
12262                                    cy - ph.cos() * radius,
12263                                    cz + ph.sin() * th.sin() * radius,
12264                                ]
12265                            } else {
12266                                let th = u * tau;
12267                                let ph = v * pi;
12268                                [
12269                                    cx + ph.sin() * th.cos() * radius,
12270                                    cy + ph.cos() * radius,
12271                                    cz + ph.sin() * th.sin() * radius,
12272                                ]
12273                            }
12274                        };
12275                        let nrm = |u: f32, v: f32| -> [f32; 3] {
12276                            if kind == 0 {
12277                                [0.0, -1.0, 0.0]
12278                            } else if kind == 2 {
12279                                let th = u * tau;
12280                                [th.cos(), 0.0, th.sin()]
12281                            } else if kind == 3 {
12282                                let th = u * tau;
12283                                let s = (radius / height.max(0.01)).atan();
12284                                [th.cos() * s.cos(), s.sin(), th.sin() * s.cos()]
12285                            } else if kind == 4 {
12286                                let th = u * tau;
12287                                let ph = v * pi * 0.5;
12288                                [ph.sin() * th.cos(), -ph.cos(), ph.sin() * th.sin()]
12289                            } else {
12290                                let th = u * tau;
12291                                let ph = v * pi;
12292                                [ph.sin() * th.cos(), ph.cos(), ph.sin() * th.sin()]
12293                            }
12294                        };
12295                        let gwf = gw as f32;
12296                        let ghf = gh as f32;
12297                        let mut cyc = 0usize;
12298                        while cyc < gh {
12299                            let mut cxc = 0usize;
12300                            while cxc < gw {
12301                                // cull by the cell centre's outward normal
12302                                let uc = (cxc as f32 + 0.5) / gwf;
12303                                let vc = (cyc as f32 + 0.5) / ghf;
12304                                let c = sp(uc, vc);
12305                                let n = nrm(uc, vc);
12306                                let dc = cam.depth(c[0], c[1], c[2]);
12307                                if dc > near {
12308                                    let cull = kind != 0
12309                                        && cam.depth(
12310                                            c[0] + n[0] * 0.06,
12311                                            c[1] + n[1] * 0.06,
12312                                            c[2] + n[2] * 0.06,
12313                                        ) > dc;
12314                                    if !cull {
12315                                        // project the 4 cell corners → a filled AA vector quad
12316                                        let u0 = cxc as f32 / gwf;
12317                                        let u1 = (cxc + 1) as f32 / gwf;
12318                                        let v0 = cyc as f32 / ghf;
12319                                        let v1 = (cyc + 1) as f32 / ghf;
12320                                        let q = [sp(u0, v0), sp(u1, v0), sp(u1, v1), sp(u0, v1)];
12321                                        let mut poly: Vec<[f32; 2]> = Vec::with_capacity(5);
12322                                        let mut ok = true;
12323                                        for p in &q {
12324                                            if cam.depth(p[0], p[1], p[2]) <= near {
12325                                                ok = false;
12326                                                break;
12327                                            }
12328                                            let (sx, sy, _) = cam.project(p[0], p[1], p[2]);
12329                                            poly.push([sx, sy]);
12330                                        }
12331                                        if ok {
12332                                            let p0 = poly[0];
12333                                            poly.push(p0);
12334                                            let col = g.sample_rgb(cxc, cyc);
12335                                            crate::gfx::raster::fill_contours_aa(
12336                                                &mut gfx.buffer,
12337                                                w,
12338                                                h,
12339                                                col,
12340                                                add,
12341                                                std::slice::from_ref(&poly),
12342                                            );
12343                                        }
12344                                    }
12345                                }
12346                                cxc += 1;
12347                            }
12348                            cyc += 1;
12349                        }
12350                    }
12351                }
12352                #[cfg(target_arch = "wasm32")]
12353                {
12354                    // WASM: liquid_draw_surface is a no-op for now (would need WebGL shader)
12355                    // The liquid simulation still runs, just not rendered to 3D surfaces
12356                }
12357                return Ok(Value::Unit);
12358            },
12359            // sparkle(x, y, w, h, count [, t]) — scatter twinkling vector star-sparkles
12360            // in a rect (snowglobe effect) in the current colour + blend mode.
12361            #[cfg(not(target_arch = "wasm32"))]
12362            "sparkle" | "闪光" | "きらめき" | "반짝임" | "ประกาย" | "درخشش" | "بريق" | "נצנוץ" | "چمک" | "scintillement" | "funkeln" | "искриться" => {
12363                let x = self.arg_num(&args, 0, 0.)? as f32;
12364                let y = self.arg_num(&args, 1, 0.)? as f32;
12365                let ww = self.arg_num(&args, 2, 200.)? as f32;
12366                let hh = self.arg_num(&args, 3, 200.)? as f32;
12367                let count = self.arg_num(&args, 4, 40.)? as i32;
12368                let t = self.arg_num(&args, 5, 0.)? as f32;
12369                let mut gfx = self.gfx.borrow_mut();
12370                let (w, h, add, color) = (gfx.width, gfx.height, gfx.blend == 1, gfx.color);
12371                let (cr, cg, cb) = (
12372                    (color >> 16 & 0xFF) as f32,
12373                    (color >> 8 & 0xFF) as f32,
12374                    (color & 0xFF) as f32,
12375                );
12376                let mut n = 0i32;
12377                while n < count {
12378                    let hsh = (n as u32).wrapping_mul(2654435761).wrapping_add(0x9E3779B9);
12379                    let u = ((hsh >> 8) & 1023) as f32 / 1023.0;
12380                    let v = ((hsh >> 18) & 1023) as f32 / 1023.0;
12381                    let phase = (hsh & 255) as f32 / 255.0;
12382                    let tw = (t * 3.0 + phase * std::f32::consts::TAU + n as f32).sin() * 0.5 + 0.5;
12383                    let sz = 1.5 + tw * 5.0;
12384                    let px = x + u * ww;
12385                    let py = y + v * hh;
12386                    let b = tw * tw; // sharp twinkle
12387                    let col =
12388                        (((cr * b) as u32) << 16) | (((cg * b) as u32) << 8) | ((cb * b) as u32);
12389                    crate::gfx::raster::draw_line_aa(
12390                        &mut gfx.buffer,
12391                        w,
12392                        h,
12393                        col,
12394                        add,
12395                        px - sz,
12396                        py,
12397                        px + sz,
12398                        py,
12399                    );
12400                    crate::gfx::raster::draw_line_aa(
12401                        &mut gfx.buffer,
12402                        w,
12403                        h,
12404                        col,
12405                        add,
12406                        px,
12407                        py - sz,
12408                        px,
12409                        py + sz,
12410                    );
12411                    let d = sz * 0.55;
12412                    crate::gfx::raster::draw_line_aa(
12413                        &mut gfx.buffer,
12414                        w,
12415                        h,
12416                        col,
12417                        add,
12418                        px - d,
12419                        py - d,
12420                        px + d,
12421                        py + d,
12422                    );
12423                    crate::gfx::raster::draw_line_aa(
12424                        &mut gfx.buffer,
12425                        w,
12426                        h,
12427                        col,
12428                        add,
12429                        px - d,
12430                        py + d,
12431                        px + d,
12432                        py - d,
12433                    );
12434                    n += 1;
12435                }
12436                return Ok(Value::Unit);
12437            },
12438
12439            // ══════════════════════════════════════════════════════════════════
12440            // DIALOG BUILTINS  (crates/ling-game/src/dialog.rs) — cinematic,
12441            // typed-out, colour-coded text boxes. Markup: {n}name{/} {p}place{/}
12442            // {i}item{/}, \n newline, || page break.
12443            // ══════════════════════════════════════════════════════════════════
12444            #[cfg(not(target_arch = "wasm32"))]
12445            "dialog_show" | "对话显示" | "会話表示" | "대화표시" | "แสดงบทสนทนา" | "نمایش_گفتگو" | "اعرض_الحوار" | "הצג_דיאלוג" | "مکالمہ_دکھاؤ" | "afficher_dialogue" | "dialog_anzeigen" | "показать_диалог" =>
12446            {
12447                let text = self.arg_str(&args, 0, "");
12448                let cps = self.arg_num(&args, 1, 32.0)? as f32;
12449                self.dialog = Some(ling_game::dialog::Dialog::new(&text, cps));
12450                return Ok(Value::Unit);
12451            },
12452            #[cfg(not(target_arch = "wasm32"))]
12453            "dialog_step" | "对话步进" | "会話更新" | "대화스텝" | "ก้าวบทสนทนา" | "گام_گفتگو" | "خطوة_الحوار" | "צעד_דיאלוג" | "مکالمہ_قدم" | "pas_dialogue" | "dialog_schritt" | "шаг_диалога" =>
12454            {
12455                let dt = self.arg_num(&args, 0, 0.016)? as f32;
12456                if let Some(d) = self.dialog.as_mut() {
12457                    d.update(dt);
12458                }
12459                return Ok(Value::Unit);
12460            },
12461            #[cfg(not(target_arch = "wasm32"))]
12462            "dialog_advance" | "对话推进" | "会話送り" | "대화진행" | "เลื่อนบทสนทนา" | "پیشروی_گفتگو" | "تقدّم_الحوار" | "קדם_דיאלוג" | "مکالمہ_آگے_بڑھاؤ" | "avancer_dialogue" | "dialog_weiter" | "продолжить_диалог" =>
12463            {
12464                if let Some(d) = self.dialog.as_mut() {
12465                    d.advance();
12466                }
12467                return Ok(Value::Unit);
12468            },
12469            #[cfg(not(target_arch = "wasm32"))]
12470            "dialog_active" | "对话激活" | "会話中" | "대화중" | "บทสนทนาทำงาน" | "گفتگو_فعال" | "الحوار_نشط" | "דיאלוג_פעיל" | "مکالمہ_فعال" | "dialogue_actif" | "dialog_aktiv" | "диалог_активен" =>
12471            {
12472                let a = self
12473                    .dialog
12474                    .as_ref()
12475                    .map(|d| !d.is_closed())
12476                    .unwrap_or(false);
12477                return Ok(Value::Bool(a));
12478            },
12479            #[cfg(not(target_arch = "wasm32"))]
12480            "dialog_typing" | "对话打字" | "会話タイプ中" | "대화타이핑" | "กำลังพิมพ์บทสนทนา" | "گفتگو_در_حال_تایپ" | "الحوار_يكتب" | "דיאלוג_מקליד" | "مکالمہ_ٹائپنگ" | "dialogue_frappe" | "dialog_tippen" | "диалог_печатает" =>
12481            {
12482                use ling_game::dialog::Dialog;
12483
12484                let a = self
12485                    .dialog
12486                    .as_ref()
12487                    .map(|d: &Dialog| !d.is_closed() && d.is_typing())
12488                    .unwrap_or(false);
12489                return Ok(Value::Bool(a));
12490            },
12491            #[cfg(not(target_arch = "wasm32"))]
12492            "dialog_close" | "对话关闭" | "会話閉じる" | "대화닫기" | "ปิดบทสนทนา" | "بستن_گفتگو" | "أغلق_الحوار" | "סגור_דיאלוג" | "مکالمہ_بند" | "fermer_dialogue" | "dialog_schließen" | "закрыть_диалог" =>
12493            {
12494                self.dialog = None;
12495                return Ok(Value::Unit);
12496            },
12497            // dialog_color(role, r, g, b) — role: 0 text · 1 name · 2 place · 3 item
12498            #[cfg(not(target_arch = "wasm32"))]
12499            "dialog_color" | "对话颜色" | "会話色" | "대화색" | "สีบทสนทนา" | "رنگ_گفتگو" | "لون_الحوار" | "צבע_דיאלוג" | "مکالمہ_رنگ" | "couleur_dialogue" | "dialog_farbe" | "цвет_диалога" =>
12500            {
12501                let role = (self.arg_num(&args, 0, 0.0)? as usize).min(3);
12502                let r = self.arg_num(&args, 1, 255.0)? as u32 & 0xFF;
12503                let g = self.arg_num(&args, 2, 255.0)? as u32 & 0xFF;
12504                let b = self.arg_num(&args, 3, 255.0)? as u32 & 0xFF;
12505                self.dialog_colors[role] = (r << 16) | (g << 8) | b;
12506                return Ok(Value::Unit);
12507            },
12508            // dialog_draw(x, y, w, h [, font_handle]) — draw the box + typed text
12509            #[cfg(not(target_arch = "wasm32"))]
12510            "dialog_draw" | "对话绘制" | "会話描画" | "대화그리기" | "วาดบทสนทนา" | "رسم_گفتگو" | "ارسم_الحوار" | "צייר_דיאלוג" | "مکالمہ_کھینچو" | "dessiner_dialogue" | "dialog_zeichnen" | "рисовать_диалог" =>
12511            {
12512                let x = self.arg_num(&args, 0, 40.0)? as f32;
12513                let y = self.arg_num(&args, 1, 0.0)? as f32;
12514                let ww = self.arg_num(&args, 2, 720.0)? as f32;
12515                let hh = self.arg_num(&args, 3, 150.0)? as f32;
12516                let font = self.arg_num(&args, 4, -1.0)? as i64;
12517                let t = (crate::runtime::now_secs() - self.start_time_secs) as f32;
12518                self.render_dialog(x, y, ww, hh, font, t);
12519                return Ok(Value::Unit);
12520            },
12521
12522            // text_poll() — fold newly-typed keys into the input buffer, return it.
12523            // Repeat is enabled (KeyRepeat::Yes) so holding a key/Backspace behaves
12524            // like a normal text field; length is capped so a stuck key or a runaway
12525            // script can't grow the buffer without bound.
12526            #[cfg(not(target_arch = "wasm32"))]
12527            "text_poll" => {
12528                const TEXT_BUFFER_MAX: usize = 240;
12529                // See key_down/key_pressed: our topmost fullscreen window can
12530                // be visually in front without real Win32 keyboard focus, so
12531                // WM_KEYDOWN/WM_CHAR (what minifb's get_keys_pressed reads)
12532                // never arrive. Poll the OS key-state table directly instead
12533                // — no focus required — with our own repeat-aware edge
12534                // detection (key_repeat_fire) so holding a key behaves like
12535                // the KeyRepeat::Yes path below: one char on press, then
12536                // repeats after a short hold delay.
12537                #[cfg(windows)]
12538                {
12539                    let topmost = self.gfx.borrow().topmost_window;
12540                    if topmost {
12541                        if !window_is_foreground(self.gfx.borrow().hwnd) {
12542                            return Ok(Value::Str(self.text_buffer.clone()));
12543                        }
12544                        let shift = os_key_down(VK_SHIFT);
12545                        let now = crate::runtime::now_secs();
12546                        let mut gfx = self.gfx.borrow_mut();
12547                        let back_idx = VK_BACK as usize;
12548                        let back_down = os_key_down(VK_BACK);
12549                        let back_was = gfx.raw_keys_prev[back_idx];
12550                        let (mut back_since, mut back_fire) = (
12551                            gfx.raw_keys_down_since[back_idx],
12552                            gfx.raw_keys_last_fire[back_idx],
12553                        );
12554                        if key_repeat_fire(now, back_down, back_was, &mut back_since, &mut back_fire) {
12555                            self.text_buffer.pop();
12556                        }
12557                        gfx.raw_keys_down_since[back_idx] = back_since;
12558                        gfx.raw_keys_last_fire[back_idx] = back_fire;
12559                        gfx.raw_keys_prev[back_idx] = back_down;
12560                        for &vk in TEXT_POLL_VKS {
12561                            let idx = (vk as usize) & 0xFF;
12562                            let down = os_key_down(vk);
12563                            let was = gfx.raw_keys_prev[idx];
12564                            let (mut since, mut fire) =
12565                                (gfx.raw_keys_down_since[idx], gfx.raw_keys_last_fire[idx]);
12566                            if key_repeat_fire(now, down, was, &mut since, &mut fire) {
12567                                if let Some(c) = vk_char(vk, shift) {
12568                                    if self.text_buffer.chars().count() < TEXT_BUFFER_MAX {
12569                                        self.text_buffer.push(c);
12570                                    }
12571                                }
12572                            }
12573                            gfx.raw_keys_down_since[idx] = since;
12574                            gfx.raw_keys_last_fire[idx] = fire;
12575                            gfx.raw_keys_prev[idx] = down;
12576                        }
12577                        return Ok(Value::Str(self.text_buffer.clone()));
12578                    }
12579                }
12580                let (keys, shift) = {
12581                    let gfx = self.gfx.borrow();
12582                    match gfx.window.as_ref() {
12583                        Some(w) => (
12584                            w.get_keys_pressed(minifb::KeyRepeat::Yes),
12585                            w.is_key_down(minifb::Key::LeftShift)
12586                                || w.is_key_down(minifb::Key::RightShift),
12587                        ),
12588                        None => (Vec::new(), false),
12589                    }
12590                };
12591                for k in keys {
12592                    if k == minifb::Key::Backspace {
12593                        self.text_buffer.pop();
12594                    } else if let Some(c) = key_char(k, shift) {
12595                        if self.text_buffer.chars().count() < TEXT_BUFFER_MAX {
12596                            self.text_buffer.push(c);
12597                        }
12598                    }
12599                }
12600                return Ok(Value::Str(self.text_buffer.clone()));
12601            },
12602            #[cfg(target_arch = "wasm32")]
12603            "text_poll" => {
12604                return Ok(Value::Str(self.text_buffer.clone()));
12605            },
12606            "text_get" => return Ok(Value::Str(self.text_buffer.clone())),
12607            "text_set" => {
12608                self.text_buffer = self.arg_str(&args, 0, "");
12609                return Ok(Value::Unit);
12610            },
12611            "text_clear" => {
12612                self.text_buffer.clear();
12613                return Ok(Value::Unit);
12614            },
12615            // record_frame() — append the current framebuffer as a PPM, return frame #
12616            #[cfg(not(target_arch = "wasm32"))]
12617            "record_frame" => {
12618                let n = self.record_n;
12619                let (buf, w, h) = {
12620                    let gfx = self.gfx.borrow();
12621                    (gfx.buffer.clone(), gfx.width, gfx.height)
12622                };
12623                let _ = std::fs::create_dir_all("recordings");
12624                let mut out = Vec::with_capacity(w * h * 3 + 32);
12625                out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes());
12626                for px in &buf {
12627                    let p = *px;
12628                    out.push((p >> 16) as u8);
12629                    out.push((p >> 8) as u8);
12630                    out.push(p as u8);
12631                }
12632                let _ = std::fs::write(format!("recordings/frame_{n:05}.ppm"), out);
12633                self.record_n += 1;
12634                return Ok(Value::Number(n as f64));
12635            },
12636            "record_count" => return Ok(Value::Number(self.record_n as f64)),
12637            // ── screenshot(mode) → PNG in ./screenshots/ with timestamp + mode + size ──
12638            #[cfg(not(target_arch = "wasm32"))]
12639            "screenshot" | "บันทึกภาพ" | "عکس‌صفحه" | "لقطة_شاشة" | "צילום_מסך" | "اسکرین_شاٹ" => {
12640                let mode = self.arg_str(&args, 0, "game");
12641                let (buf, w, h) = {
12642                    let gfx = self.gfx.borrow();
12643                    (gfx.buffer.clone(), gfx.width, gfx.height)
12644                };
12645                let _ = std::fs::create_dir_all("screenshots");
12646                let ts = std::time::SystemTime::now()
12647                    .duration_since(std::time::UNIX_EPOCH)
12648                    .map(|d| d.as_secs())
12649                    .unwrap_or(0);
12650                let safe: String = mode
12651                    .chars()
12652                    .map(|c| if c.is_alphanumeric() { c } else { '_' })
12653                    .collect();
12654                let path = format!("screenshots/ss_{ts}_{safe}_{w}x{h}.png");
12655                let mut rgb = Vec::with_capacity(w * h * 3);
12656                for px in &buf {
12657                    let p = *px;
12658                    rgb.push((p >> 16) as u8);
12659                    rgb.push((p >> 8) as u8);
12660                    rgb.push(p as u8);
12661                }
12662                if let Some(img) = image::RgbImage::from_raw(w as u32, h as u32, rgb) {
12663                    let _ = img.save(&path);
12664                }
12665                return Ok(Value::Str(path));
12666            },
12667            // ── microphone → crypto donut ──
12668            // mic_capture() — append the latest mic samples to the record buffer
12669            // (call each frame while recording). Returns the buffer length.
12670            #[cfg(not(target_arch = "wasm32"))]
12671            "mic_capture" => {
12672                if let Some(mic) = self.mic.as_ref() {
12673                    let s = mic.latest_samples();
12674                    self.mic_buffer.extend_from_slice(&s);
12675                    let cap = 96_000usize; // ~2 s @ 48 kHz
12676                    if self.mic_buffer.len() > cap {
12677                        let drop = self.mic_buffer.len() - cap;
12678                        self.mic_buffer.drain(0..drop);
12679                    }
12680                }
12681                return Ok(Value::Number(self.mic_buffer.len() as f64));
12682            },
12683            // mic_seed() — SHA3-256 hex of the recorded audio, usable as a donut seed
12684            #[cfg(not(target_arch = "wasm32"))]
12685            "mic_seed" => {
12686                let mut bytes = Vec::with_capacity(self.mic_buffer.len() * 4);
12687                for f in &self.mic_buffer {
12688                    bytes.extend_from_slice(&f.to_le_bytes());
12689                }
12690                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(&bytes))));
12691            },
12692            #[cfg(not(target_arch = "wasm32"))]
12693            "mic_clear" => {
12694                self.mic_buffer.clear();
12695                return Ok(Value::Number(0.0));
12696            },
12697            // flush the 3-D depth queue onto the framebuffer WITHOUT presenting,
12698            // so 2-D UI drawn afterwards overlays the 3-D scene.
12699            #[cfg(not(target_arch = "wasm32"))]
12700            "flush_3d" | "render_3d" => {
12701                let mut gfx = self.gfx.borrow_mut();
12702                if !gfx.depth_queue.is_empty() {
12703                    let w = gfx.width;
12704                    let h = gfx.height;
12705                    let dt = gfx.depth_test;
12706                    let reset_z = gfx.zbuf_needs_clear;
12707                    let (bm, ba) = (gfx.blend, gfx.alpha);
12708                    let aa = gfx.antialias;
12709                    let queue = std::mem::take(&mut gfx.depth_queue);
12710                    {
12711                        let g = &mut *gfx;
12712                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12713                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12714                    }
12715                    gfx.zbuf_needs_clear = false;
12716                    gfx.depth_queue.set_state(bm, ba); // keep active blend/alpha across the mid-frame flush
12717                }
12718                return Ok(Value::Unit);
12719            },
12720            #[cfg(target_arch = "wasm32")]
12721            "flush_3d" | "render_3d" => {
12722                let mut gfx = self.gfx.borrow_mut();
12723                if !gfx.depth_queue.is_empty() {
12724                    let w = gfx.width;
12725                    let h = gfx.height;
12726                    let dt = gfx.depth_test;
12727                    let reset_z = gfx.zbuf_needs_clear;
12728                    let (bm, ba) = (gfx.blend, gfx.alpha);
12729                    let aa = gfx.antialias;
12730                    let queue = std::mem::take(&mut gfx.depth_queue);
12731                    {
12732                        let g = &mut *gfx;
12733                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12734                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12735                    }
12736                    gfx.zbuf_needs_clear = false;
12737                    gfx.depth_queue.set_state(bm, ba);
12738                }
12739                return Ok(Value::Unit);
12740            },
12741
12742            // flush_post() — flush the 3-D queue like `flush_3d`, then run the
12743            // toon post-chain (SSAO → outlines → tone ramp → bloom → FXAA) over
12744            // the SCENE immediately. `present` skips the chain this frame, so
12745            // 2-D UI drawn after this call stays exact — no bloom/blur on HUDs.
12746            "flush_post" | "post_now" | "포스트플러시" | "后期冲刷" => {
12747                let mut gfx = self.gfx.borrow_mut();
12748                if !gfx.depth_queue.is_empty() {
12749                    let w = gfx.width;
12750                    let h = gfx.height;
12751                    let dt = gfx.depth_test;
12752                    let reset_z = gfx.zbuf_needs_clear;
12753                    let (bm, ba) = (gfx.blend, gfx.alpha);
12754                    let aa = gfx.antialias;
12755                    let queue = std::mem::take(&mut gfx.depth_queue);
12756                    {
12757                        let g = &mut *gfx;
12758                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12759                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12760                    }
12761                    gfx.zbuf_needs_clear = false;
12762                    gfx.depth_queue.set_state(bm, ba);
12763                }
12764                gfx.toon_post_process();
12765                gfx.post_done = true;
12766                return Ok(Value::Unit);
12767            },
12768
12769            // Viscous full-screen distortion (warp/pucker/bloat, edge-wrapped). Call
12770            // after the 3-D flush and before the UI so only the world layer warps.
12771            #[cfg(not(target_arch = "wasm32"))]
12772            "screen_distort" | "บิดจอ" | "屏幕扭曲" | "画面歪み" | "화면왜곡" | "اعوجاج_صفحه" | "شوّه_الشاشة" | "עוות_מסך" | "اسکرین_ڈسٹورٹ" =>
12773            {
12774                let amount = self.arg_num(&args, 0, 8.0)? as f32;
12775                let t = self.arg_num(&args, 1, 0.0)? as f32;
12776                // optional `step` (default 1 = full res): 2 = half-res block warp
12777                // (~4× fewer warp computes, slightly softer — suits a liquid look).
12778                let step = self.arg_num(&args, 2, 1.0)?.max(1.0) as usize;
12779                let _d = std::time::Instant::now();
12780                self.gfx.borrow_mut().distort(amount, t, step);
12781                ling_phase_add(phase::DISTORT, _d.elapsed().as_nanos());
12782                return Ok(Value::Unit);
12783            },
12784
12785            "set_rim" | "设置边缘光" | "リム設定" | "림라이트" | "ตั้งขอบเรือง" | "تنظیم_نور_لبه" | "عيّن_إضاءة_الحافة" | "קבע_תאורת_קצה" | "رم_لائٹ_مقرر_کرو" | "définir_contour_lumineux" | "rimlicht_setzen" | "задать_контурный_свет" =>
12786            {
12787                let s = self.arg_num(&args, 0, 0.6)? as f32;
12788                let r = self.arg_num(&args, 1, 115.)? as f32 / 255.0;
12789                let g = self.arg_num(&args, 2, 217.)? as f32 / 255.0;
12790                let b = self.arg_num(&args, 3, 255.)? as f32 / 255.0;
12791                let mut gfx = self.gfx.borrow_mut();
12792                gfx.shade.rim = s;
12793                gfx.shade.rim_color = [r, g, b];
12794                return Ok(Value::Unit);
12795            },
12796
12797            // ══════════════════════════════════════════════════════════════════
12798            // 3-D PRIMITIVES  (src/gfx/shapes.rs)  — "Inkscape for 3-D"
12799            //   shape(cx,cy,cz,  sx,sy,sz,  rx,ry,rz,  mode,  e0,e1,e2)
12800            //     centre (cx,cy,cz), per-axis scale, Euler rotation (radians),
12801            //     mode: 0 filled · 1 wireframe · 2 both,
12802            //     e0..e2: shape-specific (segments / sides / ratio …).
12803            //   Pen colour (set_color) drives fill lighting and wireframe colour.
12804            // ══════════════════════════════════════════════════════════════════
12805            n if crate::gfx::shapes::canon(n).is_some() => {
12806                let kind = crate::gfx::shapes::canon(n).unwrap();
12807                let cx = self.arg_num(&args, 0, 0.)? as f32;
12808                let cy = self.arg_num(&args, 1, 0.)? as f32;
12809                let cz = self.arg_num(&args, 2, 0.)? as f32;
12810                let sx = self.arg_num(&args, 3, 1.)? as f32;
12811                let sy = self.arg_num(&args, 4, 1.)? as f32;
12812                let sz = self.arg_num(&args, 5, 1.)? as f32;
12813                let rx = self.arg_num(&args, 6, 0.)? as f32;
12814                let ry = self.arg_num(&args, 7, 0.)? as f32;
12815                let rz = self.arg_num(&args, 8, 0.)? as f32;
12816                let mode = self.arg_num(&args, 9, 0.)? as i32;
12817                let e0 = self.arg_num(&args, 10, 0.)? as f32;
12818                let e1 = self.arg_num(&args, 11, 0.)? as f32;
12819                let e2 = self.arg_num(&args, 12, 0.)? as f32;
12820                if let Some(mesh) = crate::gfx::shapes::build(
12821                    kind,
12822                    [cx, cy, cz, sx, sy, sz, rx, ry, rz],
12823                    e0,
12824                    e1,
12825                    e2,
12826                ) {
12827                    let mut gfx = self.gfx.borrow_mut();
12828                    gfx.emit_mesh(&mesh, mode);
12829                }
12830                return Ok(Value::Unit);
12831            },
12832
12833            _ => {},
12834        }
12835
12836        // `form` struct constructor: positional `Name(v0, v1, ...)`.
12837        if let Some(field_names) = self.structs.get(name).cloned() {
12838            if args.len() != field_names.len() {
12839                return Err(EvalErr::from(format!(
12840                    "{name} expects {} field(s), got {}",
12841                    field_names.len(),
12842                    args.len()
12843                )));
12844            }
12845            let fields = field_names.into_iter().zip(args).collect();
12846            return Ok(Value::Struct { name: name.to_string(), fields });
12847        }
12848
12849        // `choose` enum variant constructor: `Variant(...)` or `Enum::Variant(...)`.
12850        if let Some((enum_name, arity)) = self.enum_variants.get(name).cloned() {
12851            if args.len() != arity {
12852                return Err(EvalErr::from(format!(
12853                    "{name} expects {arity} value(s), got {}",
12854                    args.len()
12855                )));
12856            }
12857            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
12858            return Ok(Value::Variant { enum_name, variant, payload: args });
12859        }
12860
12861        #[cfg(target_arch = "wasm32")]
12862        if let Some(v) = wasm_unsupported_builtin(name) {
12863            return Ok(v);
12864        }
12865
12866        Err(EvalErr::from(format!("unknown function '{name}'")))
12867    }
12868
12869    fn call_value(&mut self, v: Value, args: Vec<Value>) -> EvalResult {
12870        match v {
12871            Value::Fn(params, body, mut captured) => {
12872                for (p, a) in params.iter().zip(args) {
12873                    captured.insert(p.clone(), a);
12874                }
12875                match self.framed("<closure>", |me| me.exec_block(&body, &mut captured)) {
12876                    Ok(v) => Ok(v.unwrap_or(Value::Unit)),
12877                    Err(EvalErr::Return(v)) => Ok(v),
12878                    Err(e) => Err(e),
12879                }
12880            },
12881            other => Err(EvalErr::from(format!("cannot call {:?}", other))),
12882        }
12883    }
12884
12885    fn call_method(&self, recv: Value, method: &str, args: Vec<Value>) -> EvalResult {
12886        match (&recv, method) {
12887            (Value::Str(s), "is_empty" | "是空") => Ok(Value::Bool(s.is_empty())),
12888            // All of `lingfu normalize`'s per-language spellings of len/push
12889            // (see ling-fu normalize.rs alias table), not just the Chinese
12890            // ones — normalize rewrites method calls into whichever language
12891            // the project is normalized to, and any spelling missing here
12892            // makes those calls un-callable post-normalize (first hit with
12893            // `.长度()`, then again with Thai `.ความยาว()`).
12894            (Value::Str(s), "len" | "长" | "长度" | "長さ" | "길이" | "ความยาว") => Ok(Value::Number(s.len() as f64)),
12895            (Value::Str(s), "to_string" | "转文") => Ok(Value::Str(s.clone())),
12896            (Value::Str(s), "contains" | "包含") => {
12897                if let Some(Value::Str(sub)) = args.first() {
12898                    Ok(Value::Bool(s.contains(sub.as_str())))
12899                } else {
12900                    Ok(Value::Bool(false))
12901                }
12902            },
12903            (Value::Str(s), "push_str" | "推_文") => {
12904                let mut s2 = s.clone();
12905                if let Some(Value::Str(a)) = args.first() {
12906                    s2.push_str(a);
12907                }
12908                Ok(Value::Str(s2))
12909            },
12910            (Value::List(v), "len" | "长" | "长度" | "長さ" | "길이" | "ความยาว") => Ok(Value::Number(v.len() as f64)),
12911            (Value::List(v), "push" | "推" | "添加" | "追加" | "추가" | "เพิ่ม") => {
12912                let mut v2: Vec<Value> = (**v).clone();
12913                if let Some(a) = args.first() {
12914                    v2.push(a.clone());
12915                }
12916                Ok(Value::List(Rc::new(v2)))
12917            },
12918            // `form` field access: `point.x` (no-arg method == field read).
12919            (Value::Struct { fields, .. }, _) if args.is_empty() => fields
12920                .iter()
12921                .find(|(k, _)| k == method)
12922                .map(|(_, v)| v.clone())
12923                .ok_or_else(|| EvalErr::from(format!("no field '{method}' on {recv}"))),
12924            // Enum introspection: `.tag` → variant name, `.is(Name)` not needed for now.
12925            (Value::Variant { variant, .. }, "tag" | "标签" | "タグ" | "태그" | "ป้าย")
12926                if args.is_empty() =>
12927            {
12928                Ok(Value::Str(variant.clone()))
12929            },
12930            (Value::Ok(inner), _) | (Value::Err(inner), _) => Ok(*inner.clone()),
12931            _ => Err(EvalErr::from(format!("no method '{method}' on {recv}"))),
12932        }
12933    }
12934
12935    // ─── Pattern matching ─────────────────────────────────────────────────────
12936
12937    fn match_pattern(&self, pat: &Pattern, val: &Value) -> Option<Env> {
12938        match (pat, val) {
12939            (Pattern::Wildcard, _) => Some(new_env()),
12940            (Pattern::Str(s), Value::Str(v)) if s == v => Some(new_env()),
12941            (Pattern::Number(n), Value::Number(v)) if (n - v).abs() < 1e-12 => Some(new_env()),
12942            (Pattern::Bool(b), Value::Bool(v)) if b == v => Some(new_env()),
12943            (Pattern::Ident(name), _) => {
12944                let mut e = new_env();
12945                e.insert(name.clone(), val.clone());
12946                Some(e)
12947            },
12948            (Pattern::Constructor(ctor, inner_pat), _) => {
12949                let (matches, inner_val) = match (ctor.as_str(), val) {
12950                    ("ok" | "好", Value::Ok(v)) => (true, Some(v.as_ref().clone())),
12951                    ("bad" | "坏", Value::Err(v)) => (true, Some(v.as_ref().clone())),
12952                    ("ok" | "好", v) if !matches!(v, Value::Err(_)) => (true, Some(v.clone())),
12953                    _ => (false, None),
12954                };
12955                if !matches {
12956                    return None;
12957                }
12958                match (inner_pat, inner_val) {
12959                    (Some(p), Some(v)) => self.match_pattern(p, &v),
12960                    (None, _) => Some(new_env()),
12961                    (Some(p), None) => self.match_pattern(p, &Value::Unit),
12962                }
12963            },
12964            // User enum variant pattern: `Circle(r)`, `Pair(a, b)`, nullary `Origin()`.
12965            (Pattern::Variant(vname, sub_pats), Value::Variant { variant, payload, .. }) => {
12966                if vname != variant || sub_pats.len() != payload.len() {
12967                    return None;
12968                }
12969                let mut bindings = new_env();
12970                for (p, v) in sub_pats.iter().zip(payload.iter()) {
12971                    bindings.extend(self.match_pattern(p, v)?);
12972                }
12973                Some(bindings)
12974            },
12975            // A zero-payload variant pattern also matches the bare result-style `ok`/`bad`
12976            // values so `Ok()`-style patterns keep working uniformly.
12977            (Pattern::Variant(vname, sub), Value::Ok(v)) if (vname == "ok" || vname == "好") => {
12978                match sub.as_slice() {
12979                    [] => Some(new_env()),
12980                    [p] => self.match_pattern(p, v),
12981                    _ => None,
12982                }
12983            },
12984            (Pattern::Variant(vname, sub), Value::Err(v))
12985                if (vname == "bad" || vname == "坏" || vname == "err") =>
12986            {
12987                match sub.as_slice() {
12988                    [] => Some(new_env()),
12989                    [p] => self.match_pattern(p, v),
12990                    _ => None,
12991                }
12992            },
12993            _ => None,
12994        }
12995    }
12996
12997    // ─── Utilities ───────────────────────────────────────────────────────────
12998
12999    fn value_to_iter(&self, val: Value) -> Result<Vec<Value>, EvalErr> {
13000        match val {
13001            Value::List(v) => Ok(Rc::try_unwrap(v).unwrap_or_else(|rc| (*rc).clone())),
13002            Value::Str(s) => Ok(s.chars().map(|c| Value::Str(c.to_string())).collect()),
13003            Value::Number(n) => Ok((0..n as i64).map(|i| Value::Number(i as f64)).collect()),
13004            other => Err(EvalErr::from(format!("cannot iterate over {:?}", other))),
13005        }
13006    }
13007
13008    pub(crate) fn is_truthy(&self, val: &Value) -> bool {
13009        match val {
13010            Value::Bool(b) => *b,
13011            Value::Unit => false,
13012            Value::Number(n) => *n != 0.0,
13013            Value::Str(s) => !s.is_empty(),
13014            Value::List(v) => !v.is_empty(),
13015            Value::Ok(_) => true,
13016            Value::Err(_) => false,
13017            Value::Fn(_, _, _) => true,
13018            Value::Struct { .. } => true,
13019            Value::Variant { .. } => true,
13020        }
13021    }
13022
13023    fn to_number(&self, val: &Value) -> Result<f64, EvalErr> {
13024        match val {
13025            Value::Number(n) => Ok(*n),
13026            Value::Str(s) => s
13027                .parse()
13028                .map_err(|_| EvalErr::from(format!("cannot convert '{s}' to number"))),
13029            other => Err(EvalErr::from(format!("expected number, got {:?}", other))),
13030        }
13031    }
13032
13033    /// Get the n-th argument as f64, falling back to `default` if missing.
13034    fn arg_num(&self, args: &[Value], n: usize, default: f64) -> Result<f64, EvalErr> {
13035        match args.get(n) {
13036            Some(v) => self.to_number(v),
13037            None => Ok(default),
13038        }
13039    }
13040
13041    fn arg_str(&self, args: &[Value], n: usize, default: &str) -> String {
13042        args.get(n)
13043            .map(|v| v.to_string())
13044            .unwrap_or_else(|| default.to_string())
13045    }
13046
13047    /// Read a list-of-numbers argument as `Vec<f32>` (empty if absent/not a list).
13048    #[allow(dead_code)]
13049    fn arg_list_f32(&self, args: &[Value], n: usize) -> Vec<f32> {
13050        match args.get(n) {
13051            Some(Value::List(v)) => v
13052                .iter()
13053                .filter_map(|x| match x {
13054                    Value::Number(n) => Some(*n as f32),
13055                    _ => None,
13056                })
13057                .collect(),
13058            _ => Vec::new(),
13059        }
13060    }
13061
13062    /// Optional `r,g,b` colour override starting at arg `i` → packed 0x00RRGGBB,
13063    /// or `default` if those three numeric args aren't present.
13064    #[cfg(not(target_arch = "wasm32"))]
13065    fn color_at(&self, args: &[Value], i: usize, default: u32) -> u32 {
13066        match (args.get(i), args.get(i + 1), args.get(i + 2)) {
13067            (Some(a), Some(b), Some(c)) => {
13068                match (self.to_number(a), self.to_number(b), self.to_number(c)) {
13069                    (Ok(r), Ok(g), Ok(bl)) => {
13070                        ((r as u32 & 0xFF) << 16) | ((g as u32 & 0xFF) << 8) | (bl as u32 & 0xFF)
13071                    },
13072                    _ => default,
13073                }
13074            },
13075            _ => default,
13076        }
13077    }
13078
13079    /// A pitch argument: a note-name string (`"C4"`, `"A#3"`) or a numeric MIDI value.
13080    #[cfg(not(target_arch = "wasm32"))]
13081    fn pitch_arg(&self, args: &[Value], i: usize, default: i32) -> i32 {
13082        match args.get(i) {
13083            Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(default),
13084            Some(Value::Number(n)) => *n as i32,
13085            _ => default,
13086        }
13087    }
13088
13089    /// Current mouse position + left-button-down (native window only).
13090    #[cfg(not(target_arch = "wasm32"))]
13091    fn mouse_now(&self) -> (f32, f32, bool) {
13092        let gfx = self.gfx.borrow();
13093        let (mx, my) = gfx
13094            .window
13095            .as_ref()
13096            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
13097            .unwrap_or((0.0, 0.0));
13098        let down = gfx
13099            .window
13100            .as_ref()
13101            .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
13102            .unwrap_or(false);
13103        (mx, my, down)
13104    }
13105
13106    /// Rasterize a UI [`ling_ui::widgets::Draw`] into the framebuffer: filled
13107    /// polygons via the AA scanline fill, polylines via AA lines, honouring the
13108    /// current blend mode.
13109    #[cfg(not(target_arch = "wasm32"))]
13110    fn draw_ui(&self, d: &ling_ui::widgets::Draw) {
13111        let mut gfx = self.gfx.borrow_mut();
13112        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
13113        for (c, poly) in &d.fills {
13114            crate::gfx::raster::fill_contours_aa(
13115                &mut gfx.buffer,
13116                w,
13117                h,
13118                *c,
13119                add,
13120                std::slice::from_ref(poly),
13121            );
13122        }
13123        for (c, pl) in &d.strokes {
13124            for s in pl.windows(2) {
13125                crate::gfx::raster::draw_line_aa(
13126                    &mut gfx.buffer,
13127                    w,
13128                    h,
13129                    *c,
13130                    add,
13131                    s[0][0],
13132                    s[0][1],
13133                    s[1][0],
13134                    s[1][1],
13135                );
13136            }
13137        }
13138    }
13139
13140    /// Parse (dst_x, dst_y, width, height) from the first four args of a tex_* builtin.
13141    fn tex_rect(&self, args: &[Value]) -> Result<(usize, usize, usize, usize), EvalErr> {
13142        let tx = self.arg_num(args, 0, 0.0)? as usize;
13143        let ty = self.arg_num(args, 1, 0.0)? as usize;
13144        let tw = self.arg_num(args, 2, 256.0)? as usize;
13145        let th = self.arg_num(args, 3, 256.0)? as usize;
13146        Ok((tx, ty, tw.max(1), th.max(1)))
13147    }
13148
13149    pub(crate) fn apply_binop(&self, op: &BinOp, l: Value, r: Value) -> EvalResult {
13150        match op {
13151            BinOp::Add => match (l, r) {
13152                (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
13153                (Value::Str(a), Value::Str(b)) => Ok(Value::Str(a + &b)),
13154                (Value::Str(a), b) => Ok(Value::Str(a + &b.to_string())),
13155                (a, Value::Str(b)) => Ok(Value::Str(a.to_string() + &b)),
13156                (a, b) => Err(EvalErr::from(format!("cannot add {:?} and {:?}", a, b))),
13157            },
13158            BinOp::Sub => Ok(Value::Number(self.to_number(&l)? - self.to_number(&r)?)),
13159            BinOp::Mul => Ok(Value::Number(self.to_number(&l)? * self.to_number(&r)?)),
13160            BinOp::Div => Ok(Value::Number(self.to_number(&l)? / self.to_number(&r)?)),
13161            BinOp::Rem => Ok(Value::Number(self.to_number(&l)? % self.to_number(&r)?)),
13162            BinOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
13163            BinOp::Ne => Ok(Value::Bool(!values_equal(&l, &r))),
13164            BinOp::Lt => Ok(Value::Bool(self.to_number(&l)? < self.to_number(&r)?)),
13165            BinOp::Gt => Ok(Value::Bool(self.to_number(&l)? > self.to_number(&r)?)),
13166            BinOp::Le => Ok(Value::Bool(self.to_number(&l)? <= self.to_number(&r)?)),
13167            BinOp::Ge => Ok(Value::Bool(self.to_number(&l)? >= self.to_number(&r)?)),
13168            BinOp::And => Ok(Value::Bool(self.is_truthy(&l) && self.is_truthy(&r))),
13169            BinOp::Or => Ok(Value::Bool(self.is_truthy(&l) || self.is_truthy(&r))),
13170        }
13171    }
13172
13173    fn builtin_format(&self, args: &[Value]) -> Result<String, EvalErr> {
13174        if args.is_empty() {
13175            return Ok(String::new());
13176        }
13177        let fmt = match &args[0] {
13178            Value::Str(s) => s.clone(),
13179            other => return Ok(other.to_string()),
13180        };
13181
13182        let mut result = String::new();
13183        let mut arg_idx = 1usize;
13184        let mut chars = fmt.chars().peekable();
13185        while let Some(c) = chars.next() {
13186            if c == '{' {
13187                if chars.peek() == Some(&'}') {
13188                    chars.next();
13189                    if arg_idx < args.len() {
13190                        result.push_str(&args[arg_idx].to_string());
13191                        arg_idx += 1;
13192                    }
13193                } else {
13194                    let mut spec = String::new();
13195                    for ch in chars.by_ref() {
13196                        if ch == '}' {
13197                            break;
13198                        }
13199                        spec.push(ch);
13200                    }
13201                    if arg_idx < args.len() {
13202                        if let Some(suffix) = spec.strip_prefix(":.") {
13203                            if let Value::Number(n) = &args[arg_idx] {
13204                                let prec: usize =
13205                                    suffix.trim_end_matches('f').parse().unwrap_or(2);
13206                                result.push_str(&format!("{:.prec$}", n));
13207                                arg_idx += 1;
13208                                continue;
13209                            }
13210                        }
13211                        result.push_str(&args[arg_idx].to_string());
13212                        arg_idx += 1;
13213                    }
13214                }
13215            } else {
13216                result.push(c);
13217            }
13218        }
13219        Ok(result)
13220    }
13221}
13222
13223#[cfg(not(target_arch = "wasm32"))]
13224/// Map a friendly button name (any vendor / d-pad alias) to a gamepad button.
13225#[cfg(not(target_arch = "wasm32"))]
13226fn parse_pad_button(name: &str) -> Option<ling_input::GamepadButton> {
13227    use ling_input::GamepadButton as B;
13228    Some(match name.to_ascii_lowercase().as_str() {
13229        "a" | "south" | "cross" => B::South,
13230        "b" | "east" | "circle" => B::East,
13231        "x" | "west" | "square" => B::West,
13232        "y" | "north" | "triangle" => B::North,
13233        "lb" | "l1" | "left_shoulder" => B::LeftShoulder,
13234        "rb" | "r1" | "right_shoulder" => B::RightShoulder,
13235        "lt" | "l2" | "left_trigger" => B::LeftTrigger,
13236        "rt" | "r2" | "right_trigger" => B::RightTrigger,
13237        "start" | "menu" | "options" | "démarrer" | "начать" => B::Start,
13238        "select" | "back" | "share" | "view" => B::Select,
13239        "guide" | "home" => B::Guide,
13240        "l3" | "left_stick" => B::LeftStick,
13241        "r3" | "right_stick" => B::RightStick,
13242        "up" | "dpad_up" => B::DpadUp,
13243        "down" | "dpad_down" => B::DpadDown,
13244        "left" | "dpad_left" => B::DpadLeft,
13245        "right" | "dpad_right" => B::DpadRight,
13246        _ => return None,
13247    })
13248}
13249
13250#[cfg(not(target_arch = "wasm32"))]
13251fn str_to_minifb_key(name: &str) -> Option<minifb::Key> {
13252    use minifb::Key;
13253    Some(match name {
13254        "numpad0" | "kp0" => Key::NumPad0,
13255        "numpad1" | "kp1" => Key::NumPad1,
13256        "numpad2" | "kp2" => Key::NumPad2,
13257        "numpad3" | "kp3" => Key::NumPad3,
13258        "numpad4" | "kp4" => Key::NumPad4,
13259        "numpad5" | "kp5" => Key::NumPad5,
13260        "numpad6" | "kp6" => Key::NumPad6,
13261        "numpad7" | "kp7" => Key::NumPad7,
13262        "numpad8" | "kp8" => Key::NumPad8,
13263        "numpad9" | "kp9" => Key::NumPad9,
13264        "numpad+" | "kp+" => Key::NumPadPlus,
13265        "numpad-" | "kp-" => Key::NumPadMinus,
13266        "numpad*" | "kp*" => Key::NumPadAsterisk,
13267        "numpad/" | "kp/" => Key::NumPadSlash,
13268        "left" => Key::Left,
13269        "right" => Key::Right,
13270        "up" => Key::Up,
13271        "down" => Key::Down,
13272        "space" => Key::Space,
13273        "enter" => Key::Enter,
13274        "escape" => Key::Escape,
13275        "pageup" => Key::PageUp,
13276        "pagedown" => Key::PageDown,
13277        "lshift" | "leftshift" => Key::LeftShift,
13278        "rshift" | "rightshift" => Key::RightShift,
13279        "lctrl" | "leftctrl" => Key::LeftCtrl,
13280        "rctrl" | "rightctrl" => Key::RightCtrl,
13281        "lalt" | "leftalt" => Key::LeftAlt,
13282        "ralt" | "rightalt" => Key::RightAlt,
13283        "tab" => Key::Tab,
13284        "backspace" => Key::Backspace,
13285        "delete" => Key::Delete,
13286        "insert" => Key::Insert,
13287        "home" => Key::Home,
13288        "end" => Key::End,
13289        "a" => Key::A,
13290        "b" => Key::B,
13291        "c" => Key::C,
13292        "d" => Key::D,
13293        "e" => Key::E,
13294        "f" => Key::F,
13295        "g" => Key::G,
13296        "h" => Key::H,
13297        "i" => Key::I,
13298        "j" => Key::J,
13299        "k" => Key::K,
13300        "l" => Key::L,
13301        "m" => Key::M,
13302        "n" => Key::N,
13303        "o" => Key::O,
13304        "p" => Key::P,
13305        "q" => Key::Q,
13306        "r" => Key::R,
13307        "s" => Key::S,
13308        "t" => Key::T,
13309        "u" => Key::U,
13310        "v" => Key::V,
13311        "w" => Key::W,
13312        "x" => Key::X,
13313        "y" => Key::Y,
13314        "z" => Key::Z,
13315        "0" => Key::Key0,
13316        "1" => Key::Key1,
13317        "2" => Key::Key2,
13318        "3" => Key::Key3,
13319        "4" => Key::Key4,
13320        "5" => Key::Key5,
13321        "6" => Key::Key6,
13322        "7" => Key::Key7,
13323        "8" => Key::Key8,
13324        "9" => Key::Key9,
13325        _ => return None,
13326    })
13327}
13328
13329pub(crate) fn values_equal(a: &Value, b: &Value) -> bool {
13330    match (a, b) {
13331        (Value::Number(x), Value::Number(y)) => (x - y).abs() < 1e-12,
13332        (Value::Str(x), Value::Str(y)) => x == y,
13333        (Value::Bool(x), Value::Bool(y)) => x == y,
13334        (Value::Unit, Value::Unit) => true,
13335        _ => false,
13336    }
13337}
13338
13339// Rasteriser functions live in crate::gfx::raster — imported at top of file.
13340
13341// ── Window platform helpers ────────────────────────────────────────────────────
13342
13343/// Strip *all* window chrome from `hwnd` and make it cover the whole primary
13344/// monitor (0,0 → screen_w × screen_h), above the taskbar. This turns the
13345/// minifb window into a true borderless-fullscreen surface: no title bar, no
13346/// frame, no resize grips — there is no visible window "handle" left.
13347#[cfg(all(not(target_arch = "wasm32"), windows))]
13348fn make_borderless_fullscreen(hwnd: isize, screen_w: i32, screen_h: i32) {
13349    if hwnd == 0 {
13350        return;
13351    }
13352    unsafe {
13353        extern "system" {
13354            fn SetWindowLongPtrW(hwnd: isize, index: i32, new: isize) -> isize;
13355            fn SetWindowPos(
13356                hwnd: isize,
13357                insert_after: isize,
13358                x: i32,
13359                y: i32,
13360                cx: i32,
13361                cy: i32,
13362                flags: u32,
13363            ) -> i32;
13364            fn ShowWindow(hwnd: isize, cmd: i32) -> i32;
13365        }
13366        const GWL_STYLE: i32 = -16;
13367        const GWL_EXSTYLE: i32 = -20;
13368        // WS_POPUP (0x80000000) | WS_VISIBLE (0x10000000) — a bare top-level
13369        // window with no caption, border, or system menu.
13370        SetWindowLongPtrW(hwnd, GWL_STYLE, 0x9000_0000isize);
13371        // Clear extended edges (WS_EX_WINDOWEDGE / CLIENTEDGE / DLGMODALFRAME).
13372        SetWindowLongPtrW(hwnd, GWL_EXSTYLE, 0);
13373        // HWND_TOPMOST = -1; SWP_FRAMECHANGED (0x0020) | SWP_SHOWWINDOW (0x0040).
13374        SetWindowPos(hwnd, -1isize, 0, 0, screen_w, screen_h, 0x0020 | 0x0040);
13375        ShowWindow(hwnd, 3); // SW_MAXIMIZE-equivalent paint; 3 = SW_SHOWMAXIMIZED
13376    }
13377}
13378
13379/// Force real OS keyboard focus onto `hwnd`, not just Z-order prominence.
13380/// Windows' foreground-lock can leave a freshly-created window topmost — so
13381/// VISUALLY it covers everything — without actually handing it keyboard
13382/// focus, e.g. when launched from a terminal that still holds real focus:
13383/// clicks can nudge focus over (a more "user-driven" event) but typed keys
13384/// silently keep going to whatever app really has it, which looks exactly
13385/// like "clicking a text field doesn't focus it". AttachThreadInput is the
13386/// standard documented workaround — it lets SetForegroundWindow succeed even
13387/// under the lock by sharing input state with whichever thread currently
13388/// owns the foreground window. Call this LAST, after every other
13389/// window-visibility change for this launch (anything that shows/hides a
13390/// window afterward — e.g. hiding the launching console — can itself
13391/// reassign the foreground window and undo an earlier focus claim).
13392#[cfg(all(not(target_arch = "wasm32"), windows))]
13393fn force_window_focus(hwnd: isize) {
13394    if hwnd == 0 {
13395        return;
13396    }
13397    unsafe {
13398        extern "system" {
13399            fn GetForegroundWindow() -> isize;
13400            fn GetWindowThreadProcessId(hwnd: isize, pid: *mut u32) -> u32;
13401            fn GetCurrentThreadId() -> u32;
13402            fn AttachThreadInput(id_attach: u32, id_attach_to: u32, attach: i32) -> i32;
13403            fn SetForegroundWindow(hwnd: isize) -> i32;
13404            fn BringWindowToTop(hwnd: isize) -> i32;
13405            fn SetFocus(hwnd: isize) -> isize;
13406            fn SetActiveWindow(hwnd: isize) -> isize;
13407        }
13408        let fg = GetForegroundWindow();
13409        if fg != 0 && fg != hwnd {
13410            let mut fg_pid: u32 = 0;
13411            let fg_tid = GetWindowThreadProcessId(fg, &mut fg_pid);
13412            let my_tid = GetCurrentThreadId();
13413            if fg_tid != 0 && fg_tid != my_tid {
13414                AttachThreadInput(my_tid, fg_tid, 1);
13415                SetForegroundWindow(hwnd);
13416                BringWindowToTop(hwnd);
13417                SetFocus(hwnd);
13418                SetActiveWindow(hwnd);
13419                AttachThreadInput(my_tid, fg_tid, 0);
13420                return;
13421            }
13422        }
13423        SetForegroundWindow(hwnd);
13424        BringWindowToTop(hwnd);
13425        SetFocus(hwnd);
13426        SetActiveWindow(hwnd);
13427    }
13428}
13429
13430/// Toggle `hwnd`'s HWND_TOPMOST z-order style without moving/resizing/
13431/// activating it — used to drop the borderless-fullscreen window's topmost
13432/// flag on alt-tab (so it stops covering whatever the user switched to) and
13433/// restore it when the user switches back.
13434#[cfg(all(not(target_arch = "wasm32"), windows))]
13435fn set_window_topmost(hwnd: isize, topmost: bool) {
13436    if hwnd == 0 {
13437        return;
13438    }
13439    unsafe {
13440        extern "system" {
13441            fn SetWindowPos(
13442                hwnd: isize,
13443                insert_after: isize,
13444                x: i32,
13445                y: i32,
13446                cx: i32,
13447                cy: i32,
13448                flags: u32,
13449            ) -> i32;
13450        }
13451        let insert_after: isize = if topmost { -1 } else { -2 }; // HWND_TOPMOST / HWND_NOTOPMOST
13452        // SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE — pure z-order change,
13453        // must not steal focus back when restoring topmost on refocus.
13454        SetWindowPos(hwnd, insert_after, 0, 0, 0, 0, 0x0002 | 0x0001 | 0x0010);
13455    }
13456}
13457
13458/// Pace `win` to `vsync`'s target rate. `LING_FPS_CAP` (0 = uncapped, else an
13459/// explicit fps) always overrides; otherwise vsync-on paces to the monitor's
13460/// real refresh rate and vsync-off runs uncapped. minifb has no swap-interval
13461/// vsync (it owns no GPU present queue), so this is frame-rate pacing to the
13462/// refresh rate, not a tear-free guarantee.
13463#[cfg(not(target_arch = "wasm32"))]
13464fn apply_frame_pacing(win: &mut minifb::Window, vsync: bool) {
13465    match std::env::var("LING_FPS_CAP")
13466        .ok()
13467        .and_then(|v| v.parse::<usize>().ok())
13468    {
13469        Some(0) => win.set_target_fps(100_000),
13470        Some(cap) => win.set_target_fps(cap),
13471        None if vsync => win.set_target_fps(monitor_info().2.max(30) as usize),
13472        None => win.set_target_fps(100_000),
13473    }
13474}
13475
13476/// Primary-monitor resolution and refresh rate as `(width, height, hz)`.
13477/// `hz` falls back to 60 when the driver reports an unknown/`default` rate.
13478#[cfg(all(not(target_arch = "wasm32"), windows))]
13479fn monitor_info() -> (i32, i32, i32) {
13480    unsafe {
13481        extern "system" {
13482            fn GetSystemMetrics(index: i32) -> i32;
13483            fn GetDC(hwnd: isize) -> isize;
13484            fn ReleaseDC(hwnd: isize, hdc: isize) -> i32;
13485            fn GetDeviceCaps(hdc: isize, index: i32) -> i32;
13486        }
13487        let w = GetSystemMetrics(0).max(1); // SM_CXSCREEN
13488        let h = GetSystemMetrics(1).max(1); // SM_CYSCREEN
13489        let hdc = GetDC(0);
13490        let mut hz = if hdc != 0 { GetDeviceCaps(hdc, 116) } else { 0 }; // VREFRESH
13491        if hdc != 0 {
13492            ReleaseDC(0, hdc);
13493        }
13494        if hz <= 1 {
13495            hz = 60; // 0 or 1 means "device default" → assume 60 Hz
13496        }
13497        (w, h, hz)
13498    }
13499}
13500
13501/// Non-Windows native fallback: resolution from [`native_screen_size`]; refresh
13502/// from the active X11/RandR mode (so a 144 Hz panel drives the loop at 144),
13503/// falling back to 60 Hz when it can't be detected (Wayland, headless, macOS).
13504#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13505fn monitor_info() -> (i32, i32, i32) {
13506    let (w, h) = native_screen_size();
13507    (w as i32, h as i32, linux_refresh_hz().unwrap_or(60))
13508}
13509
13510/// Active display refresh rate via `xrandr`. Each connected output's active
13511/// mode is the token flagged with `*` (e.g. `1920x1080 144.00*+`); we take the
13512/// max across all active outputs so a multi-monitor rig drives the loop at
13513/// its fastest panel.
13514#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13515fn linux_refresh_hz() -> Option<i32> {
13516    let out = std::process::Command::new("xrandr")
13517        .arg("--current")
13518        .output()
13519        .ok()?;
13520    if !out.status.success() {
13521        return None;
13522    }
13523    parse_xrandr_max_hz(&String::from_utf8_lossy(&out.stdout))
13524}
13525
13526/// Pure parse used by [`linux_refresh_hz`]: the highest `*`-flagged refresh
13527/// rate across all active outputs in `xrandr --current` output.
13528#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13529fn parse_xrandr_max_hz(text: &str) -> Option<i32> {
13530    text.split_whitespace()
13531        .filter(|tok| tok.contains('*'))
13532        .filter_map(|tok| {
13533            tok.trim_matches(|c: char| !c.is_ascii_digit() && c != '.')
13534                .parse::<f64>()
13535                .ok()
13536        })
13537        .map(|hz| hz.round() as i32)
13538        .filter(|&hz| (24..=1000).contains(&hz))
13539        .max()
13540}
13541
13542/// WASM fallback: the canvas is the display surface; assume 60 Hz.
13543#[cfg(target_arch = "wasm32")]
13544fn monitor_info() -> (i32, i32, i32) {
13545    let (w, h) = crate::gfx::webgl::canvas_size();
13546    (w as i32, h as i32, 60)
13547}
13548
13549#[cfg(all(test, not(target_arch = "wasm32"), not(windows)))]
13550mod xrandr_tests {
13551    use super::parse_xrandr_max_hz;
13552
13553    #[test]
13554    fn picks_highest_active_output() {
13555        let text = "\
13556eDP-1 connected primary 1920x1080+0+0
13557   1920x1080     60.00*+  59.94
13558DP-1 connected 2560x1440+1920+0
13559   2560x1440    144.00*+  120.00  60.00
13560";
13561        assert_eq!(parse_xrandr_max_hz(text), Some(144));
13562    }
13563
13564    #[test]
13565    fn single_output() {
13566        let text = "   1920x1080     75.00*+  60.00\n";
13567        assert_eq!(parse_xrandr_max_hz(text), Some(75));
13568    }
13569
13570    #[test]
13571    fn no_active_mode_returns_none() {
13572        let text = "eDP-1 disconnected\n";
13573        assert_eq!(parse_xrandr_max_hz(text), None);
13574    }
13575
13576    #[test]
13577    fn out_of_range_hz_filtered() {
13578        let text = "   1x1     5000.00*+\n";
13579        assert_eq!(parse_xrandr_max_hz(text), None);
13580    }
13581}
13582
13583/// Query the primary display resolution on non-Windows platforms.
13584/// Falls back to 1920×1080 if the size cannot be determined.
13585#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13586fn native_screen_size() -> (f64, f64) {
13587    // On Linux/macOS we don't have an easy dependency-free call; return a
13588    // sensible default. Callers can always pass explicit dimensions.
13589    (1920.0, 1080.0)
13590}
13591
13592// ════════════════════════════════════════════════════════════════════════════
13593// Builtin call profiler  (env-gated, near-zero cost when off)
13594//
13595//   LING_PROFILE=1            enable per-builtin call-count + inclusive-time tally
13596//   LING_PROFILE_EVERY=N      print the report every N frames (default 240)
13597//
13598// Every builtin call funnels through `Interp::call_named` (JIT via `ling_builtin`,
13599// tree-walker directly), so this captures the full render/physics/audio builtin
13600// hot-path. Report is sorted by total time and prints calls, calls/frame,
13601// total_ms and ms/frame — the top-down "what's making so many calls" view.
13602// ════════════════════════════════════════════════════════════════════════════
13603struct LingProfileState {
13604    enabled: bool,
13605    every: u64,
13606    frames: u64,
13607    calls: std::collections::HashMap<String, (u64, u128)>, // name -> (count, nanos)
13608}
13609
13610thread_local! {
13611    static LING_PROFILE: std::cell::RefCell<LingProfileState> = std::cell::RefCell::new({
13612        let enabled = std::env::var("LING_PROFILE").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
13613        let every = std::env::var("LING_PROFILE_EVERY").ok()
13614            .and_then(|v| v.parse::<u64>().ok()).filter(|&n| n > 0).unwrap_or(240);
13615        if enabled {
13616            eprintln!("[ling-profile] ON — report every {every} frames (set LING_PROFILE_EVERY to change)");
13617        }
13618        LingProfileState { enabled, every, frames: 0, calls: std::collections::HashMap::new() }
13619    });
13620}
13621
13622#[inline]
13623fn ling_profile_enabled() -> bool {
13624    LING_PROFILE.with(|p| p.borrow().enabled)
13625}
13626
13627thread_local! {
13628    static LING_FPS: std::cell::RefCell<(bool, f64, u32, f64)> = std::cell::RefCell::new(
13629        (std::env::var("LING_FPS").map(|v| v != "0" && !v.is_empty()).unwrap_or(false), 0.0, 0, 0.0)
13630    );
13631}
13632
13633#[cfg(not(target_arch = "wasm32"))]
13634fn ling_fps_tick() {
13635    LING_FPS.with(|s| {
13636        let mut s = s.borrow_mut();
13637        if !s.0 {
13638            return;
13639        }
13640        let now = crate::runtime::now_secs();
13641        if s.1 > 0.0 {
13642            s.3 += now - s.1;
13643            s.2 += 1;
13644            if s.2 >= 120 {
13645                let avg = s.3 / s.2 as f64;
13646                eprintln!(
13647                    "[fps] {:.1} fps  ({:.2} ms/frame, wall, {} frames)",
13648                    1.0 / avg,
13649                    avg * 1000.0,
13650                    s.2
13651                );
13652                s.2 = 0;
13653                s.3 = 0.0;
13654            }
13655        }
13656        s.1 = now;
13657    });
13658}
13659
13660// Coarse render-pipeline timers (set LING_PHASE=1). Each accumulates wall-time
13661// per frame at flush/present granularity, so the cost is negligible. Reports the
13662// software-rasteriser breakdown the builtin profiler can't separate (the work is
13663// all inside the `present`/`flush_3d` builtins).
13664thread_local! {
13665    static LING_PHASE: std::cell::RefCell<(bool, u64, [u128; 5])> = std::cell::RefCell::new(
13666        (std::env::var_os("LING_PHASE").is_some(), 0, [0; 5])
13667    );
13668}
13669
13670/// Phase indices for [`ling_phase_add`].
13671pub mod phase {
13672    pub const FLUSH: usize = 0;
13673    pub const TOON: usize = 1;
13674    pub const BLIT: usize = 2;
13675    pub const DISTORT: usize = 3;
13676    pub const SORT: usize = 4;
13677}
13678
13679#[cfg(not(target_arch = "wasm32"))]
13680#[inline]
13681pub fn ling_phase_add(idx: usize, nanos: u128) {
13682    LING_PHASE.with(|p| {
13683        let mut p = p.borrow_mut();
13684        if p.0 {
13685            p.2[idx] += nanos;
13686        }
13687    });
13688}
13689
13690#[cfg(not(target_arch = "wasm32"))]
13691fn ling_phase_frame() {
13692    LING_PHASE.with(|p| {
13693        let mut p = p.borrow_mut();
13694        if !p.0 {
13695            return;
13696        }
13697        p.1 += 1;
13698        if p.1 >= 120 {
13699            let f = p.1 as f64;
13700            let ms = |i: usize| p.2[i] as f64 / 1e6 / f;
13701            eprintln!(
13702                "[phase] sort={:.2} flush={:.2} toon={:.2} blit={:.2} distort={:.2} ms/frame",
13703                ms(phase::SORT),
13704                ms(phase::FLUSH),
13705                ms(phase::TOON),
13706                ms(phase::BLIT),
13707                ms(phase::DISTORT)
13708            );
13709            p.1 = 0;
13710            p.2 = [0; 5];
13711        }
13712    });
13713}
13714
13715fn ling_profile_record(name: &str, nanos: u128) {
13716    // Frame boundary = a present() call.
13717    let is_frame = matches!(
13718        name,
13719        "present" | "แสดงผล" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시"
13720    );
13721    LING_PROFILE.with(|p| {
13722        let mut p = p.borrow_mut();
13723        let e = p.calls.entry(name.to_string()).or_insert((0, 0));
13724        e.0 += 1;
13725        e.1 += nanos;
13726        if is_frame {
13727            p.frames += 1;
13728            if p.frames % p.every == 0 {
13729                ling_profile_print(&p);
13730            }
13731        }
13732    });
13733}
13734
13735fn ling_profile_print(p: &LingProfileState) {
13736    let mut rows: Vec<(&String, u64, u128)> =
13737        p.calls.iter().map(|(n, (c, ns))| (n, *c, *ns)).collect();
13738    use std::cmp::Reverse;
13739    rows.sort_by_key(|x| Reverse(x.2)); // by total time desc
13740    let total_ns: u128 = p.calls.values().map(|(_, ns)| *ns).sum();
13741    let total_calls: u64 = p.calls.values().map(|(c, _)| *c).sum();
13742    let fr = p.frames.max(1) as f64;
13743    eprintln!(
13744        "\n┌─ LING PROFILE ── frames={} ─ builtin calls by total inclusive time ─────────────",
13745        p.frames
13746    );
13747    eprintln!(
13748        "│ {:<24} {:>9} {:>9} {:>10} {:>9} {:>6}",
13749        "builtin", "calls", "calls/fr", "total_ms", "ms/frame", "%time"
13750    );
13751    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
13752    for (name, count, ns) in rows.iter().take(30) {
13753        let ms = *ns as f64 / 1e6;
13754        let pct = if total_ns > 0 {
13755            *ns as f64 / total_ns as f64 * 100.0
13756        } else {
13757            0.0
13758        };
13759        eprintln!(
13760            "│ {:<24} {:>9} {:>9.1} {:>10.1} {:>9.3} {:>5.1}%",
13761            truncate_name(name),
13762            count,
13763            *count as f64 / fr,
13764            ms,
13765            ms / fr,
13766            pct
13767        );
13768    }
13769    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
13770    eprintln!(
13771        "│ TOTAL {} builtin calls, {:.1} ms over {} frames  →  {:.0} calls/frame, {:.2} ms/frame in builtins",
13772        total_calls,
13773        total_ns as f64 / 1e6,
13774        p.frames,
13775        total_calls as f64 / fr,
13776        total_ns as f64 / 1e6 / fr
13777    );
13778    eprintln!("└──────────────────────────────────────────────────────────────────────────────");
13779}
13780
13781/// Trim a builtin name to fit the report column (counts chars, good enough for
13782/// the mixed-script names).
13783fn truncate_name(s: &str) -> String {
13784    let max = 24;
13785    if s.chars().count() <= max {
13786        s.to_string()
13787    } else {
13788        let mut t: String = s.chars().take(max - 1).collect();
13789        t.push('…');
13790        t
13791    }
13792}